git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/backend/appkit.rs (11.3K)

  1 //! AppKit's input vocabulary in cce-ui's terms: an `NSEvent`'s key code and
  2 //! characters as a [`Key`] and the text it types, its scroll deltas and
  3 //! phases as a [`ScrollFrame`], its modifier flags as the driver's
  4 //! [`Modifiers`]. The macOS shell (`mac::shell`) feeds these to the
  5 //! [`Driver`](super::driver::Driver), as the browser shell feeds `dom`'s.
  6 //! Plain functions of numbers and strings, so they build and are tested on
  7 //! every target — the shell itself can only be built on a Mac.
  8 
  9 use super::dom;
 10 use super::driver::{Modifiers, ScrollFrame, ScrollSource};
 11 use crate::widget::{Key, MouseButton, NamedKey};
 12 
 13 /// `NSEventModifierFlags` bits the shell reads.
 14 pub mod flags {
 15     pub const SHIFT: u64 = 1 << 17;
 16     pub const CONTROL: u64 = 1 << 18;
 17     pub const OPTION: u64 = 1 << 19;
 18     pub const COMMAND: u64 = 1 << 20;
 19 }
 20 
 21 /// `NSEventPhase` bits: a trackpad gesture's place in its life.
 22 pub mod phase {
 23     pub const BEGAN: u64 = 1;
 24     pub const STATIONARY: u64 = 2;
 25     pub const CHANGED: u64 = 4;
 26     pub const ENDED: u64 = 8;
 27     pub const CANCELLED: u64 = 16;
 28     pub const MAY_BEGIN: u64 = 32;
 29 }
 30 
 31 /// The modifier flags as the driver's. Command is the shortcut key, as the
 32 /// browser shell has it on a Mac (⌘Z is undo, matched as the `ctrl+z`
 33 /// chord): it and Control both read as `ctrl`, and there is no `logo`.
 34 pub fn modifiers(f: u64) -> Modifiers {
 35     Modifiers {
 36         ctrl: f & (flags::CONTROL | flags::COMMAND) != 0,
 37         shift: f & flags::SHIFT != 0,
 38         alt: f & flags::OPTION != 0,
 39         logo: false,
 40     }
 41 }
 42 
 43 /// An `NSEvent.buttonNumber` as the driver's button, given the flags it came
 44 /// with: a Control-click is a right click, as every Mac app reads it (one
 45 /// button is still a common mouse). The extra buttons are not the toolkit's.
 46 pub fn button(number: i64, f: u64) -> Option<MouseButton> {
 47     match number {
 48         0 if f & flags::CONTROL != 0 && f & flags::COMMAND == 0 => Some(MouseButton::Right),
 49         0 => Some(MouseButton::Left),
 50         1 => Some(MouseButton::Right),
 51         2 => Some(MouseButton::Middle),
 52         _ => None,
 53     }
 54 }
 55 
 56 /// A key as cce-ui's key and the text it types — what xkb's `utf8` would
 57 /// carry on Wayland, as `dom::map_key` gives it in a page. The named keys
 58 /// are read from the hardware `key_code` (`kVK_*`), since AppKit spells
 59 /// them in private-use characters (and Backspace as DEL); everything else
 60 /// from the characters: `characters` as typed (Option and dead keys
 61 /// applied: Option-e e is "é"), or with `accel` — Command or Control held —
 62 /// `unmodified` (`charactersIgnoringModifiers`), so ⌘Z is the letter z
 63 /// typing its control code, as Ctrl+Z is on Linux. `None` for a key the
 64 /// toolkit has no use for: a dead key (no characters yet), a function key
 65 /// past F12.
 66 pub fn map_key(key_code: u16, characters: &str, unmodified: &str, accel: bool) -> Option<(Key, Option<String>)> {
 67     let named = |n| Some((Key::Named(n), None));
 68     let typing = |n, text: &str| Some((Key::Named(n), Some(text.to_string())));
 69     match key_code {
 70         0x24 | 0x4C => typing(NamedKey::Enter, "\r"),
 71         0x30 => typing(NamedKey::Tab, "\t"),
 72         0x31 => typing(NamedKey::Space, if accel { "\0" } else { " " }),
 73         0x33 => typing(NamedKey::Backspace, "\u{8}"),
 74         0x35 => typing(NamedKey::Escape, "\u{1b}"),
 75         0x75 => typing(NamedKey::Delete, "\u{7f}"),
 76         0x7B => named(NamedKey::ArrowLeft),
 77         0x7C => named(NamedKey::ArrowRight),
 78         0x7D => named(NamedKey::ArrowDown),
 79         0x7E => named(NamedKey::ArrowUp),
 80         0x73 => named(NamedKey::Home),
 81         0x77 => named(NamedKey::End),
 82         0x74 => named(NamedKey::PageUp),
 83         0x79 => named(NamedKey::PageDown),
 84         0x37 | 0x36 => named(NamedKey::Super),
 85         0x38 | 0x3C => named(NamedKey::Shift),
 86         0x3A | 0x3D => named(NamedKey::Alt),
 87         0x3B | 0x3E => named(NamedKey::Control),
 88         0x7A => named(NamedKey::F1),
 89         0x78 => named(NamedKey::F2),
 90         0x63 => named(NamedKey::F3),
 91         0x76 => named(NamedKey::F4),
 92         0x60 => named(NamedKey::F5),
 93         0x61 => named(NamedKey::F6),
 94         0x62 => named(NamedKey::F7),
 95         0x64 => named(NamedKey::F8),
 96         0x65 => named(NamedKey::F9),
 97         0x6D => named(NamedKey::F10),
 98         0x67 => named(NamedKey::F11),
 99         0x6F => named(NamedKey::F12),
100         _ => {
101             let chars = if accel { unmodified } else { characters };
102             // AppKit's function-key characters (NSUpArrowFunctionKey …) are
103             // private use: a key named above, or one the toolkit has no name for.
104             if chars.chars().any(|c| ('\u{F700}'..='\u{F8FF}').contains(&c)) {
105                 return None;
106             }
107             dom::map_key(chars, accel)
108         }
109     }
110 }
111 
112 /// A scroll event's deltas (`scrollingDeltaX` / `Y`) as a frame in the
113 /// driver's units — a `wl_pointer` axis frame's: positive down and right, a
114 /// finger in px, a wheel notch a discrete step plus ten units — or `None`
115 /// for an event that carries nothing to scroll by.
116 ///
117 /// - **Which device**: `precise` (`hasPreciseScrollingDeltas`) is a
118 ///   trackpad or a Magic Mouse, its deltas in points: a finger. Otherwise a
119 ///   wheel, its deltas in lines, accelerated: a notch each whole line, and
120 ///   at least one each way a delta goes.
121 /// - **Which way**: AppKit's delta is what the CONTENT moves by, the
122 ///   opposite of what a list scrolls by. And the system has applied the
123 ///   user's natural-scrolling setting (`inverted`,
124 ///   `isDirectionInvertedFromDevice`) to both devices, where a Linux
125 ///   compositor applies it to the trackpad alone; so a wheel is turned back
126 ///   to its own direction, as a Wayland wheel is, and a value control reads
127 ///   the notch as it does there. A finger keeps the system's direction, and
128 ///   the shell tells `input::natural_scroll` what it is.
129 /// - **When it ends**: a finger's frame with `phase` ENDED or CANCELLED is
130 ///   the lift. What follows a lift is the system's own momentum
131 ///   (`momentum` nonzero), dropped: the toolkit coasts a flick itself
132 ///   (`ScrollMotion`), and taking both would coast twice. A gesture's
133 ///   touch-down (MAY_BEGIN) and a still finger carry nothing.
134 pub fn scroll_frame(dx: f64, dy: f64, precise: bool, inverted: bool, phase: u64, momentum: u64) -> Option<ScrollFrame> {
135     if momentum != 0 {
136         return None;
137     }
138     if precise {
139         let stop = phase & (phase::ENDED | phase::CANCELLED) != 0;
140         if dx == 0.0 && dy == 0.0 && !stop {
141             return None;
142         }
143         return Some(ScrollFrame { h: -dx, v: -dy, source: Some(ScrollSource::Finger), stop, ..Default::default() });
144     }
145     // A wheel, in its own direction: down the list is positive.
146     let device = if inverted { 1.0 } else { -1.0 };
147     let notches = |d: f64| -> i32 {
148         match d.round() as i32 {
149             _ if d == 0.0 => 0,
150             0 => d.signum() as i32,
151             n => n.clamp(-100, 100),
152         }
153     };
154     let (nh, nv) = (notches(dx * device), notches(dy * device));
155     if nh == 0 && nv == 0 {
156         return None;
157     }
158     Some(ScrollFrame {
159         h: nh as f64 * 10.0,
160         v: nv as f64 * 10.0,
161         discrete_h: nh,
162         discrete_v: nv,
163         source: Some(ScrollSource::Wheel),
164         stop: false,
165     })
166 }
167 
168 #[cfg(test)]
169 mod tests {
170     use super::*;
171 
172     #[test]
173     fn named_keys_come_from_the_key_code_and_type_what_xkb_types() {
174         // Backspace: AppKit's characters are DEL, xkb's text is BS.
175         assert_eq!(map_key(0x33, "\u{7f}", "\u{7f}", false), Some((Key::Named(NamedKey::Backspace), Some("\u{8}".into()))));
176         assert_eq!(map_key(0x75, "\u{F728}", "\u{F728}", false), Some((Key::Named(NamedKey::Delete), Some("\u{7f}".into()))));
177         assert_eq!(map_key(0x24, "\r", "\r", false), Some((Key::Named(NamedKey::Enter), Some("\r".into()))));
178         assert_eq!(map_key(0x4C, "\u{3}", "\u{3}", false), Some((Key::Named(NamedKey::Enter), Some("\r".into()))));
179         assert_eq!(map_key(0x30, "\t", "\t", false), Some((Key::Named(NamedKey::Tab), Some("\t".into()))));
180         assert_eq!(map_key(0x7B, "\u{F702}", "\u{F702}", false), Some((Key::Named(NamedKey::ArrowLeft), None)));
181         assert_eq!(map_key(0x60, "\u{F708}", "\u{F708}", false), Some((Key::Named(NamedKey::F5), None)));
182         // F13: a function key with no name here.
183         assert_eq!(map_key(0x69, "\u{F710}", "\u{F710}", false), None);
184     }
185 
186     #[test]
187     fn a_command_shortcut_is_its_letter_typing_its_control_code() {
188         // ⌘Z: the letter, unmodified, typing ^Z as Ctrl+Z does on Linux.
189         assert_eq!(map_key(0x06, "z", "z", true), Some((Key::Character("z".into()), Some("\u{1a}".into()))));
190         // ⇧⌘Z.
191         assert_eq!(map_key(0x06, "Z", "Z", true), Some((Key::Character("Z".into()), Some("\u{1a}".into()))));
192         // Control-A: AppKit's characters are already ^A; the key is still a.
193         assert_eq!(map_key(0x00, "\u{1}", "a", true), Some((Key::Character("a".into()), Some("\u{1}".into()))));
194         assert_eq!(modifiers(flags::COMMAND | flags::SHIFT), Modifiers { ctrl: true, shift: true, alt: false, logo: false });
195     }
196 
197     #[test]
198     fn option_types_what_it_composes_and_a_dead_key_types_nothing_yet() {
199         assert_eq!(map_key(0x00, "å", "a", false), Some((Key::Character("å".into()), Some("å".into()))));
200         assert_eq!(map_key(0x0E, "", "e", false), None);
201         assert_eq!(map_key(0x0E, "é", "e", false), Some((Key::Character("é".into()), Some("é".into()))));
202     }
203 
204     #[test]
205     fn a_control_click_is_a_right_click() {
206         assert_eq!(button(0, 0), Some(MouseButton::Left));
207         assert_eq!(button(0, flags::CONTROL), Some(MouseButton::Right));
208         assert_eq!(button(0, flags::CONTROL | flags::COMMAND), Some(MouseButton::Left));
209         assert_eq!(button(1, 0), Some(MouseButton::Right));
210         assert_eq!(button(2, 0), Some(MouseButton::Middle));
211         assert_eq!(button(3, 0), None);
212     }
213 
214     #[test]
215     fn a_trackpad_is_a_finger_scrolling_the_list_and_its_momentum_is_dropped() {
216         // Fingers moving up under natural scrolling: content up, list down.
217         let f = scroll_frame(0.0, -4.5, true, true, phase::CHANGED, 0).unwrap();
218         assert_eq!((f.v, f.h, f.source, f.stop), (4.5, 0.0, Some(ScrollSource::Finger), false));
219         // The lift.
220         let f = scroll_frame(0.0, 0.0, true, true, phase::ENDED, 0).unwrap();
221         assert!(f.stop);
222         // The touch-down, and the system's coast after the lift.
223         assert!(scroll_frame(0.0, 0.0, true, true, phase::MAY_BEGIN, 0).is_none());
224         assert!(scroll_frame(0.0, -12.0, true, true, 0, phase::CHANGED).is_none());
225     }
226 
227     #[test]
228     fn a_wheel_notch_is_discrete_and_in_the_wheels_own_direction() {
229         // Rolled toward the user (down the list), natural scrolling off:
230         // AppKit says the content moves up.
231         let f = scroll_frame(0.0, -1.0, false, false, 0, 0).unwrap();
232         assert_eq!((f.discrete_v, f.v, f.source), (1, 10.0, Some(ScrollSource::Wheel)));
233         // The same roll with natural scrolling on: AppKit flips the sign,
234         // the frame does not.
235         let f = scroll_frame(0.0, 1.0, false, true, 0, 0).unwrap();
236         assert_eq!(f.discrete_v, 1);
237         // Accelerated: three lines, three notches; a fraction is one.
238         assert_eq!(scroll_frame(0.0, 3.2, false, false, 0, 0).unwrap().discrete_v, -3);
239         assert_eq!(scroll_frame(0.0, 0.1, false, false, 0, 0).unwrap().discrete_v, -1);
240         assert!(scroll_frame(0.0, 0.0, false, false, 0, 0).is_none());
241     }
242 }