frus_widgets/
mouseregion.rs1use frus_core::Point;
5
6use crate::interaction::Cursor;
7use crate::widget::Widget;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct HoverRegion {
13 pub cursor: Option<Cursor>,
16 pub opaque: bool,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq)]
25pub enum HoverEvent {
26 Enter {
29 local: Point,
31 },
32 Move {
34 local: Point,
36 },
37 Exit {
39 local: Point,
41 },
42}
43
44pub 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 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 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 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 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 pub fn cursor(mut self, cursor: Cursor) -> Self {
113 self.region.cursor = Some(cursor);
114 self
115 }
116
117 pub fn opaque(mut self, opaque: bool) -> Self {
120 self.region.opaque = opaque;
121 self
122 }
123
124 fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
126 base
127 }
128}
129
130crate::transparent::forward_transparent!(hover MouseRegion {
131 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 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 #[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(®ion_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 #[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}