Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

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}