rosace_widgets/tree/
snackbar.rs1use std::sync::Arc;
12
13use rosace_core::types::{Point, Rect, Size};
14use rosace_render::Color;
15use rosace_state::Atom;
16
17use super::container::draw_rounded_rect_pub;
18use super::{LayoutCtx, PaintCtx, Widget};
19
20const PAD_H: f32 = 16.0;
21const GAP: f32 = 16.0;
22
23pub struct Snackbar {
24 message: String,
25 action_label: Option<String>,
26 on_action: Option<Arc<dyn Fn() + Send + Sync>>,
27 height: f32,
28 background: Option<Color>,
29 text_color: Option<Color>,
30 action_color: Option<Color>,
31 radius: f32,
32 font_size: Option<f32>,
36}
37
38impl Snackbar {
39 pub fn new(message: impl Into<String>) -> Self {
40 Self {
41 message: message.into(),
42 action_label: None,
43 on_action: None,
44 height: 46.0,
45 background: None,
46 text_color: None,
47 action_color: None,
48 radius: 0.0,
49 font_size: None,
50 }
51 }
52 pub fn action(mut self, label: impl Into<String>, f: impl Fn() + Send + Sync + 'static) -> Self {
54 self.action_label = Some(label.into());
55 self.on_action = Some(Arc::new(f));
56 self
57 }
58 pub fn height(mut self, h: f32) -> Self { self.height = h; self }
59 pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
62 pub fn color(mut self, c: Color) -> Self { self.text_color = Some(c); self }
64 pub fn action_color(mut self, c: Color) -> Self { self.action_color = Some(c); self }
66 pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
67 pub fn font_size(mut self, s: f32) -> Self { self.font_size = Some(s); self }
68
69 fn resolved_font_size(&self, theme: &rosace_theme::ThemeData) -> f32 {
70 self.font_size.unwrap_or(theme.typography.body_medium.size)
71 }
72
73 pub fn emit(self) {
80 use super::overlay::{push_overlay, InputBehavior, FocusBehavior, LayerPosition, OverlayEntry};
81 push_overlay(
86 OverlayEntry::new(LayerPosition::BottomAnchored, self)
87 .input(InputBehavior::PassThrough)
88 .focus(FocusBehavior::Inert),
89 );
90 }
91
92 pub fn show(open: &Atom<bool>, secs: f32) {
95 open.set(true);
96 let open = open.clone();
97 rosace_state::fire_after_ms((secs * 1000.0) as u64, move || {
100 open.set(false);
101 });
102 }
103}
104
105impl Widget for Snackbar {
106 fn layout(&self, ctx: &LayoutCtx) -> Size {
107 let font_size = self.resolved_font_size(ctx.theme);
111 let text_w = ctx.font.measure_text(&self.message, font_size);
112 let action_w = self
113 .action_label
114 .as_ref()
115 .map(|a| GAP + ctx.font.measure_text(a, font_size))
116 .unwrap_or(0.0);
117 let content_w = PAD_H * 2.0 + text_w + action_w;
118 let avail = ctx.constraints.max_width_f32();
119 let w = avail.max(content_w.min(avail));
121 Size { width: w, height: self.height }
122 }
123
124 fn paint(&self, ctx: &mut PaintCtx) {
125 let (bg, fg, action_fg) = {
127 let t = &ctx.theme.colors;
128 let inv = ctx.tc(t.on_background);
129 (
130 self.background.unwrap_or(Color::rgba(inv.r, inv.g, inv.b, 235)),
131 self.text_color.unwrap_or_else(|| ctx.tc(t.background)),
132 self.action_color.unwrap_or_else(|| ctx.tc(t.primary)),
133 )
134 };
135 let r = ctx.rect;
136
137 ctx.semantics(super::Semantics::new(rosace_core::Role::Alert).label(&self.message));
138 draw_rounded_rect_pub(ctx, r, bg, self.radius);
139
140 let font_size = self.resolved_font_size(&ctx.theme);
141 let line_h = ctx.font.line_height(font_size);
142 let ty = r.origin.y + (r.size.height - line_h) / 2.0;
143 ctx.draw_text_at(
144 &self.message,
145 Point { x: r.origin.x + PAD_H, y: ty },
146 fg,
147 font_size,
148 );
149
150 if let Some(label) = &self.action_label {
151 let aw = ctx.font.measure_text(label, font_size);
152 let ax = r.origin.x + r.size.width - PAD_H - aw;
153 let hit = Rect {
155 origin: Point { x: ax - 8.0, y: r.origin.y },
156 size: Size { width: aw + 16.0, height: r.size.height },
157 };
158 let mut action_ctx = ctx.child(hit);
159 action_ctx.semantics(super::Semantics::new(rosace_core::Role::Button).label(label));
160 action_ctx.draw_text_at(label, Point { x: ax, y: ty }, action_fg, font_size);
161 if let Some(cb) = &self.on_action {
162 action_ctx.register_hit(Arc::clone(cb));
163 }
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use rosace_layout::Constraints;
172
173 #[test]
174 fn snackbar_is_a_bar_filling_available_width_within_margins() {
175 let font = rosace_render::FontCache::embedded();
176 let theme = rosace_theme::built_in::dark_theme();
177 let ctx = LayoutCtx::new(Constraints::loose(500.0, 400.0), &font, &theme);
178
179 let plain = Snackbar::new("Saved").layout(&ctx);
182 let with_action = Snackbar::new("Saved").action("UNDO", || {}).layout(&ctx);
183 assert_eq!(plain.width, 500.0);
184 assert_eq!(with_action.width, plain.width);
185
186 let narrow = LayoutCtx::new(Constraints::loose(120.0, 400.0), &font, &theme);
187 assert!(Snackbar::new("A very long message that cannot fit").layout(&narrow).width <= 120.0);
188 }
189}