From 2e98a5bb9f67a9ad0973eff10cd7541b45c79315 Mon Sep 17 00:00:00 2001 From: zipg Date: Sun, 4 Oct 2026 17:39:56 +0800 Subject: [PATCH] fix(desktop): keep macOS traffic lights aligned while resizing --- src-tauri/src/commands/window_controls.rs | 34 +++++++++++++++++++ vendor/wry/DBX-PATCH.md | 13 +++++++ .../wkwebview/class/wry_web_view_parent.rs | 28 +++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/src-tauri/src/commands/window_controls.rs b/src-tauri/src/commands/window_controls.rs index 0c60076e62..2f136ff884 100644 --- a/src-tauri/src/commands/window_controls.rs +++ b/src-tauri/src/commands/window_controls.rs @@ -55,6 +55,14 @@ async fn set_macos_traffic_light_position_inner( button.setFrameOrigin(rect.origin); } + // AppKit re-runs the title bar layout during a live window resize and resets + // the standard buttons to their default frames. The vendored wry re-applies + // the traffic light inset from `drawRect:` (after that layout pass), which is + // what the user actually sees. Hand it the position computed above so both + // writers agree; otherwise the buttons flicker between the toolbar alignment + // and wry's window creation inset while the window is being resized. + apply_traffic_light_inset_to_wry(ns_window, target_x, target_y); + let Some(close) = ns_window.standardWindowButton(NSWindowButton::CloseButton) else { let _ = tx.send(None); return; @@ -98,6 +106,32 @@ fn validate_macos_traffic_light_position(x: f64, y: f64, scale: f64) -> Result<( Ok(()) } +/// Hands the traffic light inset DBX computed to the vendored wry parent view. +/// +/// wry re-applies the stored inset from `drawRect:` and that write is the one that +/// survives AppKit's title bar layout, so keeping it in sync with DBX's toolbar +/// alignment removes the flicker between the two during a window resize. +#[cfg(target_os = "macos")] +fn apply_traffic_light_inset_to_wry(ns_window: &objc2_app_kit::NSWindow, x: f64, y: f64) { + use objc2::runtime::Sel; + use objc2_app_kit::NSView; + + let selector: Sel = objc2::sel!(dbxUpdateTrafficLightInsetX:y:); + + fn visit(view: &NSView, selector: Sel, x: f64, y: f64) -> bool { + let responds: bool = unsafe { objc2::msg_send![view, respondsToSelector: selector] }; + if responds { + let _: () = unsafe { objc2::msg_send![view, dbxUpdateTrafficLightInsetX: x, y: y] }; + return true; + } + view.subviews().iter().any(|subview| visit(&subview, selector, x, y)) + } + + if let Some(content_view) = ns_window.contentView() { + let _ = visit(&content_view, selector, x, y); + } +} + #[derive(Debug, Clone, serde::Serialize)] pub struct MacosTrafficLightLayout { x: f64, diff --git a/vendor/wry/DBX-PATCH.md b/vendor/wry/DBX-PATCH.md index fbf1c9101f..0d44bed0f3 100644 --- a/vendor/wry/DBX-PATCH.md +++ b/vendor/wry/DBX-PATCH.md @@ -24,3 +24,16 @@ dead-key/compose input. This matches the proposal in upstream tauri-apps/wry#1711 (see also tauri-apps/wry#1175, #1177) and unblocks in-app shortcuts such as the table structure editor (t8y2/dbx#7245, landed via PR #8650). + +On macOS, DBX also adds `WryWebViewParent.dbxUpdateTrafficLightInsetX:y:` and +makes `inset_traffic_lights` place the buttons' vertical position explicitly +(top-down center-delta against the window frame) instead of relying on AppKit's +title bar layout. Three writers fought over the button frames during a live +window resize — AppKit's layout pass, the one-shot `set_macos_traffic_light_ +position` placement, and wry's `drawRect:` re-application of the creation-time +inset — which made the traffic lights jump while dragging the window edge. +DBX pushes its dynamic toolbar-aligned position through the new method so the +`drawRect:` re-application replays the latest inset, and both writers converge +on the same explicit placement. Without it, the stored static inset and AppKit +layout disagree on every resize pass (see upstream tauri-apps/wry#1747 and +tauri-apps/tauri#15451). diff --git a/vendor/wry/src/wkwebview/class/wry_web_view_parent.rs b/vendor/wry/src/wkwebview/class/wry_web_view_parent.rs index 7c5fedbf77..ae1cbcdbcd 100644 --- a/vendor/wry/src/wkwebview/class/wry_web_view_parent.rs +++ b/vendor/wry/src/wkwebview/class/wry_web_view_parent.rs @@ -70,6 +70,22 @@ define_class!( unsafe { inset_traffic_lights(&self.window().unwrap(), x, y) }; } } + + /// Replaces the traffic light inset stored at window creation. + /// + /// `drawRect:` re-applies the stored inset after AppKit's title bar layout + /// pass, which is the only write that survives a live window resize. DBX + /// keeps the buttons aligned with its toolbar, so it pushes the dynamic + /// position here to keep that re-application in sync with the one-shot + /// placement instead of fighting it while the window is resized. + #[cfg(target_os = "macos")] + #[unsafe(method(dbxUpdateTrafficLightInsetX:y:))] + fn dbx_update_traffic_light_inset(&self, x: f64, y: f64) { + self.ivars().traffic_light_inset.set(Some((x, y))); + if let Some(window) = self.window() { + unsafe { inset_traffic_lights(&window, x, y) }; + } + } } ); @@ -123,6 +139,17 @@ pub unsafe fn inset_traffic_lights(window: &NSWindow, x: f64, y: f64) { let space_between = NSView::frame(&miniaturize).origin.x - close_rect.origin.x; + // AppKit re-runs its title bar layout during a live window resize and can move + // the standard buttons back to their default frames, which is why this is also + // called from `drawRect:`. Place the buttons explicitly instead of relying on + // that layout pass so the requested inset is what ends up on screen. + let window_height = window.frame().size.height; + let current_window_rect = + close.superview().map(|view| view.convertRect_toView(close_rect, None)).unwrap_or(close_rect); + let current_center_y = window_height - (current_window_rect.origin.y + current_window_rect.size.height / 2.0); + let target_center_y = y + close_rect.size.height / 2.0; + let center_delta_y = target_center_y - current_center_y; + let mut window_buttons = vec![close, miniaturize]; if let Some(zoom) = zoom { window_buttons.push(zoom); @@ -131,6 +158,7 @@ pub unsafe fn inset_traffic_lights(window: &NSWindow, x: f64, y: f64) { for (i, button) in window_buttons.into_iter().enumerate() { let mut rect = NSView::frame(&button); rect.origin.x = x + (i as f64 * space_between); + rect.origin.y -= center_delta_y; button.setFrameOrigin(rect.origin); } }