Skip to main content

ashlar_ui/devtools/
input.rs

1//! Injected input: one [`InputFrame`] per frame, built from request parameters. Every number that
2//! comes from a request is hostile: non-finite values are refused, the rest clamped.
3
4use ashlar_command::shortcut::{Chord, Platform};
5use egui::{Event, Key, Modifiers, MouseWheelUnit, PointerButton, Pos2, TouchPhase, Vec2, pos2, vec2};
6use serde_json::Value;
7
8use crate::commands::keys::key_to_egui;
9
10/// One frame of injected input.
11#[derive(Clone, Debug, Default)]
12pub struct InputFrame {
13    pub events: Vec<Event>,
14    pub modifiers: Modifiers,
15}
16
17impl InputFrame {
18    fn one(e: Event, modifiers: Modifiers) -> Self {
19        InputFrame { events: vec![e], modifiers }
20    }
21}
22
23/// `{command, shift, alt, ctrl}` (`command` = ⌘ on macOS, Ctrl elsewhere) from `p.modifiers`.
24pub fn modifiers(p: &Value) -> Modifiers {
25    let m = p.get("modifiers");
26    let f = |k: &str| m.and_then(|m| m.get(k)).and_then(Value::as_bool).unwrap_or(false);
27    egui_modifiers(f("command"), f("ctrl"), f("alt"), f("shift"))
28}
29
30/// egui modifiers as a real keyboard on this platform reports them.
31pub fn egui_modifiers(command: bool, ctrl: bool, alt: bool, shift: bool) -> Modifiers {
32    let mac = Platform::current() == Platform::Mac;
33    Modifiers { alt, ctrl: ctrl || (command && !mac), shift, mac_cmd: command && mac, command: command || (ctrl && !mac) }
34}
35
36pub fn button(p: &Value) -> PointerButton {
37    match p.get("button").and_then(Value::as_str) {
38        Some("secondary" | "right") => PointerButton::Secondary,
39        Some("middle") => PointerButton::Middle,
40        _ => PointerButton::Primary,
41    }
42}
43
44fn finite(v: Option<&Value>) -> Option<f32> {
45    let f = v?.as_f64()? as f32;
46    f.is_finite().then_some(f.clamp(-1.0e6, 1.0e6))
47}
48
49/// `{x, y}` from params.
50pub fn point(p: &Value) -> Option<Pos2> {
51    Some(pos2(finite(p.get("x"))?, finite(p.get("y"))?))
52}
53
54/// `{dx, dy}` from params (missing = 0).
55pub fn delta(p: &Value) -> Vec2 {
56    vec2(finite(p.get("dx")).unwrap_or(0.0), finite(p.get("dy")).unwrap_or(0.0))
57}
58
59/// An egui key by name, forgiving about case and common aliases (`return`, `esc`, `up`).
60pub fn key(name: &str) -> Option<Key> {
61    Key::from_name(name).or_else(|| {
62        let lower = name.to_ascii_lowercase();
63        let mut cap = lower.clone();
64        if let Some(f) = cap.get_mut(0..1) {
65            f.make_ascii_uppercase();
66        }
67        if let Some(k) = Key::from_name(&cap) {
68            return Some(k);
69        }
70        match lower.as_str() {
71            "return" => Some(Key::Enter),
72            "esc" => Some(Key::Escape),
73            "del" => Some(Key::Delete),
74            "space" | " " => Some(Key::Space),
75            "up" => Some(Key::ArrowUp),
76            "down" => Some(Key::ArrowDown),
77            "left" => Some(Key::ArrowLeft),
78            "right" => Some(Key::ArrowRight),
79            _ if lower.chars().count() == 1 => Key::from_name(&lower.to_ascii_uppercase()),
80            _ => None,
81        }
82    })
83}
84
85/// Most chords in one `ui.press` sequence.
86pub const MAX_CHORDS: usize = 64;
87
88/// `"Mod+Shift+S"` or a sequence `"G G"` (the `ashlar_command::shortcut` syntax) as egui keys.
89pub fn chords(text: &str) -> Result<Vec<(Key, Modifiers)>, String> {
90    let mut out = Vec::new();
91    for part in text.split_whitespace() {
92        if out.len() >= MAX_CHORDS {
93            return Err(format!("at most {MAX_CHORDS} chords per ui.press"));
94        }
95        let chord = Chord::parse(part).map_err(|e| e.to_string())?;
96        let k = key_to_egui(chord.key).ok_or_else(|| format!("`{part}`: egui has no such key"))?;
97        let m = chord.mods;
98        out.push((k, egui_modifiers(m.command, m.ctrl, m.alt, m.shift)));
99    }
100    if out.is_empty() {
101        return Err("ui.press needs `keys`, like \"Mod+S\" or \"Escape\"".into());
102    }
103    Ok(out)
104}
105
106pub fn hover_frames(at: Pos2, mods: Modifiers) -> Vec<InputFrame> {
107    vec![InputFrame::one(Event::PointerMoved(at), mods)]
108}
109
110pub fn click_frames(at: Pos2, button: PointerButton, count: u32, mods: Modifiers) -> Vec<InputFrame> {
111    let mut v = hover_frames(at, mods);
112    for _ in 0..count.clamp(1, 3) {
113        v.push(InputFrame::one(Event::PointerButton { pos: at, button, pressed: true, modifiers: mods }, mods));
114        v.push(InputFrame::one(Event::PointerButton { pos: at, button, pressed: false, modifiers: mods }, mods));
115    }
116    v
117}
118
119pub fn drag_frames(from: Pos2, to: Pos2, steps: u32, mods: Modifiers) -> Vec<InputFrame> {
120    let steps = steps.clamp(1, 240);
121    let b = PointerButton::Primary;
122    let mut v = vec![
123        InputFrame::one(Event::PointerMoved(from), mods),
124        InputFrame::one(Event::PointerButton { pos: from, button: b, pressed: true, modifiers: mods }, mods),
125    ];
126    for i in 1..=steps {
127        let t = i as f32 / steps as f32;
128        v.push(InputFrame::one(Event::PointerMoved(from.lerp(to, t)), mods));
129    }
130    v.push(InputFrame::one(Event::PointerButton { pos: to, button: b, pressed: false, modifiers: mods }, mods));
131    v
132}
133
134pub fn scroll_frames(at: Pos2, delta: Vec2, mods: Modifiers) -> Vec<InputFrame> {
135    vec![
136        InputFrame::one(Event::PointerMoved(at), mods),
137        InputFrame::one(Event::MouseWheel { unit: MouseWheelUnit::Point, delta, phase: TouchPhase::Move, modifiers: mods }, mods),
138    ]
139}
140
141/// Press and release `key`: two frames.
142pub fn key_frames(key: Key, mods: Modifiers) -> Vec<InputFrame> {
143    vec![
144        InputFrame::one(Event::Key { key, physical_key: Some(key), pressed: true, repeat: false, modifiers: mods }, mods),
145        InputFrame::one(Event::Key { key, physical_key: Some(key), pressed: false, repeat: false, modifiers: mods }, Modifiers::NONE),
146    ]
147}
148
149/// Most characters one `ui.type` / `ui.fill` sends.
150pub const MAX_TEXT: usize = 100_000;
151
152pub fn type_frames(text: &str) -> Vec<InputFrame> {
153    vec![InputFrame::one(Event::Text(text.chars().take(MAX_TEXT).collect()), Modifiers::NONE)]
154}
155
156/// Replace the focused field's text: select all, then type (or delete when `text` is empty),
157/// then Enter when `submit`.
158pub fn replace_frames(text: &str, submit: bool) -> Vec<InputFrame> {
159    let mut v = key_frames(Key::A, egui_modifiers(true, false, false, false));
160    if text.is_empty() {
161        v.extend(key_frames(Key::Backspace, Modifiers::NONE));
162    } else {
163        v.extend(type_frames(text));
164    }
165    if submit {
166        v.extend(key_frames(Key::Enter, Modifiers::NONE));
167    }
168    v
169}
170
171/// An AccessKit action for the egui widget whose `Id` value is `node` (focus, scroll into view).
172pub fn action_frame(node: u64, action: egui::accesskit::Action) -> InputFrame {
173    InputFrame::one(
174        Event::AccessKitActionRequest(egui::accesskit::ActionRequest {
175            action,
176            target_tree: egui::accesskit::TreeId::ROOT,
177            target_node: egui::accesskit::NodeId(node),
178            data: None,
179        }),
180        Modifiers::NONE,
181    )
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use serde_json::json;
188
189    #[test]
190    fn hostile_params_are_tamed() {
191        assert_eq!(point(&json!({"x": "a", "y": 1})), None);
192        assert_eq!(point(&json!({"x": 1e30, "y": 1})).map(|p| p.x), Some(1.0e6));
193        assert_eq!(key("enter"), Some(Key::Enter));
194        assert_eq!(key("a"), Some(Key::A));
195        assert_eq!(key("nonsense-key"), None);
196        assert_eq!(drag_frames(Pos2::ZERO, Pos2::ZERO, u32::MAX, Modifiers::NONE).len(), 243);
197        assert!(modifiers(&json!({"modifiers": {"command": true}})).command);
198        assert_eq!(click_frames(Pos2::ZERO, PointerButton::Primary, 99, Modifiers::NONE).len(), 7);
199    }
200
201    #[test]
202    fn chords_follow_the_shortcut_syntax() {
203        let c = chords("Mod+Shift+S").unwrap();
204        assert_eq!(c.len(), 1);
205        assert_eq!(c[0].0, Key::S);
206        assert!(c[0].1.command && c[0].1.shift);
207        let seq = chords("G  G Escape").unwrap();
208        assert_eq!(seq.iter().map(|(k, _)| *k).collect::<Vec<_>>(), vec![Key::G, Key::G, Key::Escape]);
209        assert!(chords("").is_err());
210        assert!(chords("Mod+Nope").is_err());
211        assert!(chords(&"A ".repeat(MAX_CHORDS + 1)).is_err());
212    }
213
214    #[test]
215    fn replacing_text_selects_all_first() {
216        let f = replace_frames("hi", true);
217        assert!(matches!(f[0].events[0], Event::Key { key: Key::A, pressed: true, .. }));
218        assert!(f.iter().any(|f| matches!(&f.events[0], Event::Text(t) if t == "hi")));
219        assert!(matches!(f.last().unwrap().events[0], Event::Key { key: Key::Enter, pressed: false, .. }));
220        assert!(replace_frames("", false).iter().any(|f| matches!(f.events[0], Event::Key { key: Key::Backspace, .. })));
221    }
222}