Bladeren bron

app: begin dividing code between long running bg runtime, and short lived fg for UI. Restarting works and the app is running an Android foreground service for the bg_runtime. Introduce GOD into main.

darkfi 1 jaar geleden
bovenliggende
commit
17865fa10e

+ 10 - 3
bin/app/src/android.rs

@@ -17,10 +17,11 @@
  */
 
 use miniquad::native::android::{self, ndk_sys, ndk_utils};
+use parking_lot::Mutex as SyncMutex;
 use std::{
     collections::HashMap,
     path::PathBuf,
-    sync::{LazyLock, Mutex as SyncMutex},
+    sync::LazyLock
 };
 
 use crate::AndroidSuggestEvent;
@@ -62,7 +63,7 @@ struct GlobalData {
 }
 
 fn send(id: usize, ev: AndroidSuggestEvent) {
-    let globals = &GLOBALS.lock().unwrap();
+    let globals = &GLOBALS.lock();
     let Some(sender) = globals.senders.get(&id) else {
         warn!(target: "android", "Unknown composer_id={id} discard ev: {ev:?}");
         return
@@ -70,6 +71,12 @@ fn send(id: usize, ev: AndroidSuggestEvent) {
     let _ = sender.try_send(ev);
 }
 
+pub fn clear_state() {
+    let mut globals = &mut GLOBALS.lock();
+    globals.senders.clear();
+    globals.next_id = 0;
+}
+
 unsafe impl Send for GlobalData {}
 unsafe impl Sync for GlobalData {}
 
@@ -159,7 +166,7 @@ pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onDeleteSurround
 
 pub fn create_composer(sender: async_channel::Sender<AndroidSuggestEvent>) -> usize {
     let composer_id = {
-        let mut globals = GLOBALS.lock().unwrap();
+        let mut globals = GLOBALS.lock();
         let id = globals.next_id;
         globals.next_id += 1;
         globals.senders.insert(id, sender);

+ 35 - 97
bin/app/src/app/mod.rs

@@ -30,8 +30,10 @@ use std::{
     thread,
 };
 
+#[cfg(target_os = "android")]
+use crate::android;
+
 use crate::{
-    android,
     error::Error,
     expr::Op,
     gfx::{GraphicsEventPublisherPtr, RenderApi, Vertex},
@@ -61,79 +63,11 @@ macro_rules! e { ($($arg:tt)*) => { error!(target: "app", $($arg)*); } }
 //    println!("{}", std::any::type_name::<T>())
 //}
 
-pub struct AsyncRuntime {
-    signal: async_channel::Sender<()>,
-    shutdown: async_channel::Receiver<()>,
-    exec_threadpool: SyncMutex<Option<thread::JoinHandle<()>>>,
-    ex: ExecutorPtr,
-    tasks: SyncMutex<Vec<Task<()>>>,
-}
-
-impl AsyncRuntime {
-    pub fn new(ex: ExecutorPtr) -> Self {
-        let (signal, shutdown) = async_channel::unbounded::<()>();
-
-        Self {
-            signal,
-            shutdown,
-            exec_threadpool: SyncMutex::new(None),
-            ex,
-            tasks: SyncMutex::new(vec![]),
-        }
-    }
-
-    pub fn start(&self) {
-        let n_threads = thread::available_parallelism().unwrap().get();
-        let shutdown = self.shutdown.clone();
-        let ex = self.ex.clone();
-        let exec_threadpool = thread::spawn(move || {
-            easy_parallel::Parallel::new()
-                // N executor threads
-                .each(0..n_threads, |_| smol::future::block_on(ex.run(shutdown.recv())))
-                .run();
-        });
-        *self.exec_threadpool.lock().unwrap() = Some(exec_threadpool);
-        info!(target: "async_runtime", "Started runtime [{n_threads} threads]");
-    }
-
-    pub fn push_task(&self, task: Task<()>) {
-        self.tasks.lock().unwrap().push(task);
-    }
-
-    pub fn stop(&self) {
-        // Go through event graph and call stop on everything
-        // Depth first
-        d!("Stopping async runtime...");
-
-        let tasks = std::mem::take(&mut *self.tasks.lock().unwrap());
-        // Close all tasks
-        smol::future::block_on(async {
-            // Perform cleanup code
-            // If not finished in certain amount of time, then just exit
-
-            let futures = FuturesUnordered::new();
-            for task in tasks {
-                futures.push(task.cancel());
-            }
-            let _: Vec<_> = futures.collect().await;
-        });
-
-        if !self.signal.close() {
-            error!(target: "app", "exec threadpool was already shutdown");
-        }
-        let exec_threadpool = std::mem::replace(&mut *self.exec_threadpool.lock().unwrap(), None);
-        let exec_threadpool = exec_threadpool.expect("threadpool wasnt started");
-        exec_threadpool.join().unwrap();
-        i!("Stopped app");
-    }
-}
-
 pub type AppPtr = Arc<App>;
 
 pub struct App {
     pub sg_root: SceneNodePtr,
     pub render_api: RenderApi,
-    pub event_pub: GraphicsEventPublisherPtr,
     pub text_shaper: TextShaperPtr,
     pub tasks: SyncMutex<Vec<Task<()>>>,
     pub ex: ExecutorPtr,
@@ -143,7 +77,6 @@ impl App {
     pub fn new(
         sg_root: SceneNodePtr,
         render_api: RenderApi,
-        event_pub: GraphicsEventPublisherPtr,
         text_shaper: TextShaperPtr,
         ex: ExecutorPtr,
     ) -> Arc<Self> {
@@ -151,7 +84,6 @@ impl App {
             sg_root,
             ex,
             render_api,
-            event_pub,
             text_shaper,
             tasks: SyncMutex::new(vec![]),
         })
@@ -196,6 +128,7 @@ impl App {
         settings.load_settings();
 
         // Save app settings in sled when they change
+        /*
         for setting_node in settings.setting_root.get_children().iter() {
             let setting_sub = setting_node.get_property("value").unwrap().subscribe_modify();
             let settings2 = settings.clone();
@@ -206,6 +139,7 @@ impl App {
             });
             self.tasks.lock().unwrap().push(setting_task);
         }
+        */
 
         let window =
             window.setup(|me| Window::new(me, self.render_api.clone(), setting_root.clone())).await;
@@ -213,7 +147,7 @@ impl App {
         self.sg_root.clone().link(window.clone());
         self.sg_root.clone().link(setting_root.clone());
 
-        schema::make(&self, window.clone()).await;
+        schema::test::make(&self, window.clone()).await;
 
         d!("Schema loaded");
 
@@ -221,17 +155,18 @@ impl App {
         let plugin = plugin.setup_null();
         self.sg_root.clone().link(plugin.clone());
 
-        #[cfg(feature = "enable-plugins")]
-        self.load_plugins(plugin).await;
+        //#[cfg(feature = "enable-plugins")]
+        //self.load_plugins(plugin).await;
 
-        #[cfg(not(feature = "enable-plugins"))]
-        w!("Plugins are disabled in this build");
+        //#[cfg(not(feature = "enable-plugins"))]
+        //w!("Plugins are disabled in this build");
 
-        settings::make(&self, window, self.ex.clone()).await;
+        //settings::make(&self, window, self.ex.clone()).await;
 
         Ok(None)
     }
 
+    /*
     #[cfg(feature = "enable-plugins")]
     async fn load_plugins(&self, plugin: SceneNodePtr) {
         let darkirc = create_darkirc("darkirc");
@@ -360,9 +295,12 @@ impl App {
 
         i!("Plugins loaded");
     }
+*/
 
     /// Begins the draw of the tree, and then starts the UI procs.
-    pub async fn start(self: Arc<Self>) {
+    pub async fn start(self: Arc<Self>,
+        event_pub: GraphicsEventPublisherPtr
+        ) {
         d!("Starting app");
         let atom = &mut PropertyAtomicGuard::new();
 
@@ -376,16 +314,27 @@ impl App {
         drop(atom);
 
         // Access drawable in window node and call draw()
+        self.init();
         self.trigger_draw().await;
 
-        self.start_procs().await;
+        self.start_procs(event_pub).await;
         i!("App started");
     }
 
+    pub fn init(&self) {
+        let window_node = self.sg_root.clone().lookup_node("/window").unwrap();
+        match window_node.pimpl() {
+            Pimpl::Window(win) => win.init(),
+            _ => panic!("wrong pimpl"),
+        }
+    }
+
     pub fn stop(&self) {
-        smol::future::block_on(async {
-            self.async_stop().await;
-        });
+        let window_node = self.sg_root.clone().lookup_node("/window").unwrap();
+        match window_node.pimpl() {
+            Pimpl::Window(win) => win.stop(),
+            _ => panic!("wrong pimpl"),
+        }
     }
 
     async fn trigger_draw(&self) {
@@ -395,10 +344,12 @@ impl App {
             _ => panic!("wrong pimpl"),
         }
     }
-    async fn start_procs(&self) {
+    async fn start_procs(&self,
+        event_pub: GraphicsEventPublisherPtr
+        ) {
         let window_node = self.sg_root.clone().lookup_node("/window").unwrap();
         match window_node.pimpl() {
-            Pimpl::Window(win) => win.clone().start(self.event_pub.clone(), self.ex.clone()).await,
+            Pimpl::Window(win) => win.clone().start(event_pub, self.ex.clone()).await,
             _ => panic!("wrong pimpl"),
         }
 
@@ -410,19 +361,6 @@ impl App {
             }
         }
     }
-
-    /// Shutdown code here
-    async fn async_stop(&self) {
-        //self.darkirc_backend.stop().await;
-    }
-}
-
-impl Drop for App {
-    fn drop(&mut self) {
-        t!("Dropping app");
-        // This hangs
-        //self.stop();
-    }
 }
 
 // Just for testing

+ 2 - 0
bin/app/src/app/schema/test.rs

@@ -481,6 +481,7 @@ pub async fn make(app: &App, window: SceneNodePtr) {
     layer_node.clone().link(node);
     */
 
+        /*
     // Text edit
     let node = create_chatedit("editz");
     node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
@@ -553,4 +554,5 @@ pub async fn make(app: &App, window: SceneNodePtr) {
         })
         .await;
     layer_node.clone().link(node);
+    */
 }

+ 50 - 36
bin/app/src/gfx/mod.rs

@@ -43,10 +43,11 @@ pub use linalg::{Dimension, Point, Rectangle};
 mod shader;
 
 use crate::{
-    app::{AppPtr, AsyncRuntime},
+    GOD,
+    app::AppPtr,
     error::{Error, Result},
     pubsub::{Publisher, PublisherPtr, Subscription, SubscriptionId},
-    util::ansi_texture,
+    util::{AsyncRuntime, ansi_texture},
 };
 
 // This is very noisy so suppress output by default
@@ -127,21 +128,35 @@ impl std::fmt::Debug for ManagedBuffer {
     }
 }
 
+pub type EpochIndex = u32;
+
 #[derive(Clone)]
 pub struct RenderApi {
-    method_req: mpsc::Sender<GraphicsMethod>,
+    /// We are abusing async_channel since it's cloneable whereas std::sync::mpsc is shit.
+    method_req: async_channel::Sender<(EpochIndex, GraphicsMethod)>,
+    /// Keep track of the current UI epoch
+    epoch: Arc<AtomicU32>,
 }
 
 impl RenderApi {
-    pub fn new(method_req: mpsc::Sender<GraphicsMethod>) -> Self {
-        Self { method_req }
+    pub fn new(method_req: async_channel::Sender<(EpochIndex, GraphicsMethod)>) -> Self {
+        Self { method_req, epoch: Arc::new(AtomicU32::new(0)) }
+    }
+
+    fn next_epoch(&self) -> EpochIndex {
+        self.epoch.fetch_add(1, Ordering::SeqCst) + 1
+    }
+
+    fn send(&self, method: GraphicsMethod) {
+        let epoch = self.epoch.load(Ordering::Relaxed);
+        let _ = self.method_req.try_send((epoch, method)).unwrap();
     }
 
     fn new_unmanaged_texture(&self, width: u16, height: u16, data: Vec<u8>) -> GfxTextureId {
         let gfx_texture_id = NEXT_TEXTURE_ID.fetch_add(1, Ordering::SeqCst);
 
         let method = GraphicsMethod::NewTexture((width, height, data, gfx_texture_id));
-        let _ = self.method_req.send(method);
+        self.send(method);
 
         gfx_texture_id
     }
@@ -155,14 +170,14 @@ impl RenderApi {
 
     fn delete_unmanaged_texture(&self, texture: GfxTextureId) {
         let method = GraphicsMethod::DeleteTexture(texture);
-        let _ = self.method_req.send(method);
+        self.send(method);
     }
 
     fn new_unmanaged_vertex_buffer(&self, verts: Vec<Vertex>) -> GfxBufferId {
         let gfx_buffer_id = NEXT_BUFFER_ID.fetch_add(1, Ordering::SeqCst);
 
         let method = GraphicsMethod::NewVertexBuffer((verts, gfx_buffer_id));
-        let _ = self.method_req.send(method);
+        self.send(method);
 
         gfx_buffer_id
     }
@@ -171,7 +186,7 @@ impl RenderApi {
         let gfx_buffer_id = NEXT_BUFFER_ID.fetch_add(1, Ordering::SeqCst);
 
         let method = GraphicsMethod::NewIndexBuffer((indices, gfx_buffer_id));
-        let _ = self.method_req.send(method);
+        self.send(method);
 
         gfx_buffer_id
     }
@@ -191,12 +206,12 @@ impl RenderApi {
 
     fn delete_unmanaged_buffer(&self, buffer: GfxBufferId) {
         let method = GraphicsMethod::DeleteBuffer(buffer);
-        let _ = self.method_req.send(method);
+        self.send(method);
     }
 
     pub fn replace_draw_calls(&self, timest: u64, dcs: Vec<(u64, GfxDrawCall)>) {
         let method = GraphicsMethod::ReplaceDrawCalls { timest, dcs };
-        let _ = self.method_req.send(method);
+        self.send(method);
     }
 }
 
@@ -616,11 +631,6 @@ impl GraphicsEventPublisher {
 }
 
 struct Stage {
-    #[allow(dead_code)]
-    app: AppPtr,
-    #[allow(dead_code)]
-    async_runtime: AsyncRuntime,
-
     ctx: Box<dyn RenderingBackend>,
     pipeline: Pipeline,
     white_texture: miniquad::TextureId,
@@ -629,23 +639,27 @@ struct Stage {
     textures: HashMap<GfxTextureId, miniquad::TextureId>,
     buffers: HashMap<GfxBufferId, miniquad::BufferId>,
 
-    method_rep: mpsc::Receiver<GraphicsMethod>,
+    epoch: EpochIndex,
+    method_rep: async_channel::Receiver<(EpochIndex, GraphicsMethod)>,
     event_pub: GraphicsEventPublisherPtr,
 }
 
 impl Stage {
     pub fn new(
-        app: AppPtr,
-        async_runtime: AsyncRuntime,
-        method_rep: mpsc::Receiver<GraphicsMethod>,
-        event_pub: GraphicsEventPublisherPtr,
-        cv_started: Arc<CondVar>,
     ) -> Self {
         let mut ctx: Box<dyn RenderingBackend> = window::new_rendering_backend();
 
         // This will start the app to start. Needed since we cannot get window size for init
         // until window is created.
-        cv_started.notify();
+        let god = GOD.get().unwrap();
+        god.start_app();
+
+        // Start a new epoch. This is a brand new UI run.
+        let epoch = god.render_api.next_epoch();
+
+        let method_rep = god.method_rep.clone();
+        let event_pub = god.event_pub.clone();
+        drop(god);
 
         let white_texture = ctx.new_texture_from_rgba8(1, 1, &[255, 255, 255, 255]);
 
@@ -687,8 +701,6 @@ impl Stage {
         );
 
         Stage {
-            app,
-            async_runtime,
             ctx,
             pipeline,
             white_texture,
@@ -696,8 +708,11 @@ impl Stage {
                 0,
                 DrawCall { instrs: vec![], dcs: vec![], z_index: 0, timest: 0 },
             )]),
+
             textures: HashMap::new(),
             buffers: HashMap::new(),
+
+            epoch,
             method_rep,
             event_pub,
         }
@@ -820,7 +835,13 @@ impl Stage {
 impl EventHandler for Stage {
     fn update(&mut self) {
         // Process as many methods as we can
-        while let Ok(method) = self.method_rep.try_recv() {
+        while let Ok((epoch, method)) = self.method_rep.try_recv() {
+            if epoch < self.epoch {
+                // Discard old rubbish
+                trace!(target: "gfx", "Discard method with old epoch: {epoch} curr: {} [method={method:?}]", self.epoch);
+                continue
+            }
+            assert_eq!(epoch, self.epoch);
             self.process_method(method);
         }
     }
@@ -905,19 +926,12 @@ impl EventHandler for Stage {
 
     fn quit_requested_event(&mut self) {
         debug!(target: "gfx", "quit requested");
-        // Doesn't work
-        //miniquad::window::cancel_quit();
-        //self.app.stop();
-        //self.async_runtime.stop();
+        let god = GOD.get().unwrap();
+        god.stop_app();
     }
 }
 
 pub fn run_gui(
-    app: AppPtr,
-    async_runtime: AsyncRuntime,
-    method_rep: mpsc::Receiver<GraphicsMethod>,
-    event_pub: GraphicsEventPublisherPtr,
-    cv_started: Arc<CondVar>,
 ) {
     let mut window_width = 1024;
     let mut window_height = 768;
@@ -950,6 +964,6 @@ pub fn run_gui(
         if metal { conf::AppleGfxApi::Metal } else { conf::AppleGfxApi::OpenGl };
 
     miniquad::start(conf, || {
-        Box::new(Stage::new(app, async_runtime, method_rep, event_pub, cv_started))
+        Box::new(Stage::new())
     });
 }

+ 165 - 64
bin/app/src/main.rs

@@ -23,7 +23,7 @@
 use async_lock::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
 use darkfi::system::CondVar;
 use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate};
-use std::sync::{mpsc, Arc};
+use std::sync::{mpsc, Arc, OnceLock};
 
 #[macro_use]
 extern crate log;
@@ -63,94 +63,195 @@ mod text2;
 mod ui;
 mod util;
 
-use crate::{net::ZeroMQAdapter, text::TextShaper};
+use crate::{app::{App, AppPtr}, net::ZeroMQAdapter, text::TextShaper, util::AsyncRuntime};
+
+// This is historical, but ideally we can fix the entire project and remove this import.
+pub use util::ExecutorPtr;
 
 // Hides the cmd.exe terminal on Windows.
 // Enable this when making release builds.
 //#![windows_subsystem = "windows"]
 
-pub type ExecutorPtr = Arc<smol::Executor<'static>>;
-
 fn panic_hook(panic_info: &std::panic::PanicHookInfo) {
     error!("panic occurred: {panic_info}");
     error!("{}", std::backtrace::Backtrace::force_capture().to_string());
     std::process::abort()
 }
 
-fn main() {
-    // Abort the application on panic right away
-    std::panic::set_hook(Box::new(panic_hook));
+/// Contains values which persist between app restarts. For example on Android, we are
+/// running a foreground service. Everytime the UI restarts main() is called again.
+/// However the global state remains intact.
+struct God {
+    bg_runtime: AsyncRuntime,
+    bg_ex: ExecutorPtr,
+
+    fg_runtime: AsyncRuntime,
+    fg_ex: ExecutorPtr,
+
+    /// App must fully finish setup() before start() is allowed to begin.
+    cv_app_is_setup: Arc<CondVar>,
+    app: AppPtr,
+
+    /// This is the main rendering API used to send commands to the gfx subsystem.
+    /// We have a ref here so the gfx subsystem can increment the epoch counter.
+    render_api: gfx::RenderApi,
+    /// This is how the gfx subsystem receives messages from the render API.
+    method_rep: async_channel::Receiver<(gfx::EpochIndex, gfx::GraphicsMethod)>,
+    /// Publisher to send input and window events to subscribers.
+    event_pub: gfx::GraphicsEventPublisherPtr,
+}
 
-    text2::init_txt_ctx();
-    logger::setup_logging();
+impl God {
+    fn new() -> Self {
+        info!(target: "main", "Creating the app");
 
-    #[cfg(target_os = "android")]
-    {
-        use crate::android::{get_appdata_path, get_external_storage_path};
+        // Abort the application on panic right away
+        std::panic::set_hook(Box::new(panic_hook));
 
-        info!("App internal data path: {:?}", get_appdata_path());
-        info!("App external storage path: {:?}", get_external_storage_path());
+        text2::init_txt_ctx();
+        logger::setup_logging();
 
-        // Workaround for this bug
-        // https://gitlab.torproject.org/tpo/core/arti/-/issues/999
-        unsafe {
-            std::env::set_var("HOME", get_appdata_path().as_os_str());
+        #[cfg(target_os = "android")]
+        {
+            use crate::android::get_appdata_path;
+
+            // Workaround for this bug
+            // https://gitlab.torproject.org/tpo/core/arti/-/issues/999
+            unsafe {
+                std::env::set_var("HOME", get_appdata_path().as_os_str());
+            }
         }
 
-        //let paths = std::fs::read_dir("/data/data/darkfi.darkfi/").unwrap();
-        //for path in paths {
-        //    debug!("{}", path.unwrap().path().display())
-        //}
+        let exe_path = std::env::current_exe().unwrap();
+        let basename = exe_path.parent().unwrap();
+        std::env::set_current_dir(basename);
+
+        let bg_ex = Arc::new(smol::Executor::new());
+        let fg_ex = Arc::new(smol::Executor::new());
+        let sg_root = SceneNode3::root();
+
+        let bg_runtime = AsyncRuntime::new(bg_ex.clone(), "bg");
+        bg_runtime.start();
+
+        let fg_runtime = AsyncRuntime::new(fg_ex.clone(), "fg");
+
+        let (method_req, method_rep) = async_channel::unbounded();
+        // The UI actually needs to be running for this to reply back.
+        // Otherwise calls will just hang.
+        let render_api = gfx::RenderApi::new(method_req);
+        let event_pub = gfx::GraphicsEventPublisher::new();
+
+        let text_shaper = TextShaper::new();
+
+        let app = App::new(sg_root, render_api.clone(), text_shaper, fg_ex.clone());
+
+        Self {
+            bg_runtime,
+            bg_ex,
+
+            fg_runtime,
+            fg_ex,
+            cv_app_is_setup: Arc::new(CondVar::new()),
+            app,
+
+            render_api,
+            method_rep,
+            event_pub
+        }
     }
 
-    let exe_path = std::env::current_exe().unwrap();
-    let basename = exe_path.parent().unwrap();
-    std::env::set_current_dir(basename);
+    /// Restart the app but leave the backends intact.
+    fn setup_app(&self) {
+        info!(target: "main", "Restarting the app");
+        #[cfg(target_os = "android")]
+        {
+            use crate::android::{get_appdata_path, get_external_storage_path};
 
-    info!("Target OS: {}", build_info::TARGET_OS);
-    info!("Target arch: {}", build_info::TARGET_ARCH);
-    let cwd = std::env::current_dir().unwrap();
-    info!("Current dir: {}", cwd.display());
+            info!("App internal data path: {:?}", get_appdata_path());
+            info!("App external storage path: {:?}", get_external_storage_path());
 
-    let ex = Arc::new(smol::Executor::new());
-    let sg_root = SceneNode3::root();
+            //let paths = std::fs::read_dir("/data/data/darkfi.darkfi/").unwrap();
+            //for path in paths {
+            //    debug!("{}", path.unwrap().path().display())
+            //}
+        }
 
-    let async_runtime = app::AsyncRuntime::new(ex.clone());
-    async_runtime.start();
+        info!("Target OS: {}", build_info::TARGET_OS);
+        info!("Target arch: {}", build_info::TARGET_ARCH);
+        let cwd = std::env::current_dir().unwrap();
+        info!("Current dir: {}", cwd.display());
+
+        self.fg_runtime.start_with_count(2);
+
+        /*
+        #[cfg(feature = "enable-netdebug")]
+        {
+            let sg_root2 = sg_root.clone();
+            let ex2 = ex.clone();
+            let zmq_task = ex.spawn(async {
+                let zmq_rpc = ZeroMQAdapter::new(sg_root2, ex2).await;
+                zmq_rpc.run().await;
+            });
+            async_runtime.push_task(zmq_task);
+        }
+        */
 
-    #[cfg(feature = "enable-netdebug")]
-    {
-        let sg_root2 = sg_root.clone();
-        let ex2 = ex.clone();
-        let zmq_task = ex.spawn(async {
-            let zmq_rpc = ZeroMQAdapter::new(sg_root2, ex2).await;
-            zmq_rpc.run().await;
+        let app = self.app.clone();
+        let cv = self.cv_app_is_setup.clone();
+        let app_task = self.fg_ex.spawn(async move {
+            app.setup().await;
+            cv.notify();
         });
-        async_runtime.push_task(zmq_task);
+        self.fg_runtime.push_task(app_task);
     }
 
-    let (method_req, method_rep) = mpsc::channel();
-    // The UI actually needs to be running for this to reply back.
-    // Otherwise calls will just hang.
-    let render_api = gfx::RenderApi::new(method_req);
-    let event_pub = gfx::GraphicsEventPublisher::new();
-
-    let text_shaper = TextShaper::new();
-
-    let cv_gfxwin_started = Arc::new(CondVar::new());
-    let cv_gfxwin_started2 = cv_gfxwin_started.clone();
-    let cv_app_started = Arc::new(CondVar::new());
-    let cv_app_started2 = cv_app_started.clone();
-    let app = app::App::new(sg_root, render_api, event_pub.clone(), text_shaper, ex.clone());
-    let app2 = app.clone();
-    let app_task = ex.spawn(async move {
-        app2.setup().await;
-        // Needed because accessing screen_size() is not allowed until window init
-        cv_gfxwin_started2.wait().await;
-        app2.start().await;
-        cv_app_started2.notify();
-    });
-    async_runtime.push_task(app_task);
+    /// Start the app. Can only happen once the window is ready.
+    pub fn start_app(&self) {
+        let app = self.app.clone();
+        let cv = self.cv_app_is_setup.clone();
+        let event_pub = self.event_pub.clone();
+        let app_task = self.fg_ex.spawn(async move {
+            cv.wait().await;
+            app.start(event_pub).await;
+        });
+        self.fg_runtime.push_task(app_task);
+    }
+
+    /// Put the app to sleep until the next restart.
+    pub fn stop_app(&self) {
+        self.fg_runtime.stop();
+        self.app.stop();
+        self.cv_app_is_setup.reset();
+
+        #[cfg(target_os = "android")]
+        android::clear_state();
+
+        info!(target: "main", "App stopped");
+    }
+}
+
+impl std::fmt::Debug for God {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "God")
+    }
+}
+
+pub static GOD: OnceLock<God> = OnceLock::new();
+
+fn main() {
+    if GOD.get().is_none() {
+        let god = God::new();
+        GOD.set(god).unwrap();
+    }
+
+    // Reuse render_api, event_pub and text_shaper
+    // No need for setup(), just wait for gfx start then call .start()
+    // ZMQ, darkirc stay running
+
+    {
+        let god = GOD.get().unwrap();
+        god.setup_app();
+    }
 
     /*
     // Nice to see which events exist
@@ -205,7 +306,7 @@ fn main() {
     */
 
     //let stage = gfx::Stage::new(method_rep, event_pub);
-    gfx::run_gui(app, async_runtime, method_rep, event_pub, cv_gfxwin_started);
+    gfx::run_gui();
     debug!(target: "main", "Started GFX backend");
 }
 

+ 4 - 4
bin/app/src/text2/editor/android.rs

@@ -24,7 +24,7 @@ use crate::{
     text2::{TextContext, TEXT_CTX},
     AndroidSuggestEvent,
 };
-use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
 
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "text::editor::android", $($arg)*); } }
 macro_rules! w { ($($arg:tt)*) => { warn!(target: "text::editor::android", $($arg)*) } }
@@ -44,7 +44,7 @@ fn byte_to_char16_index(s: &str, byte_idx: usize) -> Option<usize> {
 
 pub struct Editor {
     pub composer_id: usize,
-    pub recvr: Option<async_channel::Receiver<AndroidSuggestEvent>>,
+    pub recvr: async_channel::Receiver<AndroidSuggestEvent>,
     is_init: bool,
     is_setup: bool,
     /// We cannot receive focus until `AndroidSuggestEvent::Init` has finished.
@@ -62,7 +62,7 @@ pub struct Editor {
 }
 
 impl Editor {
-    pub async fn new(
+    pub fn new(
         text: PropertyStr,
         font_size: PropertyFloat32,
         text_color: PropertyColor,
@@ -75,7 +75,7 @@ impl Editor {
 
         Self {
             composer_id,
-            recvr: Some(recvr),
+            recvr,
             is_init: false,
             is_setup: false,
             is_focus_req: AtomicBool::new(false),

+ 4 - 2
bin/app/src/text2/editor/parley.rs

@@ -53,6 +53,8 @@ impl Editor {
 
     pub fn init(&mut self) {}
     pub fn setup(&mut self) {}
+    pub fn focus(&self) {}
+    pub fn unfocus(&self) {}
 
     async fn refresh_layout(&mut self) {
         let font_size = self.font_size.get();
@@ -87,10 +89,10 @@ impl Editor {
         unimplemented!()
     }
 
-    pub fn get_cursor_pos(&self) -> Option<Point> {
+    pub fn get_cursor_pos(&self) -> Point {
         let cursor_rect = self.editor.cursor_geometry(0.).unwrap();
         let cursor_pos = Point::new(cursor_rect.x0 as f32, cursor_rect.y0 as f32);
-        Some(cursor_pos)
+        cursor_pos
     }
 
     pub async fn driver<'a>(

+ 60 - 30
bin/app/src/ui/chatedit.rs

@@ -29,9 +29,10 @@ use std::{
     io::Cursor,
     sync::{
         atomic::{AtomicBool, Ordering},
-        Arc, OnceLock, Weak,
+        Arc, Weak,
     },
     time::Instant,
+    ops::{Deref, DerefMut}
 };
 
 use crate::{
@@ -198,17 +199,28 @@ impl std::fmt::Debug for Editor {
 }
 */
 
-enum ColoringState {
-    Start,
-    IsCommand,
-    Normal,
+struct EditorHandle<'a> {
+    guard: async_lock::MutexGuard<'a, Option<Editor>>,
+}
+
+impl<'a> Deref for EditorHandle<'a> {
+    type Target = Editor;
+
+    fn deref(&self) -> &Self::Target {
+        self.guard.as_ref().unwrap()
+    }
+}
+impl<'a> DerefMut for EditorHandle<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.guard.as_mut().unwrap()
+    }
 }
 
 pub type ChatEditPtr = Arc<ChatEdit>;
 
 pub struct ChatEdit {
     node: SceneNodeWeak,
-    tasks: OnceLock<Vec<smol::Task<()>>>,
+    tasks: SyncMutex<Vec<smol::Task<()>>>,
     render_api: RenderApi,
     text_shaper: TextShaperPtr,
     key_repeat: SyncMutex<PressedKeysSmoothRepeat>,
@@ -269,7 +281,7 @@ pub struct ChatEdit {
     parent_rect: SyncMutex<Option<Rectangle>>,
     is_mouse_hover: AtomicBool,
 
-    editor: AsyncMutex<Editor>,
+    editor: AsyncMutex<Option<Editor>>,
 }
 
 impl ChatEdit {
@@ -333,7 +345,7 @@ impl ChatEdit {
 
         let self_ = Arc::new(Self {
             node,
-            tasks: OnceLock::new(),
+            tasks: SyncMutex::new(vec![]),
             render_api,
             text_shaper: text_shaper.clone(),
             key_repeat: SyncMutex::new(PressedKeysSmoothRepeat::new(400, 50)),
@@ -391,9 +403,7 @@ impl ChatEdit {
             parent_rect: SyncMutex::new(None),
             is_mouse_hover: AtomicBool::new(false),
 
-            editor: AsyncMutex::new(
-                Editor::new(text, font_size, text_color, window_scale, lineheight).await,
-            ),
+            editor: AsyncMutex::new(None),
         });
 
         Pimpl::ChatEdit(self_)
@@ -438,6 +448,11 @@ impl ChatEdit {
         max_scroll
     }
 
+    /// Lazy-initializes the editor and returns a handle to it
+    async fn lock_editor<'a>(&'a self) -> EditorHandle<'a> {
+        EditorHandle { guard: self.editor.lock().await }
+    }
+
     fn regen_cursor_mesh(&self) -> GfxDrawMesh {
         let cursor_width = self.cursor_width.get();
         let cursor_ascent = self.cursor_ascent.get();
@@ -514,7 +529,7 @@ impl ChatEdit {
         let key_str = key.encode_utf8(&mut tmp);
 
         let mut txt_ctx = text2::TEXT_CTX.get().await;
-        let mut editor = self.editor.lock().await;
+        let mut editor = self.lock_editor().await;
         let mut drv = editor.driver(&mut txt_ctx).await.unwrap();
         drv.insert_or_replace_selection(&key_str);
     }
@@ -534,7 +549,7 @@ impl ChatEdit {
         let action_mod = mods.logo;
 
         let mut txt_ctx = text2::TEXT_CTX.get().await;
-        let mut editor = self.editor.lock().await;
+        let mut editor = self.lock_editor().await;
         let mut drv = editor.driver(&mut txt_ctx).await.unwrap();
 
         match key {
@@ -582,7 +597,7 @@ impl ChatEdit {
         t!("handle_key({:?}, {:?}) action_mod={action_mod}", key, mods);
 
         let mut txt_ctx = text2::TEXT_CTX.get().await;
-        let mut editor = self.editor.lock().await;
+        let mut editor = self.lock_editor().await;
         let mut drv = editor.driver(&mut txt_ctx).await.unwrap();
 
         match key {
@@ -687,7 +702,7 @@ impl ChatEdit {
     async fn start_touch_select(&self, touch_pos: Point, atom: &mut PropertyAtomicGuard) {
         t!("start_touch_select({touch_pos:?})");
 
-        let mut editor = self.editor.lock().await;
+        let mut editor = self.lock_editor().await;
         editor.select_word_at_point(touch_pos);
         editor.refresh(atom).await;
 
@@ -722,7 +737,7 @@ impl ChatEdit {
     }
 
     async fn get_select_handles(&self) -> Option<(Point, Point)> {
-        let editor = self.editor.lock().await;
+        let editor = self.lock_editor().await;
         let layout = editor.layout();
 
         let sel = editor.selection();
@@ -816,7 +831,7 @@ impl ChatEdit {
                 let handle_descent = self.handle_descent.get();
                 self.abs_to_local(&mut touch_pos);
 
-                let editor = self.editor.lock().await;
+                let editor = self.lock_editor().await;
                 let sel = editor.selection();
 
                 assert!(*side == -1 || *side == 1);
@@ -897,7 +912,7 @@ impl ChatEdit {
     async fn touch_set_cursor_pos(&self, mut touch_pos: Point, atom: &mut PropertyAtomicGuard) {
         t!("touch_set_cursor_pos({touch_pos:?})");
 
-        let mut editor = self.editor.lock().await;
+        let mut editor = self.lock_editor().await;
         editor.move_to_pos(touch_pos);
         editor.refresh(atom).await;
         drop(editor);
@@ -1040,7 +1055,7 @@ impl ChatEdit {
 
         let mut cursor_instrs = vec![];
 
-        let mut cursor_pos = self.editor.lock().await.get_cursor_pos();
+        let mut cursor_pos = self.lock_editor().await.get_cursor_pos();
         cursor_pos += self.inner_pos();
         cursor_instrs.push(GfxDrawInstruction::Move(cursor_pos));
 
@@ -1074,7 +1089,7 @@ impl ChatEdit {
     async fn regen_txt_mesh(&self) -> Vec<GfxDrawInstruction> {
         let mut instrs = vec![GfxDrawInstruction::Move(self.inner_pos())];
 
-        let editor = self.editor.lock().await;
+        let editor = self.lock_editor().await;
         let layout = editor.layout();
 
         let mut render_instrs = text2::render_layout(layout, &self.render_api);
@@ -1088,7 +1103,7 @@ impl ChatEdit {
 
         let mut instrs = vec![GfxDrawInstruction::Move(self.inner_pos())];
 
-        let editor = self.editor.lock().await;
+        let editor = self.lock_editor().await;
         let layout = editor.layout();
 
         let sel = editor.selection();
@@ -1114,7 +1129,7 @@ impl ChatEdit {
 
         let scroll = self.scroll.get();
 
-        let editor = self.editor.lock().await;
+        let editor = self.lock_editor().await;
         let layout = editor.layout();
 
         let sel = editor.selection();
@@ -1162,7 +1177,7 @@ impl ChatEdit {
         // Use the width to adjust the height calcs
         let rect_w = self.rect.get_width();
         let content_height = {
-            let mut editor = self.editor.lock().await;
+            let mut editor = self.lock_editor().await;
             editor.set_width(rect_w);
             editor.refresh(atom).await;
             editor.height()
@@ -1301,7 +1316,7 @@ impl ChatEdit {
             panic!("self destroyed before insert_text_method_task was stopped!");
         };
 
-        let mut editor = self_.editor.lock().await;
+        let mut editor = self_.lock_editor().await;
         editor.focus();
         true
     }
@@ -1320,7 +1335,7 @@ impl ChatEdit {
             panic!("self destroyed before insert_text_method_task was stopped!");
         };
 
-        let mut editor = self_.editor.lock().await;
+        let mut editor = self_.lock_editor().await;
         editor.unfocus();
         true
     }
@@ -1331,7 +1346,7 @@ impl ChatEdit {
             return
         }
 
-        let mut editor = self.editor.lock().await;
+        let mut editor = self.lock_editor().await;
         match ev {
             AndroidSuggestEvent::Init => {
                 editor.init();
@@ -1372,6 +1387,12 @@ impl UIObject for ChatEdit {
         self.priority.get()
     }
 
+    fn init(&self) {
+        let mut guard = self.editor.lock_blocking();
+        assert!(guard.is_none());
+        *guard = Some(Editor::new(self.text.clone(), self.font_size.clone(), self.text_color.clone(), self.window_scale.clone(), self.lineheight.clone()));
+    }
+
     async fn start(self: Arc<Self>, ex: ExecutorPtr) {
         let me = Arc::downgrade(&self);
 
@@ -1483,7 +1504,7 @@ impl UIObject for ChatEdit {
 
         #[cfg(target_os = "android")]
         {
-            let recvr = self.editor.lock().await.recvr.take().unwrap();
+            let recvr = self.lock_editor().await.recvr.clone();
             let me2 = me.clone();
             let autosuggest_task = ex.spawn(async move {
                 loop {
@@ -1503,7 +1524,16 @@ impl UIObject for ChatEdit {
             tasks.push(autosuggest_task);
         }
 
-        self.tasks.set(tasks);
+        *self.tasks.lock() = tasks;
+    }
+
+    fn stop(&self) {
+        t!("stopping chatedit");
+        self.tasks.lock().clear();
+        *self.parent_rect.lock() = None;
+        self.key_repeat.lock().clear();
+        *self.cursor_mesh.lock() = None;
+        *self.editor.lock_blocking() = None;
     }
 
     async fn draw(
@@ -1627,7 +1657,7 @@ impl UIObject for ChatEdit {
 
         {
             let mut txt_ctx = text2::TEXT_CTX.get().await;
-            let mut editor = self.editor.lock().await;
+            let mut editor = self.lock_editor().await;
             let mut drv = editor.driver(&mut txt_ctx).await.unwrap();
             drv.move_to_point(mouse_pos.x, mouse_pos.y);
         }
@@ -1677,7 +1707,7 @@ impl UIObject for ChatEdit {
 
         let seltext = {
             let mut txt_ctx = text2::TEXT_CTX.get().await;
-            let mut editor = self.editor.lock().await;
+            let mut editor = self.lock_editor().await;
             let mut drv = editor.driver(&mut txt_ctx).await.unwrap();
             drv.extend_selection_to_point(mouse_pos.x, mouse_pos.y);
             editor.selected_text()

+ 4 - 0
bin/app/src/ui/editbox/repeat.rs

@@ -44,6 +44,10 @@ impl PressedKeysSmoothRepeat {
         Self { pressed_keys: HashMap::new(), start_delay, step_time }
     }
 
+    pub fn clear(&mut self) {
+        self.pressed_keys.clear()
+    }
+
     pub fn key_down(&mut self, key: PressedKey, repeat: bool) -> u32 {
         trace!(target: "PressedKeysSmoothRepeat", "key_down({:?}, {})", key, repeat);
 

+ 23 - 6
bin/app/src/ui/layer.rs

@@ -21,7 +21,8 @@ use async_trait::async_trait;
 use atomic_float::AtomicF32;
 use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
 use rand::{rngs::OsRng, Rng};
-use std::sync::{atomic::Ordering, Arc, Mutex as SyncMutex, OnceLock, Weak};
+use parking_lot::Mutex as SyncMutex;
+use std::sync::{atomic::Ordering, Arc, OnceLock, Weak};
 
 use crate::{
     gfx::{GfxDrawCall, GfxDrawInstruction, Point, Rectangle, RenderApi},
@@ -46,7 +47,7 @@ pub type LayerPtr = Arc<Layer>;
 pub struct Layer {
     node: SceneNodeWeak,
     render_api: RenderApi,
-    tasks: OnceLock<Vec<smol::Task<()>>>,
+    tasks: SyncMutex<Vec<smol::Task<()>>>,
     dc_key: u64,
 
     is_visible: PropertyBool,
@@ -72,7 +73,7 @@ impl Layer {
         let self_ = Arc::new(Self {
             node,
             render_api,
-            tasks: OnceLock::new(),
+            tasks: SyncMutex::new(vec![]),
             dc_key: OsRng.gen(),
 
             is_visible,
@@ -96,7 +97,7 @@ impl Layer {
         let trace_id = rand::random();
         let timest = unixtime();
         t!("Layer::redraw({:?}) [trace_id={trace_id}]", self.node.upgrade().unwrap());
-        let Some(parent_rect) = self.parent_rect.lock().unwrap().clone() else { return };
+        let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
 
         let Some(draw_update) = self.get_draw_calls(parent_rect, trace_id, atom).await else {
             error!(target: "ui::layer", "Layer failed to draw [trace_id={trace_id}]");
@@ -155,6 +156,13 @@ impl UIObject for Layer {
         self.priority.get()
     }
 
+    fn init(&self) {
+        for child in self.get_children() {
+            let obj = get_ui_object3(&child);
+            obj.init();
+        }
+    }
+
     async fn start(self: Arc<Self>, ex: ExecutorPtr) {
         let me = Arc::downgrade(&self);
 
@@ -163,7 +171,7 @@ impl UIObject for Layer {
         on_modify.when_change(self.rect.prop(), Self::redraw);
         on_modify.when_change(self.z_index.prop(), Self::redraw);
 
-        self.tasks.set(on_modify.tasks);
+        *self.tasks.lock() = on_modify.tasks;
 
         for child in self.get_children() {
             let obj = get_ui_object_ptr(&child);
@@ -171,6 +179,15 @@ impl UIObject for Layer {
         }
     }
 
+    fn stop(&self) {
+        self.tasks.lock().clear();
+        *self.parent_rect.lock() = None;
+        for child in self.get_children() {
+            let obj = get_ui_object3(&child);
+            obj.stop();
+        }
+    }
+
     async fn draw(
         &self,
         parent_rect: Rectangle,
@@ -178,7 +195,7 @@ impl UIObject for Layer {
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
         t!("Layer::draw({:?}) [trace_id={trace_id}]", self.node.upgrade().unwrap());
-        *self.parent_rect.lock().unwrap() = Some(parent_rect);
+        *self.parent_rect.lock() = Some(parent_rect);
 
         /*
         if !parent_rect.dim().contains(&offset_rect) {

+ 5 - 0
bin/app/src/ui/mod.rs

@@ -69,8 +69,13 @@ macro_rules! t { ($($arg:tt)*) => { trace!(target: "scene::on_modify", $($arg)*)
 pub trait UIObject: Sync {
     fn priority(&self) -> u32;
 
+    fn init(&self) {}
+
     async fn start(self: Arc<Self>, ex: ExecutorPtr) {}
 
+    /// Clear all buffers and caches
+    fn stop(&self) {}
+
     async fn draw(
         &self,
         parent_rect: Rectangle,

+ 12 - 6
bin/app/src/ui/text.rs

@@ -18,7 +18,8 @@
 
 use async_trait::async_trait;
 use rand::{rngs::OsRng, Rng};
-use std::sync::{Arc, Mutex as SyncMutex, OnceLock, Weak};
+use parking_lot::Mutex as SyncMutex;
+use std::sync::{Arc, Weak};
 
 use crate::{
     gfx::{
@@ -54,7 +55,7 @@ pub struct Text {
     node: SceneNodeWeak,
     render_api: RenderApi,
     text_shaper: TextShaperPtr,
-    tasks: OnceLock<Vec<smol::Task<()>>>,
+    tasks: SyncMutex<Vec<smol::Task<()>>>,
 
     dc_key: u64,
 
@@ -100,7 +101,7 @@ impl Text {
             node,
             render_api,
             text_shaper,
-            tasks: OnceLock::new(),
+            tasks: SyncMutex::new(vec![]),
             dc_key: OsRng.gen(),
 
             rect,
@@ -144,7 +145,7 @@ impl Text {
         let trace_id = rand::random();
         let timest = unixtime();
         t!("Text::redraw({:?}) [trace_id={trace_id}]", self.node.upgrade().unwrap());
-        let Some(parent_rect) = self.parent_rect.lock().unwrap().clone() else { return };
+        let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
 
         let Some(draw_update) = self.get_draw_calls(parent_rect, trace_id).await else {
             error!(target: "ui::text", "Text failed to draw [trace_id={trace_id}]");
@@ -188,7 +189,12 @@ impl UIObject for Text {
         on_modify.when_change(self.text_color.prop(), Self::redraw);
         on_modify.when_change(self.debug.prop(), Self::redraw);
 
-        self.tasks.set(on_modify.tasks);
+        *self.tasks.lock() = on_modify.tasks;
+    }
+
+    fn stop(&self) {
+        self.tasks.lock().clear();
+        *self.parent_rect.lock() = None;
     }
 
     async fn draw(
@@ -198,7 +204,7 @@ impl UIObject for Text {
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
         t!("Text::draw({:?}) [trace_id={trace_id}]", self.node.upgrade().unwrap());
-        *self.parent_rect.lock().unwrap() = Some(parent_rect);
+        *self.parent_rect.lock() = Some(parent_rect);
         self.get_draw_calls(parent_rect, trace_id).await
     }
 }

+ 12 - 6
bin/app/src/ui/vector_art/mod.rs

@@ -18,7 +18,8 @@
 
 use async_trait::async_trait;
 use rand::{rngs::OsRng, Rng};
-use std::sync::{Arc, Mutex as SyncMutex, OnceLock, Weak};
+use parking_lot::Mutex as SyncMutex;
+use std::sync::{Arc, OnceLock, Weak};
 
 use crate::{
     error::{Error, Result},
@@ -49,7 +50,7 @@ pub type VectorArtPtr = Arc<VectorArt>;
 pub struct VectorArt {
     node: SceneNodeWeak,
     render_api: RenderApi,
-    tasks: OnceLock<Vec<smol::Task<()>>>,
+    tasks: SyncMutex<Vec<smol::Task<()>>>,
 
     shape: VectorShape,
     dc_key: u64,
@@ -83,7 +84,7 @@ impl VectorArt {
         let self_ = Arc::new(Self {
             node,
             render_api,
-            tasks: OnceLock::new(),
+            tasks: SyncMutex::new(vec![]),
 
             shape,
             dc_key: OsRng.gen(),
@@ -107,7 +108,7 @@ impl VectorArt {
         let trace_id = rand::random();
         let timest = unixtime();
         trace!(target: "ui::vector_art", "VectorArt::redraw({}) [trace_id={trace_id}]", self.node_path());
-        let Some(parent_rect) = self.parent_rect.lock().unwrap().clone() else { return };
+        let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
 
         let Some(draw_update) = self.get_draw_calls(parent_rect, trace_id).await else {
             error!(target: "ui::vector_art", "Mesh failed to draw [trace_id={trace_id}]");
@@ -168,7 +169,12 @@ impl UIObject for VectorArt {
         on_modify.when_change(self.rect.prop(), Self::redraw);
         on_modify.when_change(self.z_index.prop(), Self::redraw);
 
-        self.tasks.set(on_modify.tasks);
+        *self.tasks.lock() = on_modify.tasks;
+    }
+
+    fn stop(&self) {
+        self.tasks.lock().clear();
+        *self.parent_rect.lock() = None;
     }
 
     async fn draw(
@@ -178,7 +184,7 @@ impl UIObject for VectorArt {
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
         t!("VectorArt::draw({}) [trace_id={trace_id}]", self.node_path());
-        *self.parent_rect.lock().unwrap() = Some(parent_rect);
+        *self.parent_rect.lock() = Some(parent_rect);
         self.get_draw_calls(parent_rect, trace_id).await
     }
 }

+ 22 - 7
bin/app/src/ui/win.rs

@@ -17,7 +17,8 @@
  */
 
 use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
-use std::sync::{Arc, OnceLock, Weak};
+use parking_lot::Mutex as SyncMutex;
+use std::sync::{Arc, Weak};
 
 use crate::{
     gfx::{
@@ -46,7 +47,7 @@ pub type WindowPtr = Arc<Window>;
 pub struct Window {
     node: SceneNodeWeak,
 
-    tasks: OnceLock<Vec<smol::Task<()>>>,
+    tasks: SyncMutex<Vec<smol::Task<()>>>,
     screen_size: PropertyDimension,
     scale: PropertyFloat32,
     render_api: RenderApi,
@@ -62,19 +63,25 @@ impl Window {
 
         let node_ref = &node.upgrade().unwrap();
         let screen_size = PropertyDimension::wrap(node_ref, Role::Internal, "screen_size").unwrap();
-        let scale_ = PropertyFloat32::wrap(
+        let scale = PropertyFloat32::wrap(
             &setting_root.clone().lookup_node("/scale").unwrap(),
             Role::Internal,
             "value",
             0,
-        );
-        let scale = scale_.unwrap();
+        ).unwrap();
 
-        let self_ = Arc::new(Self { node, tasks: OnceLock::new(), screen_size, scale, render_api });
+        let self_ = Arc::new(Self { node, tasks: SyncMutex::new(vec![]), screen_size, scale, render_api });
 
         Pimpl::Window(self_)
     }
 
+    pub fn init(&self) {
+        for child in self.get_children() {
+            let obj = get_ui_object3(&child);
+            obj.init();
+        }
+    }
+
     pub async fn start(self: Arc<Self>, event_pub: GraphicsEventPublisherPtr, ex: ExecutorPtr) {
         let me = Arc::downgrade(&self);
 
@@ -162,7 +169,7 @@ impl Window {
             touch_task,
         ];
         tasks.append(&mut on_modify.tasks);
-        self.tasks.set(tasks);
+        *self.tasks.lock() = tasks;
 
         for child in self.get_children() {
             let obj = get_ui_object_ptr(&child);
@@ -170,6 +177,14 @@ impl Window {
         }
     }
 
+    pub fn stop(&self) {
+        self.tasks.lock().clear();
+        for child in self.get_children() {
+            let obj = get_ui_object3(&child);
+            obj.stop();
+        }
+    }
+
     async fn process_char(me: &Weak<Self>, ev_sub: &Subscription<(char, KeyMods, bool)>) -> bool {
         let Ok((key, mods, repeat)) = ev_sub.receive().await else {
             t!("Event relayer closed");

+ 3 - 0
bin/app/src/util/mod.rs

@@ -19,6 +19,9 @@
 use colored::Colorize;
 use std::time::{SystemTime, UNIX_EPOCH};
 
+mod rt;
+pub use rt::{AsyncRuntime, ExecutorPtr};
+
 pub fn is_whitespace(s: &str) -> bool {
     s.chars().all(char::is_whitespace)
 }