git.lucas.co / cce-ui
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(&notch, 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 }