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, 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 `id`'s subtree, with the nodes on
188  /// the way down to it, itself included, pushed onto `path`. Later
189  /// children are drawn on top, so they are tried first.
190  fn hit(&self, id: NodeId, point: Point, path: &mut Vec<NodeId>) -> Option<NodeId> {
191    let node = self.node(id)?;
192    let rect = node.rect.filter(|rect| rect.contains(point))?;
193    let style = node.effective_style();
194    // A clip hides its children under its border too.
195    let border = style.border.map_or(0.0, |border| border.width.max(0.0));
196    let inside = Rect::new(
197      rect.x + border,
198      rect.y + border,
199      rect.width - 2.0 * border,
200      rect.height - 2.0 * border,
201    );
202    let children = if node.clips() && !inside.contains(point) {
203      Vec::new()
204    } else {
205      self.taffy.children(id).unwrap_or_default()
206    };
207    path.push(id);
208    if let Some(hit) = children
209      .into_iter()
210      .rev()
211      .find_map(|child| self.hit(child, point, path))
212    {
213      return Some(hit);
214    }
215    if matches!(node.kind, NodeKind::Button(_) | NodeKind::Input(_)) {
216      return Some(id);
217    }
218    path.pop();
219    None
220  }
221
222  /// Hovers the deepest button under `point`, and marks the buttons around
223  /// it as having the pointer inside.
224  fn hover_at(&mut self, point: Point) {
225    let mut path = Vec::new();
226    let hit = self.hit(self.root, point, &mut path);
227    self.set_hovered(hit, &path);
228  }
229
230  fn cursor(&self) -> CursorShape {
231    match self
232      .pointer
233      .hovered
234      .and_then(|id| self.node(id))
235      .map(|node| &node.kind)
236    {
237      Some(NodeKind::Input(_)) => CursorShape::Text,
238      Some(_) => CursorShape::Pointer,
239      None => CursorShape::Default,
240    }
241  }
242
243  /// Hover checked again after a layout, since buttons may have moved
244  /// under a pointer that didn't. Not after a layout a hover change asked
245  /// for: a hover style that moves the button off the pointer would
246  /// otherwise unhover it, move it back, hover it again, and so on.
247  pub(crate) fn pointer_after_layout(&mut self) {
248    if std::mem::take(&mut self.pointer.caused_layout) {
249      return;
250    }
251    if let Some(position) = self.pointer.position {
252      self.hover_at(position);
253    }
254  }
255
256  /// `hovered` is the button under the pointer, and `path` the nodes from
257  /// the root down to it.
258  fn set_hovered(&mut self, hovered: Option<NodeId>, path: &[NodeId]) {
259    self.set_inside(path);
260    let old = self.pointer.hovered;
261    if old == hovered {
262      return;
263    }
264    self.pointer.hovered = hovered;
265    self.pointer.wheel = 0;
266    self.pointer.pixels = 0.0;
267    if let Some(old) = old {
268      self.set_button_state(old, |button| button.hovered = false);
269    }
270    if let Some(new) = hovered {
271      self.set_button_state(new, |button| button.hovered = true);
272    }
273  }
274
275  /// Writes `false` to the `hovered` signals of the buttons the pointer
276  /// left and `true` to those it entered, among the nodes on `path`.
277  fn set_inside(&mut self, path: &[NodeId]) {
278    let inside: Vec<(NodeId, Signal<bool>)> = path
279      .iter()
280      .filter_map(|&id| match &self.node(id)?.kind {
281        NodeKind::Button(button) => Some((id, button.hover_signal?)),
282        _ => None,
283      })
284      .collect();
285    // Each signal is written once, and only on a change: `true` while any
286    // button sharing it has the pointer inside.
287    let holds = |list: &[(NodeId, Signal<bool>)], signal| list.iter().any(|(_, s)| *s == signal);
288    let mut seen = Vec::new();
289    for &(_, signal) in self.pointer.inside.iter().chain(&inside) {
290      if seen.contains(&signal) {
291        continue;
292      }
293      seen.push(signal);
294      let now = holds(&inside, signal);
295      if holds(&self.pointer.inside, signal) != now {
296        signal.try_set(now);
297      }
298    }
299    self.pointer.inside = inside;
300  }
301
302  fn set_pressed(&mut self, pressed: Option<(NodeId, PointerButton)>) {
303    let old = std::mem::replace(&mut self.pointer.pressed, pressed);
304    if let Some((old, _)) = old {
305      self.set_button_state(old, |button| button.pressed = false);
306    }
307    if let Some((new, _)) = pressed {
308      self.set_button_state(new, |button| button.pressed = true);
309    }
310  }
311
312  /// Changes a button's hover or pressed state, noting when that needs a
313  /// layout.
314  fn set_button_state(&mut self, id: NodeId, f: impl FnOnce(&mut ButtonNode)) {
315    if self.update_button_style(id, f) == Change::Layout {
316      self.pointer.caused_layout = true;
317    }
318  }
319
320  fn button(&mut self, button: PointerButton, pressed: bool) -> Option<Call> {
321    if pressed {
322      if self.pointer.pressed.is_none()
323        && let Some(target) = self.pointer.hovered
324      {
325        self.set_pressed(Some((target, button)));
326        if button == PointerButton::Left {
327          self.press_field(target);
328          return self.call(target, Action::Press);
329        }
330      }
331      return None;
332    }
333    let (target, held) = self.pointer.pressed?;
334    if held != button {
335      return None;
336    }
337    self.set_pressed(None);
338    if self.pointer.hovered != Some(target) {
339      return None;
340    }
341    let action = match button {
342      PointerButton::Left => Action::Click,
343      PointerButton::Right => Action::RightClick,
344      PointerButton::Middle => Action::MiddleClick,
345    };
346    self.call(target, action)
347  }
348
349  fn scroll(&mut self, delta: ScrollDelta) -> Option<Scrolled> {
350    let handled = self.pointer.hovered.filter(|&id| self.scrolls(id));
351    let Some(target) = handled else {
352      return self.scroll_view(delta);
353    };
354    // Steps down, rounded toward zero; the rest waits for more scrolling.
355    let down = match delta {
356      ScrollDelta::Wheel(value120) => {
357        self.pointer.wheel = self.pointer.wheel.saturating_add(value120);
358        let steps = self.pointer.wheel / VALUE120_PER_STEP;
359        self.pointer.wheel -= steps * VALUE120_PER_STEP;
360        steps
361      }
362      ScrollDelta::Pixels(pixels) => {
363        if !pixels.is_finite() {
364          return None;
365        }
366        self.pointer.pixels += pixels;
367        let steps = (self.pointer.pixels / PIXELS_PER_STEP).trunc();
368        self.pointer.pixels -= steps * PIXELS_PER_STEP;
369        steps as i32
370      }
371    };
372    if down == 0 {
373      return None;
374    }
375    // Handlers count up as positive.
376    self
377      .call(target, Action::Scroll(down.saturating_neg()))
378      .map(Scrolled::Button)
379  }
380
381  /// A press on field `id` focuses it and puts the caret under the
382  /// pointer. Anything else is left alone.
383  fn press_field(&mut self, id: NodeId) {
384    let Some(position) = self.pointer.position else {
385      return;
386    };
387    let Some(NodeKind::Input(input)) = self.node(id).map(|node| &node.kind) else {
388      return;
389    };
390    let at = input.hit(position);
391    self.focus(Some(id));
392    self.update_input(id, |input| {
393      input.edit.cursor = at;
394      input.edit.anchor = None;
395    });
396  }
397
398  /// Whether button `id` has an `on_scroll`.
399  fn scrolls(&self, id: NodeId) -> bool {
400    let Some(NodeKind::Button(button)) = self.node(id).map(|node| &node.kind) else {
401      return false;
402    };
403    untrack(|| {
404      button
405        .handlers
406        .try_with(|handlers| handlers.scroll.is_some())
407    })
408    .unwrap_or(false)
409  }
410
411  /// Scrolls the innermost scroll view under the pointer by `delta`, as
412  /// far as its content goes.
413  fn scroll_view(&mut self, delta: ScrollDelta) -> Option<Scrolled> {
414    let position = self.pointer.position?;
415    let id = self.scroll_view_at(self.root, position)?;
416    let pixels = match delta {
417      ScrollDelta::Wheel(value120) => value120 as f32 / VALUE120_PER_STEP as f32 * PIXELS_PER_NOTCH,
418      ScrollDelta::Pixels(pixels) if pixels.is_finite() => pixels,
419      ScrollDelta::Pixels(_) => return None,
420    };
421    let NodeKind::Scroll(scroll) = &self.node(id)?.kind else {
422      return None;
423    };
424    // From the signal, not the last layout, so scrolls and writes since
425    // then add up.
426    let now = untrack(|| scroll.signal.try_get())?;
427    let now = if now.is_nan() {
428      0.0
429    } else {
430      now.clamp(0.0, scroll.most)
431    };
432    let offset = (now + pixels).clamp(0.0, scroll.most);
433    (offset != now).then_some(Scrolled::View(scroll.signal, offset))
434  }
435
436  /// The innermost scroll view under `point` in `id`'s subtree, where its
437  /// ancestors' clips let the pointer reach.
438  fn scroll_view_at(&self, id: NodeId, point: Point) -> Option<NodeId> {
439    let node = self.node(id)?;
440    if !node.rect.is_some_and(|rect| rect.contains(point)) {
441      return None;
442    }
443    let children = self.taffy.children(id).unwrap_or_default();
444    children
445      .into_iter()
446      .rev()
447      .find_map(|child| self.scroll_view_at(child, point))
448      .or_else(|| matches!(node.kind, NodeKind::Scroll(_)).then_some(id))
449  }
450
451  /// A click on button `id`, as from the keyboard.
452  pub(crate) fn click(&self, id: NodeId) -> Option<Call> {
453    self.call(id, Action::Click)
454  }
455
456  fn call(&self, id: NodeId, action: Action) -> Option<Call> {
457    let node = self.node(id)?;
458    let NodeKind::Button(button) = &node.kind else {
459      return None;
460    };
461    Some(Call {
462      cx: node.cx.clone(),
463      path: Rc::clone(&node.path),
464      handlers: button.handlers,
465      action,
466    })
467  }
468}
469
470/// What a scroll does, once the tree is released.
471enum Scrolled {
472  /// Runs a button's `on_scroll`.
473  Button(Call),
474  /// Writes a scroll view's offset.
475  View(Signal<f32>, f32),
476}
477
478#[derive(Clone, Copy)]
479enum Action {
480  Click,
481  Press,
482  RightClick,
483  MiddleClick,
484  Scroll(i32),
485}
486
487/// A handler to run once the tree is released.
488pub(crate) struct Call {
489  cx: Cx,
490  path: Rc<str>,
491  handlers: Signal<Handlers>,
492  action: Action,
493}
494
495impl Call {
496  pub(crate) fn run(self) {
497    // Reading the handlers doesn't make anything depend on them.
498    let handlers = untrack(|| {
499      self.handlers.try_with(|handlers| match self.action {
500        Action::Click => handlers.click.clone().map(Either::Click),
501        Action::Press => handlers.press.clone().map(Either::Click),
502        Action::RightClick => handlers.right_click.clone().map(Either::Click),
503        Action::MiddleClick => handlers.middle_click.clone().map(Either::Click),
504        Action::Scroll(_) => handlers.scroll.clone().map(Either::Scroll),
505      })
506    });
507    let Some(Some(handler)) = handlers else {
508      return;
509    };
510    match (handler, self.action) {
511      (Either::Click(handler), action) => {
512        let name = match action {
513          Action::RightClick => "on_right_click",
514          Action::Press => "on_press",
515          Action::MiddleClick => "on_middle_click",
516          _ => "on_click",
517        };
518        run_isolated(&self.path, name, handler, |f| f(&self.cx));
519      }
520      (Either::Scroll(handler), Action::Scroll(steps)) => {
521        run_isolated(&self.path, "on_scroll", handler, |f| f(&self.cx, steps));
522      }
523      (Either::Scroll(_), _) => {}
524    }
525  }
526}
527
528enum Either {
529  Click(ClickHandler),
530  Scroll(ScrollHandler),
531}
532
533/// Calls `handler` through `call` unless it was disabled, and disables it if
534/// it panics. Returns what it returned, or `None` if it didn't run or
535/// panicked. The handler is dropped here too, isolated the same way: if
536/// the call freed its button, this was the last reference.
537pub(crate) fn run_isolated<F: ?Sized, R>(
538  path: &str,
539  name: &str,
540  handler: Rc<Handler<F>>,
541  call: impl FnOnce(&F) -> R,
542) -> Option<R> {
543  if handler.disabled.get() {
544    return None;
545  }
546  let result = match panic::catch_unwind(AssertUnwindSafe(|| call(&handler.f))) {
547    Ok(result) => Some(result),
548    Err(payload) => {
549      handler.disabled.set(true);
550      tracing::error!(
551        "handler {path}.{name} panicked and is disabled: {}",
552        panic_message(payload.as_ref())
553      );
554      None
555    }
556  };
557  if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(handler))) {
558    tracing::error!(
559      "dropping handler {path}.{name} panicked: {}",
560      panic_message(payload.as_ref())
561    );
562  }
563  result
564}
565
566pub(crate) fn panic_message(payload: &(dyn Any + Send)) -> &str {
567  if let Some(message) = payload.downcast_ref::<&str>() {
568    message
569  } else if let Some(message) = payload.downcast_ref::<String>() {
570    message
571  } else {
572    "(panic payload is not a string)"
573  }
574}