GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/backend/dom.rs (9.4K)
1 //! The DOM's input vocabulary in cce-ui's terms: a `KeyboardEvent.key` as a
2 //! [`Key`] and the text it types, a `WheelEvent` as a [`ScrollFrame`]. The
3 //! browser shell (`web::shell`) feeds these to the [`Driver`](super::driver::Driver),
4 //! as the Wayland shell maps xkb keysyms and `wl_pointer` axis frames. Plain
5 //! functions of strings and numbers, so they build and are tested natively.
6
7 use super::driver::{ScrollFrame, ScrollSource};
8 use crate::widget::{Key, NamedKey};
9
10 /// A `KeyboardEvent.key` as cce-ui's key and the text it types — the
11 /// browser's half of the Wayland shell's keysym map, text included: what
12 /// xkb's `utf8` would carry for the same key, since that is what widgets
13 /// have always been handed (Tab types "\t" and Enter "\r"; with Ctrl, or
14 /// Command on a Mac, a letter types its control code and is matched by the
15 /// letter). `None` for a key the toolkit has no use for (an IME's `Process`,
16 /// a dead key).
17 pub fn map_key(key: &str, accel: bool) -> Option<(Key, Option<String>)> {
18 let named = |n| Some((Key::Named(n), None));
19 let typing = |n, text: &str| Some((Key::Named(n), Some(text.to_string())));
20 match key {
21 "Escape" => typing(NamedKey::Escape, "\u{1b}"),
22 "Enter" => typing(NamedKey::Enter, "\r"),
23 "Backspace" => typing(NamedKey::Backspace, "\u{8}"),
24 "Tab" => typing(NamedKey::Tab, "\t"),
25 "Delete" => typing(NamedKey::Delete, "\u{7f}"),
26 " " => typing(NamedKey::Space, if accel { "\0" } else { " " }),
27 "ArrowDown" => named(NamedKey::ArrowDown),
28 "ArrowUp" => named(NamedKey::ArrowUp),
29 "ArrowLeft" => named(NamedKey::ArrowLeft),
30 "ArrowRight" => named(NamedKey::ArrowRight),
31 "PageUp" => named(NamedKey::PageUp),
32 "PageDown" => named(NamedKey::PageDown),
33 "Home" => named(NamedKey::Home),
34 "End" => named(NamedKey::End),
35 "Meta" | "OS" => named(NamedKey::Super),
36 "Alt" => named(NamedKey::Alt),
37 "Control" => named(NamedKey::Control),
38 "Shift" => named(NamedKey::Shift),
39 "F1" => named(NamedKey::F1),
40 "F2" => named(NamedKey::F2),
41 "F3" => named(NamedKey::F3),
42 "F4" => named(NamedKey::F4),
43 "F5" => named(NamedKey::F5),
44 "F6" => named(NamedKey::F6),
45 "F7" => named(NamedKey::F7),
46 "F8" => named(NamedKey::F8),
47 "F9" => named(NamedKey::F9),
48 "F10" => named(NamedKey::F10),
49 "F11" => named(NamedKey::F11),
50 "F12" => named(NamedKey::F12),
51 // A key that types: one character (a named key's name is longer).
52 k if k.chars().count() == 1 => {
53 let c = k.chars().next().unwrap();
54 let text = match c {
55 // xkb's control transformation: @, A–Z, [ \ ] ^ _ (either
56 // case of the letters) to 0x00–0x1f.
57 '@'..='_' | 'a'..='z' if accel => char::from(c.to_ascii_uppercase() as u8 & 0x1f).to_string(),
58 _ => k.to_string(),
59 };
60 Some((Key::Character(k.into()), Some(text)))
61 }
62 _ => None,
63 }
64 }
65
66 /// A clipboard shortcut, as the page's clipboard events are raised by it.
67 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
68 pub enum ClipKey {
69 Copy,
70 Cut,
71 Paste,
72 }
73
74 /// Whether a `KeyboardEvent.key` with Ctrl (or ⌘) held, and Alt not, is a
75 /// clipboard shortcut: the browser shell lets its default through, since
76 /// that default is the `copy`, `cut` or `paste` event — and a page gets the
77 /// clipboard's text only inside a `paste` event.
78 pub fn clipboard_key(key: &str, accel: bool, alt: bool) -> Option<ClipKey> {
79 if !accel || alt {
80 return None;
81 }
82 match key {
83 "c" | "C" => Some(ClipKey::Copy),
84 "x" | "X" => Some(ClipKey::Cut),
85 "v" | "V" => Some(ClipKey::Paste),
86 _ => None,
87 }
88 }
89
90 /// A text control's selection — UTF-16 offsets, as the DOM counts — as a
91 /// byte range in its text: where an input method has its cursor in a
92 /// composition. An offset past the end is the end; inside a surrogate pair,
93 /// the char it splits.
94 pub fn utf16_range_to_bytes(text: &str, start: Option<u32>, end: Option<u32>) -> Option<(usize, usize)> {
95 let byte_at = |u: u32| {
96 let mut units = 0u32;
97 for (b, c) in text.char_indices() {
98 if units >= u {
99 return b;
100 }
101 units += c.len_utf16() as u32;
102 }
103 text.len()
104 };
105 Some((byte_at(start?), byte_at(end?)))
106 }
107
108 /// A `WheelEvent`'s deltas as a scroll frame in the units the driver takes
109 /// (a `wl_pointer` axis frame's: a finger in px, a wheel notch as a discrete
110 /// step plus ten units, positive down and right — the DOM's signs too).
111 ///
112 /// A page cannot ask which device scrolled. A line or page delta is a wheel
113 /// (Firefox's notches); a pixel delta is a wheel when it comes in whole
114 /// notch-sized steps on one axis (Chromium's 100 px a notch, or 120), and a
115 /// finger otherwise — a trackpad's deltas are small and fractional.
116 pub fn wheel_frame(mode: u32, dx: f64, dy: f64) -> ScrollFrame {
117 // Whole notches, at least one each way a delta goes.
118 let notches = |d: f64, per: f64| -> i32 {
119 match (d / per).round() as i32 {
120 _ if d == 0.0 => 0,
121 0 => d.signum() as i32,
122 n => n.clamp(-100, 100),
123 }
124 };
125 let wheel = |per: f64| {
126 let (nh, nv) = (notches(dx, per), notches(dy, per));
127 ScrollFrame {
128 h: nh as f64 * 10.0,
129 v: nv as f64 * 10.0,
130 discrete_h: nh,
131 discrete_v: nv,
132 source: Some(ScrollSource::Wheel),
133 stop: false,
134 }
135 };
136 match mode {
137 // DOM_DELTA_LINE: Firefox gives three lines a notch.
138 1 => wheel(3.0),
139 // DOM_DELTA_PAGE: a notch is a page.
140 2 => wheel(1.0),
141 _ => {
142 let notch_sized = |d: f64| d == 0.0 || (d.fract() == 0.0 && d.abs() >= 50.0);
143 if (dx == 0.0 || dy == 0.0) && notch_sized(dx) && notch_sized(dy) {
144 wheel(100.0)
145 } else {
146 ScrollFrame { h: dx, v: dy, source: Some(ScrollSource::Finger), ..Default::default() }
147 }
148 }
149 }
150 }
151
152 #[cfg(test)]
153 mod tests {
154 use super::*;
155
156 #[test]
157 fn named_keys_type_what_xkb_types() {
158 assert_eq!(map_key("Tab", false), Some((Key::Named(NamedKey::Tab), Some("\t".into()))));
159 assert_eq!(map_key("Enter", false), Some((Key::Named(NamedKey::Enter), Some("\r".into()))));
160 assert_eq!(map_key("Backspace", false), Some((Key::Named(NamedKey::Backspace), Some("\u{8}".into()))));
161 assert_eq!(map_key(" ", false), Some((Key::Named(NamedKey::Space), Some(" ".into()))));
162 assert_eq!(map_key("ArrowLeft", false), Some((Key::Named(NamedKey::ArrowLeft), None)));
163 assert_eq!(map_key("Dead", false), None);
164 assert_eq!(map_key("Process", false), None);
165 }
166
167 #[test]
168 fn a_shortcut_is_matched_by_its_letter_and_types_its_control_code() {
169 assert_eq!(map_key("a", false), Some((Key::Character("a".into()), Some("a".into()))));
170 assert_eq!(map_key("z", true), Some((Key::Character("z".into()), Some("\u{1a}".into()))));
171 // Ctrl+Shift+Z: the shifted letter, the same control code.
172 assert_eq!(map_key("Z", true), Some((Key::Character("Z".into()), Some("\u{1a}".into()))));
173 assert_eq!(map_key("1", true), Some((Key::Character("1".into()), Some("1".into()))));
174 assert_eq!(map_key("é", false), Some((Key::Character("é".into()), Some("é".into()))));
175 }
176
177 #[test]
178 fn only_an_accelerated_c_x_or_v_is_a_clipboard_shortcut() {
179 assert_eq!(clipboard_key("c", true, false), Some(ClipKey::Copy));
180 assert_eq!(clipboard_key("X", true, false), Some(ClipKey::Cut));
181 assert_eq!(clipboard_key("v", true, false), Some(ClipKey::Paste));
182 // Ctrl+Shift+V, a paste too.
183 assert_eq!(clipboard_key("V", true, false), Some(ClipKey::Paste));
184 assert_eq!(clipboard_key("v", false, false), None);
185 assert_eq!(clipboard_key("v", true, true), None);
186 assert_eq!(clipboard_key("z", true, false), None);
187 }
188
189 #[test]
190 fn a_dom_selection_is_counted_in_utf16_and_given_in_bytes() {
191 // "に" is one UTF-16 unit and three bytes; "😀" two units, four bytes.
192 assert_eq!(utf16_range_to_bytes("にほ", Some(1), Some(1)), Some((3, 3)));
193 assert_eq!(utf16_range_to_bytes("😀a", Some(2), Some(3)), Some((4, 5)));
194 assert_eq!(utf16_range_to_bytes("ab", Some(0), Some(9)), Some((0, 2)));
195 assert_eq!(utf16_range_to_bytes("ab", None, Some(1)), None);
196 }
197
198 #[test]
199 fn notches_are_discrete_and_a_trackpad_is_a_finger() {
200 // Chromium's mouse wheel: 100 px a notch, three notches up.
201 let f = wheel_frame(0, 0.0, -300.0);
202 assert_eq!((f.discrete_v, f.discrete_h, f.source), (-3, 0, Some(ScrollSource::Wheel)));
203 assert_eq!(f.v, -30.0);
204 // Firefox's: three lines a notch.
205 let f = wheel_frame(1, 0.0, 3.0);
206 assert_eq!((f.discrete_v, f.source), (1, Some(ScrollSource::Wheel)));
207 // A page delta is a notch.
208 assert_eq!(wheel_frame(2, 0.0, -1.0).discrete_v, -1);
209 // A trackpad: small, fractional, often on both axes.
210 let f = wheel_frame(0, 1.5, -4.25);
211 assert_eq!((f.discrete_v, f.h, f.v, f.source), (0, 1.5, -4.25, Some(ScrollSource::Finger)));
212 // Whole but notch-sized on both axes at once: a finger too.
213 assert_eq!(wheel_frame(0, 100.0, 100.0).source, Some(ScrollSource::Finger));
214 }
215 }