message list is now fetched from the database
This commit is contained in:
parent
22f11544e0
commit
1bc6776615
6 changed files with 204 additions and 118 deletions
|
@ -67,6 +67,10 @@ pub async fn sync_main(
|
||||||
debug!("authentication successful!");
|
debug!("authentication successful!");
|
||||||
|
|
||||||
let folder_list = authed.list().await?;
|
let folder_list = authed.list().await?;
|
||||||
|
let _ = mail2ui_tx.send(MailEvent::FolderList(
|
||||||
|
acct_name.clone(),
|
||||||
|
folder_list.clone(),
|
||||||
|
));
|
||||||
debug!("mailbox list: {:?}", folder_list);
|
debug!("mailbox list: {:?}", folder_list);
|
||||||
|
|
||||||
for folder in folder_list.iter() {
|
for folder in folder_list.iter() {
|
||||||
|
@ -100,7 +104,6 @@ pub async fn sync_main(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = mail2ui_tx.send(MailEvent::FolderList(acct_name.clone(), folder_list));
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(50)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(50)).await;
|
||||||
|
|
||||||
// TODO: remove this later
|
// TODO: remove this later
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
mod client;
|
mod client;
|
||||||
mod event;
|
mod event;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod store;
|
pub mod store;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::{
|
use futures::{
|
||||||
|
|
|
@ -5,7 +5,11 @@ use std::mem;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Error, Result};
|
||||||
|
use futures::{
|
||||||
|
future::{self, FutureExt, TryFutureExt},
|
||||||
|
stream::{StreamExt, TryStreamExt},
|
||||||
|
};
|
||||||
use panorama_imap::response::AttributeValue;
|
use panorama_imap::response::AttributeValue;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
|
@ -21,7 +25,7 @@ use tokio::{
|
||||||
|
|
||||||
use crate::config::{Config, ConfigWatcher};
|
use crate::config::{Config, ConfigWatcher};
|
||||||
|
|
||||||
use super::MailEvent;
|
use super::{EmailMetadata, MailEvent};
|
||||||
|
|
||||||
static MIGRATOR: Migrator = sqlx::migrate!();
|
static MIGRATOR: Migrator = sqlx::migrate!();
|
||||||
|
|
||||||
|
@ -44,6 +48,7 @@ struct MailStoreInner {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
/// Probably an event about new emails? i forgot
|
||||||
pub struct EmailUpdateInfo {}
|
pub struct EmailUpdateInfo {}
|
||||||
|
|
||||||
impl MailStore {
|
impl MailStore {
|
||||||
|
@ -52,79 +57,44 @@ impl MailStore {
|
||||||
let config = Arc::new(RwLock::new(None));
|
let config = Arc::new(RwLock::new(None));
|
||||||
let config2 = config.clone();
|
let config2 = config.clone();
|
||||||
|
|
||||||
|
let inner = Arc::new(RwLock::new(None));
|
||||||
|
let inner2 = inner.clone();
|
||||||
|
|
||||||
let listener = async move {
|
let listener = async move {
|
||||||
while let Ok(()) = config_watcher.changed().await {
|
while let Ok(()) = config_watcher.changed().await {
|
||||||
let new_config = config_watcher.borrow().clone();
|
let new_config = config_watcher.borrow().clone();
|
||||||
|
let fut = future::try_join(
|
||||||
|
async {
|
||||||
let mut write = config2.write().await;
|
let mut write = config2.write().await;
|
||||||
|
write.replace(new_config.clone());
|
||||||
// drop old config
|
Ok::<_, Error>(())
|
||||||
if let Some(old_config) = write.take() {
|
},
|
||||||
mem::drop(old_config);
|
async {
|
||||||
|
let new_inner =
|
||||||
|
MailStoreInner::init_with_config(new_config.clone()).await?;
|
||||||
|
let mut write = inner2.write().await;
|
||||||
|
write.replace(new_inner);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
match fut.await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("during mail loop: {}", e);
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*write = Some(new_config);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let handle = tokio::spawn(listener);
|
let handle = tokio::spawn(listener);
|
||||||
|
|
||||||
MailStore {
|
MailStore {
|
||||||
config,
|
config,
|
||||||
inner: Arc::new(RwLock::new(None)),
|
inner,
|
||||||
handle: Arc::new(handle),
|
handle: Arc::new(handle),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_with_config(&self, config: Config) -> Result<()> {
|
|
||||||
let data_dir = config.data_dir.to_string_lossy();
|
|
||||||
let data_dir = PathBuf::from(shellexpand::tilde(data_dir.as_ref()).as_ref());
|
|
||||||
|
|
||||||
let mail_dir = data_dir.join("mail");
|
|
||||||
if !mail_dir.exists() {
|
|
||||||
fs::create_dir_all(&mail_dir).await?;
|
|
||||||
}
|
|
||||||
info!("using mail dir: {:?}", mail_dir);
|
|
||||||
|
|
||||||
// create database parent
|
|
||||||
let db_path = data_dir.join("panorama.db");
|
|
||||||
let db_parent = db_path.parent();
|
|
||||||
if let Some(path) = db_parent {
|
|
||||||
fs::create_dir_all(path).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let db_path_str = db_path.to_string_lossy();
|
|
||||||
let db_path = format!("sqlite:{}", db_path_str);
|
|
||||||
info!("using database path: {}", db_path_str);
|
|
||||||
|
|
||||||
// create the database file if it doesn't already exist -_ -
|
|
||||||
if !Sqlite::database_exists(&db_path_str).await? {
|
|
||||||
Sqlite::create_database(&db_path_str).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pool = SqlitePool::connect(&db_path_str).await?;
|
|
||||||
MIGRATOR.run(&pool).await?;
|
|
||||||
debug!("run migrations : {:?}", MIGRATOR);
|
|
||||||
|
|
||||||
let accounts = config
|
|
||||||
.mail_accounts
|
|
||||||
.keys()
|
|
||||||
.map(|acct| {
|
|
||||||
let folders = RwLock::new(Vec::new());
|
|
||||||
(acct.to_owned(), Arc::new(AccountRef { folders }))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// let (new_email_tx, new_email_rx) = broadcast::channel(100);
|
|
||||||
{
|
|
||||||
let mut write = self.inner.write().await;
|
|
||||||
*write = Some(MailStoreInner {
|
|
||||||
mail_dir,
|
|
||||||
pool,
|
|
||||||
accounts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given a UID and optional message-id try to identify a particular message
|
/// Given a UID and optional message-id try to identify a particular message
|
||||||
pub async fn try_identify_email(
|
pub async fn try_identify_email(
|
||||||
&self,
|
&self,
|
||||||
|
@ -158,12 +128,6 @@ impl MailStore {
|
||||||
|
|
||||||
if let Some(existing) = existing {
|
if let Some(existing) = existing {
|
||||||
let rowid = existing.0;
|
let rowid = existing.0;
|
||||||
debug!(
|
|
||||||
"folder: {:?} uid: {:?} rowid: {:?}",
|
|
||||||
folder.as_ref(),
|
|
||||||
uid,
|
|
||||||
rowid,
|
|
||||||
);
|
|
||||||
return Ok(Some(rowid));
|
return Ok(Some(rowid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,8 +246,21 @@ impl MailStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event handerl
|
/// Event handerl
|
||||||
pub fn handle_mail_event(&self, evt: MailEvent) {
|
pub async fn handle_mail_event(&self, evt: MailEvent) -> Result<()> {
|
||||||
debug!("TODO: handle {:?}", evt);
|
debug!("TODO: handle {:?}", evt);
|
||||||
|
match evt {
|
||||||
|
MailEvent::FolderList(acct, folders) => {
|
||||||
|
let inner = self.inner.write().await;
|
||||||
|
let acct_ref = match inner.as_ref().and_then(|inner| inner.accounts.get(&acct)) {
|
||||||
|
Some(inner) => inner.clone(),
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
mem::drop(inner);
|
||||||
|
acct_ref.set_folders(folders).await;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a map of the accounts that are currently being tracked as well as a reference to the
|
/// Return a map of the accounts that are currently being tracked as well as a reference to the
|
||||||
|
@ -299,15 +276,103 @@ impl MailStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MailStoreInner {
|
||||||
|
async fn init_with_config(config: Config) -> Result<Self> {
|
||||||
|
let data_dir = config.data_dir.to_string_lossy();
|
||||||
|
let data_dir = PathBuf::from(shellexpand::tilde(data_dir.as_ref()).as_ref());
|
||||||
|
|
||||||
|
let mail_dir = data_dir.join("mail");
|
||||||
|
if !mail_dir.exists() {
|
||||||
|
fs::create_dir_all(&mail_dir).await?;
|
||||||
|
}
|
||||||
|
info!("using mail dir: {:?}", mail_dir);
|
||||||
|
|
||||||
|
// create database parent
|
||||||
|
let db_path = data_dir.join("panorama.db");
|
||||||
|
let db_parent = db_path.parent();
|
||||||
|
if let Some(path) = db_parent {
|
||||||
|
fs::create_dir_all(path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db_path_str = db_path.to_string_lossy();
|
||||||
|
let db_path = format!("sqlite:{}", db_path_str);
|
||||||
|
info!("using database path: {}", db_path_str);
|
||||||
|
|
||||||
|
// create the database file if it doesn't already exist -_ -
|
||||||
|
if !Sqlite::database_exists(&db_path_str).await? {
|
||||||
|
Sqlite::create_database(&db_path_str).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool = SqlitePool::connect(&db_path_str).await?;
|
||||||
|
MIGRATOR.run(&pool).await?;
|
||||||
|
debug!("run migrations : {:?}", MIGRATOR);
|
||||||
|
|
||||||
|
let accounts = config
|
||||||
|
.mail_accounts
|
||||||
|
.keys()
|
||||||
|
.map(|acct| {
|
||||||
|
let folders = RwLock::new(Vec::new());
|
||||||
|
(
|
||||||
|
acct.to_owned(),
|
||||||
|
Arc::new(AccountRef {
|
||||||
|
folders,
|
||||||
|
pool: pool.clone(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(MailStoreInner {
|
||||||
|
mail_dir,
|
||||||
|
pool,
|
||||||
|
accounts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
/// Holds a reference to an account
|
||||||
pub struct AccountRef {
|
pub struct AccountRef {
|
||||||
folders: RwLock<Vec<String>>,
|
folders: RwLock<Vec<String>>,
|
||||||
|
pool: SqlitePool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountRef {
|
impl AccountRef {
|
||||||
pub async fn folders(&self) -> Vec<String> {
|
/// Gets the folders on this account
|
||||||
|
pub async fn get_folders(&self) -> Vec<String> {
|
||||||
self.folders.read().await.clone()
|
self.folders.read().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the folders on this account
|
||||||
|
pub async fn set_folders(&self, folders: Vec<String>) {
|
||||||
|
*self.folders.write().await = folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the n latest messages in the given folder
|
||||||
|
pub async fn get_newest_n_messages(
|
||||||
|
&self,
|
||||||
|
folder: impl AsRef<str>,
|
||||||
|
n: usize,
|
||||||
|
) -> Result<Vec<EmailMetadata>> {
|
||||||
|
let folder = folder.as_ref();
|
||||||
|
let messages: Vec<EmailMetadata> = sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT internaldate, subject FROM mail
|
||||||
|
WHERE folder = ?
|
||||||
|
ORDER BY internaldate DESC
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(folder)
|
||||||
|
.fetch(&self.pool)
|
||||||
|
.map_ok(|(date, subject): (String, String)| EmailMetadata {
|
||||||
|
subject,
|
||||||
|
..EmailMetadata::default()
|
||||||
|
})
|
||||||
|
.try_collect()
|
||||||
|
.await?;
|
||||||
|
debug!("found {} messages", messages.len());
|
||||||
|
Ok(messages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_opt<T>(res: Result<T, SqlxError>) -> Result<Option<T>> {
|
fn into_opt<T>(res: Result<T, SqlxError>) -> Result<Option<T>> {
|
||||||
|
|
|
@ -19,14 +19,18 @@ use panorama_tui::{
|
||||||
widgets::*,
|
widgets::*,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
use crate::mail::EmailMetadata;
|
use crate::mail::{store::AccountRef, EmailMetadata};
|
||||||
|
|
||||||
use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, UI};
|
use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, UI};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
/// A singular UI view of a list of mail
|
||||||
pub struct MailView {
|
pub struct MailView {
|
||||||
pub mail_store: MailStore,
|
pub mail_store: MailStore,
|
||||||
|
pub current_account: Option<Arc<AccountRef>>,
|
||||||
|
pub current_folder: Option<String>,
|
||||||
pub message_list: TableState,
|
pub message_list: TableState,
|
||||||
pub selected: Arc<AtomicU32>,
|
pub selected: Arc<AtomicU32>,
|
||||||
pub change: Arc<AtomicI8>,
|
pub change: Arc<AtomicI8>,
|
||||||
|
@ -68,8 +72,7 @@ impl Window for MailView {
|
||||||
// folder list
|
// folder list
|
||||||
let mut items = vec![];
|
let mut items = vec![];
|
||||||
for (acct_name, acct_ref) in accts.iter() {
|
for (acct_name, acct_ref) in accts.iter() {
|
||||||
let folders = acct_ref.folders().await;
|
let folders = acct_ref.get_folders().await;
|
||||||
|
|
||||||
items.push(ListItem::new(acct_name.to_owned()));
|
items.push(ListItem::new(acct_name.to_owned()));
|
||||||
for folder in folders {
|
for folder in folders {
|
||||||
items.push(ListItem::new(format!(" {}", folder)));
|
items.push(ListItem::new(format!(" {}", folder)));
|
||||||
|
@ -86,29 +89,32 @@ impl Window for MailView {
|
||||||
.highlight_symbol(">>");
|
.highlight_symbol(">>");
|
||||||
|
|
||||||
let mut rows = vec![];
|
let mut rows = vec![];
|
||||||
// for acct in accts.iter() {
|
if let Some(acct_ref) = self.current_account.as_ref() {
|
||||||
// // TODO: messages
|
let messages = acct_ref.get_newest_n_messages("INBOX", chunks[1].height as usize);
|
||||||
// let result: Option<Vec<EmailMetadata>> = None; // self.mail_store.messages_of(acct);
|
}
|
||||||
// if let Some(messages) = result {
|
|
||||||
// for meta in messages {
|
for (acct_name, acct_ref) in accts.iter() {
|
||||||
// let mut row = Row::new(vec![
|
let result: Option<Vec<EmailMetadata>> = None; // self.mail_store.messages_of(acct);
|
||||||
// String::from(if meta.unread { "\u{2b24}" } else { "" }),
|
if let Some(messages) = result {
|
||||||
// meta.uid.map(|u| u.to_string()).unwrap_or_default(),
|
for meta in messages {
|
||||||
// meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(),
|
let mut row = Row::new(vec![
|
||||||
// meta.from.clone(),
|
String::from(if meta.unread { "\u{2b24}" } else { "" }),
|
||||||
// meta.subject.clone(),
|
meta.uid.map(|u| u.to_string()).unwrap_or_default(),
|
||||||
// ]);
|
meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(),
|
||||||
// if meta.unread {
|
meta.from.clone(),
|
||||||
// row = row.style(
|
meta.subject.clone(),
|
||||||
// Style::default()
|
]);
|
||||||
// .fg(Color::LightCyan)
|
if meta.unread {
|
||||||
// .add_modifier(Modifier::BOLD),
|
row = row.style(
|
||||||
// );
|
Style::default()
|
||||||
// }
|
.fg(Color::LightCyan)
|
||||||
// rows.push(row);
|
.add_modifier(Modifier::BOLD),
|
||||||
// }
|
);
|
||||||
// }
|
}
|
||||||
// }
|
rows.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let table = Table::new(rows)
|
let table = Table::new(rows)
|
||||||
.style(Style::default().fg(Color::White))
|
.style(Style::default().fg(Color::White))
|
||||||
|
@ -128,6 +134,24 @@ impl Window for MailView {
|
||||||
f.render_widget(dirlist, chunks[0]);
|
f.render_widget(dirlist, chunks[0]);
|
||||||
f.render_widget(table, chunks[1]);
|
f.render_widget(table, chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update(&mut self) {
|
||||||
|
// make the change
|
||||||
|
if self
|
||||||
|
.change
|
||||||
|
.compare_exchange(-1, 0, Ordering::Relaxed, Ordering::Relaxed)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
self.move_up();
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.change
|
||||||
|
.compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
self.move_down();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turn a timestamp into a format that a human might read when viewing it in a table.
|
/// Turn a timestamp into a format that a human might read when viewing it in a table.
|
||||||
|
@ -152,12 +176,22 @@ impl MailView {
|
||||||
pub fn new(mail_store: MailStore) -> Self {
|
pub fn new(mail_store: MailStore) -> Self {
|
||||||
MailView {
|
MailView {
|
||||||
mail_store,
|
mail_store,
|
||||||
|
current_account: None,
|
||||||
|
current_folder: None,
|
||||||
message_list: TableState::default(),
|
message_list: TableState::default(),
|
||||||
selected: Arc::new(AtomicU32::default()),
|
selected: Arc::new(AtomicU32::default()),
|
||||||
change: Arc::new(AtomicI8::default()),
|
change: Arc::new(AtomicI8::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_current_account(&mut self, name: impl AsRef<str>) {
|
||||||
|
let name = name.as_ref();
|
||||||
|
let accounts = self.mail_store.list_accounts().await;
|
||||||
|
if let Some(acct_ref) = accounts.get(name) {
|
||||||
|
self.current_account = Some(acct_ref.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_down(&mut self) {
|
pub fn move_down(&mut self) {
|
||||||
// if self.message_uids.is_empty() {
|
// if self.message_uids.is_empty() {
|
||||||
// return;
|
// return;
|
||||||
|
@ -186,21 +220,4 @@ impl MailView {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self) {
|
|
||||||
// make the change
|
|
||||||
if self
|
|
||||||
.change
|
|
||||||
.compare_exchange(-1, 0, Ordering::Relaxed, Ordering::Relaxed)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
self.move_up();
|
|
||||||
}
|
|
||||||
if self
|
|
||||||
.change
|
|
||||||
.compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
self.move_down();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ pub async fn run_ui2(params: UiParams) -> Result<()> {
|
||||||
select! {
|
select! {
|
||||||
// got an event from the mail thread
|
// got an event from the mail thread
|
||||||
evt = mail2ui_rx.recv().fuse() => if let Some(evt) = evt {
|
evt = mail2ui_rx.recv().fuse() => if let Some(evt) = evt {
|
||||||
ui.process_mail_event(evt);
|
ui.process_mail_event(evt).await?;
|
||||||
},
|
},
|
||||||
|
|
||||||
// got an event from the ui thread
|
// got an event from the ui thread
|
||||||
|
@ -229,7 +229,8 @@ impl UI {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_mail_event(&mut self, evt: MailEvent) {
|
async fn process_mail_event(&mut self, evt: MailEvent) -> Result<()> {
|
||||||
self.mail_store.handle_mail_event(evt);
|
self.mail_store.handle_mail_event(evt).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ pub trait Window: HandlesInput {
|
||||||
// async fn draw(&self, f: FrameType, area: Rect, ui: Rc<UI>);
|
// async fn draw(&self, f: FrameType, area: Rect, ui: Rc<UI>);
|
||||||
|
|
||||||
/// Update function
|
/// Update function
|
||||||
fn update(&mut self) {}
|
async fn update(&mut self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
downcast_rs::impl_downcast!(Window);
|
downcast_rs::impl_downcast!(Window);
|
||||||
|
|
Loading…
Reference in a new issue