GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/backend/driver.rs (50.6K)
1 //! The runner's platform-neutral half: what happens to input once a shell has
2 //! it in cce-ui's own terms. A shell (the Wayland runner today) translates its
3 //! window system's events — evdev buttons, xkb keysyms, `wl_pointer` axis
4 //! frames — into the calls here, and carries out what they ask back of the
5 //! window (a cursor, an interactive move or resize). Everything between —
6 //! modifier tracking, the undo/redo and plate-navigation chords, key repeat,
7 //! the CSD hit zones, the outside-press popover close, held-button release on
8 //! a lost pointer, the scroll phase, the pinch fallback — lives here once, so
9 //! a second shell routes exactly as the first does.
10 //!
11 //! The app is never owned here: each call takes a [`Turn`], the app plus the
12 //! runner's `redraw` / `exit` flags, borrowed from wherever the shell keeps
13 //! them (the Wayland runner keeps them on `EngineState`, where clients'
14 //! `register_sources` callbacks reach `inner` and `redraw` directly).
15
16 use web_time::Instant;
17
18 use super::app::{Application, LogicalPosition, LogicalSize};
19 use cursor_icon::CursorIcon;
20 use crate::widget::{ElementState, Key, KeyEvent, MouseButton, MouseScrollDelta, NamedKey, Position, ScrollPhase, WidgetHostExt};
21
22 /// A key held down, for the runner's own key repeat.
23 pub struct PressedKey {
24 pub logical_key: Key,
25 pub text: Option<String>,
26 pub first_pressed: Instant,
27 pub last_repeated: Instant,
28 }
29
30 /// Held this long before the first repeat…
31 pub const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
32 /// …then one repeat per this.
33 pub const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
34
35 fn is_repeatable_key(key: &Key) -> bool {
36 matches!(
37 key,
38 Key::Named(NamedKey::Backspace)
39 | Key::Named(NamedKey::Delete)
40 | Key::Named(NamedKey::ArrowLeft)
41 | Key::Named(NamedKey::ArrowRight)
42 | Key::Named(NamedKey::ArrowUp)
43 | Key::Named(NamedKey::ArrowDown)
44 | Key::Named(NamedKey::Home)
45 | Key::Named(NamedKey::End)
46 | Key::Character(_)
47 )
48 }
49
50 /// The modifier keys as the keyboard last reported them.
51 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
52 pub struct Modifiers {
53 pub ctrl: bool,
54 pub shift: bool,
55 pub alt: bool,
56 pub logo: bool,
57 }
58
59 /// A window edge an interactive resize grabs: the window system's own enum,
60 /// spelled without it. The Wayland shell maps it onto `xdg_toplevel`'s.
61 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
62 pub enum ResizeEdge {
63 Top,
64 Bottom,
65 Left,
66 Right,
67 TopLeft,
68 TopRight,
69 BottomLeft,
70 BottomRight,
71 }
72
73 /// What a press turned out to be: the app's, or a grab of the window itself
74 /// that the shell carries out (and the app never hears about).
75 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
76 pub enum Press {
77 Dispatched,
78 Resize(ResizeEdge),
79 Move,
80 }
81
82 /// Where a press landed, in the window's terms.
83 #[derive(Debug, Clone, Copy)]
84 pub struct PressSite {
85 /// The surface's logical size: the CSD borders are measured from it.
86 pub size: LogicalSize,
87 /// The press came through the context menu's popup surface: it is the
88 /// menu's, never a border or a movable plate of the window.
89 pub on_popup: bool,
90 /// The shell can start an interactive move / resize right now (a window
91 /// and a seat to grab with). When it cannot, a press that would have been
92 /// a grab is the app's instead — which is what the Wayland runner did on
93 /// a layer surface, and what a shell with no grabs at all always does.
94 pub can_grab: bool,
95 /// The window system resizes the window from edges of its own (AppKit's
96 /// window frame), and offers no way to start a resize from a press: the
97 /// CSD resize band is then no grab, and a press there goes on to the
98 /// move checks and the app.
99 pub own_edges: bool,
100 }
101
102 /// Where a scroll came from, as far as the phase cares.
103 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
104 pub enum ScrollSource {
105 Wheel,
106 Finger,
107 Continuous,
108 WheelTilt,
109 }
110
111 /// One frame of scroll input, coalesced: the shell sums a frame's axis events
112 /// (in its own window system's units — `wl_pointer` axis values here) and
113 /// hands the total over once.
114 #[derive(Debug, Clone, Copy, Default)]
115 pub struct ScrollFrame {
116 pub h: f64,
117 pub v: f64,
118 pub discrete_h: i32,
119 pub discrete_v: i32,
120 /// The frame's source, if it named one.
121 pub source: Option<ScrollSource>,
122 /// A finger lifted (an axis stop) in this frame.
123 pub stop: bool,
124 }
125
126 /// The app and the runner's two flags, for the length of one dispatch.
127 pub struct Turn<'a, A: Application> {
128 pub app: &'a mut A,
129 pub redraw: &'a mut bool,
130 pub exit: &'a mut bool,
131 }
132
133 impl<A: Application> Turn<'_, A> {
134 /// Hand a message from an input handler to `update`, and fold the two
135 /// rebuild requests into the redraw flag — the shape every dispatch had.
136 fn deliver(&mut self, msg: Option<A::Message>, mut rebuild: bool) {
137 if let Some(msg) = msg {
138 let mut update_rebuild = false;
139 self.app.update(msg, &mut update_rebuild, self.exit);
140 if update_rebuild {
141 rebuild = true;
142 }
143 }
144 if rebuild {
145 *self.redraw = true;
146 }
147 }
148 }
149
150 /// The input state a session carries, and the routing over it.
151 pub struct Driver {
152 pub mods: Modifiers,
153 /// When the last input event of any kind arrived — what makes a redraw
154 /// interactive, which is the redraw the runner's warm-down is for
155 /// (`shell::Pacer`).
156 pub last_input: Option<Instant>,
157 pub pressed_key: Option<PressedKey>,
158 /// The pointer's last position, window-logical (popup events translated).
159 pub cursor_pos: (f32, f32),
160 /// Mouse buttons held, as a bitmask (1 Left / 2 Right / 4 Middle). On a
161 /// lost pointer the real release goes to whatever surface takes the
162 /// pointer next (fullscreen switches, layout animations), so
163 /// [`pointer_leave`](Self::pointer_leave) synthesizes releases for the
164 /// held set — a drag must end, not stay armed and steered by later
165 /// motion — and only then runs the off-screen hover-clear (which would
166 /// otherwise corrupt the drag: a ramp key snapped to the graph corner).
167 pub buttons_down: u32,
168 pub last_pinch_scale: f32,
169 /// The `undo` / `redo` chords, resolved from `input.kdl` when the
170 /// session starts.
171 pub undo_chord: String,
172 pub redo_chord: String,
173 /// `focus_next_group` / `focus_prev_group` (input.kdl, cce-ui domain):
174 /// the plate-navigation group jump, for apps that opt in.
175 pub group_next_chord: String,
176 pub group_prev_chord: String,
177 }
178
179 impl Default for Driver {
180 fn default() -> Self {
181 Self::new()
182 }
183 }
184
185 /// The CSD border's width, in logical px.
186 const CSD_BORDER: f32 = 8.0;
187
188 /// Close the open popovers a left press at (lx, ly) misses, BEFORE the app's
189 /// dispatch: apps commonly region-gate their routing, so an open menu's owner
190 /// may never hear about a press elsewhere.
191 fn close_popovers_missed_by<A: Application>(app: &mut A, lx: f32, ly: f32) {
192 let offsets: Vec<_> = app
193 .ui_context()
194 .map(|ctx| ctx.popover_owners())
195 .unwrap_or_default()
196 .into_iter()
197 .map(|id| (id, app.popover_offset(id)))
198 .collect();
199 if let Some(ctx) = app.ui_context_mut() {
200 ctx.close_popovers_missed_by_press_with(lx, ly, |id| {
201 offsets.iter().find(|(o, _)| *o == id).map_or((0.0, 0.0), |&(_, d)| d)
202 });
203 }
204 }
205
206 fn button_bit(btn: MouseButton) -> u32 {
207 match btn {
208 MouseButton::Left => 1,
209 MouseButton::Right => 2,
210 _ => 4,
211 }
212 }
213
214 impl Driver {
215 pub fn new() -> Self {
216 Self {
217 mods: Modifiers::default(),
218 pressed_key: None,
219 last_input: None,
220 cursor_pos: (0.0, 0.0),
221 buttons_down: 0,
222 last_pinch_scale: 1.0,
223 undo_chord: crate::input::app_chord("undo", "ctrl+z"),
224 redo_chord: crate::input::app_chord("redo", "ctrl+shift+z"),
225 group_next_chord: crate::input::app_chord("focus_next_group", "ctrl+tab"),
226 group_prev_chord: crate::input::app_chord("focus_prev_group", "ctrl+shift+tab"),
227 }
228 }
229
230 /// Copy the modifiers into the app's widget context, where widgets read them.
231 fn sync_mods<A: Application>(&self, app: &mut A) {
232 if let Some(ctx) = app.ui_context_mut() {
233 ctx.ctrl_pressed = self.mods.ctrl;
234 ctx.shift_pressed = self.mods.shift;
235 ctx.alt_pressed = self.mods.alt;
236 ctx.logo_pressed = self.mods.logo;
237 }
238 }
239
240 /// The cursor for the pointer at (lx, ly): the app's
241 /// [`Application::cursor_icon`] override, else the standard-CSD edge
242 /// cursors (status bars and non-standard-CSD apps fall back to Default).
243 pub fn cursor_icon_at<A: Application>(&self, app: &A, lx: f32, ly: f32, size: LogicalSize) -> CursorIcon {
244 // Over the context menu the pointer is the menu's,
245 // whatever of the app lies at that place under it (a splitter, a
246 // resize border) — and in their popups that place may be outside
247 // the window altogether.
248 if crate::widget::context_menu::is_visible() && crate::widget::context_menu::hit_test(lx, ly) {
249 return CursorIcon::Default;
250 }
251 if let Some(icon) = app.cursor_icon(lx, ly) {
252 return icon;
253 }
254 if !app.standard_csd()
255 || !app.csd_resize_borders()
256 {
257 return CursorIcon::Default;
258 }
259 match csd_edge(lx, ly, size) {
260 Some(ResizeEdge::TopLeft) => CursorIcon::NwResize,
261 Some(ResizeEdge::TopRight) => CursorIcon::NeResize,
262 Some(ResizeEdge::Top) => CursorIcon::NResize,
263 Some(ResizeEdge::BottomLeft) => CursorIcon::SwResize,
264 Some(ResizeEdge::BottomRight) => CursorIcon::SeResize,
265 Some(ResizeEdge::Bottom) => CursorIcon::SResize,
266 Some(ResizeEdge::Left) => CursorIcon::WResize,
267 Some(ResizeEdge::Right) => CursorIcon::EResize,
268 None => CursorIcon::Default,
269 }
270 }
271
272 /// The pointer entered at `pos`. Enter carries the pointer's position but
273 /// no motion follows until it actually moves — without this the app's
274 /// hover state is stale from enter to first move, and a press in that
275 /// window can misroute (e.g. a divider press falling through to the
276 /// movable-root plate window drag).
277 fn note_input(&mut self) {
278 self.last_input = Some(Instant::now());
279 }
280
281 pub fn pointer_enter<A: Application>(&mut self, t: Turn<'_, A>, pos: LogicalPosition) {
282 self.note_input();
283 self.pointer_motion(t, pos);
284 }
285
286 /// The pointer moved to `pos`.
287 pub fn pointer_motion<A: Application>(&mut self, t: Turn<'_, A>, pos: LogicalPosition) {
288 self.note_input();
289 let mut rebuild = false;
290 t.app.handle_pointer_move(pos, &mut rebuild);
291 if rebuild {
292 *t.redraw = true;
293 }
294 }
295
296 /// The pointer left the window. Focus can move mid-gesture (a fullscreen
297 /// switch, a relayout sliding the window away): the real release then
298 /// lands on another surface, and an armed drag would live forever. End
299 /// held gestures with synthetic releases at the last known position
300 /// first; then clear hover with an off-screen move — safe now that no
301 /// drag is held.
302 pub fn pointer_leave<A: Application>(&mut self, mut t: Turn<'_, A>) {
303 self.note_input();
304 if self.buttons_down != 0 {
305 let (px, py) = self.cursor_pos;
306 for btn in [MouseButton::Left, MouseButton::Right, MouseButton::Middle] {
307 if self.buttons_down & button_bit(btn) == 0 {
308 continue;
309 }
310 let mut rebuild = false;
311 let msg = t.app.handle_mouse_input(
312 btn,
313 ElementState::Released,
314 LogicalPosition::new(px, py),
315 &mut rebuild,
316 );
317 t.deliver(msg, rebuild);
318 }
319 self.buttons_down = 0;
320 }
321 let mut rebuild = false;
322 t.app.handle_pointer_move(LogicalPosition::new(-10000.0, -10000.0), &mut rebuild);
323 if rebuild {
324 *t.redraw = true;
325 }
326 }
327
328 /// A button went down at `pos`. Returns a grab for the shell to start
329 /// when the press is the window's (a CSD border, the titlebar band, a
330 /// movable root plate); otherwise the press has been dispatched.
331 pub fn pointer_press<A: Application>(
332 &mut self,
333 mut t: Turn<'_, A>,
334 btn: MouseButton,
335 pos: LogicalPosition,
336 site: PressSite,
337 ) -> Press {
338 self.note_input();
339 self.buttons_down |= button_bit(btn);
340 let (lx, ly) = (pos.x, pos.y);
341
342 // Client-side decorations: drag and resize. Never on the menu popup:
343 // its presses are the menu's, and its coordinates, translated into the
344 // window's, would otherwise read as a resize border or a movable plate.
345 if site.can_grab
346 && btn == MouseButton::Left
347 && !site.on_popup
348 && t.app.standard_csd()
349 {
350 // Resize borders off: the compositor's own band outside the
351 // window handles it; the move checks still run, so drag-to-move
352 // still works.
353 if t.app.csd_resize_borders() && !site.own_edges {
354 if let Some(edge) = csd_edge(lx, ly, site.size) {
355 return Press::Resize(edge);
356 }
357 }
358 // The titlebar band: y in [8, 32), clear of the top-right buttons.
359 let is_widget = t.app.ui_context().is_some_and(|ctx| ctx.is_widget_at(lx, ly));
360 if (!is_widget
361 && t.app.csd_titlebar_move()
362 && (CSD_BORDER..32.0).contains(&ly)
363 && lx < site.size.width - 70.0)
364 || t.app.is_movable_root_plate_at(lx, ly)
365 {
366 return Press::Move;
367 }
368 }
369
370 // Outside-press close for open popovers, BEFORE the app's dispatch:
371 // apps commonly region-gate their routing, so an open menu's owner may
372 // never hear about a press elsewhere.
373 if btn == MouseButton::Left {
374 close_popovers_missed_by(t.app, lx, ly);
375 }
376 // A field still open after the press is announced again, so the
377 // frame must be built (`ime::note_press`).
378 if crate::ime::note_press() {
379 *t.redraw = true;
380 }
381
382 let mut rebuild = false;
383 let msg = t.app.handle_mouse_input(btn, ElementState::Pressed, pos, &mut rebuild);
384 t.deliver(msg, rebuild);
385 Press::Dispatched
386 }
387
388 /// A button came up at `pos`.
389 pub fn pointer_release<A: Application>(&mut self, mut t: Turn<'_, A>, btn: MouseButton, pos: LogicalPosition) {
390 self.note_input();
391 self.buttons_down &= !button_bit(btn);
392 let mut rebuild = false;
393 let msg = t.app.handle_mouse_input(btn, ElementState::Released, pos, &mut rebuild);
394 t.deliver(msg, rebuild);
395 }
396
397 /// One coalesced frame of scrolling at `pos`. Publishes the frame's
398 /// phase (`scroll_motion::set_scroll_phase`) before the app sees it.
399 pub fn scroll<A: Application>(&mut self, t: Turn<'_, A>, frame: ScrollFrame, pos: LogicalPosition) {
400 self.note_input();
401 let ScrollFrame { h, v, discrete_h, discrete_v, source, stop } = frame;
402 // Per-app scroll factors from input.kdl (`<app>`/`cce-ui` domain
403 // `input { }` blocks); the compositor's global device scaling has
404 // already been applied at the source.
405 let factors = crate::input::scroll_factors();
406 let (phase, delta) = scroll_delta(&frame, factors);
407 crate::widget::scroll_motion::set_scroll_phase(phase);
408 if crate::scroll_debug() {
409 static T0: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
410 let ms = T0.get_or_init(Instant::now).elapsed().as_millis();
411 eprintln!(
412 "[scroll {ms}ms] runner: coalesced=({h:.2},{v:.2}) discrete=({discrete_h},{discrete_v}) source={source:?} stop={stop} phase={phase:?} factors=(tp {:.2}, m {:.2}) -> {delta:?} at ({:.0},{:.0})",
413 factors.trackpad, factors.mouse, pos.x, pos.y
414 );
415 }
416 let mut rebuild = false;
417 self.sync_mods(t.app);
418 t.app.handle_mouse_wheel(&delta, pos, &mut rebuild);
419 if rebuild {
420 *t.redraw = true;
421 }
422 }
423
424 /// What a touchscreen finger did, as pointer input (the shell's
425 /// [`TouchTracker`](super::touch::TouchTracker) decides which): a hover
426 /// or drag moves the pointer, a press and release are the left button's,
427 /// and a scroll is a trackpad finger scroll dispatched at `scroll_at`,
428 /// where the finger went down — the gesture belongs to what it began on,
429 /// however far the content moves.
430 ///
431 /// Never a CSD move or resize, and never an app's window action: a touch
432 /// serial does not satisfy the compositor's pointer-grab check, and an
433 /// action left queued would run on the next pointer press with that
434 /// press's serial, so one a touch queued is dropped.
435 pub fn touch<A: Application>(
436 &mut self,
437 mut t: Turn<'_, A>,
438 actions: Vec<super::touch::TouchAction>,
439 scroll_at: Option<(f32, f32)>,
440 ) {
441 self.note_input();
442 use super::touch::TouchAction;
443 for action in actions {
444 let mut rebuild = false;
445 let mut msg = None;
446 match action {
447 TouchAction::Hover(x, y) | TouchAction::Drag(x, y) => {
448 t.app.handle_pointer_move(LogicalPosition::new(x, y), &mut rebuild);
449 }
450 TouchAction::Leave => {
451 t.app.handle_pointer_move(LogicalPosition::new(-10000.0, -10000.0), &mut rebuild);
452 }
453 TouchAction::Press(x, y) => {
454 // Outside-press close for open popovers, as the pointer's
455 // press does before the app's own dispatch.
456 close_popovers_missed_by(t.app, x, y);
457 // A tapped field that stays open is announced again,
458 // so the on-screen keyboard can follow the tap.
459 if crate::ime::note_press() {
460 rebuild = true;
461 }
462 let pos = LogicalPosition::new(x, y);
463 msg = t.app.handle_mouse_input(MouseButton::Left, ElementState::Pressed, pos, &mut rebuild);
464 }
465 TouchAction::Release(x, y) => {
466 let pos = LogicalPosition::new(x, y);
467 msg = t.app.handle_mouse_input(MouseButton::Left, ElementState::Released, pos, &mut rebuild);
468 }
469 TouchAction::Scroll(dx, dy) => self.touch_scroll(t.app, scroll_at, ScrollPhase::Finger, dx, dy, &mut rebuild),
470 TouchAction::ScrollEnd => {
471 self.touch_scroll(t.app, scroll_at, ScrollPhase::FingerEnd, 0.0, 0.0, &mut rebuild)
472 }
473 }
474 t.deliver(msg, rebuild);
475 }
476 let _ = t.app.take_window_action();
477 }
478
479 /// Finger travel as a trackpad pixel scroll. A finger is always
480 /// "natural" — the content goes where it is pushed — so the dispatch runs
481 /// with natural scrolling on whatever the trackpad's setting, and a value
482 /// control (`MouseScrollDelta::value_notches_y`) reads the finger's real
483 /// direction. 1:1, without the trackpad's per-app factor: the content
484 /// stays under the finger.
485 fn touch_scroll<A: Application>(
486 &self,
487 app: &mut A,
488 scroll_at: Option<(f32, f32)>,
489 phase: ScrollPhase,
490 dx: f32,
491 dy: f32,
492 rebuild: &mut bool,
493 ) {
494 let Some((x, y)) = scroll_at else { return };
495 crate::widget::scroll_motion::set_scroll_phase(phase);
496 let delta = MouseScrollDelta::PixelDelta(Position { x: dx as f64, y: dy as f64 });
497 if crate::scroll_debug() {
498 eprintln!("[scroll] touch: phase={phase:?} -> {delta:?} at ({x:.0},{y:.0})");
499 }
500 self.sync_mods(app);
501 crate::input::with_natural_scroll(true, || {
502 app.handle_mouse_wheel(&delta, LogicalPosition::new(x, y), rebuild);
503 });
504 }
505
506 /// A pinch gesture began.
507 pub fn pinch_begin(&mut self) {
508 self.last_pinch_scale = 1.0;
509 }
510
511 /// A pinch gesture ended.
512 pub fn pinch_end(&mut self) {
513 self.last_pinch_scale = 1.0;
514 }
515
516 /// The pinch's cumulative `scale` moved, with the pointer where it last was.
517 pub fn pinch_update<A: Application>(&mut self, t: Turn<'_, A>, scale: f32) {
518 self.note_input();
519 let factor = scale / self.last_pinch_scale;
520 self.last_pinch_scale = scale;
521
522 let (px, py) = self.cursor_pos;
523 let mut rebuild = false;
524
525 // First offer the gesture as-is: apps with true pinch surfaces (the
526 // designer's 3D viewport) consume it here at 1:1 scale instead of
527 // through the wheel synthesis below.
528 if t.app.handle_pinch(factor, LogicalPosition::new(px, py), &mut rebuild) {
529 if rebuild {
530 *t.redraw = true;
531 }
532 return;
533 }
534
535 // Calculate the y_delta for PixelDelta mapping.
536 // Since cce-graph interprets factor = 1.0 + y_delta * 0.015, we reverse it:
537 let y_delta = (factor - 1.0) / 0.015;
538 let delta = MouseScrollDelta::PixelDelta(Position {
539 x: 0.0,
540 y: y_delta as f64,
541 });
542
543 if let Some(ctx) = t.app.ui_context_mut() {
544 ctx.ctrl_pressed = true; // Force ctrl_pressed = true for the pinch event
545 }
546 // A synthesized delta, not a scroll gesture: no glide, no fling.
547 crate::widget::scroll_motion::set_scroll_phase(crate::widget::ScrollPhase::Wheel);
548
549 t.app.handle_mouse_wheel(&delta, LogicalPosition::new(px, py), &mut rebuild);
550
551 if let Some(ctx) = t.app.ui_context_mut() {
552 ctx.ctrl_pressed = self.mods.ctrl; // Restore original state
553 }
554
555 if rebuild {
556 *t.redraw = true;
557 }
558 }
559
560 /// The keyboard reported new modifier state.
561 pub fn set_modifiers<A: Application>(&mut self, app: &mut A, mods: Modifiers) {
562 self.mods = mods;
563 self.sync_mods(app);
564 }
565
566 /// The window gained (`true`) or lost keyboard focus. Losing it drops a
567 /// held key and the held modifiers, whose releases go elsewhere.
568 pub fn keyboard_focus<A: Application>(&mut self, t: Turn<'_, A>, focused: bool) {
569 self.note_input();
570 if !focused {
571 self.pressed_key = None;
572 self.mods.ctrl = false;
573 self.mods.shift = false;
574 self.mods.alt = false;
575 }
576 let mut rebuild = false;
577 t.app.handle_focus_change(focused, &mut rebuild);
578 if rebuild {
579 *t.redraw = true;
580 }
581 }
582
583 /// A key went down or up, already in cce-ui's terms: the shell maps its
584 /// window system's key (an xkb keysym here) to `logical_key`, and passes
585 /// the text the key types, if any.
586 pub fn key<A: Application>(
587 &mut self,
588 mut t: Turn<'_, A>,
589 logical_key: Key,
590 text: Option<String>,
591 state: ElementState,
592 ) {
593 self.note_input();
594 let event = KeyEvent {
595 state,
596 logical_key,
597 text,
598 repeat: false,
599 ctrl: self.mods.ctrl,
600 shift: self.mods.shift,
601 alt: self.mods.alt,
602 };
603
604 if state == ElementState::Pressed {
605 if is_repeatable_key(&event.logical_key) {
606 self.pressed_key = Some(PressedKey {
607 logical_key: event.logical_key.clone(),
608 text: event.text.clone(),
609 first_pressed: Instant::now(),
610 last_repeated: Instant::now(),
611 });
612 } else {
613 self.pressed_key = None;
614 }
615 } else if state == ElementState::Released {
616 if let Some(ref pk) = self.pressed_key {
617 if pk.logical_key == event.logical_key {
618 self.pressed_key = None;
619 }
620 }
621 }
622
623 self.sync_mods(t.app);
624
625 // Escape dismisses the shared context menu before app dispatch — the
626 // toolkit-wide default, mirroring the click-outside dismissal. Consumed:
627 // while a menu is open, Escape means "close it", nothing else.
628 if state == ElementState::Pressed
629 && event.logical_key == Key::Named(NamedKey::Escape)
630 && crate::widget::context_menu::is_visible()
631 {
632 crate::widget::context_menu::hide();
633 *t.redraw = true;
634 return;
635 }
636
637 let mut rebuild = false;
638 if self.route_history_chord(t.app, &event, &mut rebuild)
639 || self.route_plate_navigation(t.app, &event, &mut rebuild)
640 {
641 *t.redraw = true;
642 return;
643 }
644 let msg = t.app.handle_key_input(&event, &mut rebuild);
645 t.deliver(msg, rebuild);
646 }
647
648 /// Text an input method committed. It is delivered as TYPED — a press
649 /// of a key whose text it is, then that key's release — so a widget
650 /// that inserts what a key types takes it as it is (see `crate::ime`).
651 /// Never a shortcut (no Ctrl, no Alt, whatever is held: an input method
652 /// commits on its own keys), never repeated, and past the chords: a
653 /// commit of "z" is a "z", not half of an undo.
654 pub fn commit_text<A: Application>(&mut self, t: Turn<'_, A>, text: String) {
655 self.note_input();
656 if text.is_empty() {
657 return;
658 }
659 let mut rebuild = false;
660 for state in [ElementState::Pressed, ElementState::Released] {
661 let event = KeyEvent {
662 state,
663 logical_key: Key::Character(text.clone()),
664 text: (state == ElementState::Pressed).then(|| text.clone()),
665 repeat: false,
666 ctrl: false,
667 shift: self.mods.shift,
668 alt: false,
669 };
670 let msg = t.app.handle_key_input(&event, &mut rebuild);
671 if let Some(msg) = msg {
672 let mut update_rebuild = false;
673 t.app.update(msg, &mut update_rebuild, t.exit);
674 rebuild |= update_rebuild;
675 }
676 }
677 *t.redraw |= rebuild;
678 // The editing widget shows the text it now holds.
679 *t.redraw = true;
680 }
681
682 /// Press and release a named key on the app's behalf — an assistive tool's Click or
683 /// Increment (`backend::a11y_unix::act`), after it has focused the widget. Through the
684 /// app's own `handle_key_input` like any key, but past the chords (a reader's Space is
685 /// not Tab), with no modifiers and no text, and never repeated.
686 pub fn press_named_key<A: Application>(&mut self, t: Turn<'_, A>, key: NamedKey) {
687 self.note_input();
688 let mut rebuild = false;
689 for state in [ElementState::Pressed, ElementState::Released] {
690 let event = KeyEvent {
691 state,
692 logical_key: Key::Named(key),
693 text: None,
694 repeat: false,
695 ctrl: false,
696 shift: false,
697 alt: false,
698 };
699 let msg = t.app.handle_key_input(&event, &mut rebuild);
700 if let Some(msg) = msg {
701 let mut update_rebuild = false;
702 t.app.update(msg, &mut update_rebuild, t.exit);
703 rebuild |= update_rebuild;
704 }
705 }
706 *t.redraw = true;
707 }
708
709 /// The input method's composition changed (`None`: it ended without a
710 /// commit, or the commit follows). The editing widget shows it from the
711 /// next frame.
712 pub fn preedit<A: Application>(&mut self, t: Turn<'_, A>, preedit: Option<crate::ime::Preedit>) {
713 self.note_input();
714 crate::ime::set_preedit(preedit);
715 *t.redraw = true;
716 }
717
718 /// The runner's key repeat: once a held key has been down
719 /// [`KEY_REPEAT_DELAY`], deliver it again every [`KEY_REPEAT_INTERVAL`].
720 /// Called once per loop turn.
721 pub fn repeat_keys<A: Application>(&mut self, mut t: Turn<'_, A>) {
722 let Some(ref mut pk) = self.pressed_key else { return };
723 let now = Instant::now();
724 if now.duration_since(pk.first_pressed) < KEY_REPEAT_DELAY
725 || now.duration_since(pk.last_repeated) < KEY_REPEAT_INTERVAL
726 {
727 return;
728 }
729 pk.last_repeated = now;
730 let event = KeyEvent {
731 state: ElementState::Pressed,
732 logical_key: pk.logical_key.clone(),
733 text: pk.text.clone(),
734 repeat: true,
735 ctrl: self.mods.ctrl,
736 shift: self.mods.shift,
737 alt: self.mods.alt,
738 };
739
740 self.sync_mods(t.app);
741
742 let mut rebuild = false;
743 if self.route_history_chord(t.app, &event, &mut rebuild)
744 || self.route_plate_navigation(t.app, &event, &mut rebuild)
745 {
746 *t.redraw = true;
747 } else {
748 let msg = t.app.handle_key_input(&event, &mut rebuild);
749 t.deliver(msg, false);
750 }
751 if rebuild {
752 *t.redraw = true;
753 }
754 }
755
756 /// Advance the app by `dt` seconds: its own `tick`, then its retained
757 /// `UiContext`'s (widget tick receivers — e.g. an animating Dropdown
758 /// popover) — but only when the app's own tick did not already tick the
759 /// context this turn. Receivers integrate `dt` (scroll glides, slider
760 /// inertia), so the old "double-ticking is harmless" assumption ran
761 /// every glide at twice its configured rate in apps that tick the
762 /// context themselves.
763 pub fn tick<A: Application>(&mut self, t: Turn<'_, A>, dt: f32) {
764 let mut rebuild = false;
765 let roster_ticks_before = t.app.ui_context_mut().map(|ctx| ctx.tick_count());
766 t.app.tick(dt, &mut rebuild);
767 if rebuild {
768 *t.redraw = true;
769 }
770 if let Some(ctx) = t.app.ui_context_mut() {
771 if Some(ctx.tick_count()) == roster_ticks_before && ctx.tick(dt) {
772 *t.redraw = true;
773 }
774 }
775 }
776
777 /// The toolkit's Tab traversal, for apps that opt in
778 /// (`Application::plate_navigation`): a bare Tab / Shift+Tab press moves
779 /// keyboard focus to the next / previous plate or well. Returns whether it
780 /// moved; otherwise the key is dispatched as usual.
781 fn route_plate_navigation<A: Application>(&self, app: &mut A, event: &KeyEvent, rebuild: &mut bool) -> bool {
782 if event.state != ElementState::Pressed {
783 return false;
784 }
785 // The group jump first (its chords carry ctrl); then a bare Tab.
786 let group_next = crate::widget::match_key_shortcut(event, &self.group_next_chord);
787 let group_prev = !group_next && crate::widget::match_key_shortcut(event, &self.group_prev_chord);
788 let bare_tab = event.logical_key == Key::Named(NamedKey::Tab)
789 && !self.mods.ctrl
790 && !self.mods.alt
791 && !self.mods.logo;
792 if !group_next && !group_prev && !bare_tab {
793 return false;
794 }
795 let reverse = if bare_tab { self.mods.shift } else { group_prev };
796 if !app.plate_navigation() {
797 return false;
798 }
799 // A focused widget that types Tab (a multi-line text box) keeps a bare Tab; the
800 // group chord still leaves it.
801 if bare_tab
802 && app.ui_context_mut().is_some_and(|ctx| {
803 ctx.focused_widget.and_then(|id| ctx.get_widget(id)).is_some_and(|w| w.keeps_tab())
804 })
805 {
806 return false;
807 }
808 let moved = app
809 .ui_context_mut()
810 .is_some_and(|ctx| if bare_tab { ctx.focus_step(reverse) } else { ctx.focus_step_group(reverse) });
811 if moved {
812 app.focus_stepped();
813 *rebuild = true;
814 }
815 moved
816 }
817
818 /// The toolkit-wide undo/redo routing: a press matching the `undo` /
819 /// `redo` chord goes to the focused widget first (`ContextAction::Undo`
820 /// / `Redo` — a text box that is editing steps its own typing), then to
821 /// the app's `Application::undo` / `redo`. Returns whether either took
822 /// it; otherwise the key is dispatched as usual, so an app with its own
823 /// scheme is undisturbed. Runs for repeats too — holding the chord walks
824 /// the history like holding Backspace walks the text.
825 fn route_history_chord<A: Application>(&self, app: &mut A, event: &KeyEvent, rebuild: &mut bool) -> bool {
826 if event.state != ElementState::Pressed {
827 return false;
828 }
829 let undo = crate::widget::match_key_shortcut(event, &self.undo_chord);
830 let redo = !undo && crate::widget::match_key_shortcut(event, &self.redo_chord);
831 if !undo && !redo {
832 return false;
833 }
834 let action = if undo { crate::widget::ContextAction::Undo } else { crate::widget::ContextAction::Redo };
835 if let Some(ctx) = app.ui_context_mut() {
836 if ctx.focused_context_action(action) {
837 *rebuild = true;
838 return true;
839 }
840 }
841 let taken = if undo { app.undo(rebuild) } else { app.redo(rebuild) };
842 if taken {
843 *rebuild = true;
844 }
845 taken
846 }
847 }
848
849 /// A coalesced scroll frame as the delta the app is handed, and the phase it
850 /// belongs to. Smooth-scroll phase: a finger lift is a stop frame (no
851 /// delta); finger/continuous sources track 1:1 and may fling on the lift;
852 /// everything else is a wheel notch that glides.
853 fn scroll_delta(frame: &ScrollFrame, factors: crate::input::ScrollFactors) -> (crate::widget::ScrollPhase, MouseScrollDelta) {
854 let ScrollFrame { h, v, discrete_h, discrete_v, source, stop } = *frame;
855 let no_delta = h == 0.0 && v == 0.0 && discrete_h == 0 && discrete_v == 0;
856 let phase = if stop && no_delta {
857 crate::widget::ScrollPhase::FingerEnd
858 } else if discrete_h == 0
859 && discrete_v == 0
860 && matches!(source, None | Some(ScrollSource::Finger) | Some(ScrollSource::Continuous))
861 {
862 crate::widget::ScrollPhase::Finger
863 } else {
864 crate::widget::ScrollPhase::Wheel
865 };
866 let delta = if discrete_h == 0 && discrete_v == 0 {
867 // Pixel scroll event from touchpad / smooth mouse
868 MouseScrollDelta::PixelDelta(Position {
869 x: -h * factors.trackpad,
870 y: -v * factors.trackpad,
871 })
872 } else {
873 // Discrete scroll event (e.g. wheel clicks)
874 let h_lines = if discrete_h != 0 { discrete_h as f32 } else { h as f32 / 10.0 };
875 let v_lines = if discrete_v != 0 { discrete_v as f32 } else { v as f32 / 10.0 };
876 MouseScrollDelta::LineDelta(-h_lines * factors.mouse as f32, -v_lines * factors.mouse as f32)
877 };
878 (phase, delta)
879 }
880
881 /// The CSD resize edge under (lx, ly), if the point is within the border.
882 fn csd_edge(lx: f32, ly: f32, size: LogicalSize) -> Option<ResizeEdge> {
883 let b = CSD_BORDER;
884 if ly < b {
885 Some(if lx < b {
886 ResizeEdge::TopLeft
887 } else if lx > size.width - b {
888 ResizeEdge::TopRight
889 } else {
890 ResizeEdge::Top
891 })
892 } else if ly > size.height - b {
893 Some(if lx < b {
894 ResizeEdge::BottomLeft
895 } else if lx > size.width - b {
896 ResizeEdge::BottomRight
897 } else {
898 ResizeEdge::Bottom
899 })
900 } else if lx < b {
901 Some(ResizeEdge::Left)
902 } else if lx > size.width - b {
903 Some(ResizeEdge::Right)
904 } else {
905 None
906 }
907 }
908
909 #[cfg(test)]
910 mod tests {
911 //! The routing, driven with no window system: a mock app records what
912 //! reaches it.
913 use super::*;
914 use crate::backend::app::{AppSender, WindowSettings};
915
916 #[derive(Debug, Clone, PartialEq)]
917 enum Seen {
918 Move(f32, f32),
919 Button(MouseButton, ElementState, f32, f32),
920 Wheel(MouseScrollDelta),
921 Key(Key, bool),
922 Focus(bool),
923 Undo,
924 Update(u32),
925 }
926
927 struct Mock {
928 seen: Vec<Seen>,
929 csd: bool,
930 takes_undo: bool,
931 /// A press makes this message, which `update` records.
932 press_msg: Option<u32>,
933 /// A widget tree, for the Tab walk; none by default, as most apps here.
934 ctx: Option<crate::context::UiContext>,
935 }
936
937 impl Application for Mock {
938 type Message = u32;
939 fn create(_: AppSender<u32>) -> Self {
940 unreachable!("built directly")
941 }
942 fn settings(&self) -> WindowSettings {
943 WindowSettings {
944 title: String::new(),
945 app_id: "mock".into(),
946 width: 400,
947 height: 300,
948 fullscreen: false,
949 min_size: None,
950 }
951 }
952 fn update(&mut self, msg: u32, needs_rebuild: &mut bool, _exit: &mut bool) {
953 self.seen.push(Seen::Update(msg));
954 *needs_rebuild = true;
955 }
956 fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
957 fn handle_pointer_move(&mut self, pos: LogicalPosition, _: &mut bool) {
958 self.seen.push(Seen::Move(pos.x, pos.y));
959 }
960 fn handle_mouse_input(
961 &mut self,
962 button: MouseButton,
963 state: ElementState,
964 pos: LogicalPosition,
965 _: &mut bool,
966 ) -> Option<u32> {
967 self.seen.push(Seen::Button(button, state, pos.x, pos.y));
968 if state == ElementState::Pressed { self.press_msg } else { None }
969 }
970 fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, _: LogicalPosition, _: &mut bool) {
971 self.seen.push(Seen::Wheel(*delta));
972 }
973 fn handle_key_input(&mut self, event: &KeyEvent, _: &mut bool) -> Option<u32> {
974 self.seen.push(Seen::Key(event.logical_key.clone(), event.repeat));
975 None
976 }
977 fn handle_focus_change(&mut self, focused: bool, _: &mut bool) {
978 self.seen.push(Seen::Focus(focused));
979 }
980 fn undo(&mut self, _: &mut bool) -> bool {
981 self.seen.push(Seen::Undo);
982 self.takes_undo
983 }
984 fn csd_resize_borders(&self) -> bool {
985 self.csd
986 }
987 fn csd_titlebar_move(&self) -> bool {
988 self.csd
989 }
990 fn ui_context_mut(&mut self) -> Option<&mut crate::context::UiContext> {
991 self.ctx.as_mut()
992 }
993 }
994
995 fn mock() -> Mock {
996 Mock { seen: Vec::new(), csd: false, takes_undo: false, press_msg: None, ctx: None }
997 }
998
999 /// The Tab walk is on unless an app says otherwise. Tab leaves a one-line field for the
1000 /// next stop and never reaches the app; a multi-line box that is editing keeps it (the
1001 /// app's key handling, which forwards it to the box, sees it) until Ctrl+Tab moves on.
1002 #[test]
1003 fn tab_walks_the_stops_but_a_multi_line_box_keeps_it() {
1004 use crate::widget::{TextBox, WidgetHost, WidgetHostExt};
1005 let tab = Key::Named(NamedKey::Tab);
1006 let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
1007 let mut ctx = crate::context::UiContext::new();
1008 let name = ctx.insert(TextBox::new(String::new()));
1009 ctx[name].set_rect(10.0, 10.0, 200.0, 24.0);
1010 let notes = ctx.insert(TextBox::new(String::new()).with_multiline(true));
1011 ctx[notes].set_rect(10.0, 50.0, 200.0, 120.0);
1012 let save = ctx.insert(crate::widget::Button::new(10.0, 200.0, 80.0, 24.0).with_label("Save"));
1013 app.ctx = Some(ctx);
1014 assert!(app.plate_navigation(), "on by default");
1015 let focused = |app: &mut Mock| app.ctx.as_ref().unwrap().focused_widget;
1016
1017 d.key(turn(&mut app, &mut f), tab.clone(), Some("\t".into()), ElementState::Pressed);
1018 assert_eq!(focused(&mut app), Some(name.id()), "the first stop");
1019 d.key(turn(&mut app, &mut f), tab.clone(), Some("\t".into()), ElementState::Pressed);
1020 assert_eq!(focused(&mut app), Some(notes.id()), "Tab leaves a one-line field");
1021 assert!(!app.seen.iter().any(|s| matches!(s, Seen::Key(k, _) if *k == tab)), "the walk took both");
1022 assert!(app.ctx.as_ref().unwrap()[notes].keeps_tab(), "focused, the multi-line box is editing");
1023
1024 d.key(turn(&mut app, &mut f), tab.clone(), Some("\t".into()), ElementState::Pressed);
1025 assert_eq!(focused(&mut app), Some(notes.id()), "the box keeps Tab");
1026 assert_eq!(app.seen.last(), Some(&Seen::Key(tab.clone(), false)), "and the app hands it on");
1027
1028 d.set_modifiers(&mut app, Modifiers { ctrl: true, shift: false, alt: false, logo: false });
1029 d.key(turn(&mut app, &mut f), tab.clone(), None, ElementState::Pressed);
1030 assert_eq!(focused(&mut app), Some(save.id()), "Ctrl+Tab leaves it");
1031 }
1032
1033 /// A driver with known chords, whatever the machine's input.kdl says.
1034 fn driver() -> Driver {
1035 Driver {
1036 undo_chord: "ctrl+z".into(),
1037 redo_chord: "ctrl+shift+z".into(),
1038 group_next_chord: "ctrl+tab".into(),
1039 group_prev_chord: "ctrl+shift+tab".into(),
1040 ..Driver::new()
1041 }
1042 }
1043
1044 struct Flags {
1045 redraw: bool,
1046 exit: bool,
1047 }
1048
1049 fn turn<'a>(app: &'a mut Mock, f: &'a mut Flags) -> Turn<'a, Mock> {
1050 Turn { app, redraw: &mut f.redraw, exit: &mut f.exit }
1051 }
1052
1053 const SIZE: LogicalSize = LogicalSize { width: 400.0, height: 300.0 };
1054
1055 fn site(can_grab: bool) -> PressSite {
1056 PressSite { size: SIZE, on_popup: false, can_grab, own_edges: false }
1057 }
1058
1059 #[test]
1060 fn a_lost_pointer_releases_what_was_held_then_clears_hover() {
1061 let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
1062 let at = LogicalPosition::new(50.0, 60.0);
1063 d.cursor_pos = (50.0, 60.0);
1064 d.pointer_press(turn(&mut app, &mut f), MouseButton::Left, at, site(false));
1065 d.pointer_press(turn(&mut app, &mut f), MouseButton::Middle, at, site(false));
1066 assert_eq!(d.buttons_down, 1 | 4);
1067 app.seen.clear();
1068
1069 d.pointer_leave(turn(&mut app, &mut f));
1070 assert_eq!(
1071 app.seen,
1072 vec![
1073 Seen::Button(MouseButton::Left, ElementState::Released, 50.0, 60.0),
1074 Seen::Button(MouseButton::Middle, ElementState::Released, 50.0, 60.0),
1075 Seen::Move(-10000.0, -10000.0),
1076 ]
1077 );
1078 assert_eq!(d.buttons_down, 0);
1079 }
1080
1081 #[test]
1082 fn a_press_on_the_border_is_the_windows_only_when_the_shell_can_grab() {
1083 let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
1084 app.csd = true;
1085 let corner = LogicalPosition::new(2.0, 2.0);
1086 let r = d.pointer_press(turn(&mut app, &mut f), MouseButton::Left, corner, site(true));
1087 assert_eq!(r, Press::Resize(ResizeEdge::TopLeft));
1088 assert!(app.seen.is_empty(), "a grab never reaches the app: {:?}", app.seen);
1089
1090 let band = LogicalPosition::new(100.0, 20.0);
1091 let r = d.pointer_press(turn(&mut app, &mut f), MouseButton::Left, band, site(true));
1092 assert_eq!(r, Press::Move);
1093
1094 // No grab to start (a layer surface, a shell without grabs): the app's.
1095 let r = d.pointer_press(turn(&mut app, &mut f), MouseButton::Left, corner, site(false));
1096 assert_eq!(r, Press::Dispatched);
1097 assert_eq!(app.seen, vec![Seen::Button(MouseButton::Left, ElementState::Pressed, 2.0, 2.0)]);
1098
1099 // A window system that resizes from its own edges: the band is no
1100 // grab, and the press is the app's.
1101 app.seen.clear();
1102 let own = PressSite { own_edges: true, ..site(true) };
1103 assert_eq!(d.pointer_press(turn(&mut app, &mut f), MouseButton::Left, corner, own), Press::Dispatched);
1104 assert_eq!(app.seen, vec![Seen::Button(MouseButton::Left, ElementState::Pressed, 2.0, 2.0)]);
1105 // But the titlebar band still moves the window.
1106 assert_eq!(d.pointer_press(turn(&mut app, &mut f), MouseButton::Left, band, own), Press::Move);
1107
1108 // Nor is a press through the menu popup ever a grab.
1109 app.seen.clear();
1110 let popup = PressSite { on_popup: true, ..site(true) };
1111 assert_eq!(d.pointer_press(turn(&mut app, &mut f), MouseButton::Left, corner, popup), Press::Dispatched);
1112 assert_eq!(app.seen.len(), 1);
1113 }
1114
1115 #[test]
1116 fn a_pressed_message_reaches_update_and_asks_for_a_frame() {
1117 let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
1118 app.press_msg = Some(7);
1119 d.pointer_press(turn(&mut app, &mut f), MouseButton::Right, LogicalPosition::new(9.0, 9.0), site(true));
1120 assert_eq!(app.seen.last(), Some(&Seen::Update(7)));
1121 assert!(f.redraw);
1122 }
1123
1124 #[test]
1125 fn a_commit_is_typed_text_and_never_a_shortcut_or_a_held_key() {
1126 let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
1127 app.takes_undo = true;
1128 // Ctrl held as the input method commits "z": typed, not an undo.
1129 d.set_modifiers(&mut app, Modifiers { ctrl: true, ..Modifiers::default() });
1130 d.commit_text(turn(&mut app, &mut f), "z".into());
1131 d.commit_text(turn(&mut app, &mut f), "日本".into());
1132 d.commit_text(turn(&mut app, &mut f), String::new());
1133 let typed = |t: &str| Seen::Key(Key::Character(t.into()), false);
1134 assert_eq!(app.seen, vec![typed("z"), typed("z"), typed("日本"), typed("日本")]);
1135 assert!(d.pressed_key.is_none(), "nothing left held to repeat");
1136 assert!(f.redraw);
1137 }
1138
1139 #[test]
1140 fn the_undo_chord_goes_to_the_app_hook_before_key_dispatch() {
1141 let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
1142 d.set_modifiers(&mut app, Modifiers { ctrl: true, ..Modifiers::default() });
1143 app.takes_undo = true;
1144 d.key(turn(&mut app, &mut f), Key::Character("z".into()), None, ElementState::Pressed);
1145 assert_eq!(app.seen, vec![Seen::Undo]);
1146 assert!(f.redraw);
1147
1148 // Declined by the app, the key is dispatched as usual.
1149 app.seen.clear();
1150 app.takes_undo = false;
1151 d.key(turn(&mut app, &mut f), Key::Character("z".into()), None, ElementState::Pressed);
1152 assert_eq!(app.seen, vec![Seen::Undo, Seen::Key(Key::Character("z".into()), false)]);
1153 }
1154
1155 #[test]
1156 fn a_held_key_repeats_after_the_delay_and_stops_on_release() {
1157 let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
1158 let bs = Key::Named(NamedKey::Backspace);
1159 d.key(turn(&mut app, &mut f), bs.clone(), None, ElementState::Pressed);
1160 app.seen.clear();
1161
1162 // Inside the delay: nothing.
1163 d.repeat_keys(turn(&mut app, &mut f));
1164 assert!(app.seen.is_empty());
1165
1166 // Past it: one repeat per turn that is an interval on.
1167 let pk = d.pressed_key.as_mut().unwrap();
1168 pk.first_pressed -= KEY_REPEAT_DELAY;
1169 pk.last_repeated -= KEY_REPEAT_INTERVAL;
1170 d.repeat_keys(turn(&mut app, &mut f));
1171 d.repeat_keys(turn(&mut app, &mut f));
1172 assert_eq!(app.seen, vec![Seen::Key(bs.clone(), true)]);
1173
1174 d.key(turn(&mut app, &mut f), bs, None, ElementState::Released);
1175 assert!(d.pressed_key.is_none());
1176 }
1177
1178 #[test]
1179 fn losing_focus_drops_the_held_key_and_modifiers() {
1180 let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
1181 d.set_modifiers(&mut app, Modifiers { ctrl: true, shift: true, alt: true, logo: true });
1182 d.key(turn(&mut app, &mut f), Key::Character("a".into()), Some("a".into()), ElementState::Pressed);
1183 assert!(d.pressed_key.is_some());
1184
1185 d.keyboard_focus(turn(&mut app, &mut f), false);
1186 assert!(d.pressed_key.is_none());
1187 // Logo is left as it was, as the runner always left it.
1188 assert_eq!(d.mods, Modifiers { ctrl: false, shift: false, alt: false, logo: true });
1189 assert_eq!(app.seen.last(), Some(&Seen::Focus(false)));
1190 }
1191
1192 #[test]
1193 fn a_finger_is_the_left_button_and_never_the_windows() {
1194 // Only the tap and the hold: a touch scroll publishes the scroll
1195 // phase process-wide, which a parallel suite must not race.
1196 use crate::backend::touch::TouchAction::*;
1197 let (mut app, mut d) = (mock(), driver());
1198 app.csd = true;
1199 app.press_msg = Some(7);
1200 let mut f = Flags { redraw: false, exit: false };
1201 // A tap on the resize border: a click there, not a resize.
1202 d.touch(turn(&mut app, &mut f), vec![Hover(2.0, 2.0), Press(2.0, 2.0), Release(2.0, 2.0), Leave], None);
1203 assert_eq!(
1204 app.seen,
1205 vec![
1206 Seen::Move(2.0, 2.0),
1207 Seen::Button(MouseButton::Left, ElementState::Pressed, 2.0, 2.0),
1208 Seen::Update(7),
1209 Seen::Button(MouseButton::Left, ElementState::Released, 2.0, 2.0),
1210 Seen::Move(-10000.0, -10000.0),
1211 ]
1212 );
1213 assert!(f.redraw, "the press's message asked for a frame");
1214 // A hold, then a drag: the held left button's motion.
1215 app.seen.clear();
1216 d.touch(turn(&mut app, &mut f), vec![Press(10.0, 10.0), Drag(40.0, 10.0), Release(40.0, 10.0)], None);
1217 assert_eq!(app.seen[2], Seen::Move(40.0, 10.0));
1218 assert_eq!(d.buttons_down, 0, "a finger holds no pointer button");
1219 }
1220
1221 #[test]
1222 fn a_scroll_frame_is_its_phase_and_delta() {
1223 // The pure half of `scroll`: the dispatch itself publishes the phase
1224 // process-wide, which a parallel suite must not race.
1225 use crate::input::ScrollFactors;
1226 use crate::widget::ScrollPhase;
1227 let unit = ScrollFactors { mouse: 1.0, trackpad: 1.0 };
1228 let notch = ScrollFrame { v: 15.0, discrete_v: 1, source: Some(ScrollSource::Wheel), ..ScrollFrame::default() };
1229 assert_eq!(scroll_delta(¬ch, unit), (ScrollPhase::Wheel, MouseScrollDelta::LineDelta(-0.0, -1.0)));
1230
1231 let finger = ScrollFrame { v: 4.0, source: Some(ScrollSource::Finger), ..ScrollFrame::default() };
1232 assert_eq!(
1233 scroll_delta(&finger, ScrollFactors { mouse: 1.0, trackpad: 2.0 }),
1234 (ScrollPhase::Finger, MouseScrollDelta::PixelDelta(Position { x: -0.0, y: -8.0 }))
1235 );
1236 // No source named is a finger too; a lift with no delta ends it.
1237 let unnamed = ScrollFrame { h: 3.0, ..ScrollFrame::default() };
1238 assert_eq!(scroll_delta(&unnamed, unit).0, ScrollPhase::Finger);
1239 let lift = ScrollFrame { stop: true, source: Some(ScrollSource::Finger), ..ScrollFrame::default() };
1240 assert_eq!(scroll_delta(&lift, unit).0, ScrollPhase::FingerEnd);
1241 // A tilt wheel, or anything newer, glides like a wheel.
1242 let tilt = ScrollFrame { h: 5.0, source: Some(ScrollSource::WheelTilt), ..ScrollFrame::default() };
1243 assert_eq!(scroll_delta(&tilt, unit).0, ScrollPhase::Wheel);
1244 }
1245 }