Skip to main content

tui_lipan/widgets/mouse_region/
mod.rs

1//! Mouse region widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub(crate) use self::layout::measure_mouse_region;
8pub use self::node::MouseRegionNode;
9pub(crate) use self::reconcile::reconcile_mouse_region;
10
11use crate::callback::Callback;
12use crate::core::element::{Element, ElementKind};
13use crate::core::event::{KeyMods, MouseDragEvent, MouseEvent, MouseMoveEvent};
14use crate::core::mask::CellMask;
15use crate::style::{LayoutConstraints, Length, Style, StyleSlot, VisualEffect};
16use std::sync::Arc;
17
18/// Pointer travel, in `(columns, rows)`, a drag must exceed before its callbacks start.
19///
20/// Columns are looser than rows because a cell is roughly twice as tall as it is wide, so the
21/// same hand tremor covers more of them. Override per region with
22/// [`MouseRegion::drag_threshold`].
23pub const DEFAULT_DRAG_THRESHOLD: (u16, u16) = (3, 1);
24
25/// A wrapper that handles pointer interactions for its subtree.
26#[derive(Clone, Default)]
27pub struct MouseRegion {
28    pub(crate) child: Option<Box<Element>>,
29    pub(crate) on_click: Option<Callback<MouseEvent>>,
30    pub(crate) on_mouse_down: Option<Callback<MouseEvent>>,
31    pub(crate) bubble_mouse_down: bool,
32    pub(crate) on_mouse_up: Option<Callback<MouseEvent>>,
33    pub(crate) on_mouse_move: Option<Callback<MouseMoveEvent>>,
34    pub(crate) on_drag_start: Option<Callback<MouseDragEvent>>,
35    pub(crate) on_drag: Option<Callback<MouseDragEvent>>,
36    pub(crate) on_drag_end: Option<Callback<MouseDragEvent>>,
37    pub(crate) drag_required_mods: Option<KeyMods>,
38    pub(crate) drag_threshold: Option<(u16, u16)>,
39    pub(crate) on_right_drag_start: Option<Callback<MouseDragEvent>>,
40    pub(crate) on_right_drag: Option<Callback<MouseDragEvent>>,
41    pub(crate) on_right_drag_end: Option<Callback<MouseDragEvent>>,
42    pub(crate) right_drag_required_mods: Option<KeyMods>,
43    pub(crate) on_hover_change: Option<Callback<bool>>,
44    pub(crate) hit_test: Option<Arc<dyn Fn(u16, u16) -> bool + Send + Sync>>,
45    pub(crate) capture_click: bool,
46    pub(crate) capture_required_mods: Option<KeyMods>,
47    pub(crate) hover_style: StyleSlot,
48    pub(crate) hover_effects: Vec<VisualEffect>,
49    pub(crate) enabled: bool,
50}
51
52impl MouseRegion {
53    /// Create an empty mouse region.
54    pub fn new() -> Self {
55        Self {
56            enabled: true,
57            ..Self::default()
58        }
59    }
60
61    /// Set wrapped child content.
62    pub fn child(mut self, child: impl Into<Element>) -> Self {
63        self.child = Some(Box::new(child.into()));
64        self
65    }
66
67    /// Set pointer-move callback.
68    pub fn on_mouse_move(mut self, cb: Callback<MouseMoveEvent>) -> Self {
69        self.on_mouse_move = Some(cb);
70        self
71    }
72
73    /// Set drag-start callback (fires once after left-button movement exceeds click threshold).
74    pub fn on_drag_start(mut self, cb: Callback<MouseDragEvent>) -> Self {
75        self.on_drag_start = Some(cb);
76        self
77    }
78
79    /// Set drag callback (fires on each left-button drag tick after drag start).
80    pub fn on_drag(mut self, cb: Callback<MouseDragEvent>) -> Self {
81        self.on_drag = Some(cb);
82        self
83    }
84
85    /// Set drag-end callback (fires on left-button release after a drag started).
86    pub fn on_drag_end(mut self, cb: Callback<MouseDragEvent>) -> Self {
87        self.on_drag_end = Some(cb);
88        self
89    }
90
91    /// Require these modifiers before left-button drag callbacks can start.
92    ///
93    /// Each `true` flag in `mods` must be held when the mouse button is pressed;
94    /// extra modifiers are allowed. `KeyMods::ALT` therefore means "Alt must be
95    /// held", not "Alt and no other modifiers".
96    pub fn drag_requires_mods(mut self, mods: KeyMods) -> Self {
97        self.drag_required_mods = Some(mods);
98        self
99    }
100
101    /// Pointer travel, in cells, that must be exceeded before drag callbacks start.
102    ///
103    /// Defaults to [`DEFAULT_DRAG_THRESHOLD`]: 3 columns or 1 row, which keeps a jittery
104    /// click from selecting text or dragging a list item. A region whose whole purpose is
105    /// dragging - a resize handle, a split divider, a slider thumb - has no click gesture to
106    /// disambiguate from, so it wants `(1, 1)` and the pointer tracked from its first step.
107    ///
108    /// Applies to both buttons. Either axis satisfies it, so `(1, 1)` reacts to the first
109    /// movement in any direction.
110    pub fn drag_threshold(mut self, columns: u16, rows: u16) -> Self {
111        self.drag_threshold = Some((columns, rows));
112        self
113    }
114
115    /// Set right-button drag-start callback.
116    ///
117    /// Fires once after right-button movement exceeds the click threshold.
118    pub fn on_right_drag_start(mut self, cb: Callback<MouseDragEvent>) -> Self {
119        self.on_right_drag_start = Some(cb);
120        self
121    }
122
123    /// Set right-button drag callback.
124    ///
125    /// Fires on each right-button drag tick after drag start.
126    pub fn on_right_drag(mut self, cb: Callback<MouseDragEvent>) -> Self {
127        self.on_right_drag = Some(cb);
128        self
129    }
130
131    /// Set right-button drag-end callback.
132    ///
133    /// Fires on right-button release after a drag started.
134    pub fn on_right_drag_end(mut self, cb: Callback<MouseDragEvent>) -> Self {
135        self.on_right_drag_end = Some(cb);
136        self
137    }
138
139    /// Require these modifiers before right-button drag callbacks can start.
140    ///
141    /// Each `true` flag in `mods` must be held when the mouse button is pressed;
142    /// extra modifiers are allowed.
143    pub fn right_drag_requires_mods(mut self, mods: KeyMods) -> Self {
144        self.right_drag_required_mods = Some(mods);
145        self
146    }
147
148    /// Set hover change callback (fires true on enter, false on leave).
149    ///
150    /// A hover transition repaints on its own only when the region has hover
151    /// visuals (`hover_style` or `hover_effects`). For click-only regions the
152    /// `Update` returned from this callback's message decides — return
153    /// `Update::none()` when nothing rendered depends on hover state.
154    pub fn on_hover_change(mut self, cb: Callback<bool>) -> Self {
155        self.on_hover_change = Some(cb);
156        self
157    }
158
159    /// Set click callback (fires on mouse-up after a press on the same node).
160    pub fn on_click(mut self, cb: Callback<MouseEvent>) -> Self {
161        self.on_click = Some(cb);
162        self
163    }
164
165    /// Set custom hit-test predicate using local coordinates.
166    pub fn hit_test(mut self, f: impl Fn(u16, u16) -> bool + Send + Sync + 'static) -> Self {
167        self.hit_test = Some(Arc::new(f));
168        self
169    }
170
171    /// Pointer hit testing against a [`CellMask`] in region-local coordinates (same as effect scope).
172    pub fn cell_mask(mut self, mask: Arc<CellMask>) -> Self {
173        let m = Arc::clone(&mask);
174        self.hit_test = Some(Arc::new(move |x, y| m.test_scope_local(x as i16, y as i16)));
175        self
176    }
177
178    /// Set mouse-down callback (fires immediately on left button press).
179    pub fn on_mouse_down(mut self, cb: Callback<MouseEvent>) -> Self {
180        self.on_mouse_down = Some(cb);
181        self
182    }
183
184    /// Also fire `on_mouse_down` when a descendant receives the left-button press.
185    ///
186    /// This is non-consuming: the descendant still receives its normal click or
187    /// focus handling. It is useful for container-level focus policies such as a
188    /// window manager focusing a pane when any child is clicked.
189    pub fn bubble_mouse_down(mut self, bubble: bool) -> Self {
190        self.bubble_mouse_down = bubble;
191        self
192    }
193
194    /// Set mouse-up callback (fires immediately on left button release over the region).
195    pub fn on_mouse_up(mut self, cb: Callback<MouseEvent>) -> Self {
196        self.on_mouse_up = Some(cb);
197        self
198    }
199
200    /// Control whether this region captures left-clicks over interactive children.
201    pub fn capture_click(mut self, capture: bool) -> Self {
202        self.capture_click = capture;
203        self
204    }
205
206    /// Capture pointer handling over descendants while these modifiers are held.
207    ///
208    /// This is useful for compositor-style wrappers around focusable children such as
209    /// terminals: `capture_requires_mods(KeyMods::ALT)` lets Alt-click/Alt-drag be
210    /// consumed by the wrapper instead of starting terminal selection or forwarding a
211    /// mouse report to the PTY. Extra modifiers are allowed.
212    pub fn capture_requires_mods(mut self, mods: KeyMods) -> Self {
213        self.capture_required_mods = Some(mods);
214        self
215    }
216
217    /// Set style applied while hovered as an underlay block.
218    ///
219    /// Painted before children, so it is effective for `bg` color and modifier
220    /// changes (e.g. `BOLD`, `UNDERLINE`). Setting only `fg` has no visible
221    /// effect because child content overwrites foreground colors on top. To
222    /// change text color on hover use
223    /// `hover_effect(VisualEffect::transform_fg(ColorTransform::Tint(color, 1.0)))`.
224    pub fn hover_style(mut self, style: Style) -> Self {
225        self.hover_style = StyleSlot::Replace(style);
226        self
227    }
228
229    /// Extend the active theme's hover style with additional fields.
230    pub fn extend_hover_style(mut self, style: Style) -> Self {
231        self.hover_style = StyleSlot::Extend(style);
232        self
233    }
234
235    /// Inherit hover style from the active theme.
236    pub fn inherit_hover_style(mut self) -> Self {
237        self.hover_style = StyleSlot::Inherit;
238        self
239    }
240
241    /// Add a visual effect applied while hovered (post-processing, affects text fg/bg).
242    ///
243    /// Use [`VisualEffect`] constructors for common cases:
244    /// `VisualEffect::transform_fg(ColorTransform::Tint(color, 1.0))`,
245    /// `VisualEffect::dim(0.3)`, etc.
246    pub fn hover_effect(mut self, effect: VisualEffect) -> Self {
247        self.hover_effects.push(effect);
248        self
249    }
250
251    /// Enable/disable click, move, and hover behavior.
252    pub fn enabled(mut self, enabled: bool) -> Self {
253        self.enabled = enabled;
254        self
255    }
256}
257
258impl From<MouseRegion> for Element {
259    fn from(value: MouseRegion) -> Self {
260        let (min_w, min_h) = measure_mouse_region(&value, None, None);
261        Element::new(ElementKind::MouseRegion(value)).with_layout(
262            LayoutConstraints::default()
263                .min_width(Length::Px(min_w))
264                .min_height(Length::Px(min_h)),
265        )
266    }
267}
268
269impl crate::layout::hash::LayoutHash for MouseRegion {
270    fn layout_hash(
271        &self,
272        hasher: &mut impl std::hash::Hasher,
273        recurse: &dyn Fn(&Element) -> Option<u64>,
274    ) -> Option<()> {
275        use std::hash::Hash;
276        self.enabled.hash(hasher);
277        self.on_click.is_some().hash(hasher);
278        self.on_mouse_down.is_some().hash(hasher);
279        self.bubble_mouse_down.hash(hasher);
280        self.on_mouse_up.is_some().hash(hasher);
281        self.on_mouse_move.is_some().hash(hasher);
282        self.on_drag_start.is_some().hash(hasher);
283        self.on_drag.is_some().hash(hasher);
284        self.on_drag_end.is_some().hash(hasher);
285        self.drag_required_mods.hash(hasher);
286        self.drag_threshold.hash(hasher);
287        self.on_right_drag_start.is_some().hash(hasher);
288        self.on_right_drag.is_some().hash(hasher);
289        self.on_right_drag_end.is_some().hash(hasher);
290        self.right_drag_required_mods.hash(hasher);
291        self.on_hover_change.is_some().hash(hasher);
292        self.capture_click.hash(hasher);
293        self.capture_required_mods.hash(hasher);
294        self.hover_style.hash(hasher);
295        self.hover_effects.hash(hasher);
296        if let Some(child) = self.child.as_ref() {
297            recurse(child.as_ref())?.hash(hasher);
298        }
299        Some(())
300    }
301}