Skip to main content

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}