diff --git a/main/src/infrastructure/mod.rs b/main/src/infrastructure/mod.rs index ae6e7be35..72a5b9463 100644 --- a/main/src/infrastructure/mod.rs +++ b/main/src/infrastructure/mod.rs @@ -4,3 +4,4 @@ mod plugin; mod server; mod test; mod ui; +mod worker; diff --git a/main/src/infrastructure/plugin/app.rs b/main/src/infrastructure/plugin/app.rs index 0508839c3..0ee9bb786 100644 --- a/main/src/infrastructure/plugin/app.rs +++ b/main/src/infrastructure/plugin/app.rs @@ -363,6 +363,7 @@ impl App { #[cfg(feature = "playtime")] fn init_clip_engine() { + use playtime_clip_engine::ClipEngine; let license_manager = crate::infrastructure::data::LicenseManager::new( App::helgoboss_resource_dir_path().join("licensing.json"), ); @@ -384,7 +385,7 @@ impl App { tap_sound_file: Self::realearn_high_click_sound_path(), metrics_recorder, }; - playtime_clip_engine::ClipEngine::get().init(args); + ClipEngine::make_available_globally(ClipEngine::new(args)); } fn reconnect_osc_devices(&self) { diff --git a/main/src/infrastructure/ui/app/app_instance.rs b/main/src/infrastructure/ui/app/app_instance.rs index af377dc7f..56d00957c 100644 --- a/main/src/infrastructure/ui/app/app_instance.rs +++ b/main/src/infrastructure/ui/app/app_instance.rs @@ -2,6 +2,7 @@ use crate::application::WeakSession; use crate::infrastructure::plugin::App; use crate::infrastructure::ui::bindings::root; use crate::infrastructure::ui::AppHandle; +use crate::infrastructure::worker::spawn_in_main_worker; use anyhow::{anyhow, Context, Result}; use playtime_clip_engine::proto::{ event_reply, reply, ClipEngineReceivers, Empty, EventReply, Reply, @@ -13,6 +14,7 @@ use std::fmt::Debug; use std::rc::Rc; use std::time::Duration; use swell_ui::{SharedView, View, ViewContext, Window}; +use tokio::task::JoinHandle; use validator::HasLen; pub type SharedAppInstance = Rc>; @@ -22,7 +24,7 @@ pub trait AppInstance: Debug { fn start_or_show(&mut self, owning_window: Window) -> Result<()>; - fn stop(&mut self); + fn stop(&mut self) -> Result<()>; fn send(&self, reply: &Reply) -> Result<()>; @@ -82,8 +84,9 @@ impl AppInstance for ParentedAppInstance { Ok(()) } - fn stop(&mut self) { - self.panel.close() + fn stop(&mut self) -> Result<()> { + self.panel.close(); + Ok(()) } fn send(&self, reply: &Reply) -> Result<()> { @@ -101,7 +104,21 @@ impl AppInstance for ParentedAppInstance { #[derive(Debug)] struct StandaloneAppInstance { session: WeakSession, - running_state: Option, + running_state: Option, +} + +#[derive(Debug)] +struct StandaloneAppRunningState { + common_state: CommonAppRunningState, + event_subscription_join_handle: Option>, +} + +impl Drop for StandaloneAppRunningState { + fn drop(&mut self) { + if let Some(join_handle) = self.event_subscription_join_handle.take() { + join_handle.abort(); + } + } } impl AppInstance for StandaloneAppInstance { @@ -111,31 +128,37 @@ impl AppInstance for StandaloneAppInstance { fn start_or_show(&mut self, _owning_window: Window) -> Result<()> { let app_library = App::get_or_load_app_library()?; - let session_id = self - .session - .upgrade() - .ok_or_else(|| anyhow!("session gone"))? - .borrow() - .id() - .to_string(); - let app_handle = app_library.run_in_parent(None, session_id)?; - let running_state = RunningAppState { - app_handle, - app_callback: None, - event_receivers: Some(subscribe_to_events()), + if let Some(running_state) = &self.running_state { + app_library.show_app_instance(None, running_state.common_state.app_handle)?; + return Ok(()); + } + let session_id = extract_session_id(&self.session)?; + let app_handle = app_library.start_app_instance(None, session_id)?; + app_library.show_app_instance(None, app_handle)?; + let running_state = StandaloneAppRunningState { + common_state: CommonAppRunningState { + app_handle, + app_callback: None, + }, + event_subscription_join_handle: None, }; self.running_state = Some(running_state); Ok(()) } - fn stop(&mut self) { - todo!() + fn stop(&mut self) -> Result<()> { + self.running_state + .take() + .ok_or(anyhow!("app was already stopped"))? + .common_state + .stop(None) } fn send(&self, reply: &Reply) -> Result<()> { self.running_state .as_ref() .context("app not open")? + .common_state .send(reply) } @@ -143,10 +166,24 @@ impl AppInstance for StandaloneAppInstance { let Some(running_state) = &mut self.running_state else { return; }; + let Ok(session_id) = extract_session_id(&self.session) else { + return; + }; // Handshake finished! The app has the host callback and we have the app callback. - running_state.app_callback = Some(callback); + running_state.common_state.app_callback = Some(callback); // Now we can start passing events to the app callback - // self.start_timer(); + let mut receivers = subscribe_to_events(); + let join_handle = spawn_in_main_worker(async move { + receivers + .keep_processing_updates(&session_id, &|event_reply| { + let reply = Reply { + value: Some(reply::Value::EventReply(event_reply)), + }; + send_to_app(callback, &reply); + }) + .await; + }); + running_state.event_subscription_join_handle = Some(join_handle); } } @@ -154,15 +191,35 @@ impl AppInstance for StandaloneAppInstance { pub struct AppPanel { view: ViewContext, session: WeakSession, - open_state: RefCell>, + running_state: RefCell>, +} + +#[derive(Debug)] +struct ParentedAppRunningState { + common_state: CommonAppRunningState, + event_receivers: Option, +} + +impl ParentedAppRunningState { + pub fn send_pending_events(&mut self, session_id: &str) { + let (Some(app_callback), Some(event_receivers)) = + (self.common_state.app_callback, &mut self.event_receivers) + else { + return; + }; + event_receivers.process_pending_updates(session_id, &|event_reply| { + let reply = Reply { + value: Some(reply::Value::EventReply(event_reply)), + }; + send_to_app(app_callback, &reply); + }); + } } #[derive(Debug)] -struct RunningAppState { +struct CommonAppRunningState { app_handle: AppHandle, app_callback: Option, - // TODO-medium This is too specific. - event_receivers: Option, } impl AppPanel { @@ -170,34 +227,26 @@ impl AppPanel { Self { view: Default::default(), session, - open_state: RefCell::new(None), + running_state: RefCell::new(None), } } pub fn send_to_app(&self, reply: &Reply) -> Result<()> { - self.open_state + self.running_state .borrow() .as_ref() .context("app not open")? + .common_state .send(reply) } - pub fn toggle_full_screen(&self) -> Result<()> { - // Because the full-screen windowing code is a mess and highly platform-specific, it's best - // to use a platform-specific language to do the job. In case of macOS, Swift is the best - // choice. The app itself has easy access to Swift, so let's just call into the app library - // so it takes care of handling its host window. - // TODO-low It's a bit weird to ask the app (a guest) to deal with a host window. Improve. - App::get_or_load_app_library()?.toggle_full_screen(self.view.require_window()) - } - pub fn notify_app_is_ready(&self, callback: AppCallback) { - let mut open_state = self.open_state.borrow_mut(); + let mut open_state = self.running_state.borrow_mut(); let Some(open_state) = open_state.as_mut() else { return; }; // Handshake finished! The app has the host callback and we have the app callback. - open_state.app_callback = Some(callback); + open_state.common_state.app_callback = Some(callback); // Now we can start passing events to the app callback self.start_timer(); } @@ -215,56 +264,38 @@ impl AppPanel { fn open_internal(&self, window: Window) -> Result<()> { window.set_text("Playtime"); let app_library = App::get_or_load_app_library()?; - let session_id = self - .session - .upgrade() - .ok_or_else(|| anyhow!("session gone"))? - .borrow() - .id() - .to_string(); - let app_handle = app_library.run_in_parent(Some(window), session_id)?; - let open_state = RunningAppState { - app_handle, - app_callback: None, + let session_id = extract_session_id(&self.session)?; + let app_handle = app_library.start_app_instance(Some(window), session_id)?; + let running_state = ParentedAppRunningState { + common_state: CommonAppRunningState { + app_handle, + app_callback: None, + }, event_receivers: Some(subscribe_to_events()), }; - *self.open_state.borrow_mut() = Some(open_state); + *self.running_state.borrow_mut() = Some(running_state); Ok(()) } - fn close_internal(&self, window: Window) -> Result<()> { - let open_state = self - .open_state + fn stop(&self, window: Window) -> Result<()> { + self.running_state .borrow_mut() .take() - .ok_or(anyhow!("app was already closed"))?; - open_state.close_app(window) + .ok_or(anyhow!("app was already stopped"))? + .common_state + .stop(Some(window)) } } -impl RunningAppState { +impl CommonAppRunningState { pub fn send(&self, reply: &Reply) -> Result<()> { let app_callback = self.app_callback.context("app callback not known yet")?; send_to_app(app_callback, reply); Ok(()) } - pub fn send_pending_events(&mut self, session_id: &str) { - let (Some(app_callback), Some(event_receivers)) = - (self.app_callback, &mut self.event_receivers) - else { - return; - }; - event_receivers.process_pending_updates(session_id, &|event_reply| { - let reply = Reply { - value: Some(reply::Value::EventReply(event_reply)), - }; - send_to_app(app_callback, &reply); - }); - } - - pub fn close_app(&self, window: Window) -> Result<()> { - App::get_or_load_app_library()?.close(window, self.app_handle) + pub fn stop(&self, window: Option) -> Result<()> { + App::get_or_load_app_library()?.stop_app_instance(window, self.app_handle) } } @@ -289,13 +320,13 @@ impl View for AppPanel { } fn closed(self: SharedView, window: Window) { - self.close_internal(window).unwrap(); + self.stop(window).unwrap(); } fn shown_or_hidden(self: SharedView, shown: bool) -> bool { if shown { // Send events to app again. - if let Some(open_state) = self.open_state.borrow_mut().as_mut() { + if let Some(open_state) = self.running_state.borrow_mut().as_mut() { open_state.event_receivers = Some(subscribe_to_events()); } else { // We also get called when the window is first opened, *before* `opened` is called! @@ -316,7 +347,7 @@ impl View for AppPanel { }); } else { // Don't process events while hidden - if let Some(open_state) = self.open_state.borrow_mut().as_mut() { + if let Some(open_state) = self.running_state.borrow_mut().as_mut() { open_state.event_receivers = None; } self.stop_timer(); @@ -346,7 +377,7 @@ impl View for AppPanel { if id != TIMER_ID { return false; } - let mut open_state = self.open_state.borrow_mut(); + let mut open_state = self.running_state.borrow_mut(); let Some(open_state) = open_state.as_mut() else { return false; }; @@ -398,3 +429,19 @@ pub type AppCallback = unsafe extern "C" fn(data: *const u8, length: i32); fn subscribe_to_events() -> ClipEngineReceivers { App::get().clip_engine_hub().senders().subscribe_to_all() } + +// TODO-high-ms4 We extract the session ID manually whenever we start the app instead of assigning +// it to the AppInstance right at the start. Reason: The session ID can be changed by the user. +// This is not ideal. It won't event prevent that the user changes the session ID during app +// lifetime ... it just won't work anymore if that happens. I think we need to use the InstanceId +// and hold a global mapping from session ID to instance ID in the app. Or maybe better: We use +// the instance ID whenever we are embedded, not the session ID! Then the "matrix ID" refers +// to the instance ID when embedded and to the session ID when remote. +fn extract_session_id(session: &WeakSession) -> Result { + Ok(session + .upgrade() + .ok_or_else(|| anyhow!("session gone"))? + .borrow() + .id() + .to_string()) +} diff --git a/main/src/infrastructure/ui/app/app_library.rs b/main/src/infrastructure/ui/app/app_library.rs index cb21e0c96..38f15967b 100644 --- a/main/src/infrastructure/ui/app/app_library.rs +++ b/main/src/infrastructure/ui/app/app_library.rs @@ -95,7 +95,7 @@ impl AppLibrary { Ok(library) } - pub fn run_in_parent( + pub fn start_app_instance( &self, parent_window: Option, session_id: String, @@ -109,13 +109,13 @@ impl AppLibrary { let session_id_c_string = CString::new(session_id).map_err(|_| anyhow!("session ID contains a nul byte"))?; with_temporarily_changed_working_directory(&self.app_base_dir, || { - prepare_app_launch(); + prepare_app_start(); let app_handle = unsafe { - let symbol: Symbol = self + let start_app_instance: Symbol = self .main_library - .get(b"run_app_in_parent\0") - .map_err(|_| anyhow!("failed to load run_app_in_parent function"))?; - symbol( + .get(b"start_app_instance\0") + .map_err(|_| anyhow!("failed to load start_app_instance function"))?; + start_app_instance( parent_window.map(|w| w.raw()).unwrap_or(null_mut()), app_base_dir_c_string.as_ptr(), invoke_host, @@ -123,30 +123,44 @@ impl AppLibrary { ) }; let Some(app_handle) = app_handle else { - bail!("couldn't launch app"); + bail!("couldn't start app"); }; Ok(app_handle) }) } - pub fn toggle_full_screen(&self, parent_window: Window) -> Result<()> { + pub fn show_app_instance( + &self, + parent_window: Option, + app_handle: AppHandle, + ) -> Result<()> { unsafe { - let symbol: Symbol = - self.main_library - .get(b"toggle_full_screen\0") - .map_err(|_| anyhow!("failed to load toggle_full_screen function"))?; - symbol(parent_window.raw()); + let show_app_instance: Symbol = self + .main_library + .get(b"show_app_instance\0") + .map_err(|_| anyhow!("failed to load show_app_instance function"))?; + show_app_instance( + parent_window.map(|w| w.raw()).unwrap_or(null_mut()), + app_handle, + ); }; Ok(()) } - pub fn close(&self, parent_window: Window, app_handle: AppHandle) -> Result<()> { + pub fn stop_app_instance( + &self, + parent_window: Option, + app_handle: AppHandle, + ) -> Result<()> { unsafe { - let symbol: Symbol = self + let stop_app_instance: Symbol = self .main_library - .get(b"close_app\0") - .map_err(|_| anyhow!("failed to load close_app function"))?; - symbol(parent_window.raw(), app_handle); + .get(b"stop_app_instance\0") + .map_err(|_| anyhow!("failed to load stop_app_instance function"))?; + stop_app_instance( + parent_window.map(|w| w.raw()).unwrap_or(null_mut()), + app_handle, + ); }; Ok(()) } @@ -207,19 +221,19 @@ fn process_request(matrix_id: String, request_value: request::Value) -> Result<( pub type AppHandle = NonNull; -/// Signature of the function that we use to open a new App window. -type RunAppInParent = unsafe extern "C" fn( +/// Signature of the function that we use to start an app instance. +type StartAppInstance = unsafe extern "C" fn( parent_window: HWND, app_base_dir_utf8_c_str: *const c_char, host_callback: HostCallback, session_id: *const c_char, ) -> Option; -/// Signature of the function that we use to toggle full-screen. -type ToggleFullScreen = unsafe extern "C" fn(parent_window: HWND); +/// Signature of the function that we use to show an app instance. +type ShowAppInstance = unsafe extern "C" fn(parent_window: HWND, app_handle: AppHandle); -/// Signature of the function that we use to close the App. -type CloseApp = unsafe extern "C" fn(parent_window: HWND, app_handle: AppHandle); +/// Signature of the function that we use to stop an app instance. +type StopAppInstance = unsafe extern "C" fn(parent_window: HWND, app_handle: AppHandle); /// Signature of the function that's used from the app in order to call the host. type HostCallback = extern "C" fn(data: *const u8, length: i32); @@ -249,7 +263,7 @@ fn with_temporarily_changed_working_directory( r } -fn prepare_app_launch() { +fn prepare_app_start() { #[cfg(target_os = "macos")] { // This is only necessary and only considered by Flutter Engine when Flutter is compiled in diff --git a/main/src/infrastructure/ui/independent_panel_manager.rs b/main/src/infrastructure/ui/independent_panel_manager.rs index 2d4c619f9..cd2bd9b46 100644 --- a/main/src/infrastructure/ui/independent_panel_manager.rs +++ b/main/src/infrastructure/ui/independent_panel_manager.rs @@ -113,7 +113,7 @@ impl IndependentPanelManager { #[cfg(feature = "playtime")] pub fn stop_app_instance(&self) { - self.app_instance.borrow_mut().stop(); + let _ = self.app_instance.borrow_mut().stop(); } #[cfg(feature = "playtime")] @@ -161,7 +161,7 @@ impl IndependentPanelManager { } self.mapping_panels.clear(); #[cfg(feature = "playtime")] - self.app_instance.borrow_mut().stop(); + let _ = self.app_instance.borrow_mut().stop(); } fn request_panel(&mut self) -> SharedView { diff --git a/main/src/infrastructure/worker/mod.rs b/main/src/infrastructure/worker/mod.rs new file mode 100644 index 000000000..d88a27a12 --- /dev/null +++ b/main/src/infrastructure/worker/mod.rs @@ -0,0 +1,20 @@ +use once_cell::sync::Lazy; +use std::future::Future; +use tokio::runtime::Runtime; +use tokio::task::JoinHandle; + +// TODO-high-ms2 Shutdown correctly +static MAIN_WORKER_RUNTIME: Lazy> = Lazy::new(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_time() + .thread_name("ReaLearn Main Worker") + .worker_threads(1) + .build() +}); + +pub fn spawn_in_main_worker(f: impl Future + Send + 'static) -> JoinHandle +where + R: Send + 'static, +{ + MAIN_WORKER_RUNTIME.as_ref().unwrap().spawn(f) +} diff --git a/playtime-clip-engine b/playtime-clip-engine index ff222f61b..496defc22 160000 --- a/playtime-clip-engine +++ b/playtime-clip-engine @@ -1 +1 @@ -Subproject commit ff222f61b2dbe11f2889fbdd3eb1b66b08fef6bb +Subproject commit 496defc22053012c3c55c6616b25df096367df66 diff --git a/swell-ui/src/view.rs b/swell-ui/src/view.rs index 700124bbd..bab3288b9 100644 --- a/swell-ui/src/view.rs +++ b/swell-ui/src/view.rs @@ -76,7 +76,7 @@ pub trait View: Debug { false } - /// WM_COSE. + /// WM_CLOSE. /// /// Should return `true` if the window must not be destroyed. fn close_requested(self: SharedView) -> bool {