frus_widgets/
toasthost.rs1use frus_core::{Curve, Insets, Rect, Scene};
13use frus_layout::{Align, FlexDirection, Justify, Style};
14
15use crate::animated::AnimatedOpacity;
16use crate::interaction::Status;
17use crate::theme::Theme;
18use crate::widget::Widget;
19
20const HOST_PAD: f32 = 0.0;
29const STACK_GAP: f32 = 8.0;
31
32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
34pub enum SnackBarPosition {
35 TopStart,
37 TopCenter,
39 TopEnd,
41 BottomStart,
43 BottomCenter,
45 BottomEnd,
47}
48
49impl SnackBarPosition {
50 fn justify(self) -> Justify {
52 match self {
53 SnackBarPosition::TopStart | SnackBarPosition::TopCenter | SnackBarPosition::TopEnd => {
54 Justify::Start
55 }
56 _ => Justify::End,
57 }
58 }
59
60 fn align(self) -> Align {
62 match self {
63 SnackBarPosition::TopStart | SnackBarPosition::BottomStart => Align::Start,
64 SnackBarPosition::TopCenter | SnackBarPosition::BottomCenter => Align::Center,
65 SnackBarPosition::TopEnd | SnackBarPosition::BottomEnd => Align::End,
66 }
67 }
68}
69
70pub struct ScaffoldMessenger<Msg = crate::callback::Callback> {
72 position: SnackBarPosition,
73 padding: f32,
74 children: Vec<Box<dyn Widget<Msg>>>,
75}
76
77impl<Msg: Clone + 'static> ScaffoldMessenger<Msg> {
78 pub fn new(position: SnackBarPosition) -> Self {
80 Self {
81 position,
82 padding: HOST_PAD,
83 children: Vec::new(),
84 }
85 }
86
87 pub fn padding(mut self, padding: f32) -> Self {
91 self.padding = padding;
92 self
93 }
94
95 pub fn toast(mut self, widget: impl Widget<Msg> + 'static) -> Self {
97 self.children.push(Box::new(widget));
98 self
99 }
100
101 pub fn fade_in(self, duration: f32) -> Self {
104 self.wrap_opacity(1.0, duration)
105 }
106
107 pub fn fade_out(self, duration: f32) -> Self {
112 self.wrap_opacity(0.0, duration)
113 }
114
115 fn wrap_opacity(mut self, target: f32, duration: f32) -> Self {
117 self.children = self
118 .children
119 .into_iter()
120 .map(|child| {
121 Box::new(AnimatedOpacity::new(
122 target,
123 duration,
124 Curve::ease_in_out(),
125 child,
126 )) as Box<dyn Widget<Msg>>
127 })
128 .collect();
129 self
130 }
131}
132
133impl<Msg: Clone> Widget<Msg> for ScaffoldMessenger<Msg> {
134 fn style(&self) -> Style {
139 Style {
140 flex_direction: FlexDirection::Column,
141 justify: self.position.justify(),
142 align: self.position.align(),
143 gap: STACK_GAP,
144 padding: Insets::new(self.padding, self.padding, self.padding, self.padding),
145 ..Default::default()
146 }
147 }
148
149 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
151 crate::widget::FillAxes::BOTH
152 }
153
154 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
155 &self.children
156 }
157
158 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
159
160 fn on_click(&self) -> Option<Msg> {
161 None
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::text::Text;
169
170 #[test]
171 fn empty_host_has_no_children() {
172 let host = ScaffoldMessenger::<()>::new(SnackBarPosition::TopEnd);
173 assert!(Widget::<()>::children(&host).is_empty());
174 }
175
176 #[test]
177 fn position_maps_to_justify_and_align() {
178 let host = ScaffoldMessenger::<()>::new(SnackBarPosition::BottomEnd).toast(Text::new("x"));
179 let style = Widget::<()>::style(&host);
180 assert!(
181 matches!(style.justify, Justify::End),
182 "bottom → justify End"
183 );
184 assert!(matches!(style.align, Align::End), "right → align End");
185 assert_eq!(Widget::<()>::children(&host).len(), 1);
186
187 let top_center =
188 Widget::<()>::style(&ScaffoldMessenger::<()>::new(SnackBarPosition::TopCenter));
189 assert!(matches!(top_center.justify, Justify::Start));
190 assert!(matches!(top_center.align, Align::Center));
191 }
192
193 #[test]
194 fn stacks_multiple_and_fade_in_preserves_count() {
195 let host = ScaffoldMessenger::<()>::new(SnackBarPosition::BottomCenter)
196 .toast(Text::new("a"))
197 .toast(Text::new("b"))
198 .fade_in(0.2);
199 assert_eq!(
200 Widget::<()>::children(&host).len(),
201 2,
202 "two toasts, wrapped in a fade"
203 );
204 }
205
206 #[test]
207 fn fade_out_wraps_children() {
208 let host = ScaffoldMessenger::<()>::new(SnackBarPosition::BottomCenter)
209 .toast(Text::new("bye"))
210 .fade_out(0.3);
211 assert_eq!(
212 Widget::<()>::children(&host).len(),
213 1,
214 "toast wrapped in a fade-out"
215 );
216 }
217}