Skip to main content

material_ui_rs/widget/component/
snackbar.rs

1//! Material 3 snackbar surface constructors.
2
3use iced_widget::button::{Status, Style};
4use iced_widget::core::text as core_text;
5use iced_widget::core::time::{Duration, Instant};
6use iced_widget::core::widget::{self, Tree, tree};
7use iced_widget::core::{
8    Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Rectangle,
9    Shadow, Shell, Size, Vector, Widget, alignment, border, layout, mouse, overlay, renderer,
10};
11use iced_widget::graphics::geometry;
12use iced_widget::renderer::wgpu::primitive;
13use iced_widget::text;
14use iced_widget::{Container, Row, Stack, Text};
15
16use super::support::{alpha_color, duration_ms, lerp};
17use super::{absolute_line_height, button::Button};
18use crate::utils::{shadow_from_level, state_layer};
19use crate::{Theme, fonts, tokens};
20
21/// Android snackbar visibility animation state.
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct Transition {
24    phase: TransitionPhase,
25    started_at: Option<Instant>,
26    shown_at: Option<Instant>,
27    duration: Duration,
28}
29
30/// Android snackbar transition phase.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum TransitionPhase {
33    Hidden,
34    Showing,
35    Shown,
36    Dismissing,
37}
38
39impl Default for Transition {
40    fn default() -> Self {
41        Self {
42            phase: TransitionPhase::Hidden,
43            started_at: None,
44            shown_at: None,
45            duration: duration_ms(tokens::component::snackbar::LONG_DURATION_MS),
46        }
47    }
48}
49
50impl Transition {
51    /// Starts showing the snackbar using Android's default long snackbar duration.
52    pub fn show(&mut self, now: Instant) {
53        self.show_for(
54            now,
55            duration_ms(tokens::component::snackbar::LONG_DURATION_MS),
56        );
57    }
58
59    /// Starts showing the snackbar using a custom visible duration.
60    pub fn show_for(&mut self, now: Instant, duration: Duration) {
61        match self.phase {
62            TransitionPhase::Showing | TransitionPhase::Shown => {
63                self.duration = duration;
64                self.shown_at = Some(now);
65            }
66            TransitionPhase::Hidden | TransitionPhase::Dismissing => {
67                self.phase = TransitionPhase::Showing;
68                self.started_at = Some(now);
69                self.shown_at = None;
70                self.duration = duration;
71            }
72        }
73    }
74
75    /// Starts dismissing the snackbar.
76    pub fn dismiss(&mut self, now: Instant) {
77        if self.is_active() && self.phase != TransitionPhase::Dismissing {
78            self.phase = TransitionPhase::Dismissing;
79            self.started_at = Some(now);
80            self.shown_at = None;
81        }
82    }
83
84    /// Advances timers and hides the snackbar after its Android timeout.
85    pub fn advance(&mut self, now: Instant) -> bool {
86        match self.phase {
87            TransitionPhase::Hidden => {}
88            TransitionPhase::Showing => {
89                if self.slide_progress(now) >= 1.0 {
90                    self.phase = TransitionPhase::Shown;
91                    self.started_at = None;
92                    self.shown_at = Some(now);
93                }
94            }
95            TransitionPhase::Shown => {
96                if self.shown_at.is_some_and(|shown_at| {
97                    now.saturating_duration_since(shown_at) >= self.duration
98                }) {
99                    self.dismiss(now);
100                }
101            }
102            TransitionPhase::Dismissing => {
103                if self.slide_progress(now) >= 1.0 {
104                    *self = Self::default();
105                }
106            }
107        }
108
109        self.is_active()
110    }
111
112    /// Returns whether the snackbar should remain in the view tree.
113    pub fn is_active(&self) -> bool {
114        self.phase != TransitionPhase::Hidden
115    }
116
117    /// Returns whether the snackbar is currently running an enter or exit animation.
118    pub fn is_animating(&self) -> bool {
119        matches!(
120            self.phase,
121            TransitionPhase::Showing | TransitionPhase::Dismissing
122        )
123    }
124
125    /// Returns the current transition phase.
126    pub fn phase(&self) -> TransitionPhase {
127        self.phase
128    }
129
130    /// Computes the Android slide translation for the provided hidden distance.
131    pub fn translation_y(&self, now: Instant, hidden_distance: f32) -> f32 {
132        let eased =
133            tokens::component::snackbar::SLIDE_ANIMATION_EASING.transform(self.slide_progress(now));
134
135        match self.phase {
136            TransitionPhase::Hidden => hidden_distance,
137            TransitionPhase::Showing => lerp(hidden_distance, 0.0, eased),
138            TransitionPhase::Shown => 0.0,
139            TransitionPhase::Dismissing => lerp(0.0, hidden_distance, eased),
140        }
141    }
142
143    /// Computes the Android content fade alpha.
144    pub fn content_alpha(&self, now: Instant) -> f32 {
145        match self.phase {
146            TransitionPhase::Hidden => 0.0,
147            TransitionPhase::Showing => {
148                let Some(started_at) = self.started_at else {
149                    return 1.0;
150                };
151
152                let elapsed = now.saturating_duration_since(started_at);
153                let delay = duration_ms(
154                    tokens::component::snackbar::SLIDE_ANIMATION_DURATION_MS
155                        - tokens::component::snackbar::CONTENT_FADE_ANIMATION_DURATION_MS,
156                );
157
158                if elapsed <= delay {
159                    return 0.0;
160                }
161
162                let fade_duration =
163                    duration_ms(tokens::component::snackbar::CONTENT_FADE_ANIMATION_DURATION_MS);
164                let progress =
165                    ((elapsed - delay).as_secs_f32() / fade_duration.as_secs_f32()).clamp(0.0, 1.0);
166
167                tokens::component::snackbar::CONTENT_FADE_ANIMATION_EASING.transform(progress)
168            }
169            TransitionPhase::Shown => 1.0,
170            TransitionPhase::Dismissing => {
171                let Some(started_at) = self.started_at else {
172                    return 0.0;
173                };
174
175                let fade_duration =
176                    duration_ms(tokens::component::snackbar::CONTENT_FADE_ANIMATION_DURATION_MS);
177                let progress = (now.saturating_duration_since(started_at).as_secs_f32()
178                    / fade_duration.as_secs_f32())
179                .clamp(0.0, 1.0);
180
181                1.0 - tokens::component::snackbar::CONTENT_FADE_ANIMATION_EASING.transform(progress)
182            }
183        }
184    }
185
186    fn slide_progress(&self, now: Instant) -> f32 {
187        let Some(started_at) = self.started_at else {
188            return match self.phase {
189                TransitionPhase::Showing | TransitionPhase::Dismissing => 1.0,
190                TransitionPhase::Hidden | TransitionPhase::Shown => 0.0,
191            };
192        };
193
194        let duration = duration_ms(tokens::component::snackbar::SLIDE_ANIMATION_DURATION_MS);
195
196        (now.saturating_duration_since(started_at).as_secs_f32() / duration.as_secs_f32())
197            .clamp(0.0, 1.0)
198    }
199}
200
201/// Snackbar text layout.
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum Lines {
204    Single,
205    Two,
206}
207
208impl Lines {
209    const fn container_height(self) -> f32 {
210        match self {
211            Self::Single => tokens::component::snackbar::WITH_SINGLE_LINE_CONTAINER_HEIGHT,
212            Self::Two => tokens::component::snackbar::WITH_TWO_LINES_CONTAINER_HEIGHT,
213        }
214    }
215}
216
217/// Visual options for snackbar content.
218#[derive(Debug, Clone, Copy, PartialEq)]
219pub struct Options {
220    pub lines: Lines,
221    pub content_alpha: f32,
222}
223
224impl Default for Options {
225    fn default() -> Self {
226        Self {
227            lines: Lines::Single,
228            content_alpha: 1.0,
229        }
230    }
231}
232
233impl Options {
234    /// Sets the snackbar text layout.
235    pub fn lines(mut self, lines: Lines) -> Self {
236        self.lines = lines;
237        self
238    }
239
240    /// Sets the Android content fade alpha.
241    pub fn content_alpha(mut self, content_alpha: f32) -> Self {
242        self.content_alpha = content_alpha;
243        self
244    }
245}
246
247/// Visual options for snackbar action buttons.
248#[derive(Debug, Clone, Copy, PartialEq)]
249pub struct ActionOptions {
250    pub content_alpha: f32,
251}
252
253impl Default for ActionOptions {
254    fn default() -> Self {
255        Self { content_alpha: 1.0 }
256    }
257}
258
259impl ActionOptions {
260    /// Sets the Android content fade alpha.
261    pub fn content_alpha(mut self, content_alpha: f32) -> Self {
262        self.content_alpha = content_alpha;
263        self
264    }
265}
266
267/// Creates a snackbar surface.
268pub fn surface<'a, Message, Renderer>(
269    message: impl text::IntoFragment<'a>,
270    action: Option<Element<'a, Message, Theme, Renderer>>,
271    options: Options,
272) -> Container<'a, Message, Theme, Renderer>
273where
274    Message: 'a,
275    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
276{
277    surface_container(
278        message,
279        action,
280        options.lines.container_height(),
281        options.content_alpha,
282    )
283}
284
285/// Creates a snackbar text action button.
286pub fn action<'a, Message, Renderer>(
287    label: impl text::IntoFragment<'a>,
288) -> Button<'a, Message, Renderer>
289where
290    Message: Clone + 'a,
291    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
292{
293    action_with(label, ActionOptions::default())
294}
295
296/// Creates a snackbar text action with an on-press message.
297pub fn action_button<'a, Message, Renderer>(
298    label: impl text::IntoFragment<'a>,
299    on_press: Message,
300) -> Element<'a, Message, Theme, Renderer>
301where
302    Message: Clone + 'a,
303    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
304{
305    action_button_with(label, on_press, ActionOptions::default())
306}
307
308/// Creates a snackbar text action button with custom visual options.
309pub fn action_with<'a, Message, Renderer>(
310    label: impl text::IntoFragment<'a>,
311    options: ActionOptions,
312) -> Button<'a, Message, Renderer>
313where
314    Message: Clone + 'a,
315    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
316{
317    let label_text = tokens::component::snackbar::ACTION_LABEL_TEXT;
318
319    Button::new(
320        Container::new(
321            Text::new(label)
322                .size(label_text.size)
323                .line_height(absolute_line_height(label_text.line_height)),
324        )
325        .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
326        .padding(Padding {
327            top: 0.0,
328            right: tokens::component::button::TRAILING_SPACE,
329            bottom: 0.0,
330            left: tokens::component::button::LEADING_SPACE,
331        })
332        .align_y(alignment::Vertical::Center),
333    )
334    .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
335    .padding(Padding::ZERO)
336    .style(move |theme, status| action_style_alpha(theme, status, options.content_alpha))
337}
338
339/// Creates a snackbar text action button with custom visual options.
340pub fn action_button_with<'a, Message, Renderer>(
341    label: impl text::IntoFragment<'a>,
342    on_press: Message,
343    options: ActionOptions,
344) -> Element<'a, Message, Theme, Renderer>
345where
346    Message: Clone + 'a,
347    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
348{
349    action_with(label, options).on_press(on_press).into()
350}
351
352/// Creates a snackbar icon action, typically used for dismiss.
353pub fn icon_action<'a, Message, Renderer>(
354    icon_name: impl text::IntoFragment<'a>,
355) -> Button<'a, Message, Renderer>
356where
357    Message: Clone + 'a,
358    Renderer: geometry::Renderer + core_text::Renderer + 'a,
359    iced_widget::core::Font: Into<Renderer::Font>,
360{
361    Button::new(
362        Container::new(fonts::icon(
363            icon_name,
364            tokens::component::snackbar::ICON_SIZE,
365        ))
366        .center_x(Length::Fixed(
367            tokens::component::icon_button::CONTAINER_WIDTH,
368        ))
369        .center_y(Length::Fixed(
370            tokens::component::icon_button::CONTAINER_HEIGHT,
371        )),
372    )
373    .width(Length::Fixed(
374        tokens::component::icon_button::CONTAINER_WIDTH,
375    ))
376    .height(Length::Fixed(
377        tokens::component::icon_button::CONTAINER_HEIGHT,
378    ))
379    .padding(Padding::ZERO)
380    .style(icon_action_style)
381}
382
383/// Creates a snackbar icon action with an on-press message.
384pub fn icon_action_button<'a, Message, Renderer>(
385    icon_name: impl text::IntoFragment<'a>,
386    on_press: Message,
387) -> Element<'a, Message, Theme, Renderer>
388where
389    Message: Clone + 'a,
390    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
391    iced_widget::core::Font: Into<Renderer::Font>,
392{
393    icon_action(icon_name).on_press(on_press).into()
394}
395
396/// Places snackbar content above the app content and translates it from the bottom edge.
397pub fn overlay<'a, Message, Renderer>(
398    content: impl Into<Element<'a, Message, Theme, Renderer>>,
399    snackbar: impl Into<Element<'a, Message, Theme, Renderer>>,
400    translation_y: f32,
401) -> Element<'a, Message, Theme, Renderer>
402where
403    Message: 'a,
404    Renderer: iced_widget::core::Renderer + 'a,
405{
406    Stack::with_children([
407        content.into(),
408        floating_layer(snackbar, translation_y).into(),
409    ])
410    .width(Length::Fill)
411    .height(Length::Fill)
412    .into()
413}
414
415/// Places an Android-animated single-line snackbar with one action over content.
416pub fn host<'a, Message, Renderer>(
417    content: impl Into<Element<'a, Message, Theme, Renderer>>,
418    transition: &Transition,
419    now: Instant,
420    message: impl text::IntoFragment<'a>,
421    action_label: impl text::IntoFragment<'a>,
422    on_action: Message,
423) -> Element<'a, Message, Theme, Renderer>
424where
425    Message: Clone + 'a,
426    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
427{
428    if !transition.is_active() {
429        return content.into();
430    }
431
432    let alpha = transition.content_alpha(now);
433    let hidden_distance = tokens::component::snackbar::WITH_SINGLE_LINE_CONTAINER_HEIGHT
434        + tokens::component::snackbar::BOTTOM_MARGIN;
435    let translation_y = transition.translation_y(now, hidden_distance);
436    let snackbar = surface(
437        message,
438        Some(action_button_with(
439            action_label,
440            on_action,
441            ActionOptions::default().content_alpha(alpha),
442        )),
443        Options::default().content_alpha(alpha),
444    );
445
446    overlay(content, snackbar, translation_y)
447}
448
449fn surface_container<'a, Message, Renderer>(
450    message: impl text::IntoFragment<'a>,
451    action: Option<Element<'a, Message, Theme, Renderer>>,
452    height: f32,
453    content_alpha: f32,
454) -> Container<'a, Message, Theme, Renderer>
455where
456    Message: 'a,
457    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
458{
459    let supporting_text = tokens::component::snackbar::SUPPORTING_TEXT;
460    let mut content = Row::new()
461        .push(
462            Text::new(message)
463                .size(supporting_text.size)
464                .line_height(absolute_line_height(supporting_text.line_height))
465                .wrapping(text::Wrapping::Word)
466                .style(move |theme: &Theme| text::Style {
467                    color: Some(alpha_color(
468                        theme.colors().inverse.inverse_surface_text,
469                        content_alpha,
470                    )),
471                })
472                .width(Length::Fill),
473        )
474        .spacing(8)
475        .align_y(alignment::Vertical::Center);
476
477    if let Some(action) = action {
478        content = content.push(action);
479    }
480
481    Container::new(content)
482        .height(Length::Fixed(height))
483        .width(Length::Fill)
484        .padding(Padding {
485            top: 0.0,
486            right: 8.0,
487            bottom: 0.0,
488            left: 16.0,
489        })
490        .align_y(alignment::Vertical::Center)
491        .style(container_style)
492}
493
494fn floating_layer<'a, Message, Renderer>(
495    snackbar: impl Into<Element<'a, Message, Theme, Renderer>>,
496    translation_y: f32,
497) -> Container<'a, Message, Theme, Renderer>
498where
499    Message: 'a,
500    Renderer: iced_widget::core::Renderer + 'a,
501{
502    let snackbar = Container::new(snackbar)
503        .width(Length::Fill)
504        .max_width(tokens::component::snackbar::MAX_WIDTH);
505
506    Container::new(translated(snackbar, Vector::new(0.0, translation_y)))
507        .width(Length::Fill)
508        .height(Length::Fill)
509        .padding(Padding {
510            top: 0.0,
511            right: tokens::component::snackbar::HORIZONTAL_MARGIN,
512            bottom: tokens::component::snackbar::BOTTOM_MARGIN,
513            left: tokens::component::snackbar::HORIZONTAL_MARGIN,
514        })
515        .align_x(alignment::Horizontal::Center)
516        .align_y(alignment::Vertical::Bottom)
517}
518
519fn translated<'a, Message, Renderer>(
520    content: impl Into<Element<'a, Message, Theme, Renderer>>,
521    translation: Vector,
522) -> Element<'a, Message, Theme, Renderer>
523where
524    Message: 'a,
525    Renderer: iced_widget::core::Renderer + 'a,
526{
527    Element::new(Translated {
528        content: content.into(),
529        translation,
530    })
531}
532
533struct Translated<'a, Message, Renderer> {
534    content: Element<'a, Message, Theme, Renderer>,
535    translation: Vector,
536}
537
538impl<Message, Renderer> std::fmt::Debug for Translated<'_, Message, Renderer> {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        f.debug_struct("Translated")
541            .field("translation", &self.translation)
542            .finish_non_exhaustive()
543    }
544}
545
546impl<Message, Renderer> Widget<Message, Theme, Renderer> for Translated<'_, Message, Renderer>
547where
548    Renderer: iced_widget::core::Renderer,
549{
550    fn tag(&self) -> tree::Tag {
551        self.content.as_widget().tag()
552    }
553
554    fn state(&self) -> tree::State {
555        self.content.as_widget().state()
556    }
557
558    fn children(&self) -> Vec<Tree> {
559        self.content.as_widget().children()
560    }
561
562    fn diff(&self, tree: &mut Tree) {
563        self.content.as_widget().diff(tree);
564    }
565
566    fn size(&self) -> Size<Length> {
567        self.content.as_widget().size()
568    }
569
570    fn size_hint(&self) -> Size<Length> {
571        self.content.as_widget().size_hint()
572    }
573
574    fn layout(
575        &mut self,
576        tree: &mut Tree,
577        renderer: &Renderer,
578        limits: &layout::Limits,
579    ) -> layout::Node {
580        self.content.as_widget_mut().layout(tree, renderer, limits)
581    }
582
583    fn operate(
584        &mut self,
585        tree: &mut Tree,
586        layout: Layout<'_>,
587        renderer: &Renderer,
588        operation: &mut dyn widget::Operation,
589    ) {
590        self.content
591            .as_widget_mut()
592            .operate(tree, layout, renderer, operation);
593    }
594
595    fn update(
596        &mut self,
597        tree: &mut Tree,
598        event: &Event,
599        layout: Layout<'_>,
600        cursor: mouse::Cursor,
601        renderer: &Renderer,
602        clipboard: &mut dyn Clipboard,
603        shell: &mut Shell<'_, Message>,
604        viewport: &Rectangle,
605    ) {
606        let translation = self.translation;
607
608        self.content.as_widget_mut().update(
609            tree,
610            event,
611            layout,
612            cursor - translation,
613            renderer,
614            clipboard,
615            shell,
616            &(*viewport - translation),
617        );
618    }
619
620    fn mouse_interaction(
621        &self,
622        tree: &Tree,
623        layout: Layout<'_>,
624        cursor: mouse::Cursor,
625        viewport: &Rectangle,
626        renderer: &Renderer,
627    ) -> mouse::Interaction {
628        let translation = self.translation;
629
630        self.content.as_widget().mouse_interaction(
631            tree,
632            layout,
633            cursor - translation,
634            &(*viewport - translation),
635            renderer,
636        )
637    }
638
639    fn draw(
640        &self,
641        tree: &Tree,
642        renderer: &mut Renderer,
643        theme: &Theme,
644        style: &renderer::Style,
645        layout: Layout<'_>,
646        cursor: mouse::Cursor,
647        viewport: &Rectangle,
648    ) {
649        let Some(viewport) = layout.bounds().intersection(viewport) else {
650            return;
651        };
652        let translation = self.translation;
653
654        renderer.with_layer(viewport, |renderer| {
655            renderer.with_translation(translation, |renderer| {
656                self.content.as_widget().draw(
657                    tree,
658                    renderer,
659                    theme,
660                    style,
661                    layout,
662                    cursor - translation,
663                    &(viewport - translation),
664                );
665            });
666        });
667    }
668
669    fn overlay<'b>(
670        &'b mut self,
671        tree: &'b mut Tree,
672        layout: Layout<'b>,
673        renderer: &Renderer,
674        viewport: &Rectangle,
675        translation: Vector,
676    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
677        self.content.as_widget_mut().overlay(
678            tree,
679            layout,
680            renderer,
681            viewport,
682            translation + self.translation,
683        )
684    }
685}
686
687fn container_style(theme: &Theme) -> iced_widget::container::Style {
688    let colors = theme.colors();
689
690    // M3 snackbars use inverse roles, so dark themes intentionally get
691    // a light snackbar surface for contrast against the app surface.
692    iced_widget::container::Style {
693        background: Some(Background::Color(colors.inverse.inverse_surface)),
694        text_color: Some(colors.inverse.inverse_surface_text),
695        border: border::rounded(tokens::component::snackbar::CONTAINER_SHAPE),
696        shadow: shadow_from_level(
697            tokens::component::snackbar::CONTAINER_ELEVATION_LEVEL,
698            colors.shadow,
699        ),
700        snap: cfg!(feature = "crisp"),
701    }
702}
703
704fn action_style_alpha(theme: &Theme, status: Status, content_alpha: f32) -> Style {
705    let colors = theme.colors();
706    let foreground = alpha_color(colors.inverse.inverse_primary, content_alpha);
707    let active = Style {
708        background: None,
709        text_color: foreground,
710        border: border::rounded(tokens::component::button::CONTAINER_SHAPE),
711        shadow: Shadow::default(),
712        snap: cfg!(feature = "crisp"),
713    };
714
715    match status {
716        Status::Active => active,
717        Status::Hovered => Style {
718            background: Some(Background::Color(state_layer(
719                foreground,
720                tokens::state::HOVER_STATE_LAYER_OPACITY,
721            ))),
722            ..active
723        },
724        Status::Pressed => Style {
725            background: Some(Background::Color(state_layer(
726                foreground,
727                tokens::state::PRESSED_STATE_LAYER_OPACITY,
728            ))),
729            ..active
730        },
731        Status::Disabled => Style {
732            text_color: Color {
733                a: tokens::state::DISABLED_LABEL_TEXT_OPACITY,
734                ..foreground
735            },
736            ..active
737        },
738    }
739}
740
741fn icon_action_style(theme: &Theme, status: Status) -> Style {
742    let colors = theme.colors();
743    let foreground = colors.inverse.inverse_surface_text;
744    let active = Style {
745        background: None,
746        text_color: foreground,
747        border: Border {
748            color: Color::TRANSPARENT,
749            width: 0.0,
750            radius: tokens::component::icon_button::CONTAINER_SHAPE.into(),
751        },
752        shadow: Shadow::default(),
753        snap: cfg!(feature = "crisp"),
754    };
755
756    match status {
757        Status::Active => active,
758        Status::Hovered => Style {
759            background: Some(Background::Color(state_layer(
760                foreground,
761                tokens::state::HOVER_STATE_LAYER_OPACITY,
762            ))),
763            ..active
764        },
765        Status::Pressed => Style {
766            background: Some(Background::Color(state_layer(
767                foreground,
768                tokens::state::PRESSED_STATE_LAYER_OPACITY,
769            ))),
770            ..active
771        },
772        Status::Disabled => Style {
773            text_color: Color {
774                a: tokens::state::DISABLED_LABEL_TEXT_OPACITY,
775                ..foreground
776            },
777            ..active
778        },
779    }
780}
781
782#[cfg(test)]
783#[path = "../../../tests/widget/component/snackbar.rs"]
784mod tests;