Skip to content

Commit

Permalink
macOS: reimplement g_application_run in rust to fix UI getting stuck
Browse files Browse the repository at this point in the history
* 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;
  • Loading branch information
arteme committed Dec 1, 2024
1 parent 9163c9d commit e59dfef
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 26 deletions.
13 changes: 11 additions & 2 deletions gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -639,7 +648,7 @@ async fn main() -> Result<()> {
}
});

let exit_code = app.run_with_args::<String>(&[]);
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
Expand Down Expand Up @@ -1233,7 +1242,7 @@ fn activate(app: &gtk::Application, title: &String, opts: Opts, sentry_enabled:
r.emit_by_name::<()>("group-changed", &[]);

// quit
app.quit();
app_quit(&app);
}
}

Expand Down
11 changes: 11 additions & 0 deletions gui/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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. <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<String>,
}

pub fn generate_help_text() -> Result<String> {
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)?;
Expand Down
177 changes: 153 additions & 24 deletions gui/src/platform.rs
Original file line number Diff line number Diff line change
@@ -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<PlatformHackFlags> = 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<PlatformHackFlags> {
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::<Vec<_>>()
.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<AtomicBool> {
static BOOL: OnceLock<Arc<AtomicBool>> = 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::<String>(&[])
}
}

pub fn app_quit(app: &gtk::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];
}
}
}
}

0 comments on commit e59dfef

Please sign in to comment.