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 _ => None,
222 }
223}
224
225pub fn translate_mouse_wheel(
227 delta: winit::event::MouseScrollDelta,
228 _phase: winit::event::TouchPhase,
229 state: &TranslationState,
230) -> Option<WidgetEvent> {
231 const LINES_PER_NOTCH: f32 = 3.0;
239 let scroll_delta = match delta {
240 winit::event::MouseScrollDelta::LineDelta(x, y) => ScrollDelta::Lines {
241 x: -x * LINES_PER_NOTCH,
242 y: -y * LINES_PER_NOTCH,
243 },
244 winit::event::MouseScrollDelta::PixelDelta(pos) => ScrollDelta::Pixels {
245 x: -(pos.x / state.scale_factor) as f32,
246 y: -(pos.y / state.scale_factor) as f32,
247 },
248 };
249 Some(WidgetEvent::Scroll {
250 delta: scroll_delta,
251 modifiers: state.current_modifiers,
252 })
253}
254
255pub fn translate_pinch_gesture(
263 delta: f64,
264 phase: winit::event::TouchPhase,
265 state: &TranslationState,
266) -> Option<WidgetEvent> {
267 let center = state.cursor_position.unwrap_or(Point::ZERO);
268 let gesture = match phase {
269 winit::event::TouchPhase::Started => GestureEvent::PinchStarted { center },
270 winit::event::TouchPhase::Moved => GestureEvent::PinchChanged {
271 center,
272 scale: 1.0 + delta as f32,
273 rotation: 0.0,
274 },
275 winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
276 GestureEvent::PinchEnded
277 }
278 };
279 Some(WidgetEvent::Gesture { gesture })
280}
281
282pub fn translate_rotation_gesture(
286 delta_degrees: f32,
287 phase: winit::event::TouchPhase,
288 state: &TranslationState,
289) -> Option<WidgetEvent> {
290 let center = state.cursor_position.unwrap_or(Point::ZERO);
291 let gesture = match phase {
292 winit::event::TouchPhase::Started => GestureEvent::PinchStarted { center },
293 winit::event::TouchPhase::Moved => GestureEvent::PinchChanged {
294 center,
295 scale: 1.0,
296 rotation: delta_degrees,
297 },
298 winit::event::TouchPhase::Ended | winit::event::TouchPhase::Cancelled => {
299 GestureEvent::PinchEnded
300 }
301 };
302 Some(WidgetEvent::Gesture { gesture })
303}
304
305pub fn translate_double_tap_gesture(state: &TranslationState) -> Option<WidgetEvent> {
313 let position = state.cursor_position.unwrap_or(Point::ZERO);
314 Some(WidgetEvent::Gesture {
315 gesture: GestureEvent::DoubleTap(TapEvent::new(
316 position,
317 PointerButton::Primary,
318 state.current_modifiers,
319 )),
320 })
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn cursor_moved_to_pointer_move() {
329 let mut state = TranslationState::new();
330 let event = translate_cursor_moved(100.0, 50.0, &mut state).unwrap();
331 if let WidgetEvent::PointerMove { position } = event {
332 assert_eq!(position.x, 100.0);
333 assert_eq!(position.y, 50.0);
334 } else {
335 panic!("Expected PointerMove");
336 }
337 }
338
339 #[test]
340 fn scale_factor_divides_physical_coords() {
341 let mut state = TranslationState::new();
342 state.set_scale_factor(2.0);
343 let event = translate_cursor_moved(200.0, 100.0, &mut state).unwrap();
344 if let WidgetEvent::PointerMove { position } = event {
345 assert_eq!(position.x, 100.0);
346 assert_eq!(position.y, 50.0);
347 } else {
348 panic!("Expected PointerMove");
349 }
350 }
351
352 #[test]
353 fn mouse_button_translation() {
354 assert_eq!(
355 translate_mouse_button(winit::event::MouseButton::Left),
356 Some(PointerButton::Primary)
357 );
358 assert_eq!(
359 translate_mouse_button(winit::event::MouseButton::Right),
360 Some(PointerButton::Secondary)
361 );
362 }
363
364 #[test]
365 fn mouse_press_to_pointer_down() {
366 let mut state = TranslationState::new();
367 translate_cursor_moved(50.0, 25.0, &mut state);
368 let event = translate_mouse_input(
369 winit::event::ElementState::Pressed,
370 winit::event::MouseButton::Left,
371 &state,
372 )
373 .unwrap();
374 assert!(matches!(
375 event,
376 WidgetEvent::PointerDown {
377 button: PointerButton::Primary,
378 ..
379 }
380 ));
381 }
382
383 #[test]
384 fn key_translation() {
385 let key = winit::keyboard::Key::Named(winit::keyboard::NamedKey::Space);
386 assert_eq!(translate_key(&key), Some(Key::Space));
387 }
388
389 #[test]
390 fn pinch_gesture_started() {
391 let mut state = TranslationState::new();
392 translate_cursor_moved(100.0, 50.0, &mut state);
393 let event =
394 translate_pinch_gesture(0.0, winit::event::TouchPhase::Started, &state).unwrap();
395 assert!(matches!(
396 event,
397 WidgetEvent::Gesture {
398 gesture: GestureEvent::PinchStarted { .. }
399 }
400 ));
401 }
402
403 #[test]
404 fn pinch_gesture_changed() {
405 let mut state = TranslationState::new();
406 translate_cursor_moved(100.0, 50.0, &mut state);
407 let event = translate_pinch_gesture(0.5, winit::event::TouchPhase::Moved, &state).unwrap();
408 if let WidgetEvent::Gesture {
409 gesture: GestureEvent::PinchChanged { scale, .. },
410 } = event
411 {
412 assert!((scale - 1.5).abs() < 0.001);
413 } else {
414 panic!("Expected PinchChanged");
415 }
416 }
417
418 #[test]
419 fn pinch_gesture_ended() {
420 let state = TranslationState::new();
421 let event = translate_pinch_gesture(0.0, winit::event::TouchPhase::Ended, &state).unwrap();
422 assert!(matches!(
423 event,
424 WidgetEvent::Gesture {
425 gesture: GestureEvent::PinchEnded
426 }
427 ));
428 }
429
430 #[test]
431 fn rotation_gesture_translates() {
432 let mut state = TranslationState::new();
433 translate_cursor_moved(50.0, 50.0, &mut state);
434 let event =
435 translate_rotation_gesture(15.0, winit::event::TouchPhase::Moved, &state).unwrap();
436 if let WidgetEvent::Gesture {
437 gesture: GestureEvent::PinchChanged {
438 rotation, scale, ..
439 },
440 } = event
441 {
442 assert!((rotation - 15.0).abs() < 0.001);
443 assert!((scale - 1.0).abs() < 0.001);
444 } else {
445 panic!("Expected PinchChanged with rotation");
446 }
447 }
448
449 #[test]
450 fn double_tap_gesture_translates() {
451 let mut state = TranslationState::new();
452 translate_cursor_moved(75.0, 25.0, &mut state);
453 let event = translate_double_tap_gesture(&state).unwrap();
454 if let WidgetEvent::Gesture {
455 gesture: GestureEvent::DoubleTap(tap_event),
456 } = event
457 {
458 assert_eq!(tap_event.position.x, 75.0);
459 assert_eq!(tap_event.position.y, 25.0);
460 assert_eq!(tap_event.button, PointerButton::Primary);
461 } else {
462 panic!("Expected DoubleTap");
463 }
464 }
465
466 #[test]
467 fn ime_preedit_to_composition_preserves_cursor_bytes() {
468 let evt = translate_ime(winit::event::Ime::Preedit("a".to_string(), Some((1, 1)))).unwrap();
469 assert!(matches!(
470 evt,
471 WidgetEvent::ImeComposition { ref text, cursor: Some(ref r) }
472 if text == "a" && *r == (1..1)
473 ));
474 }
475
476 #[test]
477 fn ime_preedit_hide_cursor_maps_to_none() {
478 let evt = translate_ime(winit::event::Ime::Preedit(String::new(), None)).unwrap();
479 assert!(matches!(
480 evt,
481 WidgetEvent::ImeComposition { ref text, cursor: None } if text.is_empty()
482 ));
483 }
484
485 #[test]
486 fn ime_commit_translates() {
487 let evt = translate_ime(winit::event::Ime::Commit("你".to_string())).unwrap();
488 assert!(matches!(evt, WidgetEvent::ImeCommit { ref text } if text == "你"));
489 }
490
491 #[test]
492 fn ime_enabled_disabled_produce_no_event() {
493 assert!(translate_ime(winit::event::Ime::Enabled).is_none());
494 assert!(translate_ime(winit::event::Ime::Disabled).is_none());
495 }
496}