frus_widgets/listener.rs
1//! [`Listener`]: the pointer's raw events on a part of the screen, and [`OrientationBuilder`].
2
3use frus_core::{Orientation, Point, Size};
4
5use crate::layoutbuilder::LayoutBuilder;
6use crate::widget::Widget;
7
8/// What happened to the pointer, as a [`Listener`] hears it (milestone 586).
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ListenerEventKind {
11 /// A finger touched, or a mouse button went down, inside the listener.
12 Down,
13 /// The pointer that went down inside moved — wherever it is now.
14 Move,
15 /// The pointer that went down inside lifted — wherever it is now.
16 Up,
17 /// The gesture was interrupted: the application went to the background, the touch was
18 /// taken back by the system.
19 Cancel,
20 /// A mouse moved inside the listener with no button held.
21 Hover,
22}
23
24/// One raw event of the pointer, in the listener's own coordinates (milestone 586).
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct ListenerEvent {
27 /// What happened.
28 pub kind: ListenerEventKind,
29 /// Where the pointer is, in the listener's coordinates — outside its box for a move or a
30 /// release that left it.
31 pub local: Point,
32 /// Whether the pointer is a finger rather than a mouse.
33 pub touch: bool,
34}
35
36/// **Hears the pointer's raw events** on its child: down, move, up, cancel, and a mouse's
37/// hover — the reference's `Listener`.
38///
39/// Unlike a [`GestureDetector`](crate::GestureDetector), it does not recognise anything and
40/// competes with nothing: a scroll under the finger still scrolls, a button still takes its
41/// tap, and the listener hears every event all the same. A pointer that went down inside it
42/// is followed **wherever it goes** until it lifts. That is what a drawing surface, a custom
43/// slider or a gesture of the application's own is built on.
44///
45/// ```
46/// use frus_widgets::{Container, Listener, ListenerEventKind};
47///
48/// #[derive(Clone)]
49/// enum Msg { Ink(f32, f32), Lift }
50///
51/// let _pad: Listener<Msg> = Listener::new(Container::new().width(300.0).height(200.0), |event| {
52/// match event.kind {
53/// ListenerEventKind::Down | ListenerEventKind::Move => {
54/// Some(Msg::Ink(event.local.x, event.local.y))
55/// }
56/// ListenerEventKind::Up | ListenerEventKind::Cancel => Some(Msg::Lift),
57/// ListenerEventKind::Hover => None,
58/// }
59/// });
60/// ```
61///
62/// A transparent wrapper: it is its child, laid out and painted exactly as the child would be
63/// without it. Every listener under the pointer hears a press, the innermost first.
64pub struct Listener<Msg = crate::callback::Callback> {
65 inner: Box<dyn Widget<Msg>>,
66 on_event: Box<dyn Fn(ListenerEvent) -> Option<Msg>>,
67}
68
69impl<Msg> Listener<Msg> {
70 /// Hears the pointer on `child`: `on_event` answers each event with a message, or with
71 /// nothing.
72 pub fn new(
73 child: impl Widget<Msg> + 'static,
74 on_event: impl Fn(ListenerEvent) -> Option<Msg> + 'static,
75 ) -> Self {
76 Self {
77 inner: Box::new(child),
78 on_event: Box::new(on_event),
79 }
80 }
81
82 /// A listener is not a box: its child's own, unchanged.
83 fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
84 base
85 }
86}
87
88crate::transparent::forward_transparent!(listener Listener {
89 /// This one — and a child that is a listener too is the same place, heard by both.
90 fn pointer_listener(&self) -> bool {
91 true
92 }
93
94 fn on_pointer_event(&self, event: ListenerEvent) -> Vec<Msg> {
95 let mut messages = self.inner.on_pointer_event(event);
96 messages.extend((self.on_event)(event));
97 messages
98 }
99
100 /// Forwarded, as for every transparent wrapper: a listener is not an identity, a place, a
101 /// theme, a surface, a form, nor a selection area.
102 fn key(&self) -> Option<u64> {
103 self.inner.key()
104 }
105 fn positioned(&self) -> Option<crate::positioned::Positioning> {
106 self.inner.positioned()
107 }
108 fn theme_override(
109 &self,
110 inherited: &crate::theme::Theme,
111 ) -> Option<Box<crate::theme::Theme>> {
112 self.inner.theme_override(inherited)
113 }
114 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
115 self.inner.media_override(inherited)
116 }
117 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
118 self.inner.scaffold_override()
119 }
120 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
121 self.inner.scroll_behavior_override()
122 }
123 fn autofill_group(&self) -> bool {
124 self.inner.autofill_group()
125 }
126 fn area_toolbar(
127 &self,
128 context: crate::ToolbarContext,
129 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
130 self.inner.area_toolbar(context)
131 }
132});
133
134/// Builds from **the orientation of the box it is given** — the reference's
135/// `OrientationBuilder`: landscape when the box is wider than tall, portrait otherwise.
136///
137/// The box, not the window: a panel beside a list on a wide window is portrait if it is
138/// taller than wide, whatever the window is. It takes all the room on offer, as an
139/// [`Aligned`](crate::Aligned) does, so that the box it decides on is that room and not the
140/// size of what it built; along a row's or a column's own direction, shared with other
141/// children, the room is only its content's. It is a [`LayoutBuilder`], with what that says
142/// about state: the content is built on the fly from the box, and keeps none.
143///
144/// ```
145/// use frus_core::Orientation;
146/// use frus_widgets::{column, row, text, OrientationBuilder};
147///
148/// let _gallery: frus_widgets::LayoutBuilder<()> = OrientationBuilder::new(|orientation| {
149/// match orientation {
150/// Orientation::Landscape => row![text("a"), text("b")],
151/// Orientation::Portrait => column![text("a"), text("b")],
152/// }
153/// });
154/// ```
155pub struct OrientationBuilder;
156
157impl OrientationBuilder {
158 /// A builder from `build`, given the orientation of the box.
159 #[allow(clippy::new_ret_no_self)]
160 pub fn new<Msg: 'static, W: Widget<Msg> + 'static>(
161 build: impl Fn(Orientation) -> W + 'static,
162 ) -> LayoutBuilder<Msg> {
163 LayoutBuilder::new(move |size: Size| build(Orientation::from_size(size.width, size.height)))
164 .filling()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::{build_ui, Container, Keyed, Runtime, Theme};
172 use frus_core::{Color, Primitive};
173
174 /// **The orientation is the box's, not the window's**: the same builder in a wide box and
175 /// a tall one, in one window, builds each way.
176 #[test]
177 fn the_orientation_is_the_boxs() {
178 let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0));
179 let pick = move || {
180 OrientationBuilder::new(move |orientation| {
181 let color = if orientation == Orientation::Landscape {
182 red
183 } else {
184 blue
185 };
186 Container::<()>::new().width(10.0).height(10.0).color(color)
187 })
188 };
189 let colour_in = |width: f32, height: f32| {
190 let root = Container::<()>::new()
191 .width(width)
192 .height(height)
193 .child(pick());
194 let ui = build_ui(
195 &root,
196 Size::new(400.0, 400.0),
197 &Runtime::default(),
198 &Theme::default(),
199 );
200 ui.scene().primitives().iter().find_map(|p| match p {
201 Primitive::Rect { color, .. } if *color == red || *color == blue => Some(*color),
202 _ => None,
203 })
204 };
205 assert_eq!(colour_in(300.0, 100.0), Some(red), "a wide box");
206 assert_eq!(
207 colour_in(100.0, 300.0),
208 Some(blue),
209 "a tall box, in the same window"
210 );
211 }
212
213 /// **Two listeners at one place both hear**, the inner one first, and a wrapper forwards
214 /// them.
215 #[test]
216 fn listeners_at_one_place_both_hear() {
217 let inner = Listener::new(Container::<u8>::new(), |_| Some(1));
218 let outer = Listener::new(inner, |_| Some(2));
219 let wrapped = Keyed::new(5u64, outer);
220 assert!(wrapped.pointer_listener());
221 let event = ListenerEvent {
222 kind: ListenerEventKind::Down,
223 local: Point::new(0.0, 0.0),
224 touch: true,
225 };
226 assert_eq!(wrapped.on_pointer_event(event), vec![1, 2]);
227 assert!(!Widget::<u8>::pointer_listener(&Container::<u8>::new()));
228 }
229
230 /// **A listener is found under the pointer, the innermost first, also from a frame replayed
231 /// from the paint cache.**
232 #[test]
233 fn a_listener_is_found_even_from_the_cache() {
234 let tree = Listener::new(
235 Container::<u8>::new()
236 .repaint_boundary()
237 .width(200.0)
238 .height(200.0)
239 .child(Listener::new(
240 Container::new().width(50.0).height(50.0),
241 |_| None,
242 )),
243 |_| None,
244 );
245 let (rt, theme) = (Runtime::default(), Theme::default());
246 let size = Size::new(200.0, 200.0);
247 let first = build_ui(&tree, size, &rt, &theme);
248 let second = build_ui(&tree, size, &rt, &theme);
249 assert_eq!(rt.paint_cache.borrow().last_frame_stats().0, 1, "replayed");
250 for ui in [&first, &second] {
251 let inside = ui.pointer_listeners_at(Point::new(20.0, 20.0));
252 assert_eq!(inside.len(), 2);
253 assert_eq!(inside[0].1.width, 50.0, "the inner one first");
254 assert_eq!(ui.pointer_listeners_at(Point::new(150.0, 150.0)).len(), 1);
255 }
256 }
257}