Skip to main content

repose_material/material3/
snackbar.rs

1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use crate::{Icon, Symbol};
6use repose_core::*;
7use repose_ui::{
8    Box, Column, Row, Spacer, Text, TextStyle, ViewExt, anim::animate_f32_from,
9    overlay::SnackbarAction, overlay::snackbar_is_dismissing,
10};
11
12use super::*;
13
14/// Configuration for [`Snackbar`].
15#[derive(Clone)]
16pub struct SnackbarConfig {
17    pub modifier: Modifier,
18    pub container_color: Color,
19    pub content_color: Color,
20    pub action_color: Color,
21    pub dismiss_action_content_color: Color,
22    pub action_on_new_line: bool,
23    /// Whether a close (dismiss) icon button is shown at the end.
24    pub show_dismiss_action: bool,
25    /// Called when the dismiss icon is clicked.
26    pub on_dismiss: Option<Rc<dyn Fn()>>,
27    pub shape_radius: f32,
28    pub min_height: f32,
29    pub min_width: f32,
30    pub max_width: f32,
31}
32
33impl std::fmt::Debug for SnackbarConfig {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("SnackbarConfig")
36            .field("modifier", &self.modifier)
37            .field("container_color", &self.container_color)
38            .field("content_color", &self.content_color)
39            .field("action_color", &self.action_color)
40            .field(
41                "dismiss_action_content_color",
42                &self.dismiss_action_content_color,
43            )
44            .field("action_on_new_line", &self.action_on_new_line)
45            .field("show_dismiss_action", &self.show_dismiss_action)
46            .field("on_dismiss", &self.on_dismiss.as_ref().map(|_| ".."))
47            .field("shape_radius", &self.shape_radius)
48            .field("min_height", &self.min_height)
49            .field("min_width", &self.min_width)
50            .field("max_width", &self.max_width)
51            .finish()
52    }
53}
54
55impl Default for SnackbarConfig {
56    fn default() -> Self {
57        Self {
58            modifier: Modifier::new(),
59            container_color: SnackbarDefaults::container_color(),
60            content_color: SnackbarDefaults::content_color(),
61            action_color: SnackbarDefaults::action_color(),
62            dismiss_action_content_color: SnackbarDefaults::dismiss_action_content_color(),
63            action_on_new_line: false,
64            show_dismiss_action: false,
65            on_dismiss: None,
66            shape_radius: SnackbarDefaults::SHAPE_RADIUS,
67            min_height: SnackbarDefaults::MIN_HEIGHT,
68            min_width: SnackbarDefaults::MIN_WIDTH,
69            max_width: SnackbarDefaults::MAX_WIDTH,
70        }
71    }
72}
73
74pub fn Snackbar(
75    message: impl Into<String>,
76    action: Option<SnackbarAction>,
77    modifier: Modifier,
78    config: SnackbarConfig,
79) -> View {
80    let msg = message.into();
81    let th = theme();
82    let bg = config.container_color;
83    let fg = config.content_color;
84    let action_color = config.action_color;
85
86    let dismissing = snackbar_is_dismissing();
87
88    let slide_target = if dismissing { 80.0 } else { 0.0 };
89    let slide = animate_f32_from("snackbar_slide", 80.0, slide_target, th.motion.overlay);
90
91    let alpha_target = if dismissing { 0.0 } else { 1.0 };
92    let alpha = animate_f32_from("snackbar_alpha", 0.0, alpha_target, th.motion.overlay);
93
94    let snackbar = Box(Modifier::new()
95        .translate(0.0, slide)
96        .alpha(alpha)
97        .min_height(config.min_height)
98        .min_width(config.min_width)
99        .max_width(config.max_width)
100        .background(bg)
101        .clip_rounded(config.shape_radius)
102        .shadow(th.elevation.level3, 0.0));
103
104    let dismiss_btn = if config.show_dismiss_action {
105        let d = config.on_dismiss.clone();
106        Some(IconButton(
107            Icon(Symbol::new("close", '\u{E5CD}')).size(24.0),
108            move || {
109                if let Some(cb) = &d {
110                    cb();
111                }
112            },
113            IconButtonConfig {
114                colors: IconButtonColors {
115                    container_color: Color::TRANSPARENT,
116                    content_color: config.dismiss_action_content_color,
117                    disabled_container_color: Color::TRANSPARENT,
118                    disabled_content_color: config
119                        .dismiss_action_content_color
120                        .with_alpha_f32(0.38),
121                },
122                container_size: Some(48.0),
123                ..Default::default()
124            },
125        ))
126    } else {
127        None
128    };
129
130    let content = if config.action_on_new_line {
131        Column(Modifier::new().padding_values(PaddingValues {
132            left: 16.0,
133            right: 8.0,
134            top: 0.0,
135            bottom: 0.0,
136        }))
137        .child((
138            Text(msg)
139                .modifier(Modifier::new().padding_values(PaddingValues {
140                    left: 0.0,
141                    right: 0.0,
142                    top: 14.0,
143                    bottom: 14.0,
144                }))
145                .color(fg)
146                .size(th.typography.body_medium)
147                .max_lines(2)
148                .overflow_ellipsize(),
149            action
150                .map(|a| {
151                    let label = a.label.clone();
152                    Row(Modifier::new()
153                        .fill_max_width()
154                        .justify_content(repose_core::JustifyContent::END))
155                    .child(TextButton(
156                        Modifier::new(),
157                        move || (a.on_click)(),
158                        ButtonConfig {
159                            colors: Some(ButtonColors {
160                                container_color: Color::TRANSPARENT,
161                                content_color: action_color,
162                                disabled_container_color: Color::TRANSPARENT,
163                                disabled_content_color: action_color.with_alpha_f32(0.38),
164                            }),
165                            ..Default::default()
166                        },
167                        || Text(label).size(th.typography.label_large).single_line(),
168                    ))
169                })
170                .unwrap_or(Box(Modifier::new())),
171            dismiss_btn.unwrap_or(Box(Modifier::new())),
172        ))
173    } else {
174        Row(Modifier::new()
175            .fill_max_width()
176            .padding_values(PaddingValues {
177                left: 16.0,
178                right: 8.0,
179                top: 0.0,
180                bottom: 0.0,
181            })
182            .align_items(repose_core::AlignItems::CENTER))
183        .child((
184            Text(msg)
185                .modifier(Modifier::new().padding_values(PaddingValues {
186                    left: 0.0,
187                    right: 0.0,
188                    top: 14.0,
189                    bottom: 14.0,
190                }))
191                .color(fg)
192                .size(th.typography.body_medium)
193                .max_lines(2)
194                .overflow_ellipsize(),
195            Spacer(),
196            action
197                .map(|a| {
198                    let label = a.label.clone();
199                    TextButton(
200                        Modifier::new(),
201                        move || (a.on_click)(),
202                        ButtonConfig {
203                            colors: Some(ButtonColors {
204                                container_color: Color::TRANSPARENT,
205                                content_color: action_color,
206                                disabled_container_color: Color::TRANSPARENT,
207                                disabled_content_color: action_color.with_alpha_f32(0.38),
208                            }),
209                            ..Default::default()
210                        },
211                        || Text(label).size(th.typography.label_large).single_line(),
212                    )
213                })
214                .unwrap_or(Box(Modifier::new())),
215            dismiss_btn.unwrap_or(Box(Modifier::new())),
216        ))
217    };
218    let snackbar = snackbar.child(with_content_color(fg, move || content));
219
220    Box(Modifier::new()
221        .absolute()
222        .offset_bottom(0.0)
223        .fill_max_width()
224        .justify_content(repose_core::JustifyContent::CENTER)
225        .then(modifier))
226    .child(snackbar)
227}