use std::sync::Arc;
use rosace_core::types::{Point, Rect, Size};
use rosace_render::Color;
use rosace_state::Atom;
use super::container::draw_rounded_rect_pub;
use super::{LayoutCtx, PaintCtx, Widget};
const PAD_H: f32 = 16.0;
const GAP: f32 = 16.0;
pub struct Snackbar {
message: String,
action_label: Option<String>,
on_action: Option<Arc<dyn Fn() + Send + Sync>>,
height: f32,
background: Option<Color>,
text_color: Option<Color>,
action_color: Option<Color>,
radius: f32,
font_size: Option<f32>,
}
impl Snackbar {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
action_label: None,
on_action: None,
height: 46.0,
background: None,
text_color: None,
action_color: None,
radius: 0.0,
font_size: None,
}
}
pub fn action(mut self, label: impl Into<String>, f: impl Fn() + Send + Sync + 'static) -> Self {
self.action_label = Some(label.into());
self.on_action = Some(Arc::new(f));
self
}
pub fn height(mut self, h: f32) -> Self { self.height = h; self }
pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
pub fn color(mut self, c: Color) -> Self { self.text_color = Some(c); self }
pub fn action_color(mut self, c: Color) -> Self { self.action_color = Some(c); self }
pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
pub fn font_size(mut self, s: f32) -> Self { self.font_size = Some(s); self }
fn resolved_font_size(&self, theme: &rosace_theme::ThemeData) -> f32 {
self.font_size.unwrap_or(theme.typography.body_medium.size)
}
pub fn emit(self) {
use super::overlay::{push_overlay, InputBehavior, FocusBehavior, LayerPosition, OverlayEntry};
push_overlay(
OverlayEntry::new(LayerPosition::BottomAnchored, self)
.input(InputBehavior::PassThrough)
.focus(FocusBehavior::Inert),
);
}
pub fn show(open: &Atom<bool>, secs: f32) {
open.set(true);
let open = open.clone();
rosace_state::fire_after_ms((secs * 1000.0) as u64, move || {
open.set(false);
});
}
}
impl Widget for Snackbar {
fn layout(&self, ctx: &LayoutCtx) -> Size {
let font_size = self.resolved_font_size(ctx.theme);
let text_w = ctx.font.measure_text(&self.message, font_size);
let action_w = self
.action_label
.as_ref()
.map(|a| GAP + ctx.font.measure_text(a, font_size))
.unwrap_or(0.0);
let content_w = PAD_H * 2.0 + text_w + action_w;
let avail = ctx.constraints.max_width_f32();
let w = avail.max(content_w.min(avail));
Size { width: w, height: self.height }
}
fn paint(&self, ctx: &mut PaintCtx) {
let (bg, fg, action_fg) = {
let t = &ctx.theme.colors;
let inv = ctx.tc(t.on_background);
(
self.background.unwrap_or(Color::rgba(inv.r, inv.g, inv.b, 235)),
self.text_color.unwrap_or_else(|| ctx.tc(t.background)),
self.action_color.unwrap_or_else(|| ctx.tc(t.primary)),
)
};
let r = ctx.rect;
ctx.semantics(super::Semantics::new(rosace_core::Role::Alert).label(&self.message));
draw_rounded_rect_pub(ctx, r, bg, self.radius);
let font_size = self.resolved_font_size(&ctx.theme);
let line_h = ctx.font.line_height(font_size);
let ty = r.origin.y + (r.size.height - line_h) / 2.0;
ctx.draw_text_at(
&self.message,
Point { x: r.origin.x + PAD_H, y: ty },
fg,
font_size,
);
if let Some(label) = &self.action_label {
let aw = ctx.font.measure_text(label, font_size);
let ax = r.origin.x + r.size.width - PAD_H - aw;
let hit = Rect {
origin: Point { x: ax - 8.0, y: r.origin.y },
size: Size { width: aw + 16.0, height: r.size.height },
};
let mut action_ctx = ctx.child(hit);
action_ctx.semantics(super::Semantics::new(rosace_core::Role::Button).label(label));
action_ctx.draw_text_at(label, Point { x: ax, y: ty }, action_fg, font_size);
if let Some(cb) = &self.on_action {
action_ctx.register_hit(Arc::clone(cb));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rosace_layout::Constraints;
#[test]
fn snackbar_is_a_bar_filling_available_width_within_margins() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = LayoutCtx::new(Constraints::loose(500.0, 400.0), &font, &theme);
let plain = Snackbar::new("Saved").layout(&ctx);
let with_action = Snackbar::new("Saved").action("UNDO", || {}).layout(&ctx);
assert_eq!(plain.width, 500.0);
assert_eq!(with_action.width, plain.width);
let narrow = LayoutCtx::new(Constraints::loose(120.0, 400.0), &font, &theme);
assert!(Snackbar::new("A very long message that cannot fit").layout(&narrow).width <= 120.0);
}
}