1use 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#[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#[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 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 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 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 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 pub fn is_active(&self) -> bool {
114 self.phase != TransitionPhase::Hidden
115 }
116
117 pub fn is_animating(&self) -> bool {
119 matches!(
120 self.phase,
121 TransitionPhase::Showing | TransitionPhase::Dismissing
122 )
123 }
124
125 pub fn phase(&self) -> TransitionPhase {
127 self.phase
128 }
129
130 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 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#[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#[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 pub fn lines(mut self, lines: Lines) -> Self {
236 self.lines = lines;
237 self
238 }
239
240 pub fn content_alpha(mut self, content_alpha: f32) -> Self {
242 self.content_alpha = content_alpha;
243 self
244 }
245}
246
247#[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 pub fn content_alpha(mut self, content_alpha: f32) -> Self {
262 self.content_alpha = content_alpha;
263 self
264 }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq)]
269pub struct HostOptions {
270 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 pub fn bottom_margin(mut self, bottom_margin: f32) -> Self {
285 self.bottom_margin = bottom_margin.max(0.0);
286 self
287 }
288
289 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
300pub 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
318pub 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
329pub 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
341pub 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
372pub 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
385pub 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
416pub 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
429pub 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
442pub 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
462pub 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
486pub 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 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;