denise_macos/view.rs
1//! `DeniseView`: an `NSView` subclass a Cocoa application can drop into its own
2//! window.
3//!
4//! The split here is the same one `Ui::render` makes, and for the same reason.
5//! [`DeniseView::update`] consumes input, repaints the surface and tells AppKit
6//! which rectangles changed; `drawRect:` only blits. Doing both in `drawRect:`
7//! would mean the tree could not damage anything, because by then AppKit has
8//! already decided what it is going to composite.
9
10use std::cell::RefCell;
11
12use denise::{ElementState, InputEvent, Modifiers, Point, PointerButton, Rect, Size, Surface};
13use objc2::rc::Retained;
14use objc2::runtime::AnyObject;
15use objc2::{AnyThread, DefinedClass, MainThreadOnly, define_class, msg_send};
16use objc2_app_kit::{
17 NSEvent, NSEventModifierFlags, NSGraphicsContext, NSTrackingArea, NSTrackingAreaOptions, NSView,
18};
19use objc2_core_foundation::CGFloat;
20use objc2_foundation::{MainThreadMarker, NSPoint, NSRect, NSSize};
21use objc2_io_surface::IOSurfaceRef;
22use objc2_quartz_core::CATransaction;
23
24use crate::Error;
25use crate::keymap::key_code;
26use crate::surface::ViewSurface;
27
28/// Nominal pixels per wheel notch, for the coarse scroll a mouse produces. A
29/// trackpad reports precise deltas already in points and needs no scaling.
30const LINE_HEIGHT_PX: f32 = 16.0;
31
32/// What the application implements to put something in the view.
33///
34/// Deliberately not "here is a `Ui`": a signage application drawing its own scene
35/// with `denise-render` has no tree at all, and this backend has
36/// no business requiring one.
37pub trait ViewDelegate {
38 /// Handles `events`, repaints `surface`, and appends what changed to `damage`.
39 ///
40 /// `damage` arrives empty. Leaving it empty means nothing changed and AppKit
41 /// is told nothing, which is what makes an idle panel cost nothing.
42 fn update(&mut self, surface: &mut ViewSurface, events: &[InputEvent], damage: &mut Vec<Rect>);
43
44 /// Milliseconds until this delegate wants updating again, or `None` if it is
45 /// waiting only on input.
46 ///
47 /// A blinking caret is the reason this exists. The host turns it into an
48 /// `NSTimer` — see the `embed` example.
49 fn next_wake_ms(&self) -> Option<u64> {
50 None
51 }
52}
53
54/// The view's mutable state. Reachable through [`DeniseView::state`].
55pub struct ViewState {
56 /// The pixels, and the geometry that describes them.
57 pub surface: ViewSurface,
58 delegate: Box<dyn ViewDelegate>,
59 events: Vec<InputEvent>,
60 damage: Vec<Rect>,
61 /// Modifiers as of the last event, because AppKit reports them per event and
62 /// Denise's `InputEvent::Text` has nowhere to carry them.
63 modifiers: Modifiers,
64 tracking: Option<Retained<NSTrackingArea>>,
65}
66
67impl ViewState {
68 /// Queues an event for the next [`DeniseView::update`].
69 fn push(&mut self, event: InputEvent) {
70 self.events.push(event);
71 }
72}
73
74define_class!(
75 /// An `NSView` that draws a Denise surface and forwards Cocoa input to it.
76 ///
77 /// Create with [`DeniseView::new`] and add it to a host view as usual. It is
78 /// an ordinary `NSView`: it can be autoresized, put in a split view, or made
79 /// the content view of a window.
80 // SAFETY:
81 // - `NSView` has no subclassing requirement beyond being used on the main
82 // thread, which `MainThreadOnly` enforces.
83 // - `DeniseView` does not implement `Drop`; the ivars do their own.
84 #[unsafe(super(NSView))]
85 #[thread_kind = MainThreadOnly]
86 #[name = "DeniseView"]
87 #[ivars = RefCell<ViewState>]
88 pub struct DeniseView;
89
90 impl DeniseView {
91 /// Top-left origin, running downwards — the same convention Denise uses,
92 /// so nothing between here and hit testing has to flip a coordinate.
93 ///
94 /// It also decides how `ViewSurface::draw_into` orients the image, which
95 /// is why that function says so in its safety contract.
96 #[unsafe(method(isFlipped))]
97 fn is_flipped(&self) -> bool {
98 true
99 }
100
101 /// Keyboard input goes to the first responder, and a control that cannot
102 /// become one cannot be typed into.
103 #[unsafe(method(acceptsFirstResponder))]
104 fn accepts_first_responder(&self) -> bool {
105 true
106 }
107
108 /// The click that focuses a window should also reach the widget under it.
109 /// Without this the first click on an unfocused window is swallowed, which
110 /// users read as the control being broken.
111 #[unsafe(method(acceptsFirstMouse:))]
112 fn accepts_first_mouse(&self, _event: Option<&NSEvent>) -> bool {
113 true
114 }
115
116 /// Take the `updateLayer` path rather than the `drawRect:` one.
117 ///
118 /// This is the whole of the zero-copy present. Answering `true` tells
119 /// AppKit not to allocate a backing store and not to ask for drawing;
120 /// it calls `updateLayer` instead, and what that assigns is the buffer
121 /// the rasteriser has already written. Nothing is copied on the way to
122 /// the screen.
123 ///
124 /// `drawRect:` remains below for hosts that drive the view themselves.
125 #[unsafe(method(wantsUpdateLayer))]
126 fn wants_update_layer(&self) -> bool {
127 true
128 }
129
130 /// Hands the compositor the surface. Called instead of `drawRect:`.
131 #[unsafe(method(updateLayer))]
132 fn update_layer(&self) {
133 let Some(layer) = self.layer() else {
134 return;
135 };
136 let state = self.ivars().borrow();
137 let surface = state.surface.io_surface();
138
139 // A layer's contents must be told the scale it is in, or a Retina
140 // surface is drawn at twice its size and the bottom right of the
141 // panel goes missing.
142 layer.setContentsScale(CGFloat::from(state.surface.scale_factor()));
143
144 // SAFETY: `IOSurfaceRef` is toll-free bridged to the `IOSurface`
145 // class — documented, and the reason `contents` accepts one at all.
146 // The pointer is valid while `state.surface` lives, and the layer
147 // retains what it is given.
148 let contents: &AnyObject =
149 unsafe { &*(surface as *const IOSurfaceRef).cast::<AnyObject>() };
150 // Actions off: `contents` is animatable, and its default action
151 // cross-fades over a quarter of a second. A view redrawing at sixty
152 // frames a second would spend all of them fading between buffers.
153 CATransaction::begin();
154 CATransaction::setDisableActions(true);
155 // SAFETY: assigning `contents` on the main thread, which is where
156 // AppKit calls `updateLayer`.
157 unsafe { layer.setContents(Some(contents)) };
158 CATransaction::commit();
159 }
160
161 #[unsafe(method(drawRect:))]
162 fn draw_rect(&self, _dirty: NSRect) {
163 let Some(context) = NSGraphicsContext::currentContext() else {
164 return;
165 };
166 let state = self.ivars().borrow();
167 let bounds = self.bounds();
168 // SAFETY: this is AppKit's context for a flipped view — `isFlipped`
169 // above is what makes that true — and it is live for the duration of
170 // `drawRect:`.
171 unsafe { state.surface.draw_into(&context.CGContext(), bounds) };
172 }
173
174 /// AppKit calls this when the view is resized or moves between screens,
175 /// which is also when the backing scale can change.
176 #[unsafe(method(updateTrackingAreas))]
177 fn update_tracking_areas(&self) {
178 // SAFETY: calling the superclass implementation, which the docs
179 // require before installing a replacement.
180 let _: () = unsafe { msg_send![super(self), updateTrackingAreas] };
181 self.install_tracking_area();
182 }
183
184 /// A target for an `NSTimer`, for a host that wants a heartbeat rather
185 /// than its own run-loop source.
186 ///
187 /// Anything that animates on its own — a blinking caret, a progress bar —
188 /// needs waking without input. Ask [`DeniseView::next_wake_ms`] how soon,
189 /// or just fire at a fixed rate and accept the wasted wakeups: an update
190 /// with nothing to do costs one empty event list and no invalidation.
191 #[unsafe(method(deniseTick:))]
192 fn denise_tick(&self, _timer: *mut objc2::runtime::AnyObject) {
193 self.update();
194 }
195
196 #[unsafe(method(viewDidChangeBackingProperties))]
197 fn backing_properties_changed(&self) {
198 self.sync_surface_size();
199 }
200
201 // ----------------------------------------------------------- pointer
202
203 #[unsafe(method(mouseMoved:))]
204 fn mouse_moved(&self, event: &NSEvent) {
205 self.pointer_moved(event);
206 }
207
208 /// A drag is a move with a button held, and AppKit reports it separately.
209 /// A control that only handles `mouseMoved:` loses the pointer the moment
210 /// anyone presses on it.
211 #[unsafe(method(mouseDragged:))]
212 fn mouse_dragged(&self, event: &NSEvent) {
213 self.pointer_moved(event);
214 }
215
216 #[unsafe(method(rightMouseDragged:))]
217 fn right_mouse_dragged(&self, event: &NSEvent) {
218 self.pointer_moved(event);
219 }
220
221 #[unsafe(method(otherMouseDragged:))]
222 fn other_mouse_dragged(&self, event: &NSEvent) {
223 self.pointer_moved(event);
224 }
225
226 #[unsafe(method(mouseExited:))]
227 fn mouse_exited(&self, _event: &NSEvent) {
228 self.ivars().borrow_mut().push(InputEvent::PointerLeft);
229 self.update();
230 }
231
232 #[unsafe(method(mouseDown:))]
233 fn mouse_down(&self, event: &NSEvent) {
234 self.pointer_button(event, PointerButton::Left, ElementState::Down);
235 }
236
237 #[unsafe(method(mouseUp:))]
238 fn mouse_up(&self, event: &NSEvent) {
239 self.pointer_button(event, PointerButton::Left, ElementState::Up);
240 }
241
242 #[unsafe(method(rightMouseDown:))]
243 fn right_mouse_down(&self, event: &NSEvent) {
244 self.pointer_button(event, PointerButton::Right, ElementState::Down);
245 }
246
247 #[unsafe(method(rightMouseUp:))]
248 fn right_mouse_up(&self, event: &NSEvent) {
249 self.pointer_button(event, PointerButton::Right, ElementState::Up);
250 }
251
252 #[unsafe(method(otherMouseDown:))]
253 fn other_mouse_down(&self, event: &NSEvent) {
254 let button = other_button(event);
255 self.pointer_button(event, button, ElementState::Down);
256 }
257
258 #[unsafe(method(otherMouseUp:))]
259 fn other_mouse_up(&self, event: &NSEvent) {
260 let button = other_button(event);
261 self.pointer_button(event, button, ElementState::Up);
262 }
263
264 #[unsafe(method(scrollWheel:))]
265 fn scroll_wheel(&self, event: &NSEvent) {
266 let position = self.event_position(event);
267 // A trackpad reports precise deltas already in points; a wheel reports
268 // notches, which are only meaningful multiplied by a line height.
269 let precise = event.hasPreciseScrollingDeltas();
270 let scale = if precise { 1.0 } else { LINE_HEIGHT_PX };
271 let backing = self.ivars().borrow().surface.scale_factor();
272 let delta_x = -(event.scrollingDeltaX() as f32) * scale * backing;
273 // AppKit's positive y is content moving up under the fingers; Denise's
274 // positive y scrolls content down. They are opposites, and a backend
275 // that forwards the sign unchanged scrolls the wrong way.
276 let delta_y = -(event.scrollingDeltaY() as f32) * scale * backing;
277 self.ivars().borrow_mut().push(InputEvent::PointerScroll {
278 delta_x,
279 delta_y,
280 position,
281 });
282 self.update();
283 }
284
285 // ---------------------------------------------------------- keyboard
286
287 #[unsafe(method(keyDown:))]
288 fn key_down(&self, event: &NSEvent) {
289 self.key(event, ElementState::Down);
290 }
291
292 #[unsafe(method(keyUp:))]
293 fn key_up(&self, event: &NSEvent) {
294 self.key(event, ElementState::Up);
295 }
296
297 /// Shift, control, option and command produce no key events of their own;
298 /// AppKit reports them as a flags change. Without this, holding shift and
299 /// pressing Tab would look like a plain Tab.
300 #[unsafe(method(flagsChanged:))]
301 fn flags_changed(&self, event: &NSEvent) {
302 let modifiers = modifiers_of(event);
303 let code = key_code(event.keyCode());
304 let was = self.ivars().borrow().modifiers;
305 // Whether the key that changed went down or up is not reported, so it
306 // is inferred: more modifiers held than before means down.
307 let state = if bit_count(modifiers) > bit_count(was) {
308 ElementState::Down
309 } else {
310 ElementState::Up
311 };
312 let mut state_ref = self.ivars().borrow_mut();
313 state_ref.modifiers = modifiers;
314 state_ref.push(InputEvent::Key {
315 code,
316 state,
317 repeat: false,
318 modifiers,
319 });
320 drop(state_ref);
321 self.update();
322 }
323 }
324);
325
326impl DeniseView {
327 /// Creates a view of `frame` points, drawing whatever `delegate` paints.
328 ///
329 /// `scale_factor` is the backing scale to start with. AppKit corrects it
330 /// through `viewDidChangeBackingProperties` once the view has a window, so a
331 /// host that does not know it yet can pass `1.0`.
332 pub fn new(
333 mtm: MainThreadMarker,
334 frame: NSRect,
335 scale_factor: f32,
336 delegate: Box<dyn ViewDelegate>,
337 ) -> Result<Retained<Self>, Error> {
338 let size = physical_size(frame.size, scale_factor);
339 let surface = ViewSurface::new(size, scale_factor)?;
340
341 let this = Self::alloc(mtm).set_ivars(RefCell::new(ViewState {
342 surface,
343 delegate,
344 events: Vec::new(),
345 damage: Vec::new(),
346 modifiers: Modifiers::NONE,
347 tracking: None,
348 }));
349 // SAFETY: `initWithFrame:` is `NSView`'s designated initialiser and the
350 // ivars are set before it runs, as `define_class!` requires.
351 let this: Retained<Self> = unsafe { msg_send![super(this), initWithFrame: frame] };
352 // Layer-backed, because `updateLayer` above is only ever called for a
353 // view that has a layer to update.
354 this.setWantsLayer(true);
355 this.install_tracking_area();
356 Ok(this)
357 }
358
359 /// The view's state, including the surface.
360 ///
361 /// A `RefCell` because AppKit calls in re-entrantly and Rust has no way to
362 /// know that; every borrow here is short and none spans a call back into
363 /// AppKit.
364 pub fn state(&self) -> &RefCell<ViewState> {
365 self.ivars()
366 }
367
368 /// Runs the delegate over the queued input and invalidates what changed.
369 ///
370 /// Called after every event, and from the host's timer for anything that
371 /// animates on its own. Safe to call when nothing has happened: an empty
372 /// event list and no damage means no work and no invalidation.
373 pub fn update(&self) {
374 let mut borrow = self.ivars().borrow_mut();
375 let state = &mut *borrow;
376
377 state.damage.clear();
378 let events = core::mem::take(&mut state.events);
379 state
380 .delegate
381 .update(&mut state.surface, &events, &mut state.damage);
382
383 // Published here rather than by the delegate, so that a delegate written
384 // against the single-buffered version keeps working. With two surfaces
385 // alternating, `present` is what makes the frame just drawn the one the
386 // layer shows — a delegate that painted and never presented would draw
387 // for ever into a buffer nobody is looking at.
388 //
389 // A no-op when the delegate did not acquire a frame: `present` only
390 // swaps if there was something to swap, so an update that changed
391 // nothing leaves the current frame on screen.
392 let _ = state.surface.present(&state.damage);
393 // Reuse the allocation rather than the contents.
394 state.events = events;
395 state.events.clear();
396
397 // Collected before the borrow ends, because `setNeedsDisplayInRect:` can
398 // re-enter and a live `RefMut` would panic if it did.
399 let rects: Vec<NSRect> = state
400 .damage
401 .iter()
402 .map(|rect| state.surface.damage_to_points(*rect))
403 .map(|cg| {
404 NSRect::new(
405 NSPoint::new(cg.origin.x, cg.origin.y),
406 NSSize::new(cg.size.width, cg.size.height),
407 )
408 })
409 .collect();
410 drop(borrow);
411
412 for rect in rects {
413 self.setNeedsDisplayInRect(rect);
414 }
415 }
416
417 /// Milliseconds until the delegate next wants updating, or `None`.
418 pub fn next_wake_ms(&self) -> Option<u64> {
419 self.ivars().borrow().delegate.next_wake_ms()
420 }
421
422 /// Resizes the surface to match the view's current bounds and backing scale.
423 ///
424 /// Returns `true` if anything changed, in which case everything on screen is
425 /// gone and the delegate owes a full repaint. The view cannot produce one on
426 /// its own: damage belongs to whatever owns the scene.
427 pub fn sync_surface_size(&self) -> bool {
428 let bounds = self.bounds();
429 let scale = self.backing_scale();
430 let size = physical_size(bounds.size, scale);
431 if size.is_empty() {
432 return false;
433 }
434 let changed = self
435 .ivars()
436 .borrow_mut()
437 .surface
438 .resize(size, scale)
439 .unwrap_or(false);
440 if changed {
441 self.setNeedsDisplay(true);
442 }
443 changed
444 }
445
446 /// The window's backing scale, or 1.0 before the view has a window.
447 fn backing_scale(&self) -> f32 {
448 let unit = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(1.0, 1.0));
449 let backing = self.convertRectToBacking(unit);
450 if backing.size.width > 0.0 {
451 backing.size.width as f32
452 } else {
453 1.0
454 }
455 }
456
457 /// Installs a tracking area covering the whole view, so hover works without
458 /// the host having to set `acceptsMouseMovedEvents` on its window.
459 ///
460 /// An embedded control that needed the host to configure the window would be
461 /// a control that stops working the day somebody reuses the window.
462 fn install_tracking_area(&self) {
463 if let Some(old) = self.ivars().borrow_mut().tracking.take() {
464 self.removeTrackingArea(&old);
465 }
466 let options = NSTrackingAreaOptions::MouseEnteredAndExited
467 | NSTrackingAreaOptions::MouseMoved
468 | NSTrackingAreaOptions::ActiveInKeyWindow
469 | NSTrackingAreaOptions::InVisibleRect;
470 // SAFETY: the owner outlives the tracking area — it is the view holding
471 // it — and `userInfo` is allowed to be null.
472 let area = unsafe {
473 NSTrackingArea::initWithRect_options_owner_userInfo(
474 NSTrackingArea::alloc(),
475 self.bounds(),
476 options,
477 Some(self),
478 None,
479 )
480 };
481 self.addTrackingArea(&area);
482 self.ivars().borrow_mut().tracking = Some(area);
483 }
484
485 /// An event's location in physical pixels, top-left origin.
486 fn event_position(&self, event: &NSEvent) -> Point {
487 let window_point = event.locationInWindow();
488 let local = self.convertPoint_fromView(window_point, None);
489 let scale = self.backing_scale();
490 Point::new(
491 (local.x as f32 * scale).round() as i32,
492 (local.y as f32 * scale).round() as i32,
493 )
494 }
495
496 fn pointer_moved(&self, event: &NSEvent) {
497 let position = self.event_position(event);
498 self.ivars()
499 .borrow_mut()
500 .push(InputEvent::PointerMoved { position });
501 self.update();
502 }
503
504 fn pointer_button(&self, event: &NSEvent, button: PointerButton, state: ElementState) {
505 let position = self.event_position(event);
506 let modifiers = modifiers_of(event);
507 let mut ivars = self.ivars().borrow_mut();
508 ivars.modifiers = modifiers;
509 ivars.push(InputEvent::PointerButton {
510 button,
511 state,
512 position,
513 modifiers,
514 });
515 drop(ivars);
516 self.update();
517 }
518
519 fn key(&self, event: &NSEvent, state: ElementState) {
520 let code = key_code(event.keyCode());
521 let modifiers = modifiers_of(event);
522 let repeat = state == ElementState::Down && event.isARepeat();
523
524 let mut ivars = self.ivars().borrow_mut();
525 ivars.modifiers = modifiers;
526 ivars.push(InputEvent::Key {
527 code,
528 state,
529 repeat,
530 modifiers,
531 });
532
533 // AppKit has already run the layout, the dead keys and any input method
534 // by the time it gets here, so this is committed text and nothing else
535 // needs to know which layout produced it. Control characters are dropped:
536 // Enter, Tab and Backspace are keys, and a field that inserted a `\r`
537 // would hold a character it can never draw.
538 if state == ElementState::Down
539 && !modifiers.contains(Modifiers::CTRL)
540 && !modifiers.contains(Modifiers::SUPER)
541 && let Some(characters) = event.characters()
542 {
543 for ch in characters.to_string().chars().filter(|c| !c.is_control()) {
544 ivars.push(InputEvent::Text { ch });
545 }
546 }
547 drop(ivars);
548 self.update();
549 }
550}
551
552/// The physical pixel extent of a view that is `size` points across.
553fn physical_size(size: NSSize, scale_factor: f32) -> Size {
554 let scale = scale_factor.max(0.01);
555 Size::new(
556 (size.width as f32 * scale).round().max(0.0) as u32,
557 (size.height as f32 * scale).round().max(0.0) as u32,
558 )
559}
560
561fn modifiers_of(event: &NSEvent) -> Modifiers {
562 let flags = event.modifierFlags();
563 let mut out = Modifiers::NONE;
564 for (flag, modifier) in [
565 (NSEventModifierFlags::Shift, Modifiers::SHIFT),
566 (NSEventModifierFlags::Control, Modifiers::CTRL),
567 (NSEventModifierFlags::Option, Modifiers::ALT),
568 (NSEventModifierFlags::Command, Modifiers::SUPER),
569 ] {
570 if flags.contains(flag) {
571 out |= modifier;
572 }
573 }
574 out
575}
576
577fn bit_count(modifiers: Modifiers) -> u32 {
578 [
579 Modifiers::SHIFT,
580 Modifiers::CTRL,
581 Modifiers::ALT,
582 Modifiers::SUPER,
583 ]
584 .into_iter()
585 .filter(|m| modifiers.contains(*m))
586 .count() as u32
587}
588
589/// AppKit's button 2 is the wheel; anything above that is a side button and keeps
590/// its own index.
591fn other_button(event: &NSEvent) -> PointerButton {
592 match event.buttonNumber() {
593 2 => PointerButton::Middle,
594 other => PointerButton::Other(other.max(0) as u16),
595 }
596}