From 6a69e7faef8cb942d576e455de224dd545974596 Mon Sep 17 00:00:00 2001 From: Benjamin Klum Date: Sun, 12 Nov 2023 23:31:52 +0100 Subject: [PATCH] Playtime: Support standalone window mode on Windows (not enabled though) --- .../src/infrastructure/ui/app/app_instance.rs | 39 +++++++++++++------ main/src/infrastructure/ui/app/app_library.rs | 12 ++++++ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/main/src/infrastructure/ui/app/app_instance.rs b/main/src/infrastructure/ui/app/app_instance.rs index 56d00957c..229533d56 100644 --- a/main/src/infrastructure/ui/app/app_instance.rs +++ b/main/src/infrastructure/ui/app/app_instance.rs @@ -32,8 +32,24 @@ pub trait AppInstance: Debug { } pub fn create_app_instance(session: WeakSession) -> impl AppInstance { + // I was experimenting with 2 different ways of embedding the app GUI into REAPER: + // + // - Parented mode: We create a new SWELL window on ReaLearn side (HWND on Windows, NSView on + // macOS) and the app renders its GUI *within* it. + // - Standalone mode: The app fires up its own window. + // + // Embedding the app in "parented" mode is in theory preferable because: + // + // 1. Only "parented" mode makes it possible to dock the app GUI (in case we want to do that + // one day). + // 2. ReaLearn has full control over the window and can listen to its events. + // 3. We stop sending events when the app window is hidden, not wasting resources when the + // app is not shown anyway. (However, with just a bit more effort, we could implement this + // for standalone mode as well.) #[cfg(target_os = "windows")] { + // On Windows, parented mode works wonderfully. It needs some tricks (see View + // implementation) but it's worth it. That's why we use parented mode on Windows. let app_panel = AppPanel::new(session); ParentedAppInstance { panel: SharedView::new(app_panel), @@ -41,6 +57,15 @@ pub fn create_app_instance(session: WeakSession) -> impl AppInstance { } #[cfg(target_os = "macos")] { + // On macOS, parented mode is possible only by using Cocoa child windows (see app side + // embedding docs). This means that the app doesn't really render itself in the NSView + // provided by ReaLearn but places an NSWindow on top of the NSWindow provided by ReaLearn. + // It works but it needs a few keyboard tricks and - most importantly - it doesn't work + // well if the app itself wants to control its window (e.g. going full screen or changing + // the window opacity). It will try to control the child window, not the outer window. + // This could be solved on app side by navigating up the child/parent window chain, but + // it's not something I want to do now as long as we don't support docking anyway. + // Therefore: Standalone mode on macOS! StandaloneAppInstance { session, running_state: None, @@ -355,19 +380,11 @@ impl View for AppPanel { true } - fn button_clicked(self: SharedView, resource_id: u32) { - match resource_id { - // Escape key - raw::IDCANCEL => self.close(), - _ => {} - } - } - /// On Windows, this is necessary to resize contained app. /// - /// On macOS, this has no effect because the app window is not a child view (NSView) but a - /// child window (NSWindow). Resizing according to the parent window (the SWELL window) is done - /// on app side. + /// On macOS, this would have no effect because the app window is not a child view (NSView) but + /// a child window (NSWindow). Resizing according to the parent window (the SWELL window) is + /// done on app side. #[cfg(target_os = "windows")] fn resized(self: SharedView) -> bool { crate::infrastructure::ui::egui_views::on_parent_window_resize(self.view.require_window()) diff --git a/main/src/infrastructure/ui/app/app_library.rs b/main/src/infrastructure/ui/app/app_library.rs index 38f15967b..1a44dbf02 100644 --- a/main/src/infrastructure/ui/app/app_library.rs +++ b/main/src/infrastructure/ui/app/app_library.rs @@ -12,6 +12,7 @@ use playtime_clip_engine::proto::{ MatrixProvider, Notification, NotificationKind, QueryReply, QueryResult, Reply, Request, }; use prost::Message; +use reaper_high::Reaper; use reaper_low::raw::HWND; use std::env; use std::ffi::{c_char, c_void, CString}; @@ -120,6 +121,7 @@ impl AppLibrary { app_base_dir_c_string.as_ptr(), invoke_host, session_id_c_string.as_ptr(), + Reaper::get().main_window().as_ptr(), ) }; let Some(app_handle) = app_handle else { @@ -222,11 +224,21 @@ fn process_request(matrix_id: String, request_value: request::Value) -> Result<( pub type AppHandle = NonNull; /// Signature of the function that we use to start an app instance. +/// +/// # Arguments +/// +/// * `parent_window` - Optional parent window handle. If you pass this, the app (if supported for +/// the OS) will render itself *within* that parent window. On macOS, this is should be an NSView. +/// * `app_base_dir_utf8_c_str`- Directory where the app is located +/// * `host_callback` - Pointer to host callback function +/// * `session_id` - Session ID of the ReaLearn instance associated with this new app instance. +/// * `main_window` - Handle to REAPER's main window 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, + main_window: HWND, ) -> Option; /// Signature of the function that we use to show an app instance.