Skip to main content

frus_widgets/
mouseregion.rs

1//! [`MouseRegion`]: the pointer entering, moving over and leaving a part of the screen, and
2//! the cursor it shows there.
3
4use frus_core::Point;
5
6use crate::interaction::Cursor;
7use crate::widget::Widget;
8
9/// How a region the mouse enters and leaves behaves, as the shell asks it of the frame
10/// (milestone 583).
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct HoverRegion {
13    /// The cursor shown while the pointer is over the region, unless something inside it
14    /// asks for one of its own. `None` leaves the cursor to what is around it.
15    pub cursor: Option<Cursor>,
16    /// Whether the region **hides the regions behind it**: those it is not inside are not
17    /// entered while the pointer is over it. The regions it is inside always are.
18    pub opaque: bool,
19}
20
21/// One moment of the pointer over a region, as the shell reports it through
22/// [`Widget::on_hover_event`](crate::Widget::on_hover_event). Every position is in the
23/// region's own coordinates.
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub enum HoverEvent {
26    /// The pointer came into the region — or the region came under a pointer that had not
27    /// moved, when the screen changed around it.
28    Enter {
29        /// Where the pointer is.
30        local: Point,
31    },
32    /// The pointer moved inside the region, with no button held.
33    Move {
34        /// Where the pointer is now.
35        local: Point,
36    },
37    /// The pointer left the region, or the region went from under it.
38    Exit {
39        /// Where the pointer is, which is outside the region.
40        local: Point,
41    },
42}
43
44/// **Hears the pointer** coming into its child, moving over it and leaving it, and sets the
45/// **cursor** shown while it is there.
46///
47/// A transparent wrapper: it is its child, laid out and painted exactly as the child would be
48/// without it.
49///
50/// ```
51/// use frus_widgets::{Cursor, MouseRegion, Text};
52///
53/// #[derive(Clone)]
54/// enum Msg { Show, Hide }
55///
56/// let _card: MouseRegion<Msg> = MouseRegion::new(Text::new("Hover me"))
57///     .on_enter(|_| Msg::Show)
58///     .on_exit(|_| Msg::Hide)
59///     .cursor(Cursor::Pointer);
60/// ```
61///
62/// **Every region under the pointer is in**, the innermost first: a region inside another
63/// is entered with it. A region is **opaque** by default, as the reference's is: the regions
64/// *behind* it — overlapping it without containing it — are left while the pointer is over
65/// it. [`opaque(false)`](Self::opaque) lets them be entered too.
66///
67/// A finger is over a region **while it touches** and not after: there is no hover without a
68/// mouse. So a finger sends an enter when it goes down and an exit when it lifts, and no moves.
69pub struct MouseRegion<Msg = crate::callback::Callback> {
70    inner: Box<dyn Widget<Msg>>,
71    region: HoverRegion,
72    enter: Option<Box<dyn Fn(Point) -> Msg>>,
73    hover: Option<Box<dyn Fn(Point) -> Msg>>,
74    exit: Option<Box<dyn Fn(Point) -> Msg>>,
75}
76
77impl<Msg> MouseRegion<Msg> {
78    /// A region over `child`: opaque, with no cursor of its own, hearing nothing yet.
79    pub fn new(child: impl Widget<Msg> + 'static) -> Self {
80        Self {
81            inner: Box::new(child),
82            region: HoverRegion {
83                cursor: None,
84                opaque: true,
85            },
86            enter: None,
87            hover: None,
88            exit: None,
89        }
90    }
91
92    /// The pointer came in: `build` is given where it is, in the region's coordinates.
93    pub fn on_enter(mut self, build: impl Fn(Point) -> Msg + 'static) -> Self {
94        self.enter = Some(Box::new(build));
95        self
96    }
97
98    /// The pointer moved inside, with no button held: `build` is given where it is now.
99    pub fn on_hover(mut self, build: impl Fn(Point) -> Msg + 'static) -> Self {
100        self.hover = Some(Box::new(build));
101        self
102    }
103
104    /// The pointer left: `build` is given where it is, now outside.
105    pub fn on_exit(mut self, build: impl Fn(Point) -> Msg + 'static) -> Self {
106        self.exit = Some(Box::new(build));
107        self
108    }
109
110    /// The cursor shown while the pointer is over the region, unless something inside it
111    /// asks for another — a field's text cursor, a button's hand.
112    pub fn cursor(mut self, cursor: Cursor) -> Self {
113        self.region.cursor = Some(cursor);
114        self
115    }
116
117    /// Whether the regions behind this one are left while the pointer is over it. On by
118    /// default.
119    pub fn opaque(mut self, opaque: bool) -> Self {
120        self.region.opaque = opaque;
121        self
122    }
123
124    /// A region is not a box: its child's own, unchanged.
125    fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
126        base
127    }
128}
129
130crate::transparent::forward_transparent!(hover MouseRegion {
131    /// This one, unless the child is a region itself: then the child is the region at this
132    /// place, and this one is not a second one.
133    fn hover_region(&self) -> Option<HoverRegion> {
134        self.inner.hover_region().or(Some(self.region))
135    }
136
137    fn on_hover_event(&self, event: HoverEvent) -> Option<Msg> {
138        if self.inner.hover_region().is_some() {
139            return self.inner.on_hover_event(event);
140        }
141        match event {
142            HoverEvent::Enter { local } => self.enter.as_ref().map(|build| build(local)),
143            HoverEvent::Move { local } => self.hover.as_ref().map(|build| build(local)),
144            HoverEvent::Exit { local } => self.exit.as_ref().map(|build| build(local)),
145        }
146    }
147
148    /// Forwarded, as for every transparent wrapper: a region is not an identity, a place, a
149    /// theme, a surface, a form, nor a selection area.
150    fn key(&self) -> Option<u64> {
151        self.inner.key()
152    }
153    fn positioned(&self) -> Option<crate::positioned::Positioning> {
154        self.inner.positioned()
155    }
156    fn theme_override(
157        &self,
158        inherited: &crate::theme::Theme,
159    ) -> Option<Box<crate::theme::Theme>> {
160        self.inner.theme_override(inherited)
161    }
162    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
163        self.inner.media_override(inherited)
164    }
165    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
166        self.inner.scaffold_override()
167    }
168    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
169        self.inner.scroll_behavior_override()
170    }
171    fn autofill_group(&self) -> bool {
172        self.inner.autofill_group()
173    }
174    fn area_toolbar(
175        &self,
176        context: crate::ToolbarContext,
177    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
178        self.inner.area_toolbar(context)
179    }
180});
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::{build_ui, Container, GestureDetector, Keyed, Runtime, Theme};
186    use frus_core::Size;
187
188    fn block() -> Container<u8> {
189        Container::new().width(100.0).height(100.0)
190    }
191
192    /// **A region and a detector wrap each other either way round** and each still answers
193    /// for itself; a wrapper around a region forwards it.
194    #[test]
195    fn a_region_and_a_detector_compose() {
196        let region_outside =
197            MouseRegion::new(GestureDetector::new(block()).on_tap(1)).on_enter(|_| 2);
198        assert_eq!(Widget::on_click(&region_outside), Some(1));
199        let enter = HoverEvent::Enter {
200            local: Point::new(0.0, 0.0),
201        };
202        assert_eq!(region_outside.on_hover_event(enter), Some(2));
203
204        let detector_outside =
205            GestureDetector::new(MouseRegion::new(block()).on_exit(|_| 3)).on_tap(1);
206        let exit = HoverEvent::Exit {
207            local: Point::new(0.0, 0.0),
208        };
209        assert_eq!(Widget::on_click(&detector_outside), Some(1));
210        assert_eq!(detector_outside.on_hover_event(exit), Some(3));
211        assert!(detector_outside.hover_region().is_some());
212
213        let keyed = Keyed::new(4u64, MouseRegion::new(block()).cursor(Cursor::Move));
214        assert_eq!(
215            keyed.hover_region().and_then(|region| region.cursor),
216            Some(Cursor::Move)
217        );
218        assert_eq!(Widget::<u8>::hover_region(&block()), None);
219    }
220
221    /// **The regions under a point come innermost first, and survive a replayed frame**:
222    /// read again from the paint cache, a region inside a repaint boundary is still found.
223    #[test]
224    fn the_regions_under_a_point_come_innermost_first_even_from_the_cache() {
225        let tree = MouseRegion::new(
226            Container::<u8>::new()
227                .repaint_boundary()
228                .width(200.0)
229                .height(200.0)
230                .child(MouseRegion::new(block()).opaque(false)),
231        );
232        let size = Size::new(200.0, 200.0);
233        let rt = Runtime::default();
234        let theme = Theme::default();
235        let first = build_ui(&tree, size, &rt, &theme);
236        let second = build_ui(&tree, size, &rt, &theme);
237        assert_eq!(rt.paint_cache.borrow().last_frame_stats().0, 1, "replayed");
238        for ui in [&first, &second] {
239            let inside = ui.hover_regions_at(Point::new(20.0, 20.0));
240            assert_eq!(inside.len(), 2, "both");
241            assert_eq!(inside[0].1.width, 100.0, "the inner one first");
242            let outside = ui.hover_regions_at(Point::new(150.0, 150.0));
243            assert_eq!(outside.len(), 1, "only the outer one");
244            assert_eq!(
245                ui.hover_region_rect(outside[0].0).map(|r| r.width),
246                Some(200.0)
247            );
248        }
249    }
250}