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 }