From e98f65c5a6853a175511fec4b99d69bba924dd10 Mon Sep 17 00:00:00 2001 From: rgdevment Date: Thu, 15 Oct 2026 11:09:05 -0300 Subject: [PATCH] fix(mac): the glass covers what the picker draws and the bear windows are sized before they are placed --- crates/linkunbound-mac/src/glass.rs | 79 ++++++++++----------- crates/linkunbound-mac/tests/main_thread.rs | 13 ++-- crates/linkunbound-shell/src/mascot.rs | 26 +++++-- crates/linkunbound-shell/ui/bear.slint | 12 ++-- 4 files changed, 72 insertions(+), 58 deletions(-) diff --git a/crates/linkunbound-mac/src/glass.rs b/crates/linkunbound-mac/src/glass.rs index 9bdecd2..0dc9379 100644 --- a/crates/linkunbound-mac/src/glass.rs +++ b/crates/linkunbound-mac/src/glass.rs @@ -1,6 +1,4 @@ -use std::cell::Cell; use std::ptr::NonNull; -use std::rc::Rc; use block2::RcBlock; use objc2::Message; @@ -9,11 +7,11 @@ use objc2::runtime::AnyClass; use objc2_app_kit::{ NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSAppearanceNameDarkAqua, NSBackingStoreType, NSColor, NSGlassEffectView, NSUserInterfaceItemIdentification, NSView, - NSVisualEffectBlendingMode, NSVisualEffectMaterial, NSVisualEffectState, NSVisualEffectView, - NSWindow, NSWindowDidMoveNotification, NSWindowDidResizeNotification, NSWindowOrderingMode, - NSWindowStyleMask, NSWorkspace, + NSViewFrameDidChangeNotification, NSVisualEffectBlendingMode, NSVisualEffectMaterial, + NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowDidMoveNotification, + NSWindowDidResizeNotification, NSWindowOrderingMode, NSWindowStyleMask, NSWorkspace, }; -use objc2_foundation::{MainThreadMarker, NSNotification, NSNotificationCenter, NSPoint, NSString}; +use objc2_foundation::{MainThreadMarker, NSNotification, NSNotificationCenter, NSString}; use crate::native::window_of; @@ -119,47 +117,44 @@ fn glass_window(mtm: MainThreadMarker, picker: &NSWindow) -> Retained window } -/// AppKit grows and shrinks a window from its bottom edge: the picker keeps its top instead, as it -/// does on Windows and as the bear leaning on it expects, and the glass takes its frame again. +/// The glass covers what the picker draws, not its frame: AppKit at times gives the frame room +/// above the content the picker never paints. +fn cover(picker: &NSWindow, glass: &NSWindow) { + if let Some(content) = picker.contentView() { + glass.setFrame_display(picker.convertRectToScreen(content.frame()), true); + } +} + #[allow(unsafe_code)] fn keep_with(picker: &NSWindow, glass: &NSWindow) { - let top = Rc::new(Cell::new(None::)); - let moved = { - let (picker, glass, top) = (picker.retain(), glass.retain(), Rc::clone(&top)); - RcBlock::new(move |_: NonNull| { - let frame = picker.frame(); - top.set(Some(frame.origin.y + frame.size.height)); - glass.setFrame_display(frame, true); - }) - }; - let resized = { + let follow = { let (picker, glass) = (picker.retain(), glass.retain()); - RcBlock::new(move |_: NonNull| { - let frame = picker.frame(); - if let Some(top) = top.get() - && (frame.origin.y + frame.size.height - top).abs() > 0.5 - { - picker.setFrameTopLeftPoint(NSPoint::new(frame.origin.x, top)); - } - glass.setFrame_display(picker.frame(), true); - }) + RcBlock::new(move |_: NonNull| cover(&picker, &glass)) }; let centre = NSNotificationCenter::defaultCenter(); - // SAFETY: AppKit's own names, posted on the main thread where the blocks run; the centre keeps - // both observers for as long as the picker, which lives as long as the app. + let content = picker.contentView(); + if let Some(content) = &content { + content.setPostsFrameChangedNotifications(true); + } + // SAFETY: AppKit's own names, posted on the main thread where the block runs; the centre keeps + // the observers for as long as the picker, which lives as long as the app. unsafe { - centre.addObserverForName_object_queue_usingBlock( - Some(NSWindowDidMoveNotification), - Some(picker), - None, - &moved, - ); - centre.addObserverForName_object_queue_usingBlock( - Some(NSWindowDidResizeNotification), - Some(picker), - None, - &resized, - ); + for name in [NSWindowDidMoveNotification, NSWindowDidResizeNotification] { + centre.addObserverForName_object_queue_usingBlock( + Some(name), + Some(picker), + None, + &follow, + ); + } + if let Some(content) = &content { + centre.addObserverForName_object_queue_usingBlock( + Some(NSViewFrameDidChangeNotification), + Some(content), + None, + &follow, + ); + } } } @@ -185,7 +180,7 @@ pub fn wear_glass(view: isize, dark: bool, corner: f64) -> bool { }; let glass = glass_behind(&picker).unwrap_or_else(|| glass_window(mtm, &picker)); glass.setLevel(picker.level()); - glass.setFrame_display(picker.frame(), true); + cover(&picker, &glass); let Some(material) = glass.contentView() else { return false; }; diff --git a/crates/linkunbound-mac/tests/main_thread.rs b/crates/linkunbound-mac/tests/main_thread.rs index 6605578..a21c72a 100644 --- a/crates/linkunbound-mac/tests/main_thread.rs +++ b/crates/linkunbound-mac/tests/main_thread.rs @@ -132,16 +132,15 @@ fn main() { assert_eq!(corner(&material), 10.0); assert_eq!(tone(&material).as_deref(), Some("NSAppearanceNameAqua")); - window.setFrameTopLeftPoint(NSPoint::new(50.0, 500.0)); - let grown = NSRect::new(NSPoint::new(50.0, 490.0), NSSize::new(10.0, 50.0)); + let grown = NSRect::new(NSPoint::new(50.0, 450.0), NSSize::new(10.0, 50.0)); window.setFrame_display(grown, false); - let frame = window.frame(); + let drawn = window.convertRectToScreen(view.frame()); + assert_eq!(drawn.size.height, 50.0); assert_eq!( - frame.origin.y + frame.size.height, - 500.0, - "the picker grows downwards, keeping its top" + glass.frame(), + drawn, + "the glass follows what the picker draws" ); - assert_eq!(glass.frame(), frame, "and the glass grows with it"); take_off_glass(handle); assert_eq!( diff --git a/crates/linkunbound-shell/src/mascot.rs b/crates/linkunbound-shell/src/mascot.rs index 7385bbb..44543b5 100644 --- a/crates/linkunbound-shell/src/mascot.rs +++ b/crates/linkunbound-shell/src/mascot.rs @@ -82,8 +82,16 @@ impl Mascot { picker.get_halo(), scale, ); - put(self.head.window(), head.0, head.1); - put(self.paws.window(), paws.0, paws.1); + put( + self.head.window(), + head, + (self.head.get_wide(), self.head.get_tall()), + ); + put( + self.paws.window(), + paws, + (self.paws.get_wide(), self.paws.get_tall()), + ); self.mood(0.0, picker.get_private_on()); let _ = self.head.show(); let _ = self.paws.show(); @@ -141,11 +149,19 @@ fn where_it_is(window: &slint::Window) -> (i32, i32) { } } -fn put(window: &slint::Window, x: i32, y: i32) { +/// On macOS the size goes first: AppKit keeps a window's bottom edge when Slint resizes it later, +/// which would sink the head of a smaller pose into the picker. +fn put(window: &slint::Window, (x, y): (i32, i32), (wide, tall): (f32, f32)) { #[cfg(target_os = "macos")] - window.set_position(slint::LogicalPosition::new(x as f32, y as f32)); + { + window.set_size(slint::LogicalSize::new(wide, tall)); + window.set_position(slint::LogicalPosition::new(x as f32, y as f32)); + } #[cfg(not(target_os = "macos"))] - window.set_position(slint::PhysicalPosition::new(x, y)); + { + let _ = (wide, tall); + window.set_position(slint::PhysicalPosition::new(x, y)); + } } /// The picker says where the bear should look and whether it wears the mask, and an empty slot diff --git a/crates/linkunbound-shell/ui/bear.slint b/crates/linkunbound-shell/ui/bear.slint index c806379..bb29efa 100644 --- a/crates/linkunbound-shell/ui/bear.slint +++ b/crates/linkunbound-shell/ui/bear.slint @@ -111,12 +111,14 @@ export component BearHead inherits Window { in property masked; in property look; in property side: 92px; + out property wide: root.side; + out property tall: root.side * 256 / 312; title: "LinkUnbound"; no-frame: true; always-on-top: true; background: transparent; - width: root.side; - height: root.side * 256 / 312; + width: root.wide; + height: root.tall; Head { masked: root.masked; @@ -132,8 +134,10 @@ export component BearPaws inherits Window { no-frame: true; always-on-top: true; background: transparent; - width: root.gap + 2 * root.paw; - height: root.paw * 31 / 40; + out property wide: root.gap + 2 * root.paw; + out property tall: root.paw * 31 / 40; + width: root.wide; + height: root.tall; Paw { x: 0;