Skip to main content

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}