GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/mac/mod.rs (40.2K)
1 //! The macOS shell: an [`Application`] run in an AppKit window, the way the
2 //! Wayland shell runs one on a surface and the browser shell on a canvas. It
3 //! is the third shell over the shared pieces — [`Driver`] routes what input
4 //! means, [`Pacer::turn`] decides what a turn does, [`build_frame`] builds
5 //! the frame — and the renderer is the Vulkan one, on Metal through
6 //! MoltenVK (a `CAMetalLayer` is a [`SurfaceTarget::Metal`]). What is left
7 //! here is AppKit's side:
8 //!
9 //! - **The window.** An `NSWindow` whose content is the app's root plate:
10 //! transparent, its titlebar transparent over full-size content, so the
11 //! plate fills the window and the traffic lights stand on its corner. The
12 //! window keeps AppKit's own resize edges, so the driver's CSD resize band
13 //! is the app's (`PressSite::own_edges`); a press it reads as the
14 //! window's (the titlebar band, a movable root plate) drags the window
15 //! (`performWindowDragWithEvent:`).
16 //! - **Events in.** A layer-hosting `NSView` (flipped, so y runs down as
17 //! everywhere else in the toolkit) takes the mouse, scroll, magnify and
18 //! key events and hands them, mapped by `backend::appkit`, to the driver.
19 //! Command is the shortcut key (⌘Z is undo). AppKit's own key repeats
20 //! and scroll momentum are dropped: the driver repeats a held key and the
21 //! toolkit coasts a flick, as on Wayland.
22 //! - **Turns.** Main-queue dispatches (`dispatch2`): any event, and any
23 //! `AppSender::send` from any thread (`set_wake`), asks for a turn at most
24 //! one ACTIVE frame after the last; between, the loop sleeps for whatever
25 //! the pacer said. A turn superseded by an earlier one is dropped by its
26 //! generation.
27 //! - **Frames out.** [`build_frame`] at the view's size in points and the
28 //! window's backing scale, drawn by the [`VkRenderer`]; `stage_renderer`
29 //! (and so the portable `stage_3d`) runs before each draw, `renderer_init`
30 //! once.
31 //!
32 //! The menu is a minimal one (Quit, ⌘Q), whose quit — like the close
33 //! button — asks the app to exit the way a compositor's close does.
34 //!
35 //! - **Input methods.** The view is an `NSTextInputClient`: while a widget
36 //! is editing text, a key press goes through `interpretKeyEvents:`, whose
37 //! marked text is the composition and whose inserted text the commit
38 //! (`crate::ime`); the candidate window is put under the caret the
39 //! widget reported. The clipboard is the general `NSPasteboard`
40 //! (`widget::clipboard`).
41 //!
42 //! Not there yet: drag and drop, the context menu in a popup window (it is
43 //! drawn in the window and kept inside it, as on a layer surface), and blur
44 //! behind the window (`NSVisualEffectView`).
45 //!
46 //! **This module has never been run.** It is written against objc2's
47 //! AppKit bindings and type-checked for `aarch64-apple-darwin`
48 //! (`scripts/check-mac`) on Linux, where nothing can be linked or run: it
49 //! needs a Mac, with MoltenVK installed (the Vulkan SDK, or Homebrew's
50 //! `molten-vk` and `vulkan-loader`).
51
52 use std::cell::{Cell, RefCell};
53 use std::sync::mpsc::Receiver;
54 use std::time::Duration;
55
56 use cursor_icon::CursorIcon;
57 use dispatch2::{DispatchQueue, DispatchTime};
58 use objc2::rc::Retained;
59 use objc2::runtime::{AnyObject, NSObject, ProtocolObject, Sel};
60 use objc2::{define_class, msg_send, sel, AnyThread, DefinedClass, MainThreadOnly};
61 use objc2_app_kit::{
62 NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSApplicationTerminateReply,
63 NSBackingStoreType, NSColor, NSCursor, NSEvent, NSEventModifierFlags, NSEventType, NSMenu, NSMenuItem,
64 NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, NSWindowDelegate, NSWindowStyleMask,
65 NSWindowTitleVisibility,
66 };
67 use objc2_foundation::{
68 ns_string, MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSNotFound, NSNotification,
69 NSObjectProtocol, NSPoint, NSRange, NSRangePointer, NSRect, NSSize, NSString, NSUInteger,
70 };
71 use objc2_quartz_core::CAMetalLayer;
72 use web_time::Instant;
73
74 use crate::backend::app::{set_wake, AppSender, Application, LogicalPosition, LogicalSize};
75 use crate::backend::appkit;
76 use crate::backend::driver::{Driver, Press, PressSite, Turn};
77 use crate::backend::frame::build_frame;
78 use crate::backend::shell::{Pacer, Shell, Step, ACTIVE_DISPATCH};
79 use crate::vk::{SurfaceTarget, VkRenderer};
80 use crate::widget::{context_menu, ElementState, Key};
81 use crate::backend::text::DlText;
82
83 /// Run `A` in a window until it exits. Must be called on the main thread
84 /// (a process's `main`), as AppKit requires.
85 pub fn run<A: Application>() {
86 let mtm = MainThreadMarker::new().expect("cce-ui's macOS shell runs on the main thread");
87 // The window's interaction state (`crate::window_state`), current while the run loop
88 // below runs this window's code — which is until `run` returns.
89 let window_state = crate::window_state::WindowState::new();
90 let _window = crate::window_state::enter(&window_state);
91 let ns_app = NSApplication::sharedApplication(mtm);
92 ns_app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
93
94 let (tx, rx) = std::sync::mpsc::channel();
95 // The scale is known before the app is built, as on the other shells: a
96 // widget may read it as it is made.
97 let main_scale = objc2_app_kit::NSScreen::mainScreen(mtm).map_or(2.0, |s| s.backingScaleFactor());
98 crate::scale::set_scale_factor(main_scale as f32);
99 let mut app = A::create(AppSender::from(tx));
100 let settings = app.settings();
101 crate::scale::set_app_id(settings.app_id.clone());
102
103 let delegate = Delegate::new(mtm);
104 ns_app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
105 ns_app.setMainMenu(Some(&main_menu(mtm, &settings.title)));
106
107 let style = NSWindowStyleMask::Titled
108 | NSWindowStyleMask::Closable
109 | NSWindowStyleMask::Miniaturizable
110 | NSWindowStyleMask::Resizable
111 | NSWindowStyleMask::FullSizeContentView;
112 let rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(settings.width as f64, settings.height as f64));
113 // SAFETY: released-when-closed is turned off at once, as a window made
114 // outside a window controller must be.
115 let window = unsafe {
116 NSWindow::initWithContentRect_styleMask_backing_defer(
117 NSWindow::alloc(mtm),
118 rect,
119 style,
120 NSBackingStoreType::Buffered,
121 false,
122 )
123 };
124 unsafe { window.setReleasedWhenClosed(false) };
125 window.setTitle(&NSString::from_str(&settings.title));
126 window.setTitlebarAppearsTransparent(true);
127 window.setTitleVisibility(NSWindowTitleVisibility::Hidden);
128 window.setOpaque(false);
129 window.setBackgroundColor(Some(&NSColor::clearColor()));
130 window.setAcceptsMouseMovedEvents(true);
131 if let Some((w, h)) = settings.min_size {
132 window.setContentMinSize(NSSize::new(w as f64, h as f64));
133 }
134 window.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
135
136 let view = CceView::new(mtm, rect);
137 let layer = CAMetalLayer::new();
138 layer.setOpaque(false);
139 let scale = window.backingScaleFactor();
140 layer.setContentsScale(scale);
141 // A layer-HOSTING view: the layer is set before the view wants one.
142 view.setLayer(Some(&layer));
143 view.setWantsLayer(true);
144 window.setContentView(Some(&view));
145 let _ = window.makeFirstResponder(Some(&view));
146 window.center();
147
148 let fs = if app.load_system_fonts() {
149 crate::create_font_system_with_system_fonts()
150 } else {
151 crate::create_font_system()
152 };
153 let (pw, ph) = physical((settings.width as f32, settings.height as f32), scale);
154 let layer_ptr: *const std::ffi::c_void = Retained::as_ptr(&layer).cast();
155 // SAFETY: the layer is held by the shell, which drops the renderer first.
156 let mut renderer = match unsafe { VkRenderer::try_new_for(SurfaceTarget::Metal { layer: layer_ptr }, pw, ph, 0.0) } {
157 Ok(r) => r,
158 Err(e) => {
159 log::error!("[mac] no renderer for the window: {e}");
160 return;
161 }
162 };
163 app.renderer_init(&mut renderer);
164
165 let shell = MacShell {
166 renderer,
167 app,
168 driver: Driver::new(),
169 redraw: true,
170 exit: false,
171 rx,
172 fs,
173 swash: cosmic_text::SwashCache::new(),
174 items: Vec::new(),
175 damage_owed: true,
176 logical: (0.0, 0.0),
177 scale,
178 just_configured: true,
179 cursor: CursorIcon::Default,
180 pinch: 1.0,
181 pacer: Pacer::new(settings.title),
182 generation: 0,
183 due: None,
184 last_turn: Instant::now(),
185 stopped: false,
186 ime_caret: None,
187 mtm,
188 layer,
189 view: view.clone(),
190 window: window.clone(),
191 };
192 let shell = RefCell::new(shell);
193 shell.borrow_mut().measure();
194 SINK.with(|s| *s.borrow_mut() = Some(Box::new(move |ev| shell.borrow_mut().event(ev))));
195 // A send from any thread hops to the main queue and asks for a turn.
196 set_wake(Some(std::sync::Arc::new(|| DispatchQueue::main().exec_async(|| {
197 send(Ev::Wake);
198 }))));
199
200 window.makeKeyAndOrderFront(None);
201 #[allow(deprecated)]
202 ns_app.activateIgnoringOtherApps(true);
203 send(Ev::Wake);
204 ns_app.run();
205
206 // `stop:` returned the run loop: the app has exited. The shell — the
207 // renderer before the layer it presents to — goes with the sink.
208 set_wake(None);
209 SINK.with(|s| s.borrow_mut().take());
210 drop(view);
211 drop(window);
212 }
213
214 /// What the view, the delegate and the loop's dispatches hand the shell, in
215 /// plain values: the shell is generic over the app and the AppKit classes
216 /// cannot be, so they reach it through [`SINK`].
217 enum Ev {
218 Moved { x: f32, y: f32 },
219 Entered { x: f32, y: f32 },
220 Exited,
221 /// Answers true when the press is a window drag the view must start.
222 Pressed { number: i64, flags: u64, x: f32, y: f32 },
223 Released { number: i64, flags: u64, x: f32, y: f32 },
224 Scrolled { dx: f64, dy: f64, precise: bool, inverted: bool, phase: u64, momentum: u64, flags: u64, x: f32, y: f32 },
225 Magnified { phase: u64, magnification: f64 },
226 Keyed { code: u16, chars: String, unmodified: String, flags: u64, down: bool },
227 /// The input method's composition changed (marked text).
228 Preedit(Option<crate::ime::Preedit>),
229 /// The input method committed text.
230 Commit(String),
231 FlagsChanged { code: u16, flags: u64 },
232 Focused(bool),
233 /// The window's size or backing scale changed.
234 Resized,
235 /// The close button, or Quit: the app is asked to exit.
236 CloseRequested,
237 /// A turn scheduled under this generation is due.
238 Turn(u64),
239 /// Turn soon.
240 Wake,
241 }
242
243 thread_local! {
244 static SINK: RefCell<Option<Box<dyn FnMut(Ev) -> bool>>> = const { RefCell::new(None) };
245 }
246
247 /// Hand `ev` to the shell. An event raised from inside a dispatch (AppKit
248 /// calling back while the app runs) finds the shell busy and is dropped,
249 /// as the browser shell drops one; so is everything after the exit.
250 fn send(ev: Ev) -> bool {
251 SINK.with(|s| match s.try_borrow_mut() {
252 Ok(mut sink) => sink.as_mut().is_some_and(|f| f(ev)),
253 Err(_) => false,
254 })
255 }
256
257 /// The window's side of the run loop, as the Wayland shell's `EngineState`
258 /// is the compositor's. Fields drop in order: the renderer before the layer
259 /// it presents to.
260 struct MacShell<A: Application> {
261 renderer: VkRenderer,
262 app: A,
263 driver: Driver,
264 redraw: bool,
265 exit: bool,
266 rx: Receiver<A::Message>,
267 fs: cosmic_text::FontSystem,
268 swash: cosmic_text::SwashCache,
269 items: Vec<DlText>,
270 damage_owed: bool,
271 /// The view's size in points, and the window's backing scale.
272 logical: (f32, f32),
273 scale: f64,
274 just_configured: bool,
275 cursor: CursorIcon,
276 /// The pinch's cumulative scale (AppKit reports each step's change).
277 pinch: f32,
278 pacer: Pacer,
279 /// The generation of the turn that is due; older dispatches are stale.
280 generation: u64,
281 due: Option<Instant>,
282 last_turn: Instant,
283 stopped: bool,
284 /// The caret the input method was last told of.
285 ime_caret: Option<[f32; 4]>,
286 mtm: MainThreadMarker,
287 layer: Retained<CAMetalLayer>,
288 view: Retained<CceView>,
289 window: Retained<NSWindow>,
290 }
291
292 impl<A: Application> MacShell<A> {
293 fn event(&mut self, ev: Ev) -> bool {
294 if self.stopped {
295 return false;
296 }
297 let mut drag = false;
298 match ev {
299 Ev::Turn(g) => {
300 if g == self.generation {
301 self.take_turn();
302 }
303 return false;
304 }
305 Ev::Wake => {}
306 Ev::Moved { x, y } => {
307 let pos = self.at(x, y);
308 let (driver, t) = self.turn();
309 driver.pointer_motion(t, pos);
310 self.update_cursor();
311 }
312 Ev::Entered { x, y } => {
313 let pos = self.at(x, y);
314 let (driver, t) = self.turn();
315 driver.pointer_enter(t, pos);
316 self.update_cursor();
317 }
318 Ev::Exited => {
319 let (driver, t) = self.turn();
320 driver.pointer_leave(t);
321 }
322 Ev::Pressed { number, flags, x, y } => {
323 let Some(btn) = appkit::button(number, flags) else { return false };
324 self.sync_mods(flags);
325 let pos = self.at(x, y);
326 // AppKit resizes from its own window edges; a move is a drag.
327 let site = PressSite { size: self.size(), on_popup: false, can_grab: true, own_edges: true };
328 let (driver, t) = self.turn();
329 drag = driver.pointer_press(t, btn, pos, site) == Press::Move;
330 }
331 Ev::Released { number, flags, x, y } => {
332 let Some(btn) = appkit::button(number, flags) else { return false };
333 let pos = self.at(x, y);
334 let (driver, t) = self.turn();
335 driver.pointer_release(t, btn, pos);
336 self.update_cursor();
337 }
338 Ev::Scrolled { dx, dy, precise, inverted, phase, momentum, flags, x, y } => {
339 // The system's natural-scrolling setting is the one in force
340 // here, not input.kdl's: value controls read it through this.
341 if precise {
342 crate::input::force_natural_scroll(Some(inverted));
343 }
344 let Some(frame) = appkit::scroll_frame(dx, dy, precise, inverted, phase, momentum) else { return false };
345 self.sync_mods(flags);
346 let pos = self.at(x, y);
347 let (driver, t) = self.turn();
348 driver.scroll(t, frame, pos);
349 }
350 Ev::Magnified { phase, magnification } => {
351 if phase & appkit::phase::BEGAN != 0 {
352 self.pinch = 1.0;
353 self.driver.pinch_begin();
354 }
355 if magnification != 0.0 {
356 self.pinch *= 1.0 + magnification as f32;
357 let pinch = self.pinch;
358 let (driver, t) = self.turn();
359 driver.pinch_update(t, pinch);
360 }
361 if phase & (appkit::phase::ENDED | appkit::phase::CANCELLED) != 0 {
362 self.driver.pinch_end();
363 }
364 }
365 Ev::Keyed { code, chars, unmodified, flags, down } => {
366 let accel = flags & (appkit::flags::COMMAND | appkit::flags::CONTROL) != 0;
367 let Some((key, text)) = appkit::map_key(code, &chars, &unmodified, accel) else { return false };
368 self.sync_mods(flags);
369 let state = if down { ElementState::Pressed } else { ElementState::Released };
370 let (driver, t) = self.turn();
371 driver.key(t, key, text, state);
372 }
373 Ev::Preedit(preedit) => {
374 let (driver, t) = self.turn();
375 driver.preedit(t, preedit);
376 }
377 Ev::Commit(text) => {
378 let (driver, t) = self.turn();
379 driver.preedit(t, None);
380 let (driver, t) = self.turn();
381 driver.commit_text(t, text);
382 }
383 Ev::FlagsChanged { code, flags } => {
384 // A modifier key went down or up: the new state, and the key
385 // itself as the other shells report it.
386 self.sync_mods(flags);
387 let bit = match code {
388 0x37 | 0x36 => appkit::flags::COMMAND,
389 0x38 | 0x3C => appkit::flags::SHIFT,
390 0x3A | 0x3D => appkit::flags::OPTION,
391 0x3B | 0x3E => appkit::flags::CONTROL,
392 _ => 0,
393 };
394 if let (true, Some((key @ Key::Named(_), _))) = (bit != 0, appkit::map_key(code, "", "", false)) {
395 let state = if flags & bit != 0 { ElementState::Pressed } else { ElementState::Released };
396 let (driver, t) = self.turn();
397 driver.key(t, key, None, state);
398 }
399 }
400 Ev::Focused(focused) => {
401 let (driver, t) = self.turn();
402 driver.keyboard_focus(t, focused);
403 }
404 Ev::Resized => {
405 if self.measure() {
406 self.just_configured = true;
407 }
408 }
409 Ev::CloseRequested => self.exit = true,
410 }
411 self.wake();
412 drag
413 }
414
415 /// Ask for a turn one ACTIVE frame after the last at the latest, unless
416 /// one is due sooner already.
417 fn wake(&mut self) {
418 let at = (self.last_turn + ACTIVE_DISPATCH).max(Instant::now());
419 if self.due.is_some_and(|d| d <= at) {
420 return;
421 }
422 self.schedule(at.saturating_duration_since(Instant::now()));
423 }
424
425 fn schedule(&mut self, after: Duration) {
426 self.generation += 1;
427 self.due = Some(Instant::now() + after);
428 let g = self.generation;
429 let when = DispatchTime::try_from(after).unwrap_or(DispatchTime::NOW);
430 let _ = DispatchQueue::main().after(when, move || {
431 send(Ev::Turn(g));
432 });
433 }
434
435 fn take_turn(&mut self) {
436 self.due = None;
437 self.last_turn = Instant::now();
438 while let Ok(msg) = self.rx.try_recv() {
439 let mut rebuild = false;
440 self.app.update(msg, &mut rebuild, &mut self.exit);
441 self.redraw |= rebuild;
442 }
443 let mut pacer = std::mem::replace(&mut self.pacer, Pacer::new(String::new()));
444 let step = pacer.turn(self);
445 self.pacer = pacer;
446 match step {
447 Step::Exit => self.finish(),
448 Step::Sleep(d) => {
449 if self.due.is_none() {
450 self.schedule(d);
451 }
452 }
453 }
454 }
455
456 /// The app exited: let it take its leave, then return the run loop.
457 fn finish(&mut self) {
458 self.stopped = true;
459 self.app.on_exit();
460 let ns_app = NSApplication::sharedApplication(self.mtm);
461 ns_app.stop(None);
462 // `stop:` takes effect after the next event; this is it.
463 if let Some(ev) = NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2(
464 NSEventType::ApplicationDefined,
465 NSPoint::new(0.0, 0.0),
466 NSEventModifierFlags::empty(),
467 0.0,
468 0,
469 None,
470 0,
471 0,
472 0,
473 ) {
474 ns_app.postEvent_atStart(&ev, true);
475 }
476 }
477
478 /// Read the view's size and the backing scale, and size the drawable to
479 /// them. Whether either changed.
480 fn measure(&mut self) -> bool {
481 let bounds = self.view.bounds();
482 let (w, h) = (bounds.size.width as f32, bounds.size.height as f32);
483 let scale = self.window.backingScaleFactor();
484 if (w, h) == self.logical && scale == self.scale {
485 return false;
486 }
487 self.logical = (w, h);
488 if scale != self.scale {
489 self.scale = scale;
490 crate::scale::set_scale_factor(scale as f32);
491 }
492 // MoltenVK reports the layer's bounds times its contents scale as
493 // the surface's extent.
494 self.layer.setContentsScale(scale);
495 let (pw, ph) = physical(self.logical, self.scale);
496 self.renderer.resize(pw, ph);
497 self.redraw = true;
498 true
499 }
500
501 /// Keep the input method in step with the frame just drawn: a
502 /// composition a widget dropped, or one with no widget editing any more,
503 /// is discarded (the marked text cleared first, so the `unmarkText` this
504 /// may call commits nothing); a caret that moved has the candidate
505 /// window follow it.
506 fn sync_input_method(&mut self) {
507 let caret = crate::ime::caret();
508 let drop = crate::ime::take_reset() || (caret.is_none() && !self.view.ivars().marked.borrow().is_empty());
509 let context = self.view.inputContext();
510 if drop {
511 self.view.ivars().marked.borrow_mut().clear();
512 crate::ime::set_preedit(None);
513 if let Some(c) = &context {
514 c.discardMarkedText();
515 }
516 }
517 if caret != self.ime_caret {
518 self.ime_caret = caret;
519 if let Some(c) = &context {
520 c.invalidateCharacterCoordinates();
521 }
522 }
523 }
524
525 fn size(&self) -> LogicalSize {
526 LogicalSize::new(self.logical.0, self.logical.1)
527 }
528
529 fn at(&mut self, x: f32, y: f32) -> LogicalPosition {
530 self.driver.cursor_pos = (x, y);
531 LogicalPosition::new(x, y)
532 }
533
534 fn sync_mods(&mut self, flags: u64) {
535 let mods = appkit::modifiers(flags);
536 if mods != self.driver.mods {
537 self.driver.set_modifiers(&mut self.app, mods);
538 }
539 }
540
541 fn update_cursor(&mut self) {
542 let (x, y) = self.driver.cursor_pos;
543 let icon = self.driver.cursor_icon_at(&self.app, x, y, self.size());
544 if icon != self.cursor {
545 self.cursor = icon;
546 ns_cursor(icon).set();
547 }
548 }
549 }
550
551 impl<A: Application> Shell for MacShell<A> {
552 type App = A;
553
554 fn turn(&mut self) -> (&mut Driver, Turn<'_, A>) {
555 (&mut self.driver, Turn { app: &mut self.app, redraw: &mut self.redraw, exit: &mut self.exit })
556 }
557
558 fn app(&self) -> &A {
559 &self.app
560 }
561
562 fn redraw(&mut self) -> &mut bool {
563 &mut self.redraw
564 }
565
566 fn exit_requested(&self) -> bool {
567 self.exit
568 }
569
570 fn take_just_configured(&mut self) -> bool {
571 std::mem::replace(&mut self.just_configured, false)
572 }
573
574 fn request_size(&mut self, w: u32, h: u32) {
575 if (w as f32, h as f32) != self.logical {
576 self.window.setContentSize(NSSize::new(w as f64, h as f64));
577 self.measure();
578 }
579 }
580
581 fn sync(&mut self) {
582 // No popup window to host the menu yet: it is drawn in the window
583 // and kept inside it, frames asked for while a page turn animates.
584 if context_menu::is_visible() {
585 if context_menu::is_turning() {
586 self.redraw = true;
587 }
588 context_menu::set_hosted(false);
589 context_menu::constrain_to(0.0, 0.0, self.logical.0, self.logical.1);
590 }
591 }
592
593 fn set_title(&mut self, title: &str) {
594 self.window.setTitle(&NSString::from_str(title));
595 }
596
597 fn frame_pending(&mut self) -> bool {
598 false
599 }
600
601 fn configured(&self) -> bool {
602 self.logical.0 > 0.0 && self.logical.1 > 0.0
603 }
604
605 fn present(&mut self, _fresh: bool) {
606 let size = self.size();
607 let frame = build_frame(&mut self.app, &mut self.fs, size, self.scale, &mut self.damage_owed, &mut self.items);
608 if frame.dl_text {
609 let spans = frame.text_spans(&self.items);
610 self.renderer.prepare_text(&mut self.fs, &mut self.swash, &spans);
611 }
612 let (pw, ph) = physical(self.logical, self.scale);
613 let e = self.renderer.pending_extent();
614 if (e.width, e.height) != (pw, ph) {
615 self.renderer.resize(pw, ph);
616 }
617 if self.app.stage_renderer(&mut self.renderer, size, self.scale) {
618 self.redraw = true;
619 }
620 if self.renderer.draw_frame_2d(frame.frame2d()) {
621 self.damage_owed = false;
622 } else {
623 self.redraw = true;
624 }
625 self.sync_input_method();
626 }
627 }
628
629 fn physical(logical: (f32, f32), scale: f64) -> (u32, u32) {
630 let s = scale as f32;
631 ((logical.0 * s).round().max(1.0) as u32, (logical.1 * s).round().max(1.0) as u32)
632 }
633
634 /// The toolkit's cursor as AppKit's. AppKit has no public diagonal resize
635 /// cursors before macOS 15, so those are the arrow; the two straight ones
636 /// are deprecated there, for the directional cursors macOS 15 added, and
637 /// still what every earlier release has.
638 #[allow(deprecated)]
639 fn ns_cursor(icon: CursorIcon) -> Retained<NSCursor> {
640 match icon {
641 CursorIcon::Pointer => NSCursor::pointingHandCursor(),
642 CursorIcon::Text | CursorIcon::VerticalText => NSCursor::IBeamCursor(),
643 CursorIcon::Crosshair | CursorIcon::Cell => NSCursor::crosshairCursor(),
644 CursorIcon::Grab => NSCursor::openHandCursor(),
645 CursorIcon::Grabbing | CursorIcon::Move | CursorIcon::AllScroll => NSCursor::closedHandCursor(),
646 CursorIcon::NotAllowed | CursorIcon::NoDrop => NSCursor::operationNotAllowedCursor(),
647 CursorIcon::EResize | CursorIcon::WResize | CursorIcon::EwResize | CursorIcon::ColResize => {
648 NSCursor::resizeLeftRightCursor()
649 }
650 CursorIcon::NResize | CursorIcon::SResize | CursorIcon::NsResize | CursorIcon::RowResize => {
651 NSCursor::resizeUpDownCursor()
652 }
653 CursorIcon::ContextMenu => NSCursor::contextualMenuCursor(),
654 CursorIcon::Copy => NSCursor::dragCopyCursor(),
655 CursorIcon::Alias => NSCursor::dragLinkCursor(),
656 _ => NSCursor::arrowCursor(),
657 }
658 }
659
660 /// An application menu with Quit (⌘Q) in it: every Mac app has one, and
661 /// without it ⌘Q does nothing.
662 fn main_menu(mtm: MainThreadMarker, title: &str) -> Retained<NSMenu> {
663 let bar = NSMenu::new(mtm);
664 let app_item = NSMenuItem::new(mtm);
665 let app_menu = NSMenu::new(mtm);
666 // SAFETY: `terminate:` is NSApplication's, which answers it through the
667 // delegate's `applicationShouldTerminate:` below.
668 let quit = unsafe {
669 NSMenuItem::initWithTitle_action_keyEquivalent(
670 NSMenuItem::alloc(mtm),
671 &NSString::from_str(&format!("Quit {title}")),
672 Some(sel!(terminate:)),
673 ns_string!("q"),
674 )
675 };
676 app_menu.addItem(&quit);
677 app_item.setSubmenu(Some(&app_menu));
678 bar.addItem(&app_item);
679 bar
680 }
681
682 /// Where an event's pointer is, in the view's (flipped) coordinates.
683 fn location(view: &NSView, event: &NSEvent) -> (f32, f32) {
684 let p = view.convertPoint_fromView(event.locationInWindow(), None);
685 (p.x as f32, p.y as f32)
686 }
687
688 /// The view's input-method state, for the `NSTextInputClient` methods.
689 #[derive(Default)]
690 struct ViewIme {
691 /// The marked text (the composition) as the input method last set it.
692 marked: RefCell<String>,
693 /// Inside `interpretKeyEvents:` for a key press, whose characters are
694 /// these; and whether the input method took the press.
695 in_key: Cell<bool>,
696 consumed: Cell<bool>,
697 key_chars: RefCell<String>,
698 }
699
700 /// The text of what an input method hands over: an `NSString`, or an
701 /// `NSAttributedString` around one.
702 fn ns_text(obj: &AnyObject) -> String {
703 if let Some(a) = obj.downcast_ref::<NSAttributedString>() {
704 a.string().to_string()
705 } else if let Some(s) = obj.downcast_ref::<NSString>() {
706 s.to_string()
707 } else {
708 String::new()
709 }
710 }
711
712 define_class!(
713 // SAFETY: NSView has no subclassing requirements, and CceView no Drop.
714 #[unsafe(super(NSView, objc2_app_kit::NSResponder, NSObject))]
715 #[thread_kind = MainThreadOnly]
716 #[name = "CceUiView"]
717 #[ivars = ViewIme]
718 struct CceView;
719
720 unsafe impl NSObjectProtocol for CceView {}
721
722 // The input method's side of the view: AppKit calls these from inside
723 // `interpretKeyEvents:` (see `key`), and the candidate window asks where
724 // the caret is.
725 unsafe impl NSTextInputClient for CceView {
726 #[unsafe(method(insertText:replacementRange:))]
727 unsafe fn insert_text(&self, string: &AnyObject, _replacement: NSRange) {
728 let text = ns_text(string);
729 let ime = self.ivars();
730 let was_marked = !std::mem::take(&mut *ime.marked.borrow_mut()).is_empty();
731 // A plain key typing its own character is left to the key path,
732 // which keeps its named keys and the driver's repeat.
733 if ime.in_key.get() && !was_marked && text == *ime.key_chars.borrow() {
734 return;
735 }
736 ime.consumed.set(true);
737 send(Ev::Commit(text));
738 }
739
740 #[unsafe(method(doCommandBySelector:))]
741 unsafe fn do_command(&self, _selector: Sel) {
742 // A key the input method does not take (Return, Backspace, an
743 // arrow): left to the key path.
744 }
745
746 #[unsafe(method(setMarkedText:selectedRange:replacementRange:))]
747 unsafe fn set_marked_text(&self, string: &AnyObject, selected: NSRange, _replacement: NSRange) {
748 let text = ns_text(string);
749 let ime = self.ivars();
750 ime.consumed.set(true);
751 *ime.marked.borrow_mut() = text.clone();
752 let cursor = crate::backend::dom::utf16_range_to_bytes(
753 &text,
754 Some(selected.location as u32),
755 Some((selected.location + selected.length) as u32),
756 );
757 send(Ev::Preedit((!text.is_empty()).then(|| crate::ime::Preedit { text, cursor })));
758 }
759
760 #[unsafe(method(unmarkText))]
761 fn unmark_text(&self) {
762 // Accept the composition as it stands.
763 let text = std::mem::take(&mut *self.ivars().marked.borrow_mut());
764 if !text.is_empty() {
765 send(Ev::Commit(text));
766 }
767 }
768
769 #[unsafe(method(selectedRange))]
770 fn selected_range(&self) -> NSRange {
771 NSRange::new(NSNotFound as usize, 0)
772 }
773
774 #[unsafe(method(markedRange))]
775 fn marked_range(&self) -> NSRange {
776 let marked = self.ivars().marked.borrow();
777 if marked.is_empty() {
778 NSRange::new(NSNotFound as usize, 0)
779 } else {
780 NSRange::new(0, marked.encode_utf16().count())
781 }
782 }
783
784 #[unsafe(method(hasMarkedText))]
785 fn has_marked_text(&self) -> bool {
786 !self.ivars().marked.borrow().is_empty()
787 }
788
789 #[unsafe(method_id(attributedSubstringForProposedRange:actualRange:))]
790 unsafe fn attributed_substring(&self, _range: NSRange, _actual: NSRangePointer) -> Option<Retained<NSAttributedString>> {
791 None
792 }
793
794 #[unsafe(method_id(validAttributesForMarkedText))]
795 fn valid_attributes(&self) -> Retained<NSArray<NSAttributedStringKey>> {
796 NSArray::new()
797 }
798
799 /// Where the candidate window goes: under the editing widget's caret,
800 /// in screen coordinates.
801 #[unsafe(method(firstRectForCharacterRange:actualRange:))]
802 unsafe fn first_rect(&self, _range: NSRange, _actual: NSRangePointer) -> NSRect {
803 let [x, y, w, h] = crate::ime::caret().unwrap_or([0.0, 0.0, 1.0, 16.0]);
804 let caret = NSRect::new(NSPoint::new(x as f64, y as f64), NSSize::new(w.max(1.0) as f64, h as f64));
805 let in_window = self.convertRect_toView(caret, None);
806 self.window().map_or(in_window, |w| w.convertRectToScreen(in_window))
807 }
808
809 #[unsafe(method(characterIndexForPoint:))]
810 fn character_index(&self, _point: NSPoint) -> NSUInteger {
811 NSNotFound as NSUInteger
812 }
813 }
814
815 impl CceView {
816 #[unsafe(method(isFlipped))]
817 fn is_flipped(&self) -> bool {
818 true
819 }
820
821 #[unsafe(method(acceptsFirstResponder))]
822 fn accepts_first_responder(&self) -> bool {
823 true
824 }
825
826 #[unsafe(method(acceptsFirstMouse:))]
827 fn accepts_first_mouse(&self, _event: Option<&NSEvent>) -> bool {
828 true
829 }
830
831 #[unsafe(method(mouseMoved:))]
832 fn mouse_moved(&self, event: &NSEvent) {
833 let (x, y) = location(self, event);
834 send(Ev::Moved { x, y });
835 }
836
837 #[unsafe(method(mouseDragged:))]
838 fn mouse_dragged(&self, event: &NSEvent) {
839 let (x, y) = location(self, event);
840 send(Ev::Moved { x, y });
841 }
842
843 #[unsafe(method(rightMouseDragged:))]
844 fn right_mouse_dragged(&self, event: &NSEvent) {
845 let (x, y) = location(self, event);
846 send(Ev::Moved { x, y });
847 }
848
849 #[unsafe(method(otherMouseDragged:))]
850 fn other_mouse_dragged(&self, event: &NSEvent) {
851 let (x, y) = location(self, event);
852 send(Ev::Moved { x, y });
853 }
854
855 #[unsafe(method(mouseEntered:))]
856 fn mouse_entered(&self, event: &NSEvent) {
857 let (x, y) = location(self, event);
858 send(Ev::Entered { x, y });
859 }
860
861 #[unsafe(method(mouseExited:))]
862 fn mouse_exited(&self, _event: &NSEvent) {
863 send(Ev::Exited);
864 }
865
866 #[unsafe(method(mouseDown:))]
867 fn mouse_down(&self, event: &NSEvent) {
868 self.press(event);
869 }
870
871 #[unsafe(method(rightMouseDown:))]
872 fn right_mouse_down(&self, event: &NSEvent) {
873 self.press(event);
874 }
875
876 #[unsafe(method(otherMouseDown:))]
877 fn other_mouse_down(&self, event: &NSEvent) {
878 self.press(event);
879 }
880
881 #[unsafe(method(mouseUp:))]
882 fn mouse_up(&self, event: &NSEvent) {
883 self.release(event);
884 }
885
886 #[unsafe(method(rightMouseUp:))]
887 fn right_mouse_up(&self, event: &NSEvent) {
888 self.release(event);
889 }
890
891 #[unsafe(method(otherMouseUp:))]
892 fn other_mouse_up(&self, event: &NSEvent) {
893 self.release(event);
894 }
895
896 #[unsafe(method(scrollWheel:))]
897 fn scroll_wheel(&self, event: &NSEvent) {
898 let (x, y) = location(self, event);
899 send(Ev::Scrolled {
900 dx: event.scrollingDeltaX(),
901 dy: event.scrollingDeltaY(),
902 precise: event.hasPreciseScrollingDeltas(),
903 inverted: event.isDirectionInvertedFromDevice(),
904 phase: event.phase().0 as u64,
905 momentum: event.momentumPhase().0 as u64,
906 flags: event.modifierFlags().0 as u64,
907 x,
908 y,
909 });
910 }
911
912 #[unsafe(method(magnifyWithEvent:))]
913 fn magnify(&self, event: &NSEvent) {
914 send(Ev::Magnified { phase: event.phase().0 as u64, magnification: event.magnification() });
915 }
916
917 #[unsafe(method(keyDown:))]
918 fn key_down(&self, event: &NSEvent) {
919 self.key(event, true);
920 }
921
922 #[unsafe(method(keyUp:))]
923 fn key_up(&self, event: &NSEvent) {
924 self.key(event, false);
925 }
926
927 #[unsafe(method(flagsChanged:))]
928 fn flags_changed(&self, event: &NSEvent) {
929 send(Ev::FlagsChanged { code: event.keyCode(), flags: event.modifierFlags().0 as u64 });
930 }
931 }
932 );
933
934 impl CceView {
935 fn new(mtm: MainThreadMarker, frame: NSRect) -> Retained<Self> {
936 let this = Self::alloc(mtm).set_ivars(ViewIme::default());
937 // SAFETY: NSView's designated initializer.
938 let view: Retained<Self> = unsafe { msg_send![super(this), initWithFrame: frame] };
939 // Enter, exit and motion wherever the view is, window key or not.
940 let options = NSTrackingAreaOptions::MouseEnteredAndExited
941 | NSTrackingAreaOptions::MouseMoved
942 | NSTrackingAreaOptions::ActiveAlways
943 | NSTrackingAreaOptions::InVisibleRect;
944 // SAFETY: the view owns the area and outlives it; no user info.
945 let area = unsafe {
946 NSTrackingArea::initWithRect_options_owner_userInfo(
947 NSTrackingArea::alloc(),
948 NSRect::ZERO,
949 options,
950 Some(&view),
951 None,
952 )
953 };
954 view.addTrackingArea(&area);
955 view
956 }
957
958 fn press(&self, event: &NSEvent) {
959 let (x, y) = location(self, event);
960 let ev = Ev::Pressed { number: event.buttonNumber() as i64, flags: event.modifierFlags().0 as u64, x, y };
961 if send(ev) {
962 if let Some(window) = self.window() {
963 window.performWindowDragWithEvent(event);
964 }
965 }
966 }
967
968 fn release(&self, event: &NSEvent) {
969 let (x, y) = location(self, event);
970 send(Ev::Released { number: event.buttonNumber() as i64, flags: event.modifierFlags().0 as u64, x, y });
971 }
972
973 fn key(&self, event: &NSEvent, down: bool) {
974 let text = |s: Option<Retained<NSString>>| s.map(|s| s.to_string()).unwrap_or_default();
975 let flags = event.modifierFlags().0 as u64;
976 // While a widget is editing text, a key press goes to the input
977 // method first (`interpretKeyEvents:`, which calls back into the
978 // `NSTextInputClient` methods above); what it takes is composition
979 // or a commit, what it leaves takes the key path below. A ⌘
980 // shortcut never goes, and nothing does while nothing is editing,
981 // so an input method left on does not eat an app's single-key
982 // commands.
983 let ime = self.ivars();
984 if down && flags & appkit::flags::COMMAND == 0 && crate::ime::caret().is_some() {
985 ime.in_key.set(true);
986 ime.consumed.set(false);
987 *ime.key_chars.borrow_mut() = text(event.characters());
988 self.interpretKeyEvents(&NSArray::from_slice(&[event]));
989 ime.in_key.set(false);
990 if ime.consumed.get() {
991 return;
992 }
993 }
994 // The driver repeats a held key itself, as it does on Wayland.
995 if event.isARepeat() {
996 return;
997 }
998 let keyed = |down| Ev::Keyed {
999 code: event.keyCode(),
1000 chars: text(event.characters()),
1001 unmodified: text(event.charactersIgnoringModifiers()),
1002 flags,
1003 down,
1004 };
1005 send(keyed(down));
1006 // AppKit sends no keyUp for a key pressed with Command held, so a ⌘
1007 // shortcut is released as it is pressed: otherwise the driver would
1008 // take the key as held, and repeat ⌘Z until the window lost focus.
1009 if down && flags & appkit::flags::COMMAND != 0 {
1010 send(keyed(false));
1011 }
1012 }
1013 }
1014
1015 define_class!(
1016 // SAFETY: NSObject has no subclassing requirements, and Delegate no Drop.
1017 #[unsafe(super(NSObject))]
1018 #[thread_kind = MainThreadOnly]
1019 #[name = "CceUiDelegate"]
1020 struct Delegate;
1021
1022 unsafe impl NSObjectProtocol for Delegate {}
1023
1024 unsafe impl NSApplicationDelegate for Delegate {
1025 /// Quit asks the app to exit, as a close does; the shell stops the
1026 /// run loop once it has.
1027 #[unsafe(method(applicationShouldTerminate:))]
1028 fn should_terminate(&self, _sender: &NSApplication) -> NSApplicationTerminateReply {
1029 send(Ev::CloseRequested);
1030 NSApplicationTerminateReply::TerminateCancel
1031 }
1032 }
1033
1034 unsafe impl NSWindowDelegate for Delegate {
1035 #[unsafe(method(windowShouldClose:))]
1036 fn window_should_close(&self, _sender: &NSWindow) -> bool {
1037 send(Ev::CloseRequested);
1038 false
1039 }
1040
1041 #[unsafe(method(windowDidResize:))]
1042 fn window_did_resize(&self, _notification: &NSNotification) {
1043 send(Ev::Resized);
1044 }
1045
1046 #[unsafe(method(windowDidChangeBackingProperties:))]
1047 fn window_did_change_backing(&self, _notification: &NSNotification) {
1048 send(Ev::Resized);
1049 }
1050
1051 #[unsafe(method(windowDidBecomeKey:))]
1052 fn window_did_become_key(&self, _notification: &NSNotification) {
1053 send(Ev::Focused(true));
1054 }
1055
1056 #[unsafe(method(windowDidResignKey:))]
1057 fn window_did_resign_key(&self, _notification: &NSNotification) {
1058 send(Ev::Focused(false));
1059 }
1060 }
1061 );
1062
1063 impl Delegate {
1064 fn new(mtm: MainThreadMarker) -> Retained<Self> {
1065 let this = Self::alloc(mtm).set_ivars(());
1066 // SAFETY: NSObject's `init`.
1067 unsafe { msg_send![super(this), init] }
1068 }
1069 }