[fix](trx-frontend-appkit): run AppKit event loop on calling thread
Extract the AppKit event loop from FrontendSpawner::spawn_frontend into a new public run_appkit_main_thread() function that blocks on the calling thread. This allows the process main thread (thread 0) to drive the UI, which is required for MainThreadMarker::new() to succeed. The FrontendSpawner impl now only spawns the async state watcher task. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -11,6 +11,9 @@ pub mod server;
|
|||||||
#[cfg(all(target_os = "macos", feature = "appkit"))]
|
#[cfg(all(target_os = "macos", feature = "appkit"))]
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "macos", feature = "appkit"))]
|
||||||
|
pub use server::run_appkit_main_thread;
|
||||||
|
|
||||||
#[cfg(all(target_os = "macos", feature = "appkit"))]
|
#[cfg(all(target_os = "macos", feature = "appkit"))]
|
||||||
pub fn register_frontend() {
|
pub fn register_frontend() {
|
||||||
use trx_frontend::FrontendSpawner;
|
use trx_frontend::FrontendSpawner;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
//! thread via a std::sync::mpsc channel.
|
//! thread via a std::sync::mpsc channel.
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use objc2::MainThreadMarker;
|
use objc2::MainThreadMarker;
|
||||||
use objc2_app_kit::NSApplication;
|
use objc2_app_kit::NSApplication;
|
||||||
@@ -30,31 +29,46 @@ pub struct AppKitFrontend;
|
|||||||
impl FrontendSpawner for AppKitFrontend {
|
impl FrontendSpawner for AppKitFrontend {
|
||||||
fn spawn_frontend(
|
fn spawn_frontend(
|
||||||
state_rx: watch::Receiver<RigState>,
|
state_rx: watch::Receiver<RigState>,
|
||||||
rig_tx: mpsc::Sender<RigRequest>,
|
_rig_tx: mpsc::Sender<RigRequest>,
|
||||||
_callsign: Option<String>,
|
_callsign: Option<String>,
|
||||||
listen_addr: SocketAddr,
|
listen_addr: SocketAddr,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
// Channel for state updates: async watcher -> AppKit thread.
|
let (state_update_tx, _state_update_rx) = std::sync::mpsc::channel::<RigState>();
|
||||||
let (state_update_tx, state_update_rx) = std::sync::mpsc::channel::<RigState>();
|
|
||||||
|
|
||||||
// Channel for button actions: UI buttons -> AppKit thread main loop.
|
|
||||||
let (action_tx, action_rx) = std::sync::mpsc::channel::<ButtonAction>();
|
|
||||||
|
|
||||||
// Spawn async state watcher that forwards state changes.
|
// Spawn async state watcher that forwards state changes.
|
||||||
let handle = tokio::spawn(async move {
|
// The actual AppKit event loop is driven by `run_appkit_main_thread`
|
||||||
|
// called from main() on the process main thread.
|
||||||
|
tokio::spawn(async move {
|
||||||
info!("AppKit frontend starting (addr hint: {})", listen_addr);
|
info!("AppKit frontend starting (addr hint: {})", listen_addr);
|
||||||
run_state_watcher(state_rx, state_update_tx).await;
|
run_state_watcher(state_rx, state_update_tx).await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the AppKit event loop on the calling thread (must be the process main
|
||||||
|
/// thread, i.e. thread 0). This function **blocks forever**.
|
||||||
|
///
|
||||||
|
/// It creates the NSApplication, builds the UI window, and enters a polling
|
||||||
|
/// loop that drains AppKit events, applies rig state updates, and dispatches
|
||||||
|
/// button actions.
|
||||||
|
pub fn run_appkit_main_thread(
|
||||||
|
state_rx: watch::Receiver<RigState>,
|
||||||
|
rig_tx: mpsc::Sender<RigRequest>,
|
||||||
|
) {
|
||||||
|
// Channel for state updates: async watcher -> main thread.
|
||||||
|
let (state_update_tx, state_update_rx) = std::sync::mpsc::channel::<RigState>();
|
||||||
|
|
||||||
|
// Channel for button actions: UI buttons -> main thread loop.
|
||||||
|
let (action_tx, action_rx) = std::sync::mpsc::channel::<ButtonAction>();
|
||||||
|
|
||||||
|
// Spawn async state watcher onto the tokio runtime (running on a
|
||||||
|
// background thread).
|
||||||
|
tokio::spawn(async move {
|
||||||
|
run_state_watcher(state_rx, state_update_tx).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Spawn the AppKit main thread.
|
let mtm = MainThreadMarker::new()
|
||||||
thread::spawn(move || {
|
.expect("run_appkit_main_thread must be called from the process main thread");
|
||||||
let mtm = match MainThreadMarker::new() {
|
|
||||||
Some(m) => m,
|
|
||||||
None => {
|
|
||||||
warn!("AppKit frontend: could not obtain MainThreadMarker");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = NSApplication::sharedApplication(mtm);
|
let app = NSApplication::sharedApplication(mtm);
|
||||||
|
|
||||||
@@ -65,6 +79,8 @@ impl FrontendSpawner for AppKitFrontend {
|
|||||||
|
|
||||||
let mut model = RigStateModel::default();
|
let mut model = RigStateModel::default();
|
||||||
|
|
||||||
|
info!("AppKit frontend: entering main run loop");
|
||||||
|
|
||||||
// Run a polling loop instead of NSApplication::run() so we can
|
// Run a polling loop instead of NSApplication::run() so we can
|
||||||
// process state updates and button actions between event cycles.
|
// process state updates and button actions between event cycles.
|
||||||
loop {
|
loop {
|
||||||
@@ -86,10 +102,6 @@ impl FrontendSpawner for AppKitFrontend {
|
|||||||
// Sleep briefly to avoid busy-waiting.
|
// Sleep briefly to avoid busy-waiting.
|
||||||
std::thread::sleep(std::time::Duration::from_millis(16));
|
std::thread::sleep(std::time::Duration::from_millis(16));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
handle
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain_appkit_events(app: &NSApplication) {
|
fn drain_appkit_events(app: &NSApplication) {
|
||||||
|
|||||||
Reference in New Issue
Block a user