authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-29 04:24:02-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-29 04:24:02-07:00
logcc021c98d7181a67b148b9ce001f9411260accd5
tree184bd924f32ec7dc30c7c20e34890d90bfa62c16
parent2d9ae5806189ab5ae2c042699773045856976c65
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat(snow-leopard): textured window with the toolbar in the title gradient

On 10.6 the window is textured with a top content border as tall as the toolbar row, so AppKit's unified gradient runs through it. The GL backend presents premultiplied sRGB for the transparent surface, empty toolbar space drags the window through the 10.6 shim, and the bottom corners are cut at 10.6's radius. Assisted-by: claude-opus-5.5

5 files changed, 43 insertions(+), 23 deletions(-)

crates/snowbound/src/aqua.rs+6-14
...@@ -12,7 +12,7 @@ use objc2::{...@@ -12,7 +12,7 @@ use objc2::{
12 sel,12 sel,
13};13};
14use objc2_foundation::{MainThreadMarker, NSPoint, NSRect, NSSize};14use objc2_foundation::{MainThreadMarker, NSPoint, NSRect, NSSize};
15use std::{cell::Cell, collections::HashMap};15use std::collections::HashMap;
16use ui::{Axis, PaintedScroller, Scroller, ScrollerPart};16use ui::{Axis, PaintedScroller, Scroller, ScrollerPart};
17use winit::{17use winit::{
18 raw_window_handle::{HasWindowHandle, RawWindowHandle},18 raw_window_handle::{HasWindowHandle, RawWindowHandle},
...@@ -57,19 +57,11 @@ pub fn textured(window: &Window, row: f32) -> bool {...@@ -57,19 +57,11 @@ pub fn textured(window: &Window, row: f32) -> bool {
57 true57 true
58}58}
5959
60thread_local! {60/// `-mouseDownCanMoveWindow` for the content view: a textured window drags from any press
61 static DRAGS: Cell<bool> = const { Cell::new(false) };61/// on a view that allows it, asking once. The app drags from its toolbar's empty space
62}62/// itself, through `-performWindowDragWithEvent:`.
6363pub extern "C" fn no_window_drags(_: &AnyObject, _: Sel) -> Bool {
64/// Whether a press where the pointer is drags the window, which a textured window asks64 Bool::NO
65/// its content of: presses on empty toolbar space do, and nowhere else.
66pub fn set_window_drags(drags: bool) {
67 DRAGS.set(drags);
68}
69
70/// `-mouseDownCanMoveWindow` for the content view of a textured window.
71pub extern "C" fn window_drags(_: &AnyObject, _: Sel) -> Bool {
72 Bool::new(DRAGS.get())
73}65}
7466
75/// The cursor for moving something: winit loads its move cursor from a folder of cursors67/// The cursor for moving something: winit loads its move cursor from a folder of cursors
crates/snowbound/src/linux.rs-3
...@@ -579,9 +579,6 @@ pub fn move_cursor() -> winit::window::CursorIcon {...@@ -579,9 +579,6 @@ pub fn move_cursor() -> winit::window::CursorIcon {
579 winit::window::CursorIcon::Move579 winit::window::CursorIcon::Move
580}580}
581581
582/// Window drags are the app's own.
583pub fn set_window_drags(_: bool) {}
584
585/// Scrollbars overlay the content.582/// Scrollbars overlay the content.
586pub fn scrollers() -> Option<ui::Scrollers> {583pub fn scrollers() -> Option<ui::Scrollers> {
587 None584 None
crates/snowbound/src/macos.rs+2-2
...@@ -29,7 +29,7 @@ use winit::{...@@ -29,7 +29,7 @@ use winit::{
29 window::{Window, WindowAttributes},29 window::{Window, WindowAttributes},
30};30};
3131
32pub use crate::aqua::{move_cursor, resize_grip, scrollers, set_window_drags};32pub use crate::aqua::{move_cursor, resize_grip, scrollers};
3333
34static QUIT: OnceLock<EventLoopProxy<crate::UserEvent>> = OnceLock::new();34static QUIT: OnceLock<EventLoopProxy<crate::UserEvent>> = OnceLock::new();
35static INPUT_CLASS: OnceLock<&'static AnyClass> = OnceLock::new();35static INPUT_CLASS: OnceLock<&'static AnyClass> = OnceLock::new();
...@@ -533,7 +533,7 @@ pub fn install_text_input(window: &Window) {...@@ -533,7 +533,7 @@ pub fn install_text_input(window: &Window) {
533 if crate::aqua::before_lion() {533 if crate::aqua::before_lion() {
534 class.add_method(534 class.add_method(
535 sel!(mouseDownCanMoveWindow),535 sel!(mouseDownCanMoveWindow),
536 crate::aqua::window_drags as extern "C" fn(_, _) -> _,536 crate::aqua::no_window_drags as extern "C" fn(_, _) -> _,
537 );537 );
538 }538 }
539 class.register()539 class.register()
crates/snowbound/src/main.rs-1
...@@ -3072,7 +3072,6 @@ impl State {...@@ -3072,7 +3072,6 @@ impl State {
3072 fn input(&mut self, event: ui::Event) {3072 fn input(&mut self, event: ui::Event) {
3073 if let ui::Event::PointerMoved(point) = event {3073 if let ui::Event::PointerMoved(point) = event {
3074 self.pointer = point;3074 self.pointer = point;
3075 platform::set_window_drags(self.ui.box_at(point) == Some(strip()));
3076 if std::mem::take(&mut self.strip_held) {3075 if std::mem::take(&mut self.strip_held) {
3077 let _ = self.window.drag_window();3076 let _ = self.window.drag_window();
3078 }3077 }
platform/snow-leopard/rt/appkit.c+35-3
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1// AppKit that winit, AccessKit and the app call and 10.6 lacks, added at load time with1// AppKit that winit, AccessKit and the app call and 10.6 lacks, added at load time with
2// the neutral answer: scale 1, no precise scrolling, no appearances, no tabbing, no full2// the neutral answer: scale 1, no precise scrolling, no appearances, no tabbing, no full
3// screen. `remote.sh selectors` lists what a binary names that 10.6 lacks. Linked from3// screen; window drags are done here. `remote.sh selectors` lists what a binary names
4// the archive by the appearance name winit imports, so only AppKit binaries carry it.4// that 10.6 lacks. Linked from the archive by the appearance name winit imports, so only
5// AppKit binaries carry it.
5// class_addMethod adds nothing where the system already has the method.6// class_addMethod adds nothing where the system already has the method.
6#include <ApplicationServices/ApplicationServices.h>7#include <ApplicationServices/ApplicationServices.h>
7#include <objc/message.h>8#include <objc/message.h>
...@@ -53,6 +54,37 @@ static CGPoint point_from_screen(id self, SEL _cmd, CGPoint point) {...@@ -53,6 +54,37 @@ static CGPoint point_from_screen(id self, SEL _cmd, CGPoint point) {
53 return CGPointMake(point.x - frame.origin.x, point.y - frame.origin.y);54 return CGPointMake(point.x - frame.origin.x, point.y - frame.origin.y);
54}55}
5556
57// 10.11: the window follows the pointer until the button comes up, as a press on its
58// title does.
59static void window_drag(id self, SEL _cmd, id event) {
60 typedef CGPoint (*SendPoint)(id, SEL);
61 typedef CGRect (*SendRect)(id, SEL);
62 id event_class = (id)objc_getClass("NSEvent");
63 CGPoint start = ((SendPoint)objc_msgSend)(event_class, sel_registerName("mouseLocation"));
64 CGRect frame = ((SendRect)objc_msgSend_stret)(self, sel_registerName("frame"));
65 id app = SEND(objc_getClass("NSApplication"), "sharedApplication");
66 id forever = SEND(objc_getClass("NSDate"), "distantFuture");
67 for (;;) {
68 // NSLeftMouseDraggedMask | NSLeftMouseUpMask.
69 id next = ((id (*)(id, SEL, unsigned long long, id, CFStringRef, BOOL))objc_msgSend)(
70 app, sel_registerName("nextEventMatchingMask:untilDate:inMode:dequeue:"),
71 1 << 6 | 1 << 2, forever, CFSTR("NSEventTrackingRunLoopMode"), YES);
72 // NSLeftMouseUp.
73 if (((unsigned long (*)(id, SEL))objc_msgSend)(next, sel_registerName("type")) == 2) {
74 // The button's release stays in the queue for whoever asked for the drag.
75 ((void (*)(id, SEL, id, BOOL))objc_msgSend)(app, sel_registerName("postEvent:atStart:"), next, YES);
76 return;
77 }
78 CGPoint now = ((SendPoint)objc_msgSend)(event_class, sel_registerName("mouseLocation"));
79 CGRect moved = frame;
80 moved.origin.x += now.x - start.x;
81 moved.origin.y += now.y - start.y;
82 moved = ((CGRect (*)(id, SEL, CGRect, id))objc_msgSend_stret)(
83 self, sel_registerName("constrainFrameRect:toScreen:"), moved, SEND(self, "screen"));
84 ((void (*)(id, SEL, CGPoint))objc_msgSend)(self, sel_registerName("setFrameOrigin:"), moved.origin);
85 }
86}
87
56static id srgb_color(id self, SEL _cmd, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) {88static id srgb_color(id self, SEL _cmd, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) {
57 CGFloat components[] = {red, green, blue, alpha};89 CGFloat components[] = {red, green, blue, alpha};
58 return ((id (*)(id, SEL, id, CGFloat *, long))objc_msgSend)(90 return ((id (*)(id, SEL, id, CGFloat *, long))objc_msgSend)(
...@@ -106,7 +138,7 @@ __attribute__((constructor)) static void polyfill(void) {...@@ -106,7 +138,7 @@ __attribute__((constructor)) static void polyfill(void) {
106 add("NSWindow", 0, "backingScaleFactor", one, "d@:");138 add("NSWindow", 0, "backingScaleFactor", one, "d@:");
107 add("NSWindow", 0, "convertRectToScreen:", rect_to_screen, RECT "@:" RECT);139 add("NSWindow", 0, "convertRectToScreen:", rect_to_screen, RECT "@:" RECT);
108 add("NSWindow", 0, "convertPointFromScreen:", point_from_screen, "{CGPoint=dd}@:{CGPoint=dd}");140 add("NSWindow", 0, "convertPointFromScreen:", point_from_screen, "{CGPoint=dd}@:{CGPoint=dd}");
109 add("NSWindow", 0, "performWindowDragWithEvent:", ignore, "v@:@");141 add("NSWindow", 0, "performWindowDragWithEvent:", window_drag, "v@:@");
110 add("NSWindow", 0, "toggleFullScreen:", ignore, "v@:@");142 add("NSWindow", 0, "toggleFullScreen:", ignore, "v@:@");
111 add("NSWindow", 0, "setTabbingMode:", ignore, "v@:q");143 add("NSWindow", 0, "setTabbingMode:", ignore, "v@:q");
112 add("NSWindow", 0, "setTabbingIdentifier:", ignore, "v@:@");144 add("NSWindow", 0, "setTabbingIdentifier:", ignore, "v@:@");