Skip to main content

crabshell_ui/
input.rs

1//! Pointer input: hit testing, hover and pressed state, and calling button
2//! handlers.
3//!
4//! The platform turns `wl_pointer` events into calls on [`Window`]. Handlers
5//! run after the window's tree is released, inside `catch_unwind`, so a
6//! panicking handler is logged and disabled and the bar keeps running.
7
8use std::any::Any;
9use std::cell::Cell;
10use std::panic::{self, AssertUnwindSafe};
11use std::rc::Rc;
12
13use crabshell_reactive::{Signal, untrack};
14
15use crate::cx::Cx;
16use crate::geometry::{Point, Rect};
17use crate::keyboard::KeyHandler;
18use crate::scroll::PIXELS_PER_NOTCH;
19use crate::tree::{ButtonNode, Change, Node, NodeId, NodeKind, Tree, borrow};
20use crate::window::Window;
21
22/// Logical pixels of touchpad scrolling per scroll step.
23const PIXELS_PER_STEP: f32 = 15.0;
24
25/// `axis_value120` units per mouse-wheel notch.
26const VALUE120_PER_STEP: i32 = 120;
27
28/// A user's event handler, and whether it was disabled after panicking.
29pub(crate) struct Handler<F: ?Sized> {
30  disabled: Cell<bool>,
31  f: F,
32}
33
34impl<F> Handler<F> {
35  pub(crate) fn new(f: F) -> Handler<F> {
36    Handler {
37      disabled: Cell::new(false),
38      f,
39    }
40  }
41}
42
43type ClickHandler = Rc<Handler<dyn Fn(&Cx)>>;
44type ScrollHandler = Rc<Handler<dyn Fn(&Cx, i32)>>;
45
46/// A button's handlers.
47#[derive(Default)]
48pub(crate) struct Handlers {
49  pub(crate) click: Option<ClickHandler>,
50  pub(crate) press: Option<ClickHandler>,
51  pub(crate) right_click: Option<ClickHandler>,
52  pub(crate) middle_click: Option<ClickHandler>,
53  pub(crate) scroll: Option<ScrollHandler>,
54  pub(crate) key: Option<KeyHandler>,
55}
56
57/// A pointer button.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum PointerButton {
60  /// The primary button; it runs `on_click`.
61  Left,
62  /// The secondary button; it runs `on_right_click`.
63  Right,
64  /// The middle button; it runs `on_middle_click`.
65  Middle,
66}
67
68/// One scroll event on the vertical axis, as `wl_pointer` reports it.
69/// Positive values scroll down.
70#[derive(Clone, Copy, Debug, PartialEq)]
71pub enum ScrollDelta {
72  /// A mouse wheel, in `axis_value120` units: 120 per notch, less for a
73  /// high-resolution wheel.
74  Wheel(i32),
75  /// Smooth scrolling, such as from a touchpad, in logical pixels.
76  Pixels(f32),
77}
78
79/// The cursor the platform should show.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum CursorShape {
82  /// The default arrow.
83  Default,
84  /// A pointing hand, over a button.
85  Pointer,
86  /// An I-beam, over a text field.
87  Text,
88}
89
90/// Where the pointer is and what it is doing.
91#[derive(Default)]
92pub(crate) struct Pointer {
93  position: Option<Point>,
94  /// The button under the pointer.
95  hovered: Option<NodeId>,
96  /// The buttons with a `hovered` signal that the pointer is inside: the
97  /// hovered one and those around it.
98  inside: Vec<(NodeId, Signal<bool>)>,
99  /// The button a pointer button went down on, until it comes up.
100  pressed: Option<(NodeId, PointerButton)>,
101  /// Scrolling that hasn't added up to a whole step yet.
102  wheel: i32,
103  pixels: f32,
104  /// A hover or press change asked for the pending layout.
105  caused_layout: bool,
106}
107
108impl Pointer {
109  /// Forgets nodes that were removed from the tree.
110  pub(crate) fn forget(&mut self, removed: &[NodeId]) {
111    if self.hovered.is_some_and(|id| removed.contains(&id)) {
112      self.hovered = None;
113    }
114    let (gone, kept): (Vec<_>, Vec<_>) = std::mem::take(&mut self.inside)
115      .into_iter()
116      .partition(|(id, _)| removed.contains(id));
117    for (_, signal) in gone {
118      // Unless a button still here shares it.
119      if !kept.iter().any(|(_, other)| *other == signal) {
120        signal.try_set(false);
121      }
122    }
123    self.inside = kept;
124    if self.pressed.is_some_and(|(id, _)| removed.contains(&id)) {
125      self.pressed = None;
126    }
127  }
128}
129
130impl Window {
131  /// The pointer entered the window or moved, to `position` in logical
132  /// pixels. Returns the cursor to show.
133  pub fn pointer_motion(&self, position: Point) -> CursorShape {
134    let mut tree = borrow(&self.tree);
135    tree.pointer.position = Some(position);
136    tree.hover_at(position);
137    tree.cursor()
138  }
139
140  /// The pointer left the window.
141  pub fn pointer_leave(&self) {
142    let mut tree = borrow(&self.tree);
143    tree.pointer.position = None;
144    tree.set_pressed(None);
145    tree.set_hovered(None, &[]);
146  }
147
148  /// A pointer button went down or up. Releasing it over the button it
149  /// went down on runs that button's `on_click`, `on_right_click` or
150  /// `on_middle_click`.
151  pub fn pointer_button(&self, button: PointerButton, pressed: bool) {
152    let call = borrow(&self.tree).button(button, pressed);
153    if let Some(call) = call {
154      call.run();
155    }
156  }
157
158  /// The pointer scrolled. Once whole steps add up, the `on_scroll` of the
159  /// button under the pointer runs with them. Without one, the innermost
160  /// scroll view under the pointer scrolls.
161  pub fn pointer_scroll(&self, delta: ScrollDelta) {
162    let scrolled = borrow(&self.tree).scroll(delta);
163    match scrolled {
164      Some(Scrolled::Button(call)) => call.run(),
165      // A signal whose owner is gone just doesn't scroll.
166      Some(Scrolled::View(signal, offset)) => {
167        signal.try_set(offset);
168      }
169      None => {}
170    }
171  }
172
173  /// A touchpad scroll gesture ended (`wl_pointer.axis_stop`): scrolling
174  /// that hasn't added up to a whole step is dropped, so it doesn't carry
175  /// into the next gesture. A wheel's partial notch is kept.
176  pub fn pointer_scroll_end(&self) {
177    borrow(&self.tree).pointer.pixels = 0.0;
178  }
179
180  /// The cursor to show.
181  pub fn cursor(&self) -> CursorShape {
182    self.tree.borrow().cursor()
183  }
184}
185
186impl Tree {
187  /// The deepest button under `point`, in the coordinates of `id`'s
188  /// parent, in `id`'s subtree, with the nodes on the way down to it,
189  /// itself included, pushed onto `path`. Later children are drawn on
190  /// top, so they are tried first.
191  fn hit(&self, id: NodeId, point: Point, path: &mut Vec<NodeId>) -> Option<NodeId> {
192    let node = self.node(id)?;
193    let point = local(node, point)?;
194    let rect = node.rect?;
195    // Children reach past the box when they are moved or overflow it, so
196    // the subtree's whole area is searched, and the box only for itself.
197    if !node.inner_bounds.contains(point) {
198      return None;
199    }
200    let style = node.effective_style();
201    // A clip hides its children under its border too.
202    let border = style.border.map_or(0.0, |border| border.width.max(0.0));
203    let inside = Rect::new(
204      rect.x + border,
205      rect.y + border,
206      rect.width - 2.0 * border,
207      rect.height - 2.0 * border,
208    );
209    let children = if node.clips() && !inside.contains(point) {
210      Vec::new()
211    } else {
212      self.taffy.children(id).unwrap_or_default()
213    };
214    path.push(id);
215    if let Some(hit) = children
216      .into_iter()
217      .rev()
218      .find_map(|child| self.hit(child, point, path))
219    {
220      return Some(hit);
221    }
222    if rect.contains(point) && matches!(node.kind, NodeKind::Button(_) | NodeKind::Input(_)) {
223      return Some(id);
224    }
225    path.pop();
226    None
227  }
228
229  /// Hovers the deepest button under `point`, and marks the buttons around
230  /// it as having the pointer inside.
231  fn hover_at(&mut self, point: Point) {
232    let mut path = Vec::new();
233    let hit = self.hit(self.root, point, &mut path);
234    self.set_hovered(hit, &path);
235  }
236
237  fn cursor(&self) -> CursorShape {
238    match self
239      .pointer
240      .hovered
241      .and_then(|id| self.node(id))
242      .map(|node| &node.kind)
243    {
244      Some(NodeKind::Input(_)) => CursorShape::Text,
245      Some(_) => CursorShape::Pointer,
246      None => CursorShape::Default,
247    }
248  }
249
250  /// Hover checked again after a layout, since buttons may have moved
251  /// under a pointer that didn't. Not after a layout a hover change asked
252  /// for: a hover style that moves the button off the pointer would
253  /// otherwise unhover it, move it back, hover it again, and so on.
254  pub(crate) fn pointer_after_layout(&mut self) {
255    if std::mem::take(&mut self.pointer.caused_layout) {
256      return;
257    }
258    if let Some(position) = self.pointer.position {
259      self.hover_at(position);
260    }
261  }
262
263  /// `hovered` is the button under the pointer, and `path` the nodes from
264  /// the root down to it.
265  fn set_hovered(&mut self, hovered: Option<NodeId>, path: &[NodeId]) {
266    self.set_inside(path);
267    let old = self.pointer.hovered;
268    if old == hovered {
269      return;
270    }
271    self.pointer.hovered = hovered;
272    self.pointer.wheel = 0;
273    self.pointer.pixels = 0.0;
274    if let Some(old) = old {
275      self.set_button_state(old, |button| button.hovered = false);
276    }
277    if let Some(new) = hovered {
278      self.set_button_state(new, |button| button.hovered = true);
279    }
280  }
281
282  /// Writes `false` to the `hovered` signals of the buttons the pointer
283  /// left and `true` to those it entered, among the nodes on `path`.
284  fn set_inside(&mut self, path: &[NodeId]) {
285    let inside: Vec<(NodeId, Signal<bool>)> = path
286      .iter()
287      .filter_map(|&id| match &self.node(id)?.kind {
288        NodeKind::Button(button) => Some((id, button.hover_signal?)),
289        _ => None,
290      })
291      .collect();
292    // Each signal is written once, and only on a change: `true` while any
293    // button sharing it has the pointer inside.
294    let holds = |list: &[(NodeId, Signal<bool>)], signal| list.iter().any(|(_, s)| *s == signal);
295    let mut seen = Vec::new();
296    for &(_, signal) in self.pointer.inside.iter().chain(&inside) {
297      if seen.contains(&signal) {
298        continue;
299      }
300      seen.push(signal);
301      let now = holds(&inside, signal);
302      if holds(&self.pointer.inside, signal) != now {
303        signal.try_set(now);
304      }
305    }
306    self.pointer.inside = inside;
307  }
308
309  fn set_pressed(&mut self, pressed: Option<(NodeId, PointerButton)>) {
310    let old = std::mem::replace(&mut self.pointer.pressed, pressed);
311    if let Some((old, _)) = old {
312      self.set_button_state(old, |button| button.pressed = false);
313    }
314    if let Some((new, _)) = pressed {
315      self.set_button_state(new, |button| button.pressed = true);
316    }
317  }
318
319  /// Changes a button's hover or pressed state, noting when that needs a
320  /// layout.
321  fn set_button_state(&mut self, id: NodeId, f: impl FnOnce(&mut ButtonNode)) {
322    if self.update_button_style(id, f) == Change::Layout {
323      self.pointer.caused_layout = true;
324    }
325  }
326
327  fn button(&mut self, button: PointerButton, pressed: bool) -> Option<Call> {
328    if pressed {
329      if self.pointer.pressed.is_none()
330        && let Some(target) = self.pointer.hovered
331      {
332        self.set_pressed(Some((target, button)));
333        if button == PointerButton::Left {
334          self.press_field(target);
335          return self.call(target, Action::Press);
336        }
337      }
338      return None;
339    }
340    let (target, held) = self.pointer.pressed?;
341    if held != button {
342      return None;
343    }
344    self.set_pressed(None);
345    if self.pointer.hovered != Some(target) {
346      return None;
347    }
348    let action = match button {
349      PointerButton::Left => Action::Click,
350      PointerButton::Right => Action::RightClick,
351      PointerButton::Middle => Action::MiddleClick,
352    };
353    self.call(target, action)
354  }
355
356  fn scroll(&mut self, delta: ScrollDelta) -> Option<Scrolled> {
357    let handled = self.pointer.hovered.filter(|&id| self.scrolls(id));
358    let Some(target) = handled else {
359      return self.scroll_view(delta);
360    };
361    // Steps down, rounded toward zero; the rest waits for more scrolling.
362    let down = match delta {
363      ScrollDelta::Wheel(value120) => {
364        self.pointer.wheel = self.pointer.wheel.saturating_add(value120);
365        let steps = self.pointer.wheel / VALUE120_PER_STEP;
366        self.pointer.wheel -= steps * VALUE120_PER_STEP;
367        steps
368      }
369      ScrollDelta::Pixels(pixels) => {
370        if !pixels.is_finite() {
371          return None;
372        }
373        self.pointer.pixels += pixels;
374        let steps = (self.pointer.pixels / PIXELS_PER_STEP).trunc();
375        self.pointer.pixels -= steps * PIXELS_PER_STEP;
376        steps as i32
377      }
378    };
379    if down == 0 {
380      return None;
381    }
382    // Handlers count up as positive.
383    self
384      .call(target, Action::Scroll(down.saturating_neg()))
385      .map(Scrolled::Button)
386  }
387
388  /// A press on field `id` focuses it and puts the caret under the
389  /// pointer. Anything else is left alone.
390  fn press_field(&mut self, id: NodeId) {
391    let Some(position) = self
392      .pointer
393      .position
394      .and_then(|at| self.local_point(id, at))
395    else {
396      return;
397    };
398    let Some(NodeKind::Input(input)) = self.node(id).map(|node| &node.kind) else {
399      return;
400    };
401    let at = input.hit(position);
402    self.focus(Some(id));
403    self.update_input(id, |input| {
404      input.edit.cursor = at;
405      input.edit.anchor = None;
406    });
407  }
408
409  /// Whether button `id` has an `on_scroll`.
410  fn scrolls(&self, id: NodeId) -> bool {
411    let Some(NodeKind::Button(button)) = self.node(id).map(|node| &node.kind) else {
412      return false;
413    };
414    untrack(|| {
415      button
416        .handlers
417        .try_with(|handlers| handlers.scroll.is_some())
418    })
419    .unwrap_or(false)
420  }
421
422  /// Scrolls the innermost scroll view under the pointer by `delta`, as
423  /// far as its content goes.
424  fn scroll_view(&mut self, delta: ScrollDelta) -> Option<Scrolled> {
425    let position = self.pointer.position?;
426    let id = self.scroll_view_at(self.root, position)?;
427    let pixels = match delta {
428      ScrollDelta::Wheel(value120) => value120 as f32 / VALUE120_PER_STEP as f32 * PIXELS_PER_NOTCH,
429      ScrollDelta::Pixels(pixels) if pixels.is_finite() => pixels,
430      ScrollDelta::Pixels(_) => return None,
431    };
432    let NodeKind::Scroll(scroll) = &self.node(id)?.kind else {
433      return None;
434    };
435    // From the signal, not the last layout, so scrolls and writes since
436    // then add up.
437    let now = untrack(|| scroll.signal.try_get())?;
438    let now = if now.is_nan() {
439      0.0
440    } else {
441      now.clamp(0.0, scroll.most)
442    };
443    let offset = (now + pixels).clamp(0.0, scroll.most);
444    (offset != now).then_some(Scrolled::View(scroll.signal, offset))
445  }
446
447  /// The innermost scroll view under `point` in `id`'s subtree, where its
448  /// ancestors' clips let the pointer reach.
449  fn scroll_view_at(&self, id: NodeId, point: Point) -> Option<NodeId> {
450    let node = self.node(id)?;
451    let point = local(node, point)?;
452    // As in `hit`: the subtree's area for the children, the box for itself.
453    if !node.inner_bounds.contains(point) {
454      return None;
455    }
456    let children = self.taffy.children(id).unwrap_or_default();
457    children
458      .into_iter()
459      .rev()
460      .find_map(|child| self.scroll_view_at(child, point))
461      .or_else(|| {
462        let inside = node.rect.is_some_and(|rect| rect.contains(point));
463        (inside && matches!(node.kind, NodeKind::Scroll(_))).then_some(id)
464      })
465  }
466
467  /// `point`, in window coordinates, in the coordinates node `id`'s
468  /// rectangle is in, through the transforms of the node and its
469  /// ancestors. `None` if one of them
470  /// squashes it flat.
471  fn local_point(&self, id: NodeId, point: Point) -> Option<Point> {
472    let mut path = vec![id];
473    while let Some(parent) = path.last().and_then(|&id| self.taffy.parent(id)) {
474      path.push(parent);
475    }
476    path
477      .iter()
478      .rev()
479      .try_fold(point, |point, &id| local(self.node(id)?, point))
480  }
481
482  /// The smallest rectangle covering where `rect`, in the coordinates
483  /// node `id`'s rectangle is in, is drawn in the window: through the
484  /// transforms of the node and its ancestors.
485  pub(crate) fn window_rect(&self, id: NodeId, rect: Rect) -> Rect {
486    let mut rect = rect;
487    let mut at = Some(id);
488    while let Some(current) = at {
489      if let Some(transform) = self.node(current).and_then(|node| node.transform) {
490        rect = transform.map_rect(rect);
491      }
492      at = self.taffy.parent(current);
493    }
494    rect
495  }
496
497  /// A click on button `id`, as from the keyboard.
498  pub(crate) fn click(&self, id: NodeId) -> Option<Call> {
499    self.call(id, Action::Click)
500  }
501
502  fn call(&self, id: NodeId, action: Action) -> Option<Call> {
503    let node = self.node(id)?;
504    let NodeKind::Button(button) = &node.kind else {
505      return None;
506    };
507    Some(Call {
508      cx: node.cx.clone(),
509      path: Rc::clone(&node.path),
510      handlers: button.handlers,
511      action,
512    })
513  }
514}
515
516/// `point`, in the coordinates of `node`'s parent, in the node's own:
517/// moved back through its transform. `None` for a transform that squashes
518/// it flat, as a scale of 0 does, which nothing can point at.
519fn local(node: &Node, point: Point) -> Option<Point> {
520  match node.transform {
521    Some(transform) => Some(transform.invert()?.map(point)),
522    None => Some(point),
523  }
524}
525
526/// What a scroll does, once the tree is released.
527enum Scrolled {
528  /// Runs a button's `on_scroll`.
529  Button(Call),
530  /// Writes a scroll view's offset.
531  View(Signal<f32>, f32),
532}
533
534#[derive(Clone, Copy)]
535enum Action {
536  Click,
537  Press,
538  RightClick,
539  MiddleClick,
540  Scroll(i32),
541}
542
543/// A handler to run once the tree is released.
544pub(crate) struct Call {
545  cx: Cx,
546  path: Rc<str>,
547  handlers: Signal<Handlers>,
548  action: Action,
549}
550
551impl Call {
552  pub(crate) fn run(self) {
553    // Reading the handlers doesn't make anything depend on them.
554    let handlers = untrack(|| {
555      self.handlers.try_with(|handlers| match self.action {
556        Action::Click => handlers.click.clone().map(Either::Click),
557        Action::Press => handlers.press.clone().map(Either::Click),
558        Action::RightClick => handlers.right_click.clone().map(Either::Click),
559        Action::MiddleClick => handlers.middle_click.clone().map(Either::Click),
560        Action::Scroll(_) => handlers.scroll.clone().map(Either::Scroll),
561      })
562    });
563    let Some(Some(handler)) = handlers else {
564      return;
565    };
566    match (handler, self.action) {
567      (Either::Click(handler), action) => {
568        let name = match action {
569          Action::RightClick => "on_right_click",
570          Action::Press => "on_press",
571          Action::MiddleClick => "on_middle_click",
572          _ => "on_click",
573        };
574        run_isolated(&self.path, name, handler, |f| f(&self.cx));
575      }
576      (Either::Scroll(handler), Action::Scroll(steps)) => {
577        run_isolated(&self.path, "on_scroll", handler, |f| f(&self.cx, steps));
578      }
579      (Either::Scroll(_), _) => {}
580    }
581  }
582}
583
584enum Either {
585  Click(ClickHandler),
586  Scroll(ScrollHandler),
587}
588
589/// Calls `handler` through `call` unless it was disabled, and disables it if
590/// it panics. Returns what it returned, or `None` if it didn't run or
591/// panicked. The handler is dropped here too, isolated the same way: if
592/// the call freed its button, this was the last reference.
593pub(crate) fn run_isolated<F: ?Sized, R>(
594  path: &str,
595  name: &str,
596  handler: Rc<Handler<F>>,
597  call: impl FnOnce(&F) -> R,
598) -> Option<R> {
599  if handler.disabled.get() {
600    return None;
601  }
602  let result = match panic::catch_unwind(AssertUnwindSafe(|| call(&handler.f))) {
603    Ok(result) => Some(result),
604    Err(payload) => {
605      handler.disabled.set(true);
606      tracing::error!(
607        "handler {path}.{name} panicked and is disabled: {}",
608        panic_message(payload.as_ref())
609      );
610      None
611    }
612  };
613  if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(handler))) {
614    tracing::error!(
615      "dropping handler {path}.{name} panicked: {}",
616      panic_message(payload.as_ref())
617    );
618  }
619  result
620}
621
622pub(crate) fn panic_message(payload: &(dyn Any + Send)) -> &str {
623  if let Some(message) = payload.downcast_ref::<&str>() {
624    message
625  } else if let Some(message) = payload.downcast_ref::<String>() {
626    message
627  } else {
628    "(panic payload is not a string)"
629  }
630}