From e59dfef7e4235392c9507ae167934bbeea0685d1 Mon Sep 17 00:00:00 2001 From: Artem Egorkine Date: Sun, 1 Dec 2024 15:51:38 +0200 Subject: [PATCH] macOS: reimplement g_application_run in rust to fix UI getting stuck * macOS: reimplement g_application_run() in rust attempting to fix #62, looks like the UI now works, even though the main loop code is exactly the same as the original one; * Platform hacks were turned into a set of flags that can be turned on and off so that it is easier to test their effects and possible enable them from commnd line later; --- gui/src/main.rs | 13 +++- gui/src/opts.rs | 11 +++ gui/src/platform.rs | 177 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 175 insertions(+), 26 deletions(-) diff --git a/gui/src/main.rs b/gui/src/main.rs index 6c8d4e4..e33b2f0 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -609,6 +609,15 @@ async fn main() -> Result<()> { let opts: Opts = Opts::from_arg_matches(&cli.get_matches())?; drop(help_text); + if let Some(platform) = &opts.platform { + set_platform_hack_flags(&platform)?; + } + let platform_hack_flags = get_platform_hack_flags(); + sentry::configure_scope(|scope| { + scope.set_tag("platform", &platform_hack_flags); + }); + info!("Platform hacks: {}", &platform_hack_flags); + // glib::set_program_name needs to come before gtk::init! glib::set_program_name(Some(&title)); @@ -639,7 +648,7 @@ async fn main() -> Result<()> { } }); - let exit_code = app.run_with_args::(&[]); + let exit_code = app_run(app); // HACK: instead of dealing with USB thread clean-up (and in case there are any other // threads still running), we just call `process::exit` and let libraries clean up // after themselves @@ -1233,7 +1242,7 @@ fn activate(app: >k::Application, title: &String, opts: Opts, sentry_enabled: r.emit_by_name::<()>("group-changed", &[]); // quit - app.quit(); + app_quit(&app); } } diff --git a/gui/src/opts.rs b/gui/src/opts.rs index 5f2a320..6f033f8 100644 --- a/gui/src/opts.rs +++ b/gui/src/opts.rs @@ -3,6 +3,7 @@ use anyhow::Result; use std::fmt::Write; use pod_core::config::configs; use pod_core::midi_io::{MidiInPort, MidiOutPort, MidiPorts}; +use crate::get_platform_hack_flags; use crate::usb::*; #[derive(Parser, Clone)] @@ -57,12 +58,22 @@ pub struct Opts { /// instead of triggering any events on an already-running /// pod-ui application. pub standalone: bool, + + #[clap(short, long, value_name = "FLAGS")] + /// Set active platform hack flags. must be a comma-separated + /// list of platform hack names. To enable a specific hack, it should + /// be specified by name (e.g osx-raise). To disable a specific hack, + /// it should be specified with a `no-` prefix (e.g. no-osx-raise). + pub platform: Option, } pub fn generate_help_text() -> Result { let mut s = String::new(); let tab = " "; + writeln!(s, "Default platform hacks (-p): {}", get_platform_hack_flags())?; + writeln!(s, "")?; + writeln!(s, "Device models (-m):")?; for (i, c) in configs().iter().enumerate() { writeln!(s, "{}[{}] {}", tab, i, &c.name)?; diff --git a/gui/src/platform.rs b/gui/src/platform.rs index fcdc11e..a7bbd44 100644 --- a/gui/src/platform.rs +++ b/gui/src/platform.rs @@ -1,36 +1,165 @@ //! A collection of custom platform-specific hacks that do not make sense, //! but empirically have been shown to be needed. //! +//! +use anyhow::*; +use std::sync::OnceLock; +use bitflags::{bitflags, Flags}; + +pub use imp::*; + +bitflags! { + #[derive(Clone)] + pub struct PlatformHackFlags: u8 { + const OSX_RAISE = 0x01; + const CUSTOM_MAIN_LOOP = 0x02; + } +} + +// platform-specific default hack flags + +#[cfg(target_os = "linux")] +static DEFAULT_HACK_FLAGS: PlatformHackFlags = PlatformHackFlags::empty(); #[cfg(target_os = "macos")] -pub fn raise_app_window() { - osx::raise(); +const DEFAULT_HACK_FLAGS: PlatformHackFlags = + PlatformHackFlags::CUSTOM_MAIN_LOOP.union(PlatformHackFlags::OSX_RAISE); + +#[cfg(target_os = "windows")] +static DEFAULT_HACK_FLAGS: PlatformHackFlags = PlatformHackFlags::empty(); + +pub(in crate::platform) fn platform_hack_flags() -> &'static mut PlatformHackFlags { + static mut FLAGS: OnceLock = OnceLock::new(); + // Safety: this is NOT sound, but actually using this reference as + // mutable is only even used from the CLI-options parsing code, + // well before it is ever read. + unsafe { + FLAGS.get_or_init(|| DEFAULT_HACK_FLAGS); + FLAGS.get_mut().unwrap() + } } -#[cfg(not(target_os = "macos"))] -pub fn raise_app_window() { +pub fn set_platform_hack_flags(flags_string: &str) -> Result { + let flags = platform_hack_flags(); + + for str in flags_string.split(",") { + let (add, str) = match &str[0..3] { + "no-" => (false, &str[3..]), + _ => (true, str) + }; + let name = str.replace("-", "_").to_ascii_uppercase(); + let flag = PlatformHackFlags::from_name(&name) + .ok_or_else(|| anyhow!("Platform hack {str:?}/{name} not found"))?; + flags.set(flag, add); + } + + Ok(flags.clone()) } -#[allow(non_snake_case)] -#[cfg(target_os = "macos")] -mod osx { - use objc2::runtime::{Object, Class}; - use objc2::{class, msg_send, sel, sel_impl}; - - /** - * On macOS, gtk_window_present doesn't always bring the window to the foreground. - * Based on this SO article, however, we can remedy the issue with some ObjectiveC - * magic: - * https://stackoverflow.com/questions/47497878/gtk-window-present-does-not-move-window-to-foreground - * - * Use objc2 crate to do just that. - */ - pub fn raise() { - unsafe { - let NSApplication = class!(NSApplication); - let app: *mut Object = msg_send![NSApplication, sharedApplication]; - - let _: () = msg_send![app, activateIgnoringOtherApps:true]; +pub fn get_platform_hack_flags() -> String { + let flags = platform_hack_flags(); + PlatformHackFlags::FLAGS.iter().flat_map(|flag| { + if !flags.contains(flag.value().clone()) { + return None; + } + Some(flag.name().to_ascii_lowercase().replace("_", "-")) + }) + .collect::>() + .join(",") +} + +mod imp { + use pod_gtk::prelude::*; + use std::sync::{Arc, OnceLock}; + use std::sync::atomic::{AtomicBool, Ordering}; + use bitflags::Flags; + use crate::platform::platform_hack_flags; + use crate::PlatformHackFlags; + + // PlatformHackFlags::CUSTOM_MAIN_LOOP + // + // Due to weird UI hang on macOS (see: https://github.com/arteme/pod-ui/issues/62), + // this is a free-form re-implementation of g_application_run() from + // https://github.com/GNOME/glib/blob/9cb0e9464e6117c4ff84b648851c6ee2f5873d1b/gio/gapplication.c#L2591 + // along with a_application_quit(), as there is no way to catch the + // quit signal otherwise. Can be useful on other platforms. + + fn app_running() -> Arc { + static BOOL: OnceLock> = OnceLock::new(); + BOOL.get_or_init(|| Arc::new(AtomicBool::new(true))).clone() + } + + fn custom_app_run(app: gtk::Application) -> i32 { + let context = glib::MainContext::default(); + let _guard = context.acquire().unwrap(); + + let running = app_running(); + + app.register(None::<&gio::Cancellable>).unwrap(); + app.activate(); + + while running.load(Ordering::SeqCst) { + context.iteration(true); + }; + + 0 + } + + fn custom_app_quit() { + app_running().store(false, Ordering::SeqCst); + } + + pub fn app_run(app: gtk::Application) -> i32 { + if platform_hack_flags().contains(PlatformHackFlags::CUSTOM_MAIN_LOOP) { + custom_app_run(app) + } else { + app.run_with_args::(&[]) + } + } + + pub fn app_quit(app: >k::Application) { + if platform_hack_flags().contains(PlatformHackFlags::CUSTOM_MAIN_LOOP) { + custom_app_quit() + } else { + app.quit() + } + } + + // PlatformHackFlags::OSX_RAISE + + #[cfg(target_os = "macos")] + pub fn raise_app_window() { + if platform_hack_flags().contains(PlatformHackFlags::OSX_RAISE) { + osx::raise(); + } + } + + #[cfg(not(target_os = "macos"))] + pub fn raise_app_window() { + // nop + } + + #[allow(non_snake_case)] + #[cfg(target_os = "macos")] + mod osx { + use objc2::runtime::{Object, Class}; + use objc2::{class, msg_send, sel, sel_impl}; + + /** + * On macOS, gtk_window_present doesn't always bring the window to the foreground. + * Based on this SO article, however, we can remedy the issue with some ObjectiveC + * magic: + * https://stackoverflow.com/questions/47497878/gtk-window-present-does-not-move-window-to-foreground + * + * Use objc2 crate to do just that. + */ + pub fn raise() { + unsafe { + let NSApplication = class!(NSApplication); + let app: *mut Object = msg_send![NSApplication, sharedApplication]; + + let _: () = msg_send![app, activateIgnoringOtherApps:true]; + } } } }