git.lucas.co / cce-ui
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 }