fix(desktop): keep macOS traffic lights aligned while resizing

This commit is contained in:
zipg
2026-10-04 17:39:56 +08:00
committed by GitHub
parent 86659553a6
commit 2e98a5bb9f
3 changed files with 75 additions and 0 deletions
+34
View File
@@ -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,
+13
View File
@@ -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).
+28
View File
@@ -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);
}
}