Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 37 additions & 42 deletions crates/linkunbound-mac/src/glass.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use std::cell::Cell;
use std::ptr::NonNull;
use std::rc::Rc;

use block2::RcBlock;
use objc2::Message;
Expand All @@ -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;

Expand Down Expand Up @@ -119,47 +117,44 @@ fn glass_window(mtm: MainThreadMarker, picker: &NSWindow) -> Retained<NSWindow>
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::<f64>));
let moved = {
let (picker, glass, top) = (picker.retain(), glass.retain(), Rc::clone(&top));
RcBlock::new(move |_: NonNull<NSNotification>| {
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<NSNotification>| {
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<NSNotification>| 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,
);
}
}
}

Expand All @@ -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;
};
Expand Down
13 changes: 6 additions & 7 deletions crates/linkunbound-mac/tests/main_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
26 changes: 21 additions & 5 deletions crates/linkunbound-shell/src/mascot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,16 @@
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();
Expand Down Expand Up @@ -141,11 +149,19 @@
}
}

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));
}

Check warning on line 164 in crates/linkunbound-shell/src/mascot.rs

View workflow job for this annotation

GitHub Actions / crates / the lines this branch changed

Missed mutant

replace put with ()

Check warning on line 164 in crates/linkunbound-shell/src/mascot.rs

View workflow job for this annotation

GitHub Actions / mac / the lines this branch changed

Missed mutant

replace put with ()
}

/// The picker says where the bear should look and whether it wears the mask, and an empty slot
Expand Down
12 changes: 8 additions & 4 deletions crates/linkunbound-shell/ui/bear.slint
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,14 @@ export component BearHead inherits Window {
in property <bool> masked;
in property <float> look;
in property <length> side: 92px;
out property <length> wide: root.side;
out property <length> 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;
Expand All @@ -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 <length> wide: root.gap + 2 * root.paw;
out property <length> tall: root.paw * 31 / 40;
width: root.wide;
height: root.tall;

Paw {
x: 0;
Expand Down
Loading