1use 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#[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
23pub 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
30pub 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
49pub fn point(p: &Value) -> Option<Pos2> {
51 Some(pos2(finite(p.get("x"))?, finite(p.get("y"))?))
52}
53
54pub 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
59pub 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
85pub const MAX_CHORDS: usize = 64;
87
88pub 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
141pub 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
149pub 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
156pub 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
171pub 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}