1use teksilo_canvas::Point;
5use teksilo_core::event::{Key, Modifiers, PointerButton, ScrollDelta, WidgetEvent};
6use teksilo_core::gesture::{GestureEvent, TapEvent};
7
8pub struct TranslationState {
10 scale_factor: f64,
11 cursor_position: Option<Point>,
12 current_modifiers: Modifiers,
13}
14
15impl TranslationState {
16 pub fn new() -> Self {
17 Self {
18 scale_factor: 1.0,
19 cursor_position: None,
20 current_modifiers: Modifiers::NONE,
21 }
22 }
23
24 pub fn set_scale_factor(&mut self, factor: f64) {
25 self.scale_factor = factor;
26 }
27
28 pub fn scale_factor(&self) -> f64 {
29 self.scale_factor
30 }
31
32 pub fn cursor_position(&self) -> Option<Point> {
33 self.cursor_position
34 }
35
36 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
37 self.current_modifiers = modifiers;
38 }
39}
40
41impl Default for TranslationState {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47pub fn translate_cursor_moved(
49 physical_x: f64,
50 physical_y: f64,
51 state: &mut TranslationState,
52) -> Option<WidgetEvent> {
53 let logical_x = (physical_x / state.scale_factor) as f32;
54 let logical_y = (physical_y / state.scale_factor) as f32;
55 let position = Point::new(logical_x, logical_y);
56 state.cursor_position = Some(position);
57 Some(WidgetEvent::PointerMove { position })
58}
59
60pub fn translate_ime(ime: winit::event::Ime) -> Option<WidgetEvent> {
70 match ime {
71 winit::event::Ime::Preedit(text, cursor) => Some(WidgetEvent::ImeComposition {
72 text,
73 cursor: cursor.map(|(begin, end)| begin..end),
74 }),
75 winit::event::Ime::Commit(text) => Some(WidgetEvent::ImeCommit { text }),
76 winit::event::Ime::Enabled | winit::event::Ime::Disabled => None,
77 }
78}
79
80pub fn translate_mouse_button(button: winit::event::MouseButton) -> Option<PointerButton> {
82 match button {
83 winit::event::MouseButton::Left => Some(PointerButton::Primary),
84 winit::event::MouseButton::Right => Some(PointerButton::Secondary),
85 winit::event::MouseButton::Middle => Some(PointerButton::Middle),
86 winit::event::MouseButton::Back => Some(PointerButton::Back),
87 winit::event::MouseButton::Forward => Some(PointerButton::Forward),
88 _ => None,
91 }
92}
93
94pub fn translate_mouse_input(
96 button_state: winit::event::ElementState,
97 button: winit::event::MouseButton,
98 state: &TranslationState,
99) -> Option<WidgetEvent> {
100 let pointer_button = translate_mouse_button(button)?;
101 let position = state.cursor_position.unwrap_or(Point::ZERO);
102 match button_state {
103 winit::event::ElementState::Pressed => Some(WidgetEvent::PointerDown {
104 position,
105 button: pointer_button,
106 modifiers: state.current_modifiers,
107 }),
108 winit::event::ElementState::Released => Some(WidgetEvent::PointerUp {
109 position,
110 button: pointer_button,
111 modifiers: state.current_modifiers,
112 }),
113 }
114}
115
116pub fn translate_modifiers(mods: winit::keyboard::ModifiersState) -> Modifiers {
118 let mut result = Modifiers::NONE;
119 if mods.control_key() {
120 result = result | Modifiers::CTRL;
121 }
122 if mods.shift_key() {
123 result = result | Modifiers::SHIFT;
124 }
125 if mods.alt_key() {
126 result = result | Modifiers::ALT;
127 }
128 if mods.super_key() {
129 result = result | Modifiers::SUPER;
130 }
131 result
132}
133
134pub fn translate_key(key: &winit::keyboard::Key) -> Option<Key> {
136 match key {
137 winit::keyboard::Key::Named(named) => translate_named_key(*named),
138 winit::keyboard::Key::Character(c) => {
139 let ch = c.chars().next()?;
140 match ch.to_ascii_uppercase() {
141 'A' => Some(Key::A),
142 'B' => Some(Key::B),
143 'C' => Some(Key::C),
144 'D' => Some(Key::D),
145 'E' => Some(Key::E),
146 'F' => Some(Key::F),
147 'G' => Some(Key::G),
148 'H' => Some(Key::H),
149 'I' => Some(Key::I),
150 'J' => Some(Key::J),
151 'K' => Some(Key::K),
152 'L' => Some(Key::L),
153 'M' => Some(Key::M),
154 'N' => Some(Key::N),
155 'O' => Some(Key::O),
156 'P' => Some(Key::P),
157 'Q' => Some(Key::Q),
158 'R' => Some(Key::R),
159 'S' => Some(Key::S),
160 'T' => Some(Key::T),
161 'U' => Some(Key::U),
162 'V' => Some(Key::V),
163 'W' => Some(Key::W),
164 'X' => Some(Key::X),
165 'Y' => Some(Key::Y),
166 'Z' => Some(Key::Z),
167 _ => Some(Key::Character(ch)),
168 }
169 }
170 _ => None,
171 }
172}
173
174fn translate_named_key(key: winit::keyboard::NamedKey) -> Option<Key> {
175 use winit::keyboard::NamedKey;
176 match key {
177 NamedKey::Space => Some(Key::Space),
178 NamedKey::Enter => Some(Key::Enter),
179 NamedKey::Escape => Some(Key::Escape),
180 NamedKey::Tab => Some(Key::Tab),
181 NamedKey::Backspace => Some(Key::Backspace),
182 NamedKey::Delete => Some(Key::Delete),
183 NamedKey::Insert => Some(Key::Insert),
184 NamedKey::ArrowUp => Some(Key::ArrowUp),
185 NamedKey::ArrowDown => Some(Key::ArrowDown),
186 NamedKey::ArrowLeft => Some(Key::ArrowLeft),
187 NamedKey::ArrowRight => Some(Key::ArrowRight),
188 NamedKey::Home => Some(Key::Home),
189 NamedKey::End => Some(Key::End),
190 NamedKey::PageUp => Some(Key::PageUp),
191 NamedKey::PageDown => Some(Key::PageDown),
192 NamedKey::F1 => Some(Key::F1),
193 NamedKey::F2 => Some(Key::F2),
194 NamedKey::F3 => Some(Key::F3),
195 NamedKey::F4 => Some(Key::F4),
196 NamedKey::F5 => Some(Key::F5),
197 NamedKey::F6 => Some(Key::F6),
198 NamedKey::F7 => Some(Key::F7),
199 NamedKey::F8 => Some(Key::F8),
200 NamedKey::F9 => Some(Key::F9),
201 NamedKey::F10 => Some(Key::F10),
202 NamedKey::F11 => Some(Key::F11),
203 NamedKey::F12 => Some(Key::F12),
204 NamedKey::F13 => Some(Key::F13),
205 NamedKey::F14 => Some(Key::F14),
206 NamedKey::F15 => Some(Key::F15),
207 NamedKey::F16 => Some(Key::F16),
208 NamedKey::F17 => Some(Key::F17),
209 NamedKey::F18 => Some(Key::F18),
210 NamedKey::F19 => Some(Key::F19),
211 NamedKey::F20 => Some(Key::F20),
212 NamedKey::F21 => Some(Key::F21),
213 NamedKey::F22 => Some(Key::F22),
214 NamedKey::F23 => Some(Key::F23),
215 NamedKey::F24 => Some(Key::F24),
216 NamedKey::CapsLock => Some(Key::CapsLock),
221 NamedKey::ContextMenu => Some(Key::ContextMenu),
224 _ => None,
225 }
226}
227
228pub fn translate_mouse_wheel(
230 delta: winit::event::MouseScrollDelta,
231 _phase: winit::event::TouchPhase,
232 state: &TranslationState,
233) -> Option<WidgetEvent> {
234 const LINES_PER_NOTCH: f32 = 3.0;
242 let scroll_delta = match delta {
243 winit::event::MouseScrollDelta::LineDelta(x, y) => ScrollDelta::Lines {
244 x: -x * LINES_PER_NOTCH,
245 y: -y * LINES_PER_NOTCH,
246 },
247 winit::event::MouseScrollDelta::PixelDelta(pos) => ScrollDelta::Pixels {
248 x: -(pos.x / state.scale_factor) as f32,
249 y: -(pos.y / state.scale_factor) as f32,
250 },
251 };
252 Some(WidgetEvent::Scroll {
253 delta: scroll_delta,
254 modifiers: state.current_modifiers,
255 })
256}
257
258pub fn translate_pinch_gesture(
266 delta: f64,
267 phase: winit::event::TouchPhase,
268 state: &TranslationState,
269) -> Option<WidgetEvent> {
270 let center = state.cursor_position.unwrap_or(Point::ZERO);
271 let gesture = match phase {
272 winit::event::TouchPhase::Started => GestureEvent::PinchStarted { center },
273 winit::event::TouchPhase::Moved => GestureEvent::PinchChanged {
274 center,
275 scale: 1.0 + delta as f32,
276 rotation: 0.0,
277 },
278 winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
279 GestureEvent::PinchEnded
280 }
281 };
282 Some(WidgetEvent::Gesture { gesture })
283}
284
285pub fn translate_rotation_gesture(
289 delta_degrees: f32,
290 phase: winit::event::TouchPhase,
291 state: &TranslationState,
292) -> Option<WidgetEvent> {
293 let center = state.cursor_position.unwrap_or(Point::ZERO);
294 let gesture = match phase {
295 winit::event::TouchPhase::Started => GestureEvent::PinchStarted { center },
296 winit::event::TouchPhase::Moved => GestureEvent::PinchChanged {
297 center,
298 scale: 1.0,
299 rotation: delta_degrees,
300 },
301 winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
302 GestureEvent::PinchEnded
303 }
304 };
305 Some(WidgetEvent::Gesture { gesture })
306}
307
308pub fn translate_double_tap_gesture(state: &TranslationState) -> Option<WidgetEvent> {
316 let position = state.cursor_position.unwrap_or(Point::ZERO);
317 Some(WidgetEvent::Gesture {
318 gesture: GestureEvent::DoubleTap(TapEvent::new(
319 position,
320 PointerButton::Primary,
321 state.current_modifiers,
322 )),
323 })
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn cursor_moved_to_pointer_move() {
332 let mut state = TranslationState::new();
333 let event = translate_cursor_moved(100.0, 50.0, &mut state).unwrap();
334 if let WidgetEvent::PointerMove { position } = event {
335 assert_eq!(position.x, 100.0);
336 assert_eq!(position.y, 50.0);
337 } else {
338 panic!("Expected PointerMove");
339 }
340 }
341
342 #[test]
343 fn scale_factor_divides_physical_coords() {
344 let mut state = TranslationState::new();
345 state.set_scale_factor(2.0);
346 let event = translate_cursor_moved(200.0, 100.0, &mut state).unwrap();
347 if let WidgetEvent::PointerMove { position } = event {
348 assert_eq!(position.x, 100.0);
349 assert_eq!(position.y, 50.0);
350 } else {
351 panic!("Expected PointerMove");
352 }
353 }
354
355 #[test]
356 fn mouse_button_translation() {
357 assert_eq!(
358 translate_mouse_button(winit::event::MouseButton::Left),
359 Some(PointerButton::Primary)
360 );
361 assert_eq!(
362 translate_mouse_button(winit::event::MouseButton::Right),
363 Some(PointerButton::Secondary)
364 );
365 }
366
367 #[test]
368 fn mouse_press_to_pointer_down() {
369 let mut state = TranslationState::new();
370 translate_cursor_moved(50.0, 25.0, &mut state);
371 let event = translate_mouse_input(
372 winit::event::ElementState::Pressed,
373 winit::event::MouseButton::Left,
374 &state,
375 )
376 .unwrap();
377 assert!(matches!(
378 event,
379 WidgetEvent::PointerDown {
380 button: PointerButton::Primary,
381 ..
382 }
383 ));
384 }
385
386 #[test]
387 fn key_translation() {
388 let key = winit::keyboard::Key::Named(winit::keyboard::NamedKey::Space);
389 assert_eq!(translate_key(&key), Some(Key::Space));
390 }
391
392 #[test]
393 fn pinch_gesture_started() {
394 let mut state = TranslationState::new();
395 translate_cursor_moved(100.0, 50.0, &mut state);
396 let event =
397 translate_pinch_gesture(0.0, winit::event::TouchPhase::Started, &state).unwrap();
398 assert!(matches!(
399 event,
400 WidgetEvent::Gesture {
401 gesture: GestureEvent::PinchStarted { .. }
402 }
403 ));
404 }
405
406 #[test]
407 fn pinch_gesture_changed() {
408 let mut state = TranslationState::new();
409 translate_cursor_moved(100.0, 50.0, &mut state);
410 let event = translate_pinch_gesture(0.5, winit::event::TouchPhase::Moved, &state).unwrap();
411 if let WidgetEvent::Gesture {
412 gesture: GestureEvent::PinchChanged { scale, .. },
413 } = event
414 {
415 assert!((scale - 1.5).abs() < 0.001);
416 } else {
417 panic!("Expected PinchChanged");
418 }
419 }
420
421 #[test]
422 fn pinch_gesture_ended() {
423 let state = TranslationState::new();
424 let event = translate_pinch_gesture(0.0, winit::event::TouchPhase::Ended, &state).unwrap();
425 assert!(matches!(
426 event,
427 WidgetEvent::Gesture {
428 gesture: GestureEvent::PinchEnded
429 }
430 ));
431 }
432
433 #[test]
434 fn rotation_gesture_translates() {
435 let mut state = TranslationState::new();
436 translate_cursor_moved(50.0, 50.0, &mut state);
437 let event =
438 translate_rotation_gesture(15.0, winit::event::TouchPhase::Moved, &state).unwrap();
439 if let WidgetEvent::Gesture {
440 gesture: GestureEvent::PinchChanged {
441 rotation, scale, ..
442 },
443 } = event
444 {
445 assert!((rotation - 15.0).abs() < 0.001);
446 assert!((scale - 1.0).abs() < 0.001);
447 } else {
448 panic!("Expected PinchChanged with rotation");
449 }
450 }
451
452 #[test]
453 fn double_tap_gesture_translates() {
454 let mut state = TranslationState::new();
455 translate_cursor_moved(75.0, 25.0, &mut state);
456 let event = translate_double_tap_gesture(&state).unwrap();
457 if let WidgetEvent::Gesture {
458 gesture: GestureEvent::DoubleTap(tap_event),
459 } = event
460 {
461 assert_eq!(tap_event.position.x, 75.0);
462 assert_eq!(tap_event.position.y, 25.0);
463 assert_eq!(tap_event.button, PointerButton::Primary);
464 } else {
465 panic!("Expected DoubleTap");
466 }
467 }
468
469 #[test]
470 fn ime_preedit_to_composition_preserves_cursor_bytes() {
471 let evt = translate_ime(winit::event::Ime::Preedit("a".to_string(), Some((1, 1)))).unwrap();
472 assert!(matches!(
473 evt,
474 WidgetEvent::ImeComposition { ref text, cursor: Some(ref r) }
475 if text == "a" && *r == (1..1)
476 ));
477 }
478
479 #[test]
480 fn ime_preedit_hide_cursor_maps_to_none() {
481 let evt = translate_ime(winit::event::Ime::Preedit(String::new(), None)).unwrap();
482 assert!(matches!(
483 evt,
484 WidgetEvent::ImeComposition { ref text, cursor: None } if text.is_empty()
485 ));
486 }
487
488 #[test]
489 fn ime_commit_translates() {
490 let evt = translate_ime(winit::event::Ime::Commit("你".to_string())).unwrap();
491 assert!(matches!(evt, WidgetEvent::ImeCommit { ref text } if text == "你"));
492 }
493
494 #[test]
495 fn ime_enabled_disabled_produce_no_event() {
496 assert!(translate_ime(winit::event::Ime::Enabled).is_none());
497 assert!(translate_ime(winit::event::Ime::Disabled).is_none());
498 }
499}