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