Another project
1use core::time::Duration;
2
3use crate::layout::{LayoutOffset, LayoutPos, LayoutPx};
4
5use super::InputSnapshot;
6use super::key::{KeyChar, KeyCode, KeyEvent, ModifierMask, NamedKey};
7use super::pointer::{FrameInstant, PointerButton, PointerButtonMask, PointerSample};
8
9const DEFAULT_TICK: Duration = Duration::from_millis(16);
10const DRAG_SAMPLES: usize = 8;
11
12#[derive(Clone, Debug, PartialEq)]
13enum Step {
14 Move(LayoutPos),
15 Press(PointerButton),
16 Release(PointerButton),
17 Key(KeyEvent),
18 TextChunk(String),
19 Idle,
20}
21
22#[derive(Clone, Debug, PartialEq, Default)]
23pub struct Script {
24 steps: Vec<Step>,
25 cursor: Option<LayoutPos>,
26 modifiers: ModifierMask,
27}
28
29impl Script {
30 #[must_use]
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 #[must_use]
36 pub fn with_modifiers(mut self, modifiers: ModifierMask) -> Self {
37 self.modifiers = modifiers;
38 self
39 }
40
41 #[must_use]
42 pub fn hover(mut self, pos: LayoutPos) -> Self {
43 self.cursor = Some(pos);
44 self.steps.push(Step::Move(pos));
45 self
46 }
47
48 #[must_use]
49 pub fn press(mut self, button: PointerButton) -> Self {
50 self.steps.push(Step::Press(button));
51 self
52 }
53
54 #[must_use]
55 pub fn release(mut self, button: PointerButton) -> Self {
56 self.steps.push(Step::Release(button));
57 self
58 }
59
60 #[must_use]
61 pub fn click(self, button: PointerButton) -> Self {
62 self.press(button).release(button)
63 }
64
65 #[must_use]
66 #[allow(
67 clippy::cast_precision_loss,
68 reason = "DRAG_SAMPLES and i are small constants, far below f32 precision boundary"
69 )]
70 pub fn drag(mut self, delta: LayoutOffset) -> Self {
71 let from = self.cursor.unwrap_or(LayoutPos::ORIGIN);
72 let denom = DRAG_SAMPLES as f32;
73 (1..=DRAG_SAMPLES).for_each(|i| {
74 let t = i as f32 / denom;
75 let pos = LayoutPos::new(
76 LayoutPx::saturating(from.x.value() + delta.dx.value() * t),
77 LayoutPx::saturating(from.y.value() + delta.dy.value() * t),
78 );
79 self.steps.push(Step::Move(pos));
80 self.cursor = Some(pos);
81 });
82 self
83 }
84
85 #[must_use]
86 pub fn key(mut self, code: KeyCode) -> Self {
87 let event = KeyEvent::new(code, self.modifiers);
88 self.steps.push(Step::Key(event));
89 self
90 }
91
92 #[must_use]
93 pub fn named(self, named: NamedKey) -> Self {
94 self.key(KeyCode::Named(named))
95 }
96
97 #[must_use]
98 pub fn char(self, c: char) -> Self {
99 self.key(KeyCode::Char(KeyChar::from_char(c)))
100 }
101
102 #[must_use]
103 pub fn type_text(mut self, text: &str) -> Self {
104 self.steps.push(Step::TextChunk(text.to_owned()));
105 self
106 }
107
108 #[must_use]
109 pub fn idle(mut self) -> Self {
110 self.steps.push(Step::Idle);
111 self
112 }
113
114 #[must_use]
115 pub fn frames(&self) -> Vec<InputSnapshot> {
116 let acc = FrameAcc {
117 cursor: None,
118 now: FrameInstant::ZERO,
119 modifiers: self.modifiers,
120 };
121 let (frames, _) = self
122 .steps
123 .iter()
124 .fold((Vec::new(), acc), |(mut frames, acc), step| {
125 let (frame, next) = build_frame(acc, step);
126 frames.push(frame);
127 (frames, next)
128 });
129 frames
130 }
131}
132
133#[derive(Copy, Clone, Debug)]
134struct FrameAcc {
135 cursor: Option<LayoutPos>,
136 now: FrameInstant,
137 modifiers: ModifierMask,
138}
139
140fn build_frame(acc: FrameAcc, step: &Step) -> (InputSnapshot, FrameAcc) {
141 let mut input = InputSnapshot::idle(acc.now);
142 input.modifiers = acc.modifiers;
143 let mut next = acc;
144 match step {
145 Step::Move(pos) => {
146 next.cursor = Some(*pos);
147 input.pointer = Some(PointerSample::new(*pos));
148 }
149 Step::Press(button) => {
150 input.pointer = acc.cursor.map(PointerSample::new);
151 input.buttons_pressed = PointerButtonMask::just(*button);
152 }
153 Step::Release(button) => {
154 input.pointer = acc.cursor.map(PointerSample::new);
155 input.buttons_released = PointerButtonMask::just(*button);
156 }
157 Step::Key(event) => {
158 input.pointer = acc.cursor.map(PointerSample::new);
159 input.keys_pressed.push(*event);
160 }
161 Step::TextChunk(text) => {
162 input.pointer = acc.cursor.map(PointerSample::new);
163 text.clone_into(&mut input.text_committed);
164 }
165 Step::Idle => {
166 input.pointer = acc.cursor.map(PointerSample::new);
167 }
168 }
169 next.now = FrameInstant::from_duration(acc.now.duration() + DEFAULT_TICK);
170 (input, next)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::{DEFAULT_TICK, KeyChar, KeyCode, ModifierMask, NamedKey, PointerButton, Script};
176 use crate::input::FrameInstant;
177 use crate::layout::{LayoutOffset, LayoutPos, LayoutPx};
178
179 #[test]
180 fn empty_script_produces_no_frames() {
181 let script = Script::new();
182 assert!(script.frames().is_empty());
183 }
184
185 #[test]
186 fn click_emits_press_then_release() {
187 let frames = Script::new().click(PointerButton::Primary).frames();
188 assert_eq!(frames.len(), 2);
189 assert!(frames[0].buttons_pressed.contains(PointerButton::Primary));
190 assert!(frames[0].buttons_released.is_empty());
191 assert!(frames[1].buttons_pressed.is_empty());
192 assert!(frames[1].buttons_released.contains(PointerButton::Primary));
193 }
194
195 #[test]
196 fn hover_then_drag_carries_cursor_forward() {
197 let frames = Script::new()
198 .hover(LayoutPos::new(LayoutPx::new(10.0), LayoutPx::new(20.0)))
199 .press(PointerButton::Primary)
200 .drag(LayoutOffset::new(LayoutPx::new(8.0), LayoutPx::new(0.0)))
201 .release(PointerButton::Primary)
202 .frames();
203 assert_eq!(frames.len(), 1 + 1 + super::DRAG_SAMPLES + 1);
204 let drag_xs: Vec<f32> = frames[2..2 + super::DRAG_SAMPLES]
205 .iter()
206 .map(|f| {
207 let Some(s) = f.pointer else {
208 panic!("drag sample carries pointer");
209 };
210 s.position.x.value()
211 })
212 .collect();
213 assert!(
214 drag_xs.windows(2).all(|w| w[1] > w[0]),
215 "drag samples must be monotonically increasing in x, got {drag_xs:?}",
216 );
217 let Some(last) = frames.last() else {
218 panic!("frames produced");
219 };
220 let Some(p) = last.pointer else {
221 panic!("release frame must carry pointer position");
222 };
223 assert!((p.position.x.value() - 18.0).abs() < f32::EPSILON);
224 assert!(last.buttons_released.contains(PointerButton::Primary));
225 }
226
227 #[test]
228 fn key_steps_attach_modifiers() {
229 let frames = Script::new()
230 .with_modifiers(ModifierMask::CTRL)
231 .char('a')
232 .frames();
233 assert_eq!(frames.len(), 1);
234 assert_eq!(frames[0].keys_pressed.len(), 1);
235 let event = frames[0].keys_pressed[0];
236 assert_eq!(event.code, KeyCode::Char(KeyChar::from_char('a')));
237 assert!(event.modifiers.contains(ModifierMask::CTRL));
238 }
239
240 #[test]
241 fn named_key_step_records_named_code() {
242 let frames = Script::new().named(NamedKey::Escape).frames();
243 assert_eq!(frames.len(), 1);
244 assert_eq!(
245 frames[0].keys_pressed[0].code,
246 KeyCode::Named(NamedKey::Escape)
247 );
248 }
249
250 #[test]
251 fn type_text_emits_text_committed_on_one_frame() {
252 let frames = Script::new().type_text("hi").frames();
253 assert_eq!(frames.len(), 1);
254 assert_eq!(frames[0].text_committed, "hi");
255 }
256
257 #[test]
258 fn ticks_advance_frame_time_by_default_interval() {
259 let frames = Script::new().idle().idle().frames();
260 assert_eq!(frames[0].frame, FrameInstant::ZERO);
261 assert_eq!(frames[1].frame, FrameInstant::from_duration(DEFAULT_TICK));
262 }
263}