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/// Layout options for a snackbar host.
268#[derive(Debug, Clone, Copy, PartialEq)]
269pub struct HostOptions {
270    /// Distance between the snackbar bottom edge and the host bottom edge.
271    pub bottom_margin: f32,
272}
273
274impl Default for HostOptions {
275    fn default() -> Self {
276        Self {
277            bottom_margin: tokens::component::snackbar::BOTTOM_MARGIN,
278        }
279    }
280}
281
282impl HostOptions {
283    /// Sets the distance between the snackbar and the host bottom edge.
284    pub fn bottom_margin(mut self, bottom_margin: f32) -> Self {
285        self.bottom_margin = bottom_margin.max(0.0);
286        self
287    }
288
289    /// Places the snackbar above a floating action button, matching the
290    /// vertical stacking used by Material 3 Compose `Scaffold` while
291    /// preserving the snackbar's standard outer margin.
292    pub fn above_fab(mut self, fab_height: f32, fab_bottom_margin: f32) -> Self {
293        self.bottom_margin = fab_height.max(0.0)
294            + fab_bottom_margin.max(0.0)
295            + tokens::component::snackbar::BOTTOM_MARGIN;
296        self
297    }
298}
299
300/// Creates a snackbar surface.
301pub fn surface<'a, Message, Renderer>(
302    message: impl text::IntoFragment<'a>,
303    action: Option<Element<'a, Message, Theme, Renderer>>,
304    options: Options,
305) -> Container<'a, Message, Theme, Renderer>
306where
307    Message: 'a,
308    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
309{
310    surface_container(
311        message,
312        action,
313        options.lines.container_height(),
314        options.content_alpha,
315    )
316}
317
318/// Creates a snackbar text action button.
319pub fn action<'a, Message, Renderer>(
320    label: impl text::IntoFragment<'a>,
321) -> Button<'a, Message, Renderer>
322where
323    Message: Clone + 'a,
324    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
325{
326    action_with(label, ActionOptions::default())
327}
328
329/// Creates a snackbar text action with an on-press message.
330pub fn action_button<'a, Message, Renderer>(
331    label: impl text::IntoFragment<'a>,
332    on_press: Message,
333) -> Element<'a, Message, Theme, Renderer>
334where
335    Message: Clone + 'a,
336    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
337{
338    action_button_with(label, on_press, ActionOptions::default())
339}
340
341/// Creates a snackbar text action button with custom visual options.
342pub fn action_with<'a, Message, Renderer>(
343    label: impl text::IntoFragment<'a>,
344    options: ActionOptions,
345) -> Button<'a, Message, Renderer>
346where
347    Message: Clone + 'a,
348    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
349{
350    let label_text = tokens::component::snackbar::ACTION_LABEL_TEXT;
351
352    Button::new(
353        Container::new(
354            Text::new(label)
355                .size(label_text.size)
356                .line_height(absolute_line_height(label_text.line_height)),
357        )
358        .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
359        .padding(Padding {
360            top: 0.0,
361            right: tokens::component::button::TRAILING_SPACE,
362            bottom: 0.0,
363            left: tokens::component::button::LEADING_SPACE,
364        })
365        .align_y(alignment::Vertical::Center),
366    )
367    .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
368    .padding(Padding::ZERO)
369    .style(move |theme, status| action_style_alpha(theme, status, options.content_alpha))
370}
371
372/// Creates a snackbar text action button with custom visual options.
373pub fn action_button_with<'a, Message, Renderer>(
374    label: impl text::IntoFragment<'a>,
375    on_press: Message,
376    options: ActionOptions,
377) -> Element<'a, Message, Theme, Renderer>
378where
379    Message: Clone + 'a,
380    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
381{
382    action_with(label, options).on_press(on_press).into()
383}
384
385/// Creates a snackbar icon action, typically used for dismiss.
386pub fn icon_action<'a, Message, Renderer>(
387    icon_name: impl text::IntoFragment<'a>,
388) -> Button<'a, Message, Renderer>
389where
390    Message: Clone + 'a,
391    Renderer: geometry::Renderer + core_text::Renderer + 'a,
392    iced_widget::core::Font: Into<Renderer::Font>,
393{
394    Button::new(
395        Container::new(fonts::icon(
396            icon_name,
397            tokens::component::snackbar::ICON_SIZE,
398        ))
399        .center_x(Length::Fixed(
400            tokens::component::icon_button::CONTAINER_WIDTH,
401        ))
402        .center_y(Length::Fixed(
403            tokens::component::icon_button::CONTAINER_HEIGHT,
404        )),
405    )
406    .width(Length::Fixed(
407        tokens::component::icon_button::CONTAINER_WIDTH,
408    ))
409    .height(Length::Fixed(
410        tokens::component::icon_button::CONTAINER_HEIGHT,
411    ))
412    .padding(Padding::ZERO)
413    .style(icon_action_style)
414}
415
416/// Creates a snackbar icon action with an on-press message.
417pub fn icon_action_button<'a, Message, Renderer>(
418    icon_name: impl text::IntoFragment<'a>,
419    on_press: Message,
420) -> Element<'a, Message, Theme, Renderer>
421where
422    Message: Clone + 'a,
423    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
424    iced_widget::core::Font: Into<Renderer::Font>,
425{
426    icon_action(icon_name).on_press(on_press).into()
427}
428
429/// Places snackbar content above the app content and translates it from the bottom edge.
430pub fn overlay<'a, Message, Renderer>(
431    content: impl Into<Element<'a, Message, Theme, Renderer>>,
432    snackbar: impl Into<Element<'a, Message, Theme, Renderer>>,
433    translation_y: f32,
434) -> Element<'a, Message, Theme, Renderer>
435where
436    Message: 'a,
437    Renderer: iced_widget::core::Renderer + 'a,
438{
439    overlay_with(content, snackbar, translation_y, HostOptions::default())
440}
441
442/// Places snackbar content above app content with custom host layout options.
443pub fn overlay_with<'a, Message, Renderer>(
444    content: impl Into<Element<'a, Message, Theme, Renderer>>,
445    snackbar: impl Into<Element<'a, Message, Theme, Renderer>>,
446    translation_y: f32,
447    options: HostOptions,
448) -> Element<'a, Message, Theme, Renderer>
449where
450    Message: 'a,
451    Renderer: iced_widget::core::Renderer + 'a,
452{
453    Stack::with_children([
454        content.into(),
455        floating_layer(snackbar, translation_y, options.bottom_margin).into(),
456    ])
457    .width(Length::Fill)
458    .height(Length::Fill)
459    .into()
460}
461
462/// Places an Android-animated single-line snackbar with one action over content.
463pub fn host<'a, Message, Renderer>(
464    content: impl Into<Element<'a, Message, Theme, Renderer>>,
465    transition: &Transition,
466    now: Instant,
467    message: impl text::IntoFragment<'a>,
468    action_label: impl text::IntoFragment<'a>,
469    on_action: Message,
470) -> Element<'a, Message, Theme, Renderer>
471where
472    Message: Clone + 'a,
473    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
474{
475    host_with(
476        content,
477        transition,
478        now,
479        message,
480        action_label,
481        on_action,
482        HostOptions::default(),
483    )
484}
485
486/// Places an Android-animated single-line snackbar over content with custom
487/// host layout options.
488pub fn host_with<'a, Message, Renderer>(
489    content: impl Into<Element<'a, Message, Theme, Renderer>>,
490    transition: &Transition,
491    now: Instant,
492    message: impl text::IntoFragment<'a>,
493    action_label: impl text::IntoFragment<'a>,
494    on_action: Message,
495    options: HostOptions,
496) -> Element<'a, Message, Theme, Renderer>
497where
498    Message: Clone + 'a,
499    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
500{
501    if !transition.is_active() {
502        return content.into();
503    }
504
505    let alpha = transition.content_alpha(now);
506    let hidden_distance =
507        tokens::component::snackbar::WITH_SINGLE_LINE_CONTAINER_HEIGHT + options.bottom_margin;
508    let translation_y = transition.translation_y(now, hidden_distance);
509    let snackbar = surface(
510        message,
511        Some(action_button_with(
512            action_label,
513            on_action,
514            ActionOptions::default().content_alpha(alpha),
515        )),
516        Options::default().content_alpha(alpha),
517    );
518
519    overlay_with(content, snackbar, translation_y, options)
520}
521
522fn surface_container<'a, Message, Renderer>(
523    message: impl text::IntoFragment<'a>,
524    action: Option<Element<'a, Message, Theme, Renderer>>,
525    height: f32,
526    content_alpha: f32,
527) -> Container<'a, Message, Theme, Renderer>
528where
529    Message: 'a,
530    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
531{
532    let supporting_text = tokens::component::snackbar::SUPPORTING_TEXT;
533    let mut content = Row::new()
534        .push(
535            Text::new(message)
536                .size(supporting_text.size)
537                .line_height(absolute_line_height(supporting_text.line_height))
538                .wrapping(text::Wrapping::Word)
539                .style(move |theme: &Theme| text::Style {
540                    color: Some(alpha_color(
541                        theme.colors().inverse.inverse_surface_text,
542                        content_alpha,
543                    )),
544                })
545                .width(Length::Fill),
546        )
547        .spacing(8)
548        .align_y(alignment::Vertical::Center);
549
550    if let Some(action) = action {
551        content = content.push(action);
552    }
553
554    Container::new(content)
555        .height(Length::Fixed(height))
556        .width(Length::Fill)
557        .padding(Padding {
558            top: 0.0,
559            right: 8.0,
560            bottom: 0.0,
561            left: 16.0,
562        })
563        .align_y(alignment::Vertical::Center)
564        .style(container_style)
565}
566
567fn floating_layer<'a, Message, Renderer>(
568    snackbar: impl Into<Element<'a, Message, Theme, Renderer>>,
569    translation_y: f32,
570    bottom_margin: f32,
571) -> Container<'a, Message, Theme, Renderer>
572where
573    Message: 'a,
574    Renderer: iced_widget::core::Renderer + 'a,
575{
576    let snackbar = Container::new(snackbar)
577        .width(Length::Fill)
578        .max_width(tokens::component::snackbar::MAX_WIDTH);
579
580    Container::new(translated(snackbar, Vector::new(0.0, translation_y)))
581        .width(Length::Fill)
582        .height(Length::Fill)
583        .padding(Padding {
584            top: 0.0,
585            right: tokens::component::snackbar::HORIZONTAL_MARGIN,
586            bottom: bottom_margin,
587            left: tokens::component::snackbar::HORIZONTAL_MARGIN,
588        })
589        .align_x(alignment::Horizontal::Center)
590        .align_y(alignment::Vertical::Bottom)
591}
592
593fn translated<'a, Message, Renderer>(
594    content: impl Into<Element<'a, Message, Theme, Renderer>>,
595    translation: Vector,
596) -> Element<'a, Message, Theme, Renderer>
597where
598    Message: 'a,
599    Renderer: iced_widget::core::Renderer + 'a,
600{
601    Element::new(Translated {
602        content: content.into(),
603        translation,
604    })
605}
606
607struct Translated<'a, Message, Renderer> {
608    content: Element<'a, Message, Theme, Renderer>,
609    translation: Vector,
610}
611
612impl<Message, Renderer> std::fmt::Debug for Translated<'_, Message, Renderer> {
613    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614        f.debug_struct("Translated")
615            .field("translation", &self.translation)
616            .finish_non_exhaustive()
617    }
618}
619
620impl<Message, Renderer> Widget<Message, Theme, Renderer> for Translated<'_, Message, Renderer>
621where
622    Renderer: iced_widget::core::Renderer,
623{
624    fn tag(&self) -> tree::Tag {
625        self.content.as_widget().tag()
626    }
627
628    fn state(&self) -> tree::State {
629        self.content.as_widget().state()
630    }
631
632    fn children(&self) -> Vec<Tree> {
633        self.content.as_widget().children()
634    }
635
636    fn diff(&self, tree: &mut Tree) {
637        self.content.as_widget().diff(tree);
638    }
639
640    fn size(&self) -> Size<Length> {
641        self.content.as_widget().size()
642    }
643
644    fn size_hint(&self) -> Size<Length> {
645        self.content.as_widget().size_hint()
646    }
647
648    fn layout(
649        &mut self,
650        tree: &mut Tree,
651        renderer: &Renderer,
652        limits: &layout::Limits,
653    ) -> layout::Node {
654        self.content.as_widget_mut().layout(tree, renderer, limits)
655    }
656
657    fn operate(
658        &mut self,
659        tree: &mut Tree,
660        layout: Layout<'_>,
661        renderer: &Renderer,
662        operation: &mut dyn widget::Operation,
663    ) {
664        self.content
665            .as_widget_mut()
666            .operate(tree, layout, renderer, operation);
667    }
668
669    fn update(
670        &mut self,
671        tree: &mut Tree,
672        event: &Event,
673        layout: Layout<'_>,
674        cursor: mouse::Cursor,
675        renderer: &Renderer,
676        clipboard: &mut dyn Clipboard,
677        shell: &mut Shell<'_, Message>,
678        viewport: &Rectangle,
679    ) {
680        let translation = self.translation;
681
682        self.content.as_widget_mut().update(
683            tree,
684            event,
685            layout,
686            cursor - translation,
687            renderer,
688            clipboard,
689            shell,
690            &(*viewport - translation),
691        );
692    }
693
694    fn mouse_interaction(
695        &self,
696        tree: &Tree,
697        layout: Layout<'_>,
698        cursor: mouse::Cursor,
699        viewport: &Rectangle,
700        renderer: &Renderer,
701    ) -> mouse::Interaction {
702        let translation = self.translation;
703
704        self.content.as_widget().mouse_interaction(
705            tree,
706            layout,
707            cursor - translation,
708            &(*viewport - translation),
709            renderer,
710        )
711    }
712
713    fn draw(
714        &self,
715        tree: &Tree,
716        renderer: &mut Renderer,
717        theme: &Theme,
718        style: &renderer::Style,
719        layout: Layout<'_>,
720        cursor: mouse::Cursor,
721        viewport: &Rectangle,
722    ) {
723        let Some(viewport) = layout.bounds().intersection(viewport) else {
724            return;
725        };
726        let translation = self.translation;
727
728        renderer.with_layer(viewport, |renderer| {
729            renderer.with_translation(translation, |renderer| {
730                self.content.as_widget().draw(
731                    tree,
732                    renderer,
733                    theme,
734                    style,
735                    layout,
736                    cursor - translation,
737                    &(viewport - translation),
738                );
739            });
740        });
741    }
742
743    fn overlay<'b>(
744        &'b mut self,
745        tree: &'b mut Tree,
746        layout: Layout<'b>,
747        renderer: &Renderer,
748        viewport: &Rectangle,
749        translation: Vector,
750    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
751        self.content.as_widget_mut().overlay(
752            tree,
753            layout,
754            renderer,
755            viewport,
756            translation + self.translation,
757        )
758    }
759}
760
761fn container_style(theme: &Theme) -> iced_widget::container::Style {
762    let colors = theme.colors();
763
764    // M3 snackbars use inverse roles, so dark themes intentionally get
765    // a light snackbar surface for contrast against the app surface.
766    iced_widget::container::Style {
767        background: Some(Background::Color(colors.inverse.inverse_surface)),
768        text_color: Some(colors.inverse.inverse_surface_text),
769        border: border::rounded(tokens::component::snackbar::CONTAINER_SHAPE),
770        shadow: shadow_from_level(
771            tokens::component::snackbar::CONTAINER_ELEVATION_LEVEL,
772            colors.shadow,
773        ),
774        snap: cfg!(feature = "crisp"),
775    }
776}
777
778fn action_style_alpha(theme: &Theme, status: Status, content_alpha: f32) -> Style {
779    let colors = theme.colors();
780    let foreground = alpha_color(colors.inverse.inverse_primary, content_alpha);
781    let active = Style {
782        background: None,
783        text_color: foreground,
784        border: border::rounded(tokens::component::button::CONTAINER_SHAPE),
785        shadow: Shadow::default(),
786        snap: cfg!(feature = "crisp"),
787    };
788
789    match status {
790        Status::Active => active,
791        Status::Hovered => Style {
792            background: Some(Background::Color(state_layer(
793                foreground,
794                tokens::state::HOVER_STATE_LAYER_OPACITY,
795            ))),
796            ..active
797        },
798        Status::Pressed => Style {
799            background: Some(Background::Color(state_layer(
800                foreground,
801                tokens::state::PRESSED_STATE_LAYER_OPACITY,
802            ))),
803            ..active
804        },
805        Status::Disabled => Style {
806            text_color: Color {
807                a: tokens::state::DISABLED_LABEL_TEXT_OPACITY,
808                ..foreground
809            },
810            ..active
811        },
812    }
813}
814
815fn icon_action_style(theme: &Theme, status: Status) -> Style {
816    let colors = theme.colors();
817    let foreground = colors.inverse.inverse_surface_text;
818    let active = Style {
819        background: None,
820        text_color: foreground,
821        border: Border {
822            color: Color::TRANSPARENT,
823            width: 0.0,
824            radius: tokens::component::icon_button::CONTAINER_SHAPE.into(),
825        },
826        shadow: Shadow::default(),
827        snap: cfg!(feature = "crisp"),
828    };
829
830    match status {
831        Status::Active => active,
832        Status::Hovered => Style {
833            background: Some(Background::Color(state_layer(
834                foreground,
835                tokens::state::HOVER_STATE_LAYER_OPACITY,
836            ))),
837            ..active
838        },
839        Status::Pressed => Style {
840            background: Some(Background::Color(state_layer(
841                foreground,
842                tokens::state::PRESSED_STATE_LAYER_OPACITY,
843            ))),
844            ..active
845        },
846        Status::Disabled => Style {
847            text_color: Color {
848                a: tokens::state::DISABLED_LABEL_TEXT_OPACITY,
849                ..foreground
850            },
851            ..active
852        },
853    }
854}
855
856#[cfg(test)]
857#[path = "../../../tests/widget/component/snackbar.rs"]
858mod tests;