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;
17use crate::tree::{ButtonNode, Change, NodeId, NodeKind, Tree, borrow};
18use crate::window::Window;
19
20/// Logical pixels of touchpad scrolling per scroll step.
21const PIXELS_PER_STEP: f32 = 15.0;
22
23/// `axis_value120` units per mouse-wheel notch.
24const VALUE120_PER_STEP: i32 = 120;
25
26/// A user's event handler, and whether it was disabled after panicking.
27pub(crate) struct Handler<F: ?Sized> {
28  disabled: Cell<bool>,
29  f: F,
30}
31
32impl<F> Handler<F> {
33  pub(crate) fn new(f: F) -> Handler<F> {
34    Handler {
35      disabled: Cell::new(false),
36      f,
37    }
38  }
39}
40
41type ClickHandler = Rc<Handler<dyn Fn(&Cx)>>;
42type ScrollHandler = Rc<Handler<dyn Fn(&Cx, i32)>>;
43
44/// A button's handlers.
45#[derive(Default)]
46pub(crate) struct Handlers {
47  pub(crate) click: Option<ClickHandler>,
48  pub(crate) right_click: Option<ClickHandler>,
49  pub(crate) middle_click: Option<ClickHandler>,
50  pub(crate) scroll: Option<ScrollHandler>,
51}
52
53/// A pointer button.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum PointerButton {
56  /// The primary button; it runs `on_click`.
57  Left,
58  /// The secondary button; it runs `on_right_click`.
59  Right,
60  /// The middle button; it runs `on_middle_click`.
61  Middle,
62}
63
64/// One scroll event on the vertical axis, as `wl_pointer` reports it.
65/// Positive values scroll down.
66#[derive(Clone, Copy, Debug, PartialEq)]
67pub enum ScrollDelta {
68  /// A mouse wheel, in `axis_value120` units: 120 per notch, less for a
69  /// high-resolution wheel.
70  Wheel(i32),
71  /// Smooth scrolling, such as from a touchpad, in logical pixels.
72  Pixels(f32),
73}
74
75/// The cursor the platform should show.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum CursorShape {
78  /// The default arrow.
79  Default,
80  /// A pointing hand, over a button.
81  Pointer,
82}
83
84/// Where the pointer is and what it is doing.
85#[derive(Default)]
86pub(crate) struct Pointer {
87  position: Option<Point>,
88  /// The button under the pointer.
89  hovered: Option<NodeId>,
90  /// The button a pointer button went down on, until it comes up.
91  pressed: Option<(NodeId, PointerButton)>,
92  /// Scrolling that hasn't added up to a whole step yet.
93  wheel: i32,
94  pixels: f32,
95  /// A hover or press change asked for the pending layout.
96  caused_layout: bool,
97}
98
99impl Pointer {
100  /// Forgets nodes that were removed from the tree.
101  pub(crate) fn forget(&mut self, removed: &[NodeId]) {
102    if self.hovered.is_some_and(|id| removed.contains(&id)) {
103      self.hovered = None;
104    }
105    if self.pressed.is_some_and(|(id, _)| removed.contains(&id)) {
106      self.pressed = None;
107    }
108  }
109}
110
111impl Window {
112  /// The pointer entered the window or moved, to `position` in logical
113  /// pixels. Returns the cursor to show.
114  pub fn pointer_motion(&self, position: Point) -> CursorShape {
115    let mut tree = borrow(&self.tree);
116    tree.pointer.position = Some(position);
117    let hit = tree.hit(tree.root, position);
118    tree.set_hovered(hit);
119    tree.cursor()
120  }
121
122  /// The pointer left the window.
123  pub fn pointer_leave(&self) {
124    let mut tree = borrow(&self.tree);
125    tree.pointer.position = None;
126    tree.set_pressed(None);
127    tree.set_hovered(None);
128  }
129
130  /// A pointer button went down or up. Releasing it over the button it
131  /// went down on runs that button's `on_click`, `on_right_click` or
132  /// `on_middle_click`.
133  pub fn pointer_button(&self, button: PointerButton, pressed: bool) {
134    let call = borrow(&self.tree).button(button, pressed);
135    if let Some(call) = call {
136      call.run();
137    }
138  }
139
140  /// The pointer scrolled. Once whole steps add up, the `on_scroll` of the
141  /// button under the pointer runs with them.
142  pub fn pointer_scroll(&self, delta: ScrollDelta) {
143    let call = borrow(&self.tree).scroll(delta);
144    if let Some(call) = call {
145      call.run();
146    }
147  }
148
149  /// A touchpad scroll gesture ended (`wl_pointer.axis_stop`): scrolling
150  /// that hasn't added up to a whole step is dropped, so it doesn't carry
151  /// into the next gesture. A wheel's partial notch is kept.
152  pub fn pointer_scroll_end(&self) {
153    borrow(&self.tree).pointer.pixels = 0.0;
154  }
155
156  /// The cursor to show.
157  pub fn cursor(&self) -> CursorShape {
158    self.tree.borrow().cursor()
159  }
160}
161
162impl Tree {
163  /// The deepest button under `point` in `id`'s subtree. Later children are
164  /// drawn on top, so they are tried first.
165  fn hit(&self, id: NodeId, point: Point) -> Option<NodeId> {
166    let node = self.node(id)?;
167    if !node.rect.is_some_and(|rect| rect.contains(point)) {
168      return None;
169    }
170    let children = self.taffy.children(id).unwrap_or_default();
171    if let Some(hit) = children
172      .into_iter()
173      .rev()
174      .find_map(|child| self.hit(child, point))
175    {
176      return Some(hit);
177    }
178    matches!(node.kind, NodeKind::Button(_)).then_some(id)
179  }
180
181  fn cursor(&self) -> CursorShape {
182    match self.pointer.hovered {
183      Some(_) => CursorShape::Pointer,
184      None => CursorShape::Default,
185    }
186  }
187
188  /// Hover checked again after a layout, since buttons may have moved
189  /// under a pointer that didn't. Not after a layout a hover change asked
190  /// for: a hover style that moves the button off the pointer would
191  /// otherwise unhover it, move it back, hover it again, and so on.
192  pub(crate) fn pointer_after_layout(&mut self) {
193    if std::mem::take(&mut self.pointer.caused_layout) {
194      return;
195    }
196    if let Some(position) = self.pointer.position {
197      let hit = self.hit(self.root, position);
198      self.set_hovered(hit);
199    }
200  }
201
202  fn set_hovered(&mut self, hovered: Option<NodeId>) {
203    let old = self.pointer.hovered;
204    if old == hovered {
205      return;
206    }
207    self.pointer.hovered = hovered;
208    self.pointer.wheel = 0;
209    self.pointer.pixels = 0.0;
210    if let Some(old) = old {
211      self.set_button_state(old, |button| button.hovered = false);
212    }
213    if let Some(new) = hovered {
214      self.set_button_state(new, |button| button.hovered = true);
215    }
216  }
217
218  fn set_pressed(&mut self, pressed: Option<(NodeId, PointerButton)>) {
219    let old = std::mem::replace(&mut self.pointer.pressed, pressed);
220    if let Some((old, _)) = old {
221      self.set_button_state(old, |button| button.pressed = false);
222    }
223    if let Some((new, _)) = pressed {
224      self.set_button_state(new, |button| button.pressed = true);
225    }
226  }
227
228  /// Changes a button's hover or pressed state, noting when that needs a
229  /// layout.
230  fn set_button_state(&mut self, id: NodeId, f: impl FnOnce(&mut ButtonNode)) {
231    if self.update_button_style(id, f) == Change::Layout {
232      self.pointer.caused_layout = true;
233    }
234  }
235
236  fn button(&mut self, button: PointerButton, pressed: bool) -> Option<Call> {
237    if pressed {
238      if self.pointer.pressed.is_none()
239        && let Some(target) = self.pointer.hovered
240      {
241        self.set_pressed(Some((target, button)));
242      }
243      return None;
244    }
245    let (target, held) = self.pointer.pressed?;
246    if held != button {
247      return None;
248    }
249    self.set_pressed(None);
250    if self.pointer.hovered != Some(target) {
251      return None;
252    }
253    let action = match button {
254      PointerButton::Left => Action::Click,
255      PointerButton::Right => Action::RightClick,
256      PointerButton::Middle => Action::MiddleClick,
257    };
258    self.call(target, action)
259  }
260
261  fn scroll(&mut self, delta: ScrollDelta) -> Option<Call> {
262    let target = self.pointer.hovered?;
263    // Steps down, rounded toward zero; the rest waits for more scrolling.
264    let down = match delta {
265      ScrollDelta::Wheel(value120) => {
266        self.pointer.wheel = self.pointer.wheel.saturating_add(value120);
267        let steps = self.pointer.wheel / VALUE120_PER_STEP;
268        self.pointer.wheel -= steps * VALUE120_PER_STEP;
269        steps
270      }
271      ScrollDelta::Pixels(pixels) => {
272        if !pixels.is_finite() {
273          return None;
274        }
275        self.pointer.pixels += pixels;
276        let steps = (self.pointer.pixels / PIXELS_PER_STEP).trunc();
277        self.pointer.pixels -= steps * PIXELS_PER_STEP;
278        steps as i32
279      }
280    };
281    if down == 0 {
282      return None;
283    }
284    // Handlers count up as positive.
285    self.call(target, Action::Scroll(down.saturating_neg()))
286  }
287
288  fn call(&self, id: NodeId, action: Action) -> Option<Call> {
289    let node = self.node(id)?;
290    let NodeKind::Button(button) = &node.kind else {
291      return None;
292    };
293    Some(Call {
294      cx: node.cx.clone(),
295      path: Rc::clone(&node.path),
296      handlers: button.handlers,
297      action,
298    })
299  }
300}
301
302#[derive(Clone, Copy)]
303enum Action {
304  Click,
305  RightClick,
306  MiddleClick,
307  Scroll(i32),
308}
309
310/// A handler to run once the tree is released.
311struct Call {
312  cx: Cx,
313  path: Rc<str>,
314  handlers: Signal<Handlers>,
315  action: Action,
316}
317
318impl Call {
319  fn run(self) {
320    // Reading the handlers doesn't make anything depend on them.
321    let handlers = untrack(|| {
322      self.handlers.try_with(|handlers| match self.action {
323        Action::Click => handlers.click.clone().map(Either::Click),
324        Action::RightClick => handlers.right_click.clone().map(Either::Click),
325        Action::MiddleClick => handlers.middle_click.clone().map(Either::Click),
326        Action::Scroll(_) => handlers.scroll.clone().map(Either::Scroll),
327      })
328    });
329    let Some(Some(handler)) = handlers else {
330      return;
331    };
332    match (handler, self.action) {
333      (Either::Click(handler), action) => {
334        let name = match action {
335          Action::RightClick => "on_right_click",
336          Action::MiddleClick => "on_middle_click",
337          _ => "on_click",
338        };
339        run_isolated(&self.path, name, handler, |f| f(&self.cx));
340      }
341      (Either::Scroll(handler), Action::Scroll(steps)) => {
342        run_isolated(&self.path, "on_scroll", handler, |f| f(&self.cx, steps));
343      }
344      (Either::Scroll(_), _) => {}
345    }
346  }
347}
348
349enum Either {
350  Click(ClickHandler),
351  Scroll(ScrollHandler),
352}
353
354/// Calls `handler` through `call` unless it was disabled, and disables it if
355/// it panics. The handler is dropped here too, isolated the same way: if the
356/// call freed its button, this was the last reference.
357fn run_isolated<F: ?Sized>(path: &str, name: &str, handler: Rc<Handler<F>>, call: impl FnOnce(&F)) {
358  if handler.disabled.get() {
359    return;
360  }
361  if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| call(&handler.f))) {
362    handler.disabled.set(true);
363    tracing::error!(
364      "handler {path}.{name} panicked and is disabled: {}",
365      panic_message(payload.as_ref())
366    );
367  }
368  if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(handler))) {
369    tracing::error!(
370      "dropping handler {path}.{name} panicked: {}",
371      panic_message(payload.as_ref())
372    );
373  }
374}
375
376pub(crate) fn panic_message(payload: &(dyn Any + Send)) -> &str {
377  if let Some(message) = payload.downcast_ref::<&str>() {
378    message
379  } else if let Some(message) = payload.downcast_ref::<String>() {
380    message
381  } else {
382    "(panic payload is not a string)"
383  }
384}