Skip to main content

material_ui_rs/widget/component/
navigation.rs

1//! Material 3 navigation bar, rail, drawer, and adaptive layout helpers.
2
3use std::f32::consts::PI;
4
5use iced_widget::canvas::{self, Canvas, LineCap, Path, Stroke};
6use iced_widget::core::layout;
7use iced_widget::core::mouse;
8use iced_widget::core::overlay;
9use iced_widget::core::renderer;
10use iced_widget::core::text as core_text;
11use iced_widget::core::time::Instant;
12use iced_widget::core::touch;
13use iced_widget::core::widget::Operation;
14use iced_widget::core::widget::tree::{self, Tree};
15use iced_widget::core::{
16    Background, Clipboard, Color, Element, Event, Font, Layout, Length, Padding, Point, Rectangle,
17    Shell, Size, Vector, Widget, alignment, border, window,
18};
19use iced_widget::graphics::geometry;
20use iced_widget::renderer::wgpu::primitive;
21use iced_widget::text::{self, LineHeight};
22use iced_widget::{Column, Container, Row, Space, Stack, Text};
23
24use super::badge as badge_widget;
25use super::button::Button;
26use super::ripple::{PressRippleState, RippleConfig, RippleStart, RippleStyle, draw_ripples};
27use super::support::{AnimatedScalar, alpha_color, duration_ms, lerp};
28use crate::style::button as button_style;
29use crate::utils::{HOVERED_LAYER_OPACITY, mix, shadow_from_level, state_layer};
30use crate::{Theme, fonts, tokens};
31
32#[cfg(test)]
33use super::ripple::{ripple_target_radius, rounded_rect_span_at_y};
34
35const NAVIGATION_MENU_ICON_VIEWPORT_SIZE: f32 = 24.0;
36const NAVIGATION_MENU_ICON_START_X: f32 = 5.0;
37const NAVIGATION_MENU_ICON_END_X: f32 = 19.0;
38const NAVIGATION_MENU_ICON_CENTER_X: f32 = 12.0;
39const NAVIGATION_MENU_ICON_TOP_Y: f32 = 7.0;
40const NAVIGATION_MENU_ICON_CENTER_Y: f32 = 12.0;
41const NAVIGATION_MENU_ICON_BOTTOM_Y: f32 = 17.0;
42const NAVIGATION_MENU_ICON_ARROW_TOP_Y: f32 = 5.0;
43const NAVIGATION_MENU_ICON_ARROW_BOTTOM_Y: f32 = 19.0;
44const NAVIGATION_MENU_ICON_STROKE_WIDTH: f32 = 2.4;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum AdaptiveLayout {
48    NavigationBar,
49    NavigationRail,
50}
51
52impl AdaptiveLayout {
53    pub fn from_size(width: f32, height: f32) -> Self {
54        adaptive_layout(width, height)
55    }
56
57    pub fn item_animation_duration_ms(self) -> u16 {
58        match self {
59            Self::NavigationBar => tokens::component::navigation_bar::ITEM_ANIMATION_DURATION_MS,
60            Self::NavigationRail => tokens::component::navigation_rail::ITEM_ANIMATION_DURATION_MS,
61        }
62    }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum WindowWidthClass {
67    Compact,
68    Medium,
69    Expanded,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum WindowHeightClass {
74    Compact,
75    Medium,
76    Expanded,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct WindowSizeClass {
81    pub width: WindowWidthClass,
82    pub height: WindowHeightClass,
83}
84
85impl WindowSizeClass {
86    pub fn from_size(width: f32, height: f32) -> Self {
87        Self {
88            width: width_class(width),
89            height: height_class(height),
90        }
91    }
92
93    pub fn adaptive_navigation_layout(self) -> AdaptiveLayout {
94        if matches!(self.width, WindowWidthClass::Compact)
95            || matches!(self.height, WindowHeightClass::Compact)
96        {
97            AdaptiveLayout::NavigationBar
98        } else {
99            AdaptiveLayout::NavigationRail
100        }
101    }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct Selection<Id> {
106    selected: Id,
107    previous: Option<Id>,
108    selected_size_start: f32,
109    previous_size_start: f32,
110    size_progress: f32,
111    selected_alpha_start: f32,
112    previous_alpha_start: f32,
113    alpha_progress: f32,
114}
115
116impl<Id: Copy + Eq> Selection<Id> {
117    pub fn new(selected: Id) -> Self {
118        Self {
119            selected,
120            previous: None,
121            selected_size_start: 1.0,
122            previous_size_start: 0.0,
123            size_progress: 1.0,
124            selected_alpha_start: 1.0,
125            previous_alpha_start: 0.0,
126            alpha_progress: 1.0,
127        }
128    }
129
130    pub fn transitioning(selected: Id, previous: Id, progress: f32) -> Self {
131        Self::transitioning_from(selected, previous, 0.0, 1.0, progress)
132    }
133
134    pub fn transitioning_from(
135        selected: Id,
136        previous: Id,
137        selected_start: f32,
138        previous_start: f32,
139        progress: f32,
140    ) -> Self {
141        Self::transitioning_from_tracks(
142            selected,
143            previous,
144            TrackProgress::new(selected_start, previous_start, progress),
145            TrackProgress::new(selected_start, previous_start, progress),
146        )
147    }
148
149    fn transitioning_from_tracks(
150        selected: Id,
151        previous: Id,
152        size: TrackProgress,
153        alpha: TrackProgress,
154    ) -> Self {
155        Self {
156            selected,
157            previous: Some(previous),
158            selected_size_start: size.selected_start,
159            previous_size_start: size.previous_start,
160            size_progress: size.progress,
161            selected_alpha_start: alpha.selected_start,
162            previous_alpha_start: alpha.previous_start,
163            alpha_progress: alpha.progress,
164        }
165    }
166
167    pub fn selected(self) -> Id {
168        self.selected
169    }
170
171    pub fn progress(self, id: Id) -> f32 {
172        self.size_progress(id)
173    }
174
175    pub fn size_progress(self, id: Id) -> f32 {
176        if id == self.selected {
177            lerp(self.selected_size_start, 1.0, self.size_progress)
178        } else if self.previous.is_some_and(|previous| previous == id) {
179            lerp(self.previous_size_start, 0.0, self.size_progress)
180        } else {
181            0.0
182        }
183    }
184
185    pub fn alpha_progress(self, id: Id) -> f32 {
186        if id == self.selected {
187            lerp(self.selected_alpha_start, 1.0, self.alpha_progress)
188        } else if self.previous.is_some_and(|previous| previous == id) {
189            lerp(self.previous_alpha_start, 0.0, self.alpha_progress)
190        } else {
191            0.0
192        }
193    }
194}
195
196#[derive(Debug, Clone, Copy)]
197pub struct NavigationState<Id> {
198    selected: Id,
199    previous: Option<Id>,
200    selected_size_start: f32,
201    previous_size_start: f32,
202    selected_alpha_start: f32,
203    previous_alpha_start: f32,
204    size_progress: AnimatedScalar,
205    alpha_progress: AnimatedScalar,
206    rail_expansion: NavigationRailExpansionState,
207}
208
209impl<Id: Copy + Eq> NavigationState<Id> {
210    pub fn new(selected: Id) -> Self {
211        Self {
212            selected,
213            previous: None,
214            selected_size_start: 1.0,
215            previous_size_start: 0.0,
216            selected_alpha_start: 1.0,
217            previous_alpha_start: 0.0,
218            size_progress: AnimatedScalar::new(1.0),
219            alpha_progress: AnimatedScalar::new(1.0),
220            rail_expansion: NavigationRailExpansionState::new(false),
221        }
222    }
223
224    pub fn selected(&self) -> Id {
225        self.selected
226    }
227
228    pub fn selection(&self) -> Selection<Id> {
229        if let Some(previous) = self.previous {
230            Selection::transitioning_from_tracks(
231                self.selected,
232                previous,
233                TrackProgress::new(
234                    self.selected_size_start,
235                    self.previous_size_start,
236                    self.size_progress.value,
237                ),
238                TrackProgress::new(
239                    self.selected_alpha_start,
240                    self.previous_alpha_start,
241                    self.alpha_progress.value,
242                ),
243            )
244        } else {
245            Selection::new(self.selected)
246        }
247    }
248
249    pub fn select(&mut self, selected: Id, now: Instant, layout: AdaptiveLayout) {
250        if selected == self.selected {
251            return;
252        }
253
254        let current = self.selection();
255        let previous = self.selected;
256        let selected_size_start = current.size_progress(selected);
257        let previous_size_start = current.size_progress(previous);
258        let selected_alpha_start = current.alpha_progress(selected);
259        let previous_alpha_start = current.alpha_progress(previous);
260
261        self.selected = selected;
262        self.previous = Some(previous);
263        self.selected_size_start = selected_size_start;
264        self.previous_size_start = previous_size_start;
265        self.selected_alpha_start = selected_alpha_start;
266        self.previous_alpha_start = previous_alpha_start;
267        self.size_progress = AnimatedScalar::new(0.0);
268        self.alpha_progress = AnimatedScalar::new(0.0);
269        let duration = duration_ms(layout.item_animation_duration_ms());
270        self.size_progress
271            .set_target(1.0, now, duration, tokens::motion::EASING_LEGACY);
272        self.alpha_progress
273            .set_target(1.0, now, duration, tokens::motion::EASING_LEGACY);
274    }
275
276    pub fn select_for_size(&mut self, selected: Id, now: Instant, size: Size) {
277        self.select(selected, now, adaptive_layout(size.width, size.height));
278    }
279
280    pub fn select_now_for_size(&mut self, selected: Id, size: Size) {
281        self.select_for_size(selected, Instant::now(), size);
282    }
283
284    pub fn toggle_menu(&mut self, now: Instant) {
285        self.rail_expansion.toggle(now);
286    }
287
288    pub fn toggle_menu_now(&mut self) {
289        self.toggle_menu(Instant::now());
290    }
291
292    pub fn is_menu_open(&self) -> bool {
293        self.rail_expansion.is_open()
294    }
295
296    pub fn is_menu_visible(&self) -> bool {
297        self.rail_expansion.is_visible()
298    }
299
300    pub fn menu_progress(&self) -> f32 {
301        self.rail_expansion.progress()
302    }
303
304    pub fn is_animating(&self) -> bool {
305        self.previous.is_some() || self.rail_expansion.is_animating()
306    }
307
308    pub fn subscription<Message, F>(&self, on_frame: F) -> iced::Subscription<Message>
309    where
310        Message: 'static,
311        F: Fn(Instant) -> Message + Send + Clone + 'static,
312    {
313        if self.is_animating() {
314            iced::window::frames().map(on_frame)
315        } else {
316            iced::Subscription::none()
317        }
318    }
319
320    pub fn advance(&mut self, now: Instant) -> bool {
321        let navigation_animating =
322            self.size_progress.advance(now) | self.alpha_progress.advance(now);
323        let menu_animating = self.rail_expansion.advance(now);
324
325        if !navigation_animating {
326            self.size_progress.value = 1.0;
327            self.alpha_progress.value = 1.0;
328            self.previous = None;
329            self.selected_size_start = 1.0;
330            self.previous_size_start = 0.0;
331            self.selected_alpha_start = 1.0;
332            self.previous_alpha_start = 0.0;
333        }
334
335        navigation_animating | menu_animating
336    }
337
338    pub fn advance_frame(&mut self, now: Instant) {
339        let _ = self.advance(now);
340    }
341}
342
343#[derive(Debug, Clone, Copy)]
344pub struct NavigationRailExpansionState {
345    open: bool,
346    progress: AnimatedScalar,
347}
348
349impl NavigationRailExpansionState {
350    pub fn new(open: bool) -> Self {
351        Self {
352            open,
353            progress: AnimatedScalar::new(if open { 1.0 } else { 0.0 }),
354        }
355    }
356
357    pub fn is_open(&self) -> bool {
358        self.open
359    }
360
361    pub fn progress(&self) -> f32 {
362        self.progress.value.clamp(0.0, 1.0)
363    }
364
365    pub fn is_visible(&self) -> bool {
366        self.open || self.progress() > 0.0 || self.is_animating()
367    }
368
369    pub fn is_animating(&self) -> bool {
370        (self.progress.value - self.progress.to).abs() > 0.001
371    }
372
373    pub fn open(&mut self, now: Instant) {
374        self.open = true;
375        self.progress
376            .set_spring_target(1.0, now, navigation_rail_expansion_spring());
377    }
378
379    pub fn close(&mut self, now: Instant) {
380        self.open = false;
381        self.progress
382            .set_spring_target(0.0, now, navigation_rail_expansion_spring());
383    }
384
385    pub fn toggle(&mut self, now: Instant) {
386        if self.open {
387            self.close(now);
388        } else {
389            self.open(now);
390        }
391    }
392
393    pub fn advance(&mut self, now: Instant) -> bool {
394        let animating = self.progress.advance(now);
395        self.progress.value = self.progress.value.clamp(0.0, 1.0);
396        animating
397    }
398}
399
400fn navigation_rail_expansion_spring() -> tokens::motion::Spring {
401    tokens::motion::Spring {
402        damping_ratio: 1.0,
403        stiffness: tokens::motion::EXPRESSIVE_FAST_SPATIAL.stiffness,
404    }
405}
406
407#[derive(Debug, Clone, Copy)]
408struct TrackProgress {
409    selected_start: f32,
410    previous_start: f32,
411    progress: f32,
412}
413
414impl TrackProgress {
415    fn new(selected_start: f32, previous_start: f32, progress: f32) -> Self {
416        Self {
417            selected_start: selected_start.clamp(0.0, 1.0),
418            previous_start: previous_start.clamp(0.0, 1.0),
419            progress,
420        }
421    }
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub struct Destination<Id> {
426    pub id: Id,
427    pub icon: &'static str,
428    pub label: &'static str,
429    pub badge: Option<Badge>,
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub enum Badge {
434    Small,
435    Large(&'static str),
436}
437
438impl<Id> Destination<Id> {
439    pub const fn new(id: Id, icon: &'static str, label: &'static str) -> Self {
440        Self {
441            id,
442            icon,
443            label,
444            badge: None,
445        }
446    }
447
448    pub const fn small_badge(mut self) -> Self {
449        self.badge = Some(Badge::Small);
450        self
451    }
452
453    pub const fn badge(mut self, label: &'static str) -> Self {
454        self.badge = Some(Badge::Large(label));
455        self
456    }
457}
458
459pub fn width_class(width: f32) -> WindowWidthClass {
460    if width < tokens::component::adaptive_navigation::WIDTH_COMPACT_MAX {
461        WindowWidthClass::Compact
462    } else if width < tokens::component::adaptive_navigation::WIDTH_MEDIUM_MAX {
463        WindowWidthClass::Medium
464    } else {
465        WindowWidthClass::Expanded
466    }
467}
468
469pub fn height_class(height: f32) -> WindowHeightClass {
470    if height < tokens::component::adaptive_navigation::HEIGHT_COMPACT_MAX {
471        WindowHeightClass::Compact
472    } else if height < tokens::component::adaptive_navigation::HEIGHT_MEDIUM_MAX {
473        WindowHeightClass::Medium
474    } else {
475        WindowHeightClass::Expanded
476    }
477}
478
479pub fn adaptive_layout(width: f32, height: f32) -> AdaptiveLayout {
480    WindowSizeClass::from_size(width, height).adaptive_navigation_layout()
481}
482
483pub fn item_animation_duration_ms(layout: AdaptiveLayout) -> u16 {
484    layout.item_animation_duration_ms()
485}
486
487pub fn navigation_rail_min_height(destination_count: usize, has_header: bool) -> f32 {
488    let header_height = if has_header {
489        navigation_rail_header_slot_height()
490    } else {
491        0.0
492    };
493    let child_count = destination_count + usize::from(has_header);
494    let spacing_count = child_count.saturating_sub(1);
495
496    tokens::component::navigation_rail::CONTENT_TOP_MARGIN
497        + header_height
498        + destination_count as f32 * navigation_rail_item_slot_height()
499        + spacing_count as f32 * tokens::component::navigation_rail::VERTICAL_PADDING
500        + tokens::component::navigation_rail::VERTICAL_PADDING
501}
502
503/// Starts a builder for an adaptive navigation suite.
504///
505/// Use [`Suite::layout`] or [`Suite::window_size`] to select the adaptive
506/// layout, then call [`Suite::view`] or [`Suite::with_menu`].
507pub fn suite<'a, Id>(
508    destinations: &'a [Destination<Id>],
509    state: &'a NavigationState<Id>,
510) -> Suite<'a, Id> {
511    Suite::new(destinations, state)
512}
513
514/// A builder for a navigation bar/rail shell around page content.
515#[derive(Debug, Clone, Copy)]
516pub struct Suite<'a, Id> {
517    destinations: &'a [Destination<Id>],
518    state: &'a NavigationState<Id>,
519    layout: AdaptiveLayout,
520}
521
522impl<'a, Id> Suite<'a, Id> {
523    /// Creates a navigation suite builder.
524    ///
525    /// The default layout is [`AdaptiveLayout::NavigationRail`]. Call
526    /// [`layout`](Self::layout), [`window_size`](Self::window_size), or
527    /// [`dimensions`](Self::dimensions) for adaptive behavior.
528    pub fn new(destinations: &'a [Destination<Id>], state: &'a NavigationState<Id>) -> Self {
529        Self {
530            destinations,
531            state,
532            layout: AdaptiveLayout::NavigationRail,
533        }
534    }
535
536    /// Uses the provided adaptive layout.
537    pub fn layout(mut self, layout: AdaptiveLayout) -> Self {
538        self.layout = layout;
539        self
540    }
541
542    /// Chooses the adaptive layout for the provided window size.
543    pub fn window_size(mut self, size: Size) -> Self {
544        self.layout = adaptive_layout(size.width, size.height);
545        self
546    }
547
548    /// Chooses the adaptive layout for the provided dimensions.
549    pub fn dimensions(mut self, width: f32, height: f32) -> Self {
550        self.layout = adaptive_layout(width, height);
551        self
552    }
553
554    /// Adds a menu button and expandable rail behavior.
555    pub fn with_menu<Message>(
556        self,
557        headline: &'static str,
558        on_menu: Message,
559    ) -> SuiteWithMenu<'a, Id, Message> {
560        SuiteWithMenu {
561            suite: self,
562            headline,
563            on_menu,
564        }
565    }
566
567    /// Builds the navigation suite around the provided content.
568    pub fn view<Message, Renderer, F>(
569        self,
570        on_select: F,
571        content: impl Into<Element<'a, Message, Theme, Renderer>>,
572    ) -> Element<'a, Message, Theme, Renderer>
573    where
574        Id: Copy + Eq + 'a,
575        Message: Clone + 'a,
576        Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
577        Font: Into<Renderer::Font>,
578        F: Fn(Id) -> Message + Clone + 'a,
579    {
580        navigation_suite_for_layout(
581            self.layout,
582            self.destinations,
583            self.state.selection(),
584            on_select,
585            content,
586        )
587    }
588}
589
590/// A navigation suite builder with menu behavior enabled.
591#[derive(Debug, Clone, Copy)]
592pub struct SuiteWithMenu<'a, Id, Message> {
593    suite: Suite<'a, Id>,
594    headline: &'static str,
595    on_menu: Message,
596}
597
598impl<'a, Id, Message> SuiteWithMenu<'a, Id, Message> {
599    /// Uses the provided adaptive layout.
600    pub fn layout(mut self, layout: AdaptiveLayout) -> Self {
601        self.suite = self.suite.layout(layout);
602        self
603    }
604
605    /// Chooses the adaptive layout for the provided window size.
606    pub fn window_size(mut self, size: Size) -> Self {
607        self.suite = self.suite.window_size(size);
608        self
609    }
610
611    /// Chooses the adaptive layout for the provided dimensions.
612    pub fn dimensions(mut self, width: f32, height: f32) -> Self {
613        self.suite = self.suite.dimensions(width, height);
614        self
615    }
616
617    /// Builds the navigation suite around the provided content.
618    pub fn view<Renderer, F>(
619        self,
620        on_select: F,
621        content: impl Into<Element<'a, Message, Theme, Renderer>>,
622    ) -> Element<'a, Message, Theme, Renderer>
623    where
624        Id: Copy + Eq + 'a,
625        Message: Clone + 'a,
626        Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
627        Font: Into<Renderer::Font>,
628        F: Fn(Id) -> Message + Clone + 'a,
629    {
630        navigation_suite_for_layout_with_menu(
631            self.headline,
632            self.suite.layout,
633            self.suite.destinations,
634            self.suite.state,
635            on_select,
636            self.on_menu,
637            content,
638        )
639    }
640}
641
642pub fn navigation_suite<'a, Id, Message, Renderer, F>(
643    width: f32,
644    height: f32,
645    destinations: &'a [Destination<Id>],
646    selection: Selection<Id>,
647    on_select: F,
648    content: impl Into<Element<'a, Message, Theme, Renderer>>,
649) -> Element<'a, Message, Theme, Renderer>
650where
651    Id: Copy + Eq + 'a,
652    Message: Clone + 'a,
653    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
654    Font: Into<Renderer::Font>,
655    F: Fn(Id) -> Message + Clone + 'a,
656{
657    navigation_suite_for_layout(
658        adaptive_layout(width, height),
659        destinations,
660        selection,
661        on_select,
662        content,
663    )
664}
665
666pub fn navigation_suite_for_layout<'a, Id, Message, Renderer, F>(
667    layout: AdaptiveLayout,
668    destinations: &'a [Destination<Id>],
669    selection: Selection<Id>,
670    on_select: F,
671    content: impl Into<Element<'a, Message, Theme, Renderer>>,
672) -> Element<'a, Message, Theme, Renderer>
673where
674    Id: Copy + Eq + 'a,
675    Message: Clone + 'a,
676    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
677    Font: Into<Renderer::Font>,
678    F: Fn(Id) -> Message + Clone + 'a,
679{
680    let content = content.into();
681
682    match layout {
683        AdaptiveLayout::NavigationBar => Column::new()
684            .width(Length::Fill)
685            .height(Length::Fill)
686            .push(content)
687            .push(navigation_bar(destinations, selection, on_select))
688            .into(),
689        AdaptiveLayout::NavigationRail => Row::new()
690            .width(Length::Fill)
691            .height(Length::Fill)
692            .push(navigation_rail(destinations, selection, on_select))
693            .push(content)
694            .into(),
695    }
696}
697
698pub fn navigation_suite_with_menu<'a, Id, Message, Renderer, F>(
699    headline: &'static str,
700    width: f32,
701    height: f32,
702    destinations: &'a [Destination<Id>],
703    state: &NavigationState<Id>,
704    on_select: F,
705    on_menu: Message,
706    content: impl Into<Element<'a, Message, Theme, Renderer>>,
707) -> Element<'a, Message, Theme, Renderer>
708where
709    Id: Copy + Eq + 'a,
710    Message: Clone + 'a,
711    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
712    Font: Into<Renderer::Font>,
713    F: Fn(Id) -> Message + Clone + 'a,
714{
715    navigation_suite_for_layout_with_menu(
716        headline,
717        adaptive_layout(width, height),
718        destinations,
719        state,
720        on_select,
721        on_menu,
722        content,
723    )
724}
725
726pub fn navigation_suite_for_layout_with_menu<'a, Id, Message, Renderer, F>(
727    headline: &'static str,
728    layout: AdaptiveLayout,
729    destinations: &'a [Destination<Id>],
730    state: &NavigationState<Id>,
731    on_select: F,
732    on_menu: Message,
733    content: impl Into<Element<'a, Message, Theme, Renderer>>,
734) -> Element<'a, Message, Theme, Renderer>
735where
736    Id: Copy + Eq + 'a,
737    Message: Clone + 'a,
738    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
739    Font: Into<Renderer::Font>,
740    F: Fn(Id) -> Message + Clone + 'a,
741{
742    let menu_progress = state.menu_progress();
743    let content = content.into();
744    let selection = state.selection();
745
746    match layout {
747        AdaptiveLayout::NavigationBar => Column::new()
748            .width(Length::Fill)
749            .height(Length::Fill)
750            .push(content)
751            .push(navigation_bar(destinations, selection, on_select))
752            .into(),
753        AdaptiveLayout::NavigationRail => Row::new()
754            .width(Length::Fill)
755            .height(Length::Fill)
756            .push(if state.is_menu_visible() {
757                navigation_rail_expanded_with_menu_at_width(
758                    headline,
759                    destinations,
760                    selection,
761                    on_select,
762                    on_menu,
763                    navigation_rail_expanded_width_for_progress(menu_progress),
764                )
765            } else {
766                navigation_rail_with_menu_at_progress(
767                    destinations,
768                    selection,
769                    on_select,
770                    on_menu,
771                    menu_progress,
772                )
773            })
774            .push(content)
775            .into(),
776    }
777}
778
779pub fn navigation_bar<'a, Id, Message, Renderer, F>(
780    destinations: &'a [Destination<Id>],
781    selection: Selection<Id>,
782    on_select: F,
783) -> Container<'a, Message, Theme, Renderer>
784where
785    Id: Copy + Eq + 'a,
786    Message: Clone + 'a,
787    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
788    Font: Into<Renderer::Font>,
789    F: Fn(Id) -> Message + Clone + 'a,
790{
791    let mut items = Row::new()
792        .width(Length::Fill)
793        .height(Length::Fixed(
794            tokens::component::navigation_bar::CONTAINER_HEIGHT,
795        ))
796        .spacing(tokens::component::navigation_bar::ITEM_HORIZONTAL_PADDING);
797
798    for destination in destinations {
799        items = items.push(
800            Container::new(navigation_bar_item(
801                *destination,
802                selection,
803                on_select.clone(),
804            ))
805            .width(Length::FillPortion(1)),
806        );
807    }
808
809    Container::new(items)
810        .width(Length::Fill)
811        .height(Length::Fixed(
812            tokens::component::navigation_bar::CONTAINER_HEIGHT,
813        ))
814        .padding(Padding {
815            top: 0.0,
816            right: tokens::component::navigation_bar::ITEM_HORIZONTAL_PADDING,
817            bottom: 0.0,
818            left: tokens::component::navigation_bar::ITEM_HORIZONTAL_PADDING,
819        })
820        .style(navigation_bar_container)
821}
822
823pub fn navigation_rail<'a, Id, Message, Renderer, F>(
824    destinations: &'a [Destination<Id>],
825    selection: Selection<Id>,
826    on_select: F,
827) -> Container<'a, Message, Theme, Renderer>
828where
829    Id: Copy + Eq + 'a,
830    Message: Clone + 'a,
831    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
832    Font: Into<Renderer::Font>,
833    F: Fn(Id) -> Message + Clone + 'a,
834{
835    navigation_rail_with_optional_header(destinations, selection, on_select, None)
836}
837
838pub fn navigation_rail_fitting_content<'a, Id, Message, Renderer, F>(
839    destinations: &'a [Destination<Id>],
840    selection: Selection<Id>,
841    on_select: F,
842) -> Container<'a, Message, Theme, Renderer>
843where
844    Id: Copy + Eq + 'a,
845    Message: Clone + 'a,
846    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
847    Font: Into<Renderer::Font>,
848    F: Fn(Id) -> Message + Clone + 'a,
849{
850    navigation_rail(destinations, selection, on_select).height(Length::Fixed(
851        navigation_rail_min_height(destinations.len(), false),
852    ))
853}
854
855pub fn navigation_rail_with_header<'a, Id, Message, Renderer, F>(
856    destinations: &'a [Destination<Id>],
857    selection: Selection<Id>,
858    on_select: F,
859    header: impl Into<Element<'a, Message, Theme, Renderer>>,
860) -> Container<'a, Message, Theme, Renderer>
861where
862    Id: Copy + Eq + 'a,
863    Message: Clone + 'a,
864    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
865    Font: Into<Renderer::Font>,
866    F: Fn(Id) -> Message + Clone + 'a,
867{
868    navigation_rail_with_optional_header(destinations, selection, on_select, Some(header.into()))
869}
870
871pub fn navigation_rail_with_header_fitting_content<'a, Id, Message, Renderer, F>(
872    destinations: &'a [Destination<Id>],
873    selection: Selection<Id>,
874    on_select: F,
875    header: impl Into<Element<'a, Message, Theme, Renderer>>,
876) -> Container<'a, Message, Theme, Renderer>
877where
878    Id: Copy + Eq + 'a,
879    Message: Clone + 'a,
880    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
881    Font: Into<Renderer::Font>,
882    F: Fn(Id) -> Message + Clone + 'a,
883{
884    navigation_rail_with_header(destinations, selection, on_select, header).height(Length::Fixed(
885        navigation_rail_min_height(destinations.len(), true),
886    ))
887}
888
889pub fn navigation_rail_with_menu<'a, Id, Message, Renderer, F>(
890    destinations: &'a [Destination<Id>],
891    selection: Selection<Id>,
892    on_select: F,
893    on_menu: Message,
894) -> Container<'a, Message, Theme, Renderer>
895where
896    Id: Copy + Eq + 'a,
897    Message: Clone + 'a,
898    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
899    Font: Into<Renderer::Font>,
900    F: Fn(Id) -> Message + Clone + 'a,
901{
902    navigation_rail_with_header(
903        destinations,
904        selection,
905        on_select,
906        navigation_menu_button(on_menu, 0.0),
907    )
908}
909
910fn navigation_rail_with_menu_at_progress<'a, Id, Message, Renderer, F>(
911    destinations: &'a [Destination<Id>],
912    selection: Selection<Id>,
913    on_select: F,
914    on_menu: Message,
915    menu_progress: f32,
916) -> Container<'a, Message, Theme, Renderer>
917where
918    Id: Copy + Eq + 'a,
919    Message: Clone + 'a,
920    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
921    Font: Into<Renderer::Font>,
922    F: Fn(Id) -> Message + Clone + 'a,
923{
924    navigation_rail_with_header(
925        destinations,
926        selection,
927        on_select,
928        navigation_menu_button(on_menu, menu_progress),
929    )
930}
931
932pub fn navigation_rail_with_menu_fitting_content<'a, Id, Message, Renderer, F>(
933    destinations: &'a [Destination<Id>],
934    selection: Selection<Id>,
935    on_select: F,
936    on_menu: Message,
937) -> Container<'a, Message, Theme, Renderer>
938where
939    Id: Copy + Eq + 'a,
940    Message: Clone + 'a,
941    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
942    Font: Into<Renderer::Font>,
943    F: Fn(Id) -> Message + Clone + 'a,
944{
945    navigation_rail_with_menu(destinations, selection, on_select, on_menu).height(Length::Fixed(
946        navigation_rail_min_height(destinations.len(), true),
947    ))
948}
949
950pub fn navigation_rail_expanded_with_menu<'a, Id, Message, Renderer, F>(
951    headline: &'static str,
952    destinations: &'a [Destination<Id>],
953    selection: Selection<Id>,
954    on_select: F,
955    on_menu: Message,
956) -> Container<'a, Message, Theme, Renderer>
957where
958    Id: Copy + Eq + 'a,
959    Message: Clone + 'a,
960    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
961    Font: Into<Renderer::Font>,
962    F: Fn(Id) -> Message + Clone + 'a,
963{
964    navigation_rail_expanded_with_menu_at_width(
965        headline,
966        destinations,
967        selection,
968        on_select,
969        on_menu,
970        tokens::component::navigation_rail::EXPANDED_CONTAINER_WIDTH,
971    )
972}
973
974pub fn navigation_rail_expanded_with_menu_fitting_content<'a, Id, Message, Renderer, F>(
975    headline: &'static str,
976    destinations: &'a [Destination<Id>],
977    selection: Selection<Id>,
978    on_select: F,
979    on_menu: Message,
980) -> Container<'a, Message, Theme, Renderer>
981where
982    Id: Copy + Eq + 'a,
983    Message: Clone + 'a,
984    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
985    Font: Into<Renderer::Font>,
986    F: Fn(Id) -> Message + Clone + 'a,
987{
988    navigation_rail_expanded_with_menu(headline, destinations, selection, on_select, on_menu)
989        .height(Length::Fixed(navigation_rail_min_height(
990            destinations.len(),
991            true,
992        )))
993}
994
995pub fn navigation_rail_expanded_with_menu_at_width<'a, Id, Message, Renderer, F>(
996    headline: &'static str,
997    destinations: &'a [Destination<Id>],
998    selection: Selection<Id>,
999    on_select: F,
1000    on_menu: Message,
1001    width: f32,
1002) -> Container<'a, Message, Theme, Renderer>
1003where
1004    Id: Copy + Eq + 'a,
1005    Message: Clone + 'a,
1006    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1007    Font: Into<Renderer::Font>,
1008    F: Fn(Id) -> Message + Clone + 'a,
1009{
1010    let width = navigation_rail_expanded_container_width(width);
1011    let indicator_width = navigation_rail_expanded_indicator_width(width);
1012    let expansion_progress = navigation_rail_expanded_progress_for_width(width);
1013    let label_alpha = navigation_rail_expanded_label_alpha_for_width(width);
1014    let mut items = Column::new()
1015        .width(Length::Fixed(width))
1016        .height(Length::Fill)
1017        .spacing(tokens::component::navigation_rail::VERTICAL_PADDING)
1018        .align_x(alignment::Horizontal::Center)
1019        .push(navigation_rail_expanded_header(
1020            headline,
1021            on_menu,
1022            expansion_progress,
1023            label_alpha,
1024        ));
1025
1026    for destination in destinations {
1027        items = items.push(navigation_rail_expanded_item(
1028            *destination,
1029            selection,
1030            on_select.clone(),
1031            indicator_width,
1032            expansion_progress,
1033            label_alpha,
1034        ));
1035    }
1036
1037    Container::new(items)
1038        .width(Length::Fixed(width))
1039        .height(Length::Fill)
1040        .padding(Padding {
1041            top: tokens::component::navigation_rail::CONTENT_TOP_MARGIN,
1042            right: 0.0,
1043            bottom: tokens::component::navigation_rail::VERTICAL_PADDING,
1044            left: 0.0,
1045        })
1046        .style(navigation_rail_container)
1047}
1048
1049fn navigation_rail_with_optional_header<'a, Id, Message, Renderer, F>(
1050    destinations: &'a [Destination<Id>],
1051    selection: Selection<Id>,
1052    on_select: F,
1053    header: Option<Element<'a, Message, Theme, Renderer>>,
1054) -> Container<'a, Message, Theme, Renderer>
1055where
1056    Id: Copy + Eq + 'a,
1057    Message: Clone + 'a,
1058    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1059    Font: Into<Renderer::Font>,
1060    F: Fn(Id) -> Message + Clone + 'a,
1061{
1062    let mut items = Column::new()
1063        .width(Length::Fixed(
1064            tokens::component::navigation_rail::CONTAINER_WIDTH,
1065        ))
1066        .height(Length::Fill)
1067        .spacing(tokens::component::navigation_rail::VERTICAL_PADDING)
1068        .align_x(alignment::Horizontal::Center);
1069
1070    if let Some(header) = header {
1071        items = items.push(navigation_rail_header(header));
1072    }
1073
1074    for destination in destinations {
1075        items = items.push(navigation_rail_item(
1076            *destination,
1077            selection,
1078            on_select.clone(),
1079        ));
1080    }
1081
1082    Container::new(items)
1083        .width(Length::Fixed(
1084            tokens::component::navigation_rail::CONTAINER_WIDTH,
1085        ))
1086        .height(Length::Fill)
1087        .padding(Padding {
1088            top: tokens::component::navigation_rail::CONTENT_TOP_MARGIN,
1089            right: 0.0,
1090            bottom: tokens::component::navigation_rail::VERTICAL_PADDING,
1091            left: 0.0,
1092        })
1093        .style(navigation_rail_container)
1094}
1095
1096pub fn navigation_drawer<'a, Id, Message, Renderer, F>(
1097    headline: &'static str,
1098    destinations: &'a [Destination<Id>],
1099    selection: Selection<Id>,
1100    on_select: F,
1101) -> Container<'a, Message, Theme, Renderer>
1102where
1103    Id: Copy + Eq + 'a,
1104    Message: Clone + 'a,
1105    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1106    Font: Into<Renderer::Font>,
1107    F: Fn(Id) -> Message + Clone + 'a,
1108{
1109    navigation_drawer_at_width(
1110        headline,
1111        destinations,
1112        selection,
1113        on_select,
1114        tokens::component::navigation_drawer::CONTAINER_WIDTH,
1115    )
1116}
1117
1118pub fn navigation_drawer_at_width<'a, Id, Message, Renderer, F>(
1119    headline: &'static str,
1120    destinations: &'a [Destination<Id>],
1121    selection: Selection<Id>,
1122    on_select: F,
1123    width: f32,
1124) -> Container<'a, Message, Theme, Renderer>
1125where
1126    Id: Copy + Eq + 'a,
1127    Message: Clone + 'a,
1128    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1129    Font: Into<Renderer::Font>,
1130    F: Fn(Id) -> Message + Clone + 'a,
1131{
1132    navigation_drawer_with_optional_header(
1133        headline,
1134        destinations,
1135        selection,
1136        on_select,
1137        width,
1138        None,
1139    )
1140}
1141
1142pub fn navigation_drawer_with_menu<'a, Id, Message, Renderer, F>(
1143    headline: &'static str,
1144    destinations: &'a [Destination<Id>],
1145    selection: Selection<Id>,
1146    on_select: F,
1147    on_menu: Message,
1148) -> Container<'a, Message, Theme, Renderer>
1149where
1150    Id: Copy + Eq + 'a,
1151    Message: Clone + 'a,
1152    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1153    Font: Into<Renderer::Font>,
1154    F: Fn(Id) -> Message + Clone + 'a,
1155{
1156    navigation_drawer_with_menu_at_width(
1157        headline,
1158        destinations,
1159        selection,
1160        on_select,
1161        on_menu,
1162        tokens::component::navigation_drawer::CONTAINER_WIDTH,
1163    )
1164}
1165
1166pub fn navigation_drawer_with_menu_at_width<'a, Id, Message, Renderer, F>(
1167    headline: &'static str,
1168    destinations: &'a [Destination<Id>],
1169    selection: Selection<Id>,
1170    on_select: F,
1171    on_menu: Message,
1172    width: f32,
1173) -> Container<'a, Message, Theme, Renderer>
1174where
1175    Id: Copy + Eq + 'a,
1176    Message: Clone + 'a,
1177    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1178    Font: Into<Renderer::Font>,
1179    F: Fn(Id) -> Message + Clone + 'a,
1180{
1181    navigation_drawer_with_optional_header(
1182        headline,
1183        destinations,
1184        selection,
1185        on_select,
1186        width,
1187        Some(navigation_drawer_menu_header(headline, on_menu).into()),
1188    )
1189}
1190
1191fn navigation_drawer_with_optional_header<'a, Id, Message, Renderer, F>(
1192    headline: &'static str,
1193    destinations: &'a [Destination<Id>],
1194    selection: Selection<Id>,
1195    on_select: F,
1196    width: f32,
1197    header: Option<Element<'a, Message, Theme, Renderer>>,
1198) -> Container<'a, Message, Theme, Renderer>
1199where
1200    Id: Copy + Eq + 'a,
1201    Message: Clone + 'a,
1202    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1203    Font: Into<Renderer::Font>,
1204    F: Fn(Id) -> Message + Clone + 'a,
1205{
1206    let container_width = navigation_drawer_container_width(width);
1207    let indicator_width = navigation_drawer_indicator_width(container_width);
1208    let headline_scale = tokens::component::navigation_drawer::HEADLINE_TEXT;
1209    let mut items = Column::new()
1210        .width(Length::Fixed(container_width))
1211        .height(Length::Fill)
1212        .spacing(0);
1213
1214    if let Some(header) = header {
1215        items = items.push(header);
1216    } else {
1217        items = items.push(
1218            Container::new(type_text(headline, headline_scale).style(headline_text_style))
1219                .height(Length::Fixed(
1220                    tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1221                ))
1222                .padding(Padding {
1223                    top: 0.0,
1224                    right: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
1225                        + tokens::component::navigation_drawer::ITEM_CONTENT_TRAILING_SPACE,
1226                    bottom: 0.0,
1227                    left: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
1228                        + tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE,
1229                })
1230                .align_y(alignment::Vertical::Center),
1231        );
1232    }
1233
1234    for destination in destinations {
1235        items = items.push(navigation_drawer_item(
1236            *destination,
1237            selection,
1238            on_select.clone(),
1239            indicator_width,
1240        ));
1241    }
1242
1243    Container::new(items)
1244        .width(Length::Fixed(container_width))
1245        .height(Length::Fill)
1246        .padding(Padding {
1247            top: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING,
1248            right: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING,
1249            bottom: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING,
1250            left: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING,
1251        })
1252        .style(navigation_drawer_container)
1253}
1254
1255pub fn navigation_drawer_width_for_progress(progress: f32) -> f32 {
1256    let progress = progress.clamp(0.0, 1.0);
1257
1258    if progress <= f32::EPSILON {
1259        0.0
1260    } else {
1261        lerp(
1262            tokens::component::navigation_drawer::MINIMUM_CONTAINER_WIDTH,
1263            tokens::component::navigation_drawer::CONTAINER_WIDTH,
1264            progress,
1265        )
1266    }
1267}
1268
1269fn navigation_bar_item<'a, Id, Message, Renderer, F>(
1270    destination: Destination<Id>,
1271    selection: Selection<Id>,
1272    on_select: F,
1273) -> Element<'a, Message, Theme, Renderer>
1274where
1275    Id: Copy + Eq + 'a,
1276    Message: Clone + 'a,
1277    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1278    Font: Into<Renderer::Font>,
1279    F: Fn(Id) -> Message + Clone + 'a,
1280{
1281    let size_progress = selection.size_progress(destination.id);
1282    let alpha_progress = selection.alpha_progress(destination.id);
1283    let scale = tokens::component::navigation_bar::LABEL_TEXT;
1284    let message = on_select(destination.id);
1285    let indicator = indicator_icon_stack(
1286        destination.icon,
1287        tokens::component::navigation_bar::ICON_SIZE,
1288        tokens::component::navigation_bar::ACTIVE_INDICATOR_WIDTH,
1289        tokens::component::navigation_bar::ACTIVE_INDICATOR_HEIGHT,
1290        size_progress,
1291        alpha_progress,
1292        destination.badge,
1293        false,
1294    );
1295    let label = type_text(destination.label, scale).style(move |theme| text::Style {
1296        color: Some(bar_or_rail_label_color(theme, alpha_progress)),
1297    });
1298    let content = Column::new()
1299        .width(Length::Fill)
1300        .spacing(tokens::component::navigation_bar::INDICATOR_TO_LABEL_PADDING)
1301        .align_x(alignment::Horizontal::Center)
1302        .push(indicator)
1303        .push(label);
1304
1305    navigation_press_surface(
1306        Container::new(content)
1307            .width(Length::Fill)
1308            .height(Length::Fixed(
1309                tokens::component::navigation_bar::CONTAINER_HEIGHT,
1310            ))
1311            .padding(Padding {
1312                top: tokens::component::navigation_bar::INDICATOR_VERTICAL_OFFSET,
1313                right: 0.0,
1314                bottom: navigation_bar_item_bottom_padding(),
1315                left: 0.0,
1316            })
1317            .align_y(alignment::Vertical::Center),
1318        message,
1319        NavigationStateLayer::BarOrRail,
1320        NavigationIndicatorPlacement::TopCenter {
1321            top: tokens::component::navigation_bar::INDICATOR_VERTICAL_OFFSET,
1322            width: tokens::component::navigation_bar::ACTIVE_INDICATOR_WIDTH,
1323            height: tokens::component::navigation_bar::ACTIVE_INDICATOR_HEIGHT,
1324        },
1325    )
1326    .into()
1327}
1328
1329fn navigation_rail_item<'a, Id, Message, Renderer, F>(
1330    destination: Destination<Id>,
1331    selection: Selection<Id>,
1332    on_select: F,
1333) -> Element<'a, Message, Theme, Renderer>
1334where
1335    Id: Copy + Eq + 'a,
1336    Message: Clone + 'a,
1337    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1338    Font: Into<Renderer::Font>,
1339    F: Fn(Id) -> Message + Clone + 'a,
1340{
1341    let size_progress = selection.size_progress(destination.id);
1342    let alpha_progress = selection.alpha_progress(destination.id);
1343    let scale = tokens::component::navigation_rail::LABEL_TEXT;
1344    let message = on_select(destination.id);
1345    let indicator = indicator_icon_stack(
1346        destination.icon,
1347        tokens::component::navigation_rail::ICON_SIZE,
1348        tokens::component::navigation_rail::ACTIVE_INDICATOR_WIDTH,
1349        tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT,
1350        size_progress,
1351        alpha_progress,
1352        destination.badge,
1353        false,
1354    );
1355    let label = type_text(destination.label, scale).style(move |theme| text::Style {
1356        color: Some(bar_or_rail_label_color(theme, alpha_progress)),
1357    });
1358    let content = Column::new()
1359        .width(Length::Fixed(
1360            tokens::component::navigation_rail::ITEM_WIDTH,
1361        ))
1362        .spacing(tokens::component::navigation_rail::ITEM_VERTICAL_PADDING)
1363        .align_x(alignment::Horizontal::Center)
1364        .push(indicator)
1365        .push(label);
1366
1367    navigation_press_surface(
1368        Container::new(content)
1369            .width(Length::Fixed(
1370                tokens::component::navigation_rail::ITEM_WIDTH,
1371            ))
1372            .height(Length::Fixed(
1373                tokens::component::navigation_rail::ITEM_HEIGHT,
1374            ))
1375            .padding(Padding {
1376                top: navigation_rail_item_content_top_padding(),
1377                right: 0.0,
1378                bottom: 0.0,
1379                left: 0.0,
1380            }),
1381        message,
1382        NavigationStateLayer::BarOrRail,
1383        NavigationIndicatorPlacement::TopCenter {
1384            top: navigation_rail_item_content_top_padding(),
1385            width: tokens::component::navigation_rail::ACTIVE_INDICATOR_WIDTH,
1386            height: tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT,
1387        },
1388    )
1389    .into()
1390}
1391
1392fn navigation_rail_header<'a, Message, Renderer>(
1393    header: Element<'a, Message, Theme, Renderer>,
1394) -> Container<'a, Message, Theme, Renderer>
1395where
1396    Message: 'a,
1397    Renderer: geometry::Renderer + primitive::Renderer + 'a,
1398{
1399    Container::new(header)
1400        .width(Length::Fixed(
1401            tokens::component::navigation_rail::CONTAINER_WIDTH,
1402        ))
1403        .padding(Padding {
1404            top: 0.0,
1405            right: 0.0,
1406            bottom: navigation_rail_header_bottom_padding(),
1407            left: 0.0,
1408        })
1409        .align_x(alignment::Horizontal::Center)
1410}
1411
1412fn navigation_menu_button<'a, Message, Renderer>(
1413    on_press: Message,
1414    progress: f32,
1415) -> Button<'a, Message, Renderer>
1416where
1417    Message: Clone + 'a,
1418    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1419    Font: Into<Renderer::Font>,
1420{
1421    let icon = Canvas::new(NavigationMenuIcon { progress })
1422        .width(Length::Fixed(tokens::component::icon_button::ICON_SIZE))
1423        .height(Length::Fixed(tokens::component::icon_button::ICON_SIZE));
1424
1425    Button::new(
1426        Container::new(icon)
1427            .center_x(Length::Fixed(
1428                tokens::component::icon_button::CONTAINER_WIDTH,
1429            ))
1430            .center_y(Length::Fixed(
1431                tokens::component::icon_button::CONTAINER_HEIGHT,
1432            )),
1433    )
1434    .width(Length::Fixed(
1435        tokens::component::icon_button::CONTAINER_WIDTH,
1436    ))
1437    .height(Length::Fixed(
1438        tokens::component::icon_button::CONTAINER_HEIGHT,
1439    ))
1440    .padding(Padding::ZERO)
1441    .style(button_style::icon)
1442    .on_press(on_press)
1443}
1444
1445#[derive(Debug, Clone, Copy)]
1446struct NavigationMenuIcon {
1447    progress: f32,
1448}
1449
1450impl<Message, Renderer> canvas::Program<Message, Theme, Renderer> for NavigationMenuIcon
1451where
1452    Renderer: geometry::Renderer,
1453{
1454    type State = ();
1455
1456    fn draw(
1457        &self,
1458        _state: &Self::State,
1459        renderer: &Renderer,
1460        theme: &Theme,
1461        bounds: Rectangle,
1462        _cursor: mouse::Cursor,
1463    ) -> Vec<canvas::Geometry<Renderer>> {
1464        let size = bounds.width.min(bounds.height);
1465
1466        if size <= 0.0 {
1467            return Vec::new();
1468        }
1469
1470        let mut frame = canvas::Frame::new(renderer, bounds.size());
1471        let offset = Vector::new((bounds.width - size) / 2.0, (bounds.height - size) / 2.0);
1472        let center = Point::new(bounds.width / 2.0, bounds.height / 2.0);
1473        let stroke = Stroke::default()
1474            .with_width(navigation_menu_icon_stroke_width(size))
1475            .with_color(theme.colors().surface.text_variant)
1476            .with_line_cap(LineCap::Round);
1477
1478        frame.with_save(|frame| {
1479            frame.translate(Vector::new(center.x, center.y));
1480            frame.rotate(navigation_menu_icon_rotation_radians(self.progress));
1481            frame.translate(Vector::new(-center.x, -center.y));
1482
1483            for (from, to) in navigation_menu_icon_segments(self.progress, size) {
1484                frame.stroke(
1485                    &Path::line(
1486                        Point::new(from.x + offset.x, from.y + offset.y),
1487                        Point::new(to.x + offset.x, to.y + offset.y),
1488                    ),
1489                    stroke,
1490                );
1491            }
1492        });
1493
1494        vec![frame.into_geometry()]
1495    }
1496}
1497
1498fn navigation_menu_icon_rotation_radians(progress: f32) -> f32 {
1499    PI * progress.clamp(0.0, 1.0)
1500}
1501
1502fn navigation_menu_icon_segments(progress: f32, size: f32) -> [(Point, Point); 3] {
1503    let progress = progress.clamp(0.0, 1.0);
1504
1505    [
1506        (
1507            navigation_menu_icon_point(
1508                lerp(
1509                    NAVIGATION_MENU_ICON_START_X,
1510                    NAVIGATION_MENU_ICON_CENTER_X,
1511                    progress,
1512                ),
1513                lerp(
1514                    NAVIGATION_MENU_ICON_TOP_Y,
1515                    NAVIGATION_MENU_ICON_ARROW_TOP_Y,
1516                    progress,
1517                ),
1518                size,
1519            ),
1520            navigation_menu_icon_point(
1521                NAVIGATION_MENU_ICON_END_X,
1522                lerp(
1523                    NAVIGATION_MENU_ICON_TOP_Y,
1524                    NAVIGATION_MENU_ICON_CENTER_Y,
1525                    progress,
1526                ),
1527                size,
1528            ),
1529        ),
1530        (
1531            navigation_menu_icon_point(
1532                NAVIGATION_MENU_ICON_START_X,
1533                NAVIGATION_MENU_ICON_CENTER_Y,
1534                size,
1535            ),
1536            navigation_menu_icon_point(
1537                NAVIGATION_MENU_ICON_END_X,
1538                NAVIGATION_MENU_ICON_CENTER_Y,
1539                size,
1540            ),
1541        ),
1542        (
1543            navigation_menu_icon_point(
1544                lerp(
1545                    NAVIGATION_MENU_ICON_START_X,
1546                    NAVIGATION_MENU_ICON_CENTER_X,
1547                    progress,
1548                ),
1549                lerp(
1550                    NAVIGATION_MENU_ICON_BOTTOM_Y,
1551                    NAVIGATION_MENU_ICON_ARROW_BOTTOM_Y,
1552                    progress,
1553                ),
1554                size,
1555            ),
1556            navigation_menu_icon_point(
1557                NAVIGATION_MENU_ICON_END_X,
1558                lerp(
1559                    NAVIGATION_MENU_ICON_BOTTOM_Y,
1560                    NAVIGATION_MENU_ICON_CENTER_Y,
1561                    progress,
1562                ),
1563                size,
1564            ),
1565        ),
1566    ]
1567}
1568
1569fn navigation_menu_icon_point(x: f32, y: f32, size: f32) -> Point {
1570    Point::new(
1571        x / NAVIGATION_MENU_ICON_VIEWPORT_SIZE * size,
1572        y / NAVIGATION_MENU_ICON_VIEWPORT_SIZE * size,
1573    )
1574}
1575
1576fn navigation_menu_icon_stroke_width(size: f32) -> f32 {
1577    NAVIGATION_MENU_ICON_STROKE_WIDTH / NAVIGATION_MENU_ICON_VIEWPORT_SIZE * size
1578}
1579
1580fn navigation_rail_expanded_header<'a, Message, Renderer>(
1581    headline: &'static str,
1582    on_menu: Message,
1583    menu_progress: f32,
1584    label_alpha: f32,
1585) -> Container<'a, Message, Theme, Renderer>
1586where
1587    Message: Clone + 'a,
1588    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1589    Font: Into<Renderer::Font>,
1590{
1591    let headline_scale = tokens::component::navigation_drawer::HEADLINE_TEXT;
1592    let headline = type_text(headline, headline_scale).style(move |theme| text::Style {
1593        color: Some(alpha_color(
1594            theme.colors().surface.text_variant,
1595            label_alpha,
1596        )),
1597    });
1598    let content = Row::new()
1599        .height(Length::Fixed(
1600            tokens::component::icon_button::CONTAINER_HEIGHT,
1601        ))
1602        .spacing(navigation_rail_expanded_header_title_spacing())
1603        .align_y(alignment::Vertical::Center)
1604        .push(navigation_menu_button(on_menu, menu_progress))
1605        .push(headline);
1606
1607    Container::new(content)
1608        .height(Length::Fixed(navigation_rail_header_slot_height()))
1609        .width(Length::Fill)
1610        .padding(Padding {
1611            top: 0.0,
1612            right: tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL,
1613            bottom: navigation_rail_header_bottom_padding(),
1614            left: navigation_rail_expanded_header_leading_space(),
1615        })
1616        .align_y(alignment::Vertical::Center)
1617}
1618
1619fn navigation_rail_expanded_item<'a, Id, Message, Renderer, F>(
1620    destination: Destination<Id>,
1621    selection: Selection<Id>,
1622    on_select: F,
1623    indicator_width: f32,
1624    expansion_progress: f32,
1625    label_alpha: f32,
1626) -> Element<'a, Message, Theme, Renderer>
1627where
1628    Id: Copy + Eq + 'a,
1629    Message: Clone + 'a,
1630    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1631    Font: Into<Renderer::Font>,
1632    F: Fn(Id) -> Message + Clone + 'a,
1633{
1634    let size_progress = selection.size_progress(destination.id);
1635    let alpha_progress = selection.alpha_progress(destination.id);
1636    let indicator_height =
1637        navigation_rail_expanded_indicator_height_for_progress(expansion_progress);
1638    let vertical_inset =
1639        navigation_rail_expanded_item_vertical_inset_for_progress(expansion_progress);
1640    let scale = tokens::component::navigation_drawer::LABEL_TEXT;
1641    let message = on_select(destination.id);
1642    let badge_on_icon = navigation_rail_expanded_badge_uses_icon_anchor(label_alpha);
1643    let trailing_badge_alpha = navigation_rail_expanded_trailing_badge_alpha(label_alpha);
1644    let collapsed_label_alpha = navigation_rail_expanded_collapsed_label_alpha(label_alpha);
1645    let icon = navigation_rail_expanded_icon_layer(
1646        destination.icon,
1647        alpha_progress,
1648        indicator_height,
1649        badge_on_icon.then_some(destination.badge).flatten(),
1650    );
1651    let label = type_text(destination.label, scale).style(move |theme| text::Style {
1652        color: Some(alpha_color(
1653            drawer_content_color(theme, alpha_progress),
1654            label_alpha,
1655        )),
1656    });
1657    let content = Row::new()
1658        .width(Length::Fill)
1659        .height(Length::Fixed(indicator_height))
1660        .align_y(alignment::Vertical::Center)
1661        .push(Container::new(label).width(Length::Fill));
1662    let content = if let Some(badge) = destination.badge.filter(|_| !badge_on_icon) {
1663        content
1664            .push(Space::new().width(Length::Fixed(navigation_drawer_badge_space())))
1665            .push(destination_badge_with_alpha::<Message, Renderer>(
1666                badge,
1667                trailing_badge_alpha,
1668            ))
1669    } else {
1670        content
1671    };
1672    let content = Container::new(content)
1673        .width(Length::Fixed(indicator_width))
1674        .height(Length::Fixed(indicator_height))
1675        .padding(Padding {
1676            top: 0.0,
1677            right: tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_END,
1678            bottom: 0.0,
1679            left: navigation_rail_expanded_label_leading_padding(),
1680        })
1681        .align_y(alignment::Vertical::Center);
1682    let expanded_indicator = Stack::new()
1683        .width(Length::Fixed(indicator_width))
1684        .height(Length::Fixed(indicator_height))
1685        .push(
1686            Space::new()
1687                .width(Length::Fixed(indicator_width))
1688                .height(Length::Fixed(indicator_height)),
1689        )
1690        .push(indicator_layer(
1691            indicator_width,
1692            indicator_height,
1693            size_progress,
1694            alpha_progress,
1695        ))
1696        .push(content)
1697        .push(icon);
1698    let collapsed_label = navigation_rail_expanded_collapsed_label::<Message, Renderer>(
1699        destination.label,
1700        alpha_progress,
1701        collapsed_label_alpha,
1702    )
1703    .width(Length::Fixed(navigation_rail_collapsed_label_width()))
1704    .height(Length::Fixed(navigation_rail_item_slot_height()));
1705    let item = Stack::new()
1706        .width(Length::Fixed(indicator_width))
1707        .height(Length::Fixed(navigation_rail_item_slot_height()))
1708        .push(
1709            Container::new(expanded_indicator)
1710                .width(Length::Fixed(indicator_width))
1711                .height(Length::Fixed(navigation_rail_item_slot_height()))
1712                .padding(Padding {
1713                    top: vertical_inset,
1714                    right: 0.0,
1715                    bottom: vertical_inset,
1716                    left: 0.0,
1717                })
1718                .align_y(alignment::Vertical::Top),
1719        )
1720        .push(collapsed_label);
1721
1722    navigation_press_surface(
1723        Container::new(item)
1724            .width(Length::Fixed(indicator_width))
1725            .height(Length::Fixed(navigation_rail_item_slot_height())),
1726        message,
1727        NavigationStateLayer::Drawer {
1728            progress: alpha_progress,
1729        },
1730        NavigationIndicatorPlacement::Inset {
1731            x: 0.0,
1732            y: vertical_inset,
1733            width: indicator_width,
1734            height: indicator_height,
1735        },
1736    )
1737    .into()
1738}
1739
1740fn navigation_drawer_menu_header<'a, Message, Renderer>(
1741    headline: &'static str,
1742    on_menu: Message,
1743) -> Container<'a, Message, Theme, Renderer>
1744where
1745    Message: Clone + 'a,
1746    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1747    Font: Into<Renderer::Font>,
1748{
1749    let headline_scale = tokens::component::navigation_drawer::HEADLINE_TEXT;
1750    let content = Row::new()
1751        .height(Length::Fixed(
1752            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1753        ))
1754        .spacing(navigation_drawer_menu_header_title_spacing())
1755        .align_y(alignment::Vertical::Center)
1756        .push(navigation_menu_button(on_menu, 0.0))
1757        .push(type_text(headline, headline_scale).style(headline_text_style));
1758
1759    Container::new(content)
1760        .height(Length::Fixed(
1761            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1762        ))
1763        .padding(Padding {
1764            top: 0.0,
1765            right: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
1766                + tokens::component::navigation_drawer::ITEM_CONTENT_TRAILING_SPACE,
1767            bottom: 0.0,
1768            left: navigation_drawer_menu_header_leading_space(),
1769        })
1770        .align_y(alignment::Vertical::Center)
1771}
1772
1773fn navigation_drawer_item<'a, Id, Message, Renderer, F>(
1774    destination: Destination<Id>,
1775    selection: Selection<Id>,
1776    on_select: F,
1777    indicator_width: f32,
1778) -> Element<'a, Message, Theme, Renderer>
1779where
1780    Id: Copy + Eq + 'a,
1781    Message: Clone + 'a,
1782    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1783    Font: Into<Renderer::Font>,
1784    F: Fn(Id) -> Message + Clone + 'a,
1785{
1786    let size_progress = selection.size_progress(destination.id);
1787    let alpha_progress = selection.alpha_progress(destination.id);
1788    let scale = tokens::component::navigation_drawer::LABEL_TEXT;
1789    let message = on_select(destination.id);
1790    let icon = destination_icon::<Message, Renderer>(
1791        destination.icon,
1792        tokens::component::navigation_drawer::ICON_SIZE,
1793        alpha_progress,
1794        true,
1795    );
1796    let label = type_text(destination.label, scale).style(move |theme| text::Style {
1797        color: Some(drawer_content_color(theme, alpha_progress)),
1798    });
1799    let content = Row::new()
1800        .width(Length::Fill)
1801        .height(Length::Fixed(
1802            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1803        ))
1804        .spacing(tokens::component::navigation_drawer::ICON_LABEL_SPACE)
1805        .align_y(alignment::Vertical::Center)
1806        .push(icon)
1807        .push(Container::new(label).width(Length::Fill));
1808    let content = if let Some(badge) = destination.badge {
1809        content
1810            .push(Space::new().width(Length::Fixed(navigation_drawer_badge_space())))
1811            .push(destination_badge::<Message, Renderer>(badge))
1812    } else {
1813        content
1814    };
1815    let content = Container::new(content)
1816        .width(Length::Fixed(indicator_width))
1817        .height(Length::Fixed(
1818            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1819        ))
1820        .padding(Padding {
1821            top: 0.0,
1822            right: tokens::component::navigation_drawer::ITEM_CONTENT_TRAILING_SPACE,
1823            bottom: 0.0,
1824            left: tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE,
1825        })
1826        .align_y(alignment::Vertical::Center);
1827    let indicator = Stack::new()
1828        .width(Length::Fixed(indicator_width))
1829        .height(Length::Fixed(
1830            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1831        ))
1832        .push(
1833            Space::new()
1834                .width(Length::Fixed(indicator_width))
1835                .height(Length::Fixed(
1836                    tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1837                )),
1838        )
1839        .push(indicator_layer(
1840            indicator_width,
1841            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1842            size_progress,
1843            alpha_progress,
1844        ))
1845        .push(content);
1846
1847    navigation_press_surface(
1848        indicator,
1849        message,
1850        NavigationStateLayer::Drawer {
1851            progress: alpha_progress,
1852        },
1853        NavigationIndicatorPlacement::Full,
1854    )
1855    .into()
1856}
1857
1858fn indicator_icon_stack<'a, Message, Renderer>(
1859    icon: &'static str,
1860    icon_size: f32,
1861    indicator_width: f32,
1862    indicator_height: f32,
1863    size_progress: f32,
1864    alpha_progress: f32,
1865    badge: Option<Badge>,
1866    drawer: bool,
1867) -> Stack<'a, Message, Theme, Renderer>
1868where
1869    Message: Clone + 'a,
1870    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1871    Font: Into<Renderer::Font>,
1872{
1873    Stack::new()
1874        .width(Length::Fixed(indicator_width))
1875        .height(Length::Fixed(indicator_height))
1876        .push(
1877            Space::new()
1878                .width(Length::Fixed(indicator_width))
1879                .height(Length::Fixed(indicator_height)),
1880        )
1881        .push(indicator_layer(
1882            indicator_width,
1883            indicator_height,
1884            size_progress,
1885            alpha_progress,
1886        ))
1887        .push(
1888            destination_icon_anchor::<Message, Renderer>(
1889                icon,
1890                icon_size,
1891                alpha_progress,
1892                badge,
1893                drawer,
1894            )
1895            .width(Length::Fixed(indicator_width))
1896            .height(Length::Fixed(indicator_height)),
1897        )
1898}
1899
1900fn navigation_press_surface<'a, Message, Renderer>(
1901    content: impl Into<Element<'a, Message, Theme, Renderer>>,
1902    on_press: Message,
1903    layer: NavigationStateLayer,
1904    indicator: NavigationIndicatorPlacement,
1905) -> NavigationPressSurface<'a, Message, Renderer>
1906where
1907    Message: Clone + 'a,
1908    Renderer: geometry::Renderer + primitive::Renderer + 'a,
1909{
1910    NavigationPressSurface {
1911        content: content.into(),
1912        on_press,
1913        layer,
1914        indicator,
1915    }
1916}
1917
1918struct NavigationPressSurface<'a, Message, Renderer>
1919where
1920    Renderer: geometry::Renderer + primitive::Renderer,
1921{
1922    content: Element<'a, Message, Theme, Renderer>,
1923    on_press: Message,
1924    layer: NavigationStateLayer,
1925    indicator: NavigationIndicatorPlacement,
1926}
1927
1928#[derive(Debug, Clone, Copy)]
1929enum NavigationIndicatorPlacement {
1930    Full,
1931    TopCenter {
1932        top: f32,
1933        width: f32,
1934        height: f32,
1935    },
1936    Inset {
1937        x: f32,
1938        y: f32,
1939        width: f32,
1940        height: f32,
1941    },
1942}
1943
1944impl NavigationIndicatorPlacement {
1945    fn bounds(self, bounds: Rectangle) -> Rectangle {
1946        match self {
1947            Self::Full => bounds,
1948            Self::TopCenter { top, width, height } => Rectangle {
1949                x: bounds.x + (bounds.width - width) / 2.0,
1950                y: bounds.y + top,
1951                width,
1952                height,
1953            },
1954            Self::Inset {
1955                x,
1956                y,
1957                width,
1958                height,
1959            } => Rectangle {
1960                x: bounds.x + x,
1961                y: bounds.y + y,
1962                width,
1963                height,
1964            },
1965        }
1966    }
1967}
1968
1969#[derive(Debug)]
1970struct NavigationPressSurfaceState {
1971    is_hovered: bool,
1972    is_pressed: bool,
1973    state_layer_opacity: AnimatedScalar,
1974    ripples: PressRippleState,
1975    now: Option<Instant>,
1976}
1977
1978impl Default for NavigationPressSurfaceState {
1979    fn default() -> Self {
1980        Self {
1981            is_hovered: false,
1982            is_pressed: false,
1983            state_layer_opacity: AnimatedScalar::new(0.0),
1984            ripples: PressRippleState::default(),
1985            now: None,
1986        }
1987    }
1988}
1989
1990impl NavigationPressSurfaceState {
1991    fn sync_hover(&mut self, is_hovered: bool, now: Instant) -> bool {
1992        if self.is_hovered == is_hovered {
1993            return false;
1994        }
1995
1996        self.is_hovered = is_hovered;
1997
1998        if !self.is_pressed {
1999            if !is_hovered {
2000                self.clear_ripples();
2001            }
2002
2003            self.animate_to_interaction_target(now);
2004        }
2005
2006        true
2007    }
2008
2009    fn press(&mut self, origin: Point, now: Instant) {
2010        self.is_pressed = true;
2011        self.ripples.press(
2012            origin,
2013            now,
2014            RippleStart::Replace,
2015            RippleStyle::material_patterned(),
2016        );
2017        self.now = Some(now);
2018        self.animate_to_interaction_target(now);
2019    }
2020
2021    fn release(&mut self, is_hovered: bool, now: Instant) {
2022        self.release_with_hover(is_hovered, is_hovered, now);
2023    }
2024
2025    fn release_with_hover(&mut self, keep_ripple: bool, is_hovered: bool, now: Instant) {
2026        self.is_pressed = false;
2027        self.is_hovered = is_hovered;
2028
2029        if keep_ripple {
2030            self.ripples.release_replacing(now);
2031        } else {
2032            self.clear_ripples();
2033        }
2034
2035        self.now = Some(now);
2036        self.animate_to_interaction_target(now);
2037    }
2038
2039    fn snap_to_interaction_target(&mut self) {
2040        self.state_layer_opacity
2041            .snap_to(navigation_interaction_state_layer_target(
2042                self.is_hovered,
2043                self.is_pressed,
2044            ));
2045    }
2046
2047    fn cancel(&mut self, now: Instant) {
2048        self.is_pressed = false;
2049        self.is_hovered = false;
2050        self.clear_ripples();
2051
2052        self.now = Some(now);
2053        self.animate_to_interaction_target(now);
2054    }
2055
2056    fn advance(&mut self, now: Instant) -> bool {
2057        self.now = Some(now);
2058        self.prune(now);
2059
2060        self.state_layer_opacity.advance(now) || self.has_visible_ripples(now)
2061    }
2062
2063    fn opacity(&self) -> f32 {
2064        navigation_surface_state_layer_opacity_from_interaction(self.state_layer_opacity.value)
2065    }
2066
2067    fn animate_to_interaction_target(&mut self, now: Instant) {
2068        self.state_layer_opacity.set_target(
2069            navigation_interaction_state_layer_target(self.is_hovered, self.is_pressed),
2070            now,
2071            duration_ms(tokens::motion::DURATION_SHORT2_MS),
2072            tokens::motion::EASING_STANDARD,
2073        );
2074    }
2075
2076    fn clear_ripples(&mut self) {
2077        self.ripples.clear();
2078    }
2079
2080    fn prune(&mut self, now: Instant) {
2081        self.ripples.prune(now);
2082    }
2083
2084    fn has_visible_ripples(&self, now: Instant) -> bool {
2085        self.ripples.has_visible_ripples(now)
2086    }
2087}
2088
2089impl<Message, Renderer> Widget<Message, Theme, Renderer>
2090    for NavigationPressSurface<'_, Message, Renderer>
2091where
2092    Message: Clone,
2093    Renderer: geometry::Renderer + primitive::Renderer,
2094{
2095    fn tag(&self) -> tree::Tag {
2096        tree::Tag::of::<NavigationPressSurfaceState>()
2097    }
2098
2099    fn state(&self) -> tree::State {
2100        tree::State::new(NavigationPressSurfaceState::default())
2101    }
2102
2103    fn children(&self) -> Vec<Tree> {
2104        vec![Tree::new(&self.content)]
2105    }
2106
2107    fn diff(&self, tree: &mut Tree) {
2108        tree.diff_children(std::slice::from_ref(&self.content));
2109    }
2110
2111    fn size(&self) -> Size<Length> {
2112        self.content.as_widget().size()
2113    }
2114
2115    fn size_hint(&self) -> Size<Length> {
2116        self.content.as_widget().size_hint()
2117    }
2118
2119    fn layout(
2120        &mut self,
2121        tree: &mut Tree,
2122        renderer: &Renderer,
2123        limits: &layout::Limits,
2124    ) -> layout::Node {
2125        self.content
2126            .as_widget_mut()
2127            .layout(&mut tree.children[0], renderer, limits)
2128    }
2129
2130    fn operate(
2131        &mut self,
2132        tree: &mut Tree,
2133        layout: Layout<'_>,
2134        renderer: &Renderer,
2135        operation: &mut dyn Operation,
2136    ) {
2137        self.content
2138            .as_widget_mut()
2139            .operate(&mut tree.children[0], layout, renderer, operation);
2140    }
2141
2142    fn update(
2143        &mut self,
2144        tree: &mut Tree,
2145        event: &Event,
2146        layout: Layout<'_>,
2147        cursor: mouse::Cursor,
2148        renderer: &Renderer,
2149        clipboard: &mut dyn Clipboard,
2150        shell: &mut Shell<'_, Message>,
2151        viewport: &Rectangle,
2152    ) {
2153        self.content.as_widget_mut().update(
2154            &mut tree.children[0],
2155            event,
2156            layout,
2157            cursor,
2158            renderer,
2159            clipboard,
2160            shell,
2161            viewport,
2162        );
2163
2164        if shell.is_event_captured() {
2165            return;
2166        }
2167
2168        let state = tree.state.downcast_mut::<NavigationPressSurfaceState>();
2169        let now = match event {
2170            Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
2171            _ => None,
2172        };
2173        let is_touch_event = matches!(event, Event::Touch(_));
2174        let is_hovered = !is_touch_event && cursor.is_over(layout.bounds());
2175        let should_snap_initial_redraw_hover =
2176            navigation_should_snap_initial_redraw_hover(event, state, is_hovered);
2177
2178        if navigation_should_sync_hover(event, cursor) {
2179            if state.sync_hover(is_hovered, now.unwrap_or_else(Instant::now)) {
2180                if should_snap_initial_redraw_hover {
2181                    state.snap_to_interaction_target();
2182                }
2183
2184                shell.request_redraw();
2185            }
2186        }
2187
2188        match event {
2189            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
2190            | Event::Touch(touch::Event::FingerPressed { .. }) => {
2191                if navigation_event_is_over(event, layout.bounds(), cursor) {
2192                    let indicator_bounds = self.indicator.bounds(layout.bounds());
2193
2194                    if let Some(origin) = navigation_press_origin(event, indicator_bounds, cursor) {
2195                        state.press(origin, now.unwrap_or_else(Instant::now));
2196                        shell.request_redraw();
2197                        shell.capture_event();
2198                    }
2199                }
2200            }
2201            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
2202            | Event::Touch(touch::Event::FingerLifted { .. }) => {
2203                if state.is_pressed {
2204                    let is_released_over = navigation_event_is_over(event, layout.bounds(), cursor);
2205                    let is_touch_release = matches!(event, Event::Touch(_));
2206
2207                    if is_touch_release {
2208                        state.release_with_hover(
2209                            is_released_over,
2210                            false,
2211                            now.unwrap_or_else(Instant::now),
2212                        );
2213                    } else {
2214                        state.release(is_released_over, now.unwrap_or_else(Instant::now));
2215                    }
2216                    shell.request_redraw();
2217
2218                    if is_released_over {
2219                        shell.publish(self.on_press.clone());
2220                    }
2221
2222                    shell.capture_event();
2223                }
2224            }
2225            Event::Touch(touch::Event::FingerLost { .. }) => {
2226                if state.is_pressed {
2227                    state.cancel(now.unwrap_or_else(Instant::now));
2228                    shell.request_redraw();
2229                }
2230            }
2231            _ => {}
2232        }
2233
2234        if let Some(now) = now {
2235            if state.advance(now) {
2236                shell.request_redraw();
2237            }
2238        }
2239    }
2240
2241    fn mouse_interaction(
2242        &self,
2243        tree: &Tree,
2244        layout: Layout<'_>,
2245        cursor: mouse::Cursor,
2246        viewport: &Rectangle,
2247        renderer: &Renderer,
2248    ) -> mouse::Interaction {
2249        let content_interaction = self.content.as_widget().mouse_interaction(
2250            &tree.children[0],
2251            layout,
2252            cursor,
2253            viewport,
2254            renderer,
2255        );
2256
2257        if matches!(content_interaction, mouse::Interaction::None)
2258            && cursor.is_over(layout.bounds())
2259        {
2260            mouse::Interaction::Pointer
2261        } else {
2262            content_interaction
2263        }
2264    }
2265
2266    fn draw(
2267        &self,
2268        tree: &Tree,
2269        renderer: &mut Renderer,
2270        theme: &Theme,
2271        renderer_style: &renderer::Style,
2272        layout: Layout<'_>,
2273        cursor: mouse::Cursor,
2274        viewport: &Rectangle,
2275    ) {
2276        self.content.as_widget().draw(
2277            &tree.children[0],
2278            renderer,
2279            theme,
2280            renderer_style,
2281            layout,
2282            cursor,
2283            viewport,
2284        );
2285
2286        let state = tree.state.downcast_ref::<NavigationPressSurfaceState>();
2287        let indicator_bounds = self.indicator.bounds(layout.bounds());
2288        let now = state.now.unwrap_or_else(Instant::now);
2289        let opacity = navigation_press_surface_opacity_for_draw(state, cursor, layout.bounds());
2290        let layer_color = navigation_state_layer_color(theme, self.layer);
2291
2292        if opacity > 0.0 {
2293            renderer.fill_quad(
2294                renderer::Quad {
2295                    bounds: indicator_bounds,
2296                    border: border::rounded(tokens::shape::CORNER_FULL),
2297                    snap: cfg!(feature = "crisp"),
2298                    ..renderer::Quad::default()
2299                },
2300                state_layer(layer_color, opacity),
2301            );
2302        }
2303
2304        draw_ripples(
2305            renderer,
2306            indicator_bounds,
2307            &state.ripples,
2308            layer_color,
2309            RippleConfig::bounded(border::radius(tokens::shape::CORNER_FULL)),
2310            now,
2311        );
2312    }
2313
2314    fn overlay<'b>(
2315        &'b mut self,
2316        tree: &'b mut Tree,
2317        layout: Layout<'b>,
2318        renderer: &Renderer,
2319        viewport: &Rectangle,
2320        translation: Vector,
2321    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
2322        self.content.as_widget_mut().overlay(
2323            &mut tree.children[0],
2324            layout,
2325            renderer,
2326            viewport,
2327            translation,
2328        )
2329    }
2330}
2331
2332impl<'a, Message, Renderer> From<NavigationPressSurface<'a, Message, Renderer>>
2333    for Element<'a, Message, Theme, Renderer>
2334where
2335    Message: Clone + 'a,
2336    Renderer: geometry::Renderer + primitive::Renderer + 'a,
2337{
2338    fn from(surface: NavigationPressSurface<'a, Message, Renderer>) -> Self {
2339        Element::new(surface)
2340    }
2341}
2342
2343#[cfg(test)]
2344fn navigation_surface_state_layer_opacity(is_hovered: bool, is_pressed: bool) -> f32 {
2345    navigation_surface_state_layer_opacity_from_interaction(
2346        navigation_interaction_state_layer_target(is_hovered, is_pressed),
2347    )
2348}
2349
2350fn navigation_interaction_state_layer_target(is_hovered: bool, _is_pressed: bool) -> f32 {
2351    if is_hovered {
2352        HOVERED_LAYER_OPACITY
2353    } else {
2354        0.0
2355    }
2356}
2357
2358fn navigation_should_sync_hover(event: &Event, cursor: mouse::Cursor) -> bool {
2359    match event {
2360        Event::Window(window::Event::RedrawRequested(_)) => {
2361            !matches!(cursor, mouse::Cursor::Unavailable)
2362        }
2363        Event::Mouse(_) | Event::Touch(_) => true,
2364        _ => false,
2365    }
2366}
2367
2368fn navigation_should_snap_initial_redraw_hover(
2369    event: &Event,
2370    state: &NavigationPressSurfaceState,
2371    is_hovered: bool,
2372) -> bool {
2373    matches!(event, Event::Window(window::Event::RedrawRequested(_)))
2374        && state.now.is_none()
2375        && is_hovered
2376}
2377
2378fn navigation_press_surface_opacity_for_draw(
2379    state: &NavigationPressSurfaceState,
2380    cursor: mouse::Cursor,
2381    bounds: Rectangle,
2382) -> f32 {
2383    if cursor.is_over(bounds) && state.now.is_none() {
2384        navigation_surface_state_layer_opacity_from_interaction(
2385            navigation_interaction_state_layer_target(true, false),
2386        )
2387    } else {
2388        state.opacity()
2389    }
2390}
2391
2392fn navigation_surface_state_layer_opacity_from_interaction(interaction_opacity: f32) -> f32 {
2393    interaction_opacity
2394}
2395
2396fn navigation_press_origin(
2397    event: &Event,
2398    indicator_bounds: Rectangle,
2399    cursor: mouse::Cursor,
2400) -> Option<Point> {
2401    let position = cursor
2402        .position()
2403        .or_else(|| navigation_event_position(event))?;
2404
2405    if cursor.is_levitating() {
2406        return None;
2407    }
2408
2409    Some(position - Vector::new(indicator_bounds.x, indicator_bounds.y))
2410}
2411
2412fn navigation_event_is_over(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> bool {
2413    if cursor.position().is_some() {
2414        return cursor.is_over(bounds);
2415    }
2416
2417    if cursor.is_levitating() {
2418        return false;
2419    }
2420
2421    navigation_event_position(event)
2422        .map(|position| bounds.contains(position))
2423        .unwrap_or_else(|| cursor.is_over(bounds))
2424}
2425
2426fn navigation_event_position(event: &Event) -> Option<Point> {
2427    match event {
2428        Event::Touch(touch::Event::FingerPressed { position, .. })
2429        | Event::Touch(touch::Event::FingerMoved { position, .. })
2430        | Event::Touch(touch::Event::FingerLifted { position, .. })
2431        | Event::Touch(touch::Event::FingerLost { position, .. }) => Some(*position),
2432        _ => None,
2433    }
2434}
2435
2436fn indicator_layer<'a, Message, Renderer>(
2437    target_width: f32,
2438    height: f32,
2439    size_progress: f32,
2440    alpha_progress: f32,
2441) -> Container<'a, Message, Theme, Renderer>
2442where
2443    Message: 'a,
2444    Renderer: geometry::Renderer + primitive::Renderer + 'a,
2445{
2446    let indicator = Container::new(Space::new())
2447        .width(Length::Fixed(animated_indicator_width(
2448            target_width,
2449            size_progress,
2450        )))
2451        .height(Length::Fixed(height))
2452        .style(move |theme| active_indicator(theme, alpha_progress));
2453
2454    Container::new(indicator)
2455        .width(Length::Fixed(target_width))
2456        .height(Length::Fixed(height))
2457        .align_x(alignment::Horizontal::Center)
2458        .align_y(alignment::Vertical::Center)
2459}
2460
2461#[derive(Debug, Clone, Copy)]
2462enum NavigationStateLayer {
2463    BarOrRail,
2464    Drawer { progress: f32 },
2465}
2466
2467fn animated_indicator_width(target_width: f32, progress: f32) -> f32 {
2468    // AndroidX Material3 measures the selected indicator width from animation progress.
2469    target_width * progress.max(0.0)
2470}
2471
2472fn navigation_bar_item_bottom_padding() -> f32 {
2473    let label = tokens::component::navigation_bar::LABEL_TEXT;
2474
2475    (tokens::component::navigation_bar::CONTAINER_HEIGHT
2476        - tokens::component::navigation_bar::INDICATOR_VERTICAL_OFFSET
2477        - tokens::component::navigation_bar::ACTIVE_INDICATOR_HEIGHT
2478        - tokens::component::navigation_bar::INDICATOR_TO_LABEL_PADDING
2479        - label.line_height)
2480        .max(0.0)
2481}
2482
2483fn navigation_rail_item_content_top_padding() -> f32 {
2484    tokens::component::navigation_rail::ITEM_TOP_PADDING
2485}
2486
2487fn navigation_rail_header_bottom_padding() -> f32 {
2488    tokens::component::navigation_rail::HEADER_PADDING
2489}
2490
2491fn navigation_rail_header_slot_height() -> f32 {
2492    tokens::component::icon_button::CONTAINER_HEIGHT + navigation_rail_header_bottom_padding()
2493}
2494
2495fn navigation_rail_item_slot_height() -> f32 {
2496    tokens::component::navigation_rail::ITEM_HEIGHT
2497}
2498
2499fn navigation_rail_expanded_item_vertical_inset() -> f32 {
2500    ((tokens::component::navigation_rail::ITEM_HEIGHT
2501        - tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_HEIGHT)
2502        / 2.0)
2503        .max(0.0)
2504}
2505
2506fn navigation_rail_expanded_indicator_height_for_progress(progress: f32) -> f32 {
2507    lerp(
2508        tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT,
2509        tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_HEIGHT,
2510        progress.clamp(0.0, 1.0),
2511    )
2512}
2513
2514fn navigation_rail_expanded_item_vertical_inset_for_progress(progress: f32) -> f32 {
2515    lerp(
2516        navigation_rail_item_content_top_padding(),
2517        navigation_rail_expanded_item_vertical_inset(),
2518        progress.clamp(0.0, 1.0),
2519    )
2520}
2521
2522fn navigation_rail_expanded_icon_anchor_width() -> f32 {
2523    tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2524        + tokens::component::navigation_rail::ICON_SIZE
2525}
2526
2527fn navigation_rail_expanded_label_leading_padding() -> f32 {
2528    navigation_rail_expanded_icon_anchor_width()
2529        + tokens::component::navigation_rail::ICON_LABEL_HORIZONTAL_SPACE
2530}
2531
2532fn navigation_rail_collapsed_label_top_padding() -> f32 {
2533    navigation_rail_item_content_top_padding()
2534        + tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT
2535        + tokens::component::navigation_rail::ITEM_VERTICAL_PADDING
2536}
2537
2538fn navigation_rail_collapsed_label_width() -> f32 {
2539    tokens::component::navigation_rail::ACTIVE_INDICATOR_WIDTH
2540}
2541
2542#[cfg(test)]
2543fn navigation_rail_collapsed_icon_center_x() -> f32 {
2544    tokens::component::navigation_rail::CONTAINER_WIDTH / 2.0
2545}
2546
2547#[cfg(test)]
2548fn navigation_rail_expanded_icon_center_x() -> f32 {
2549    tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2550        + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2551        + tokens::component::navigation_rail::ICON_SIZE / 2.0
2552}
2553
2554#[cfg(test)]
2555fn navigation_rail_collapsed_icon_center_y() -> f32 {
2556    navigation_rail_item_content_top_padding()
2557        + tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT / 2.0
2558}
2559
2560#[cfg(test)]
2561fn navigation_rail_expanded_icon_center_y_for_progress(progress: f32) -> f32 {
2562    navigation_rail_expanded_item_vertical_inset_for_progress(progress)
2563        + navigation_rail_expanded_indicator_height_for_progress(progress) / 2.0
2564}
2565
2566#[cfg(test)]
2567fn navigation_rail_first_item_y_after_header() -> f32 {
2568    tokens::component::navigation_rail::CONTENT_TOP_MARGIN
2569        + navigation_rail_header_slot_height()
2570        + tokens::component::navigation_rail::VERTICAL_PADDING
2571}
2572
2573fn navigation_rail_expanded_container_width(width: f32) -> f32 {
2574    width.clamp(
2575        tokens::component::navigation_rail::CONTAINER_WIDTH,
2576        tokens::component::navigation_drawer::CONTAINER_WIDTH,
2577    )
2578}
2579
2580fn navigation_rail_expanded_indicator_width(container_width: f32) -> f32 {
2581    (container_width
2582        - tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL * 2.0)
2583        .max(0.0)
2584}
2585
2586pub fn navigation_rail_expanded_width_for_progress(progress: f32) -> f32 {
2587    lerp(
2588        tokens::component::navigation_rail::CONTAINER_WIDTH,
2589        tokens::component::navigation_rail::EXPANDED_CONTAINER_WIDTH,
2590        progress.clamp(0.0, 1.0),
2591    )
2592}
2593
2594fn navigation_rail_expanded_progress_for_width(width: f32) -> f32 {
2595    let range = tokens::component::navigation_rail::EXPANDED_CONTAINER_WIDTH
2596        - tokens::component::navigation_rail::CONTAINER_WIDTH;
2597
2598    if range <= f32::EPSILON {
2599        1.0
2600    } else {
2601        ((navigation_rail_expanded_container_width(width)
2602            - tokens::component::navigation_rail::CONTAINER_WIDTH)
2603            / range)
2604            .clamp(0.0, 1.0)
2605    }
2606}
2607
2608fn navigation_rail_expanded_label_alpha_for_width(width: f32) -> f32 {
2609    let progress = navigation_rail_expanded_progress_for_width(width);
2610
2611    ((progress - 0.6) / 0.4).clamp(0.0, 1.0)
2612}
2613
2614fn navigation_rail_expanded_badge_uses_icon_anchor(label_alpha: f32) -> bool {
2615    label_alpha <= 0.0
2616}
2617
2618fn navigation_rail_expanded_trailing_badge_alpha(label_alpha: f32) -> f32 {
2619    label_alpha.clamp(0.0, 1.0)
2620}
2621
2622fn navigation_rail_expanded_collapsed_label_alpha(label_alpha: f32) -> f32 {
2623    (1.0 - label_alpha).clamp(0.0, 1.0)
2624}
2625
2626fn navigation_rail_expanded_header_leading_space() -> f32 {
2627    tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2628        + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2629        - (tokens::component::icon_button::CONTAINER_WIDTH
2630            - tokens::component::navigation_rail::ICON_SIZE)
2631            / 2.0
2632}
2633
2634fn navigation_rail_expanded_header_title_spacing() -> f32 {
2635    let label_start =
2636        tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2637            + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2638            + tokens::component::navigation_rail::ICON_SIZE
2639            + tokens::component::navigation_rail::ICON_LABEL_HORIZONTAL_SPACE;
2640
2641    (label_start
2642        - navigation_rail_expanded_header_leading_space()
2643        - tokens::component::icon_button::CONTAINER_WIDTH)
2644        .max(0.0)
2645}
2646
2647fn navigation_drawer_container_width(width: f32) -> f32 {
2648    width.clamp(
2649        tokens::component::navigation_drawer::MINIMUM_CONTAINER_WIDTH,
2650        tokens::component::navigation_drawer::CONTAINER_WIDTH,
2651    )
2652}
2653
2654fn navigation_drawer_indicator_width(container_width: f32) -> f32 {
2655    (container_width - tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING * 2.0).max(0.0)
2656}
2657
2658fn navigation_drawer_menu_header_leading_space() -> f32 {
2659    tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
2660        + tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE
2661        - (tokens::component::icon_button::CONTAINER_WIDTH
2662            - tokens::component::navigation_drawer::ICON_SIZE)
2663            / 2.0
2664}
2665
2666fn navigation_drawer_menu_header_title_spacing() -> f32 {
2667    let label_start = tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
2668        + tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE
2669        + tokens::component::navigation_drawer::ICON_SIZE
2670        + tokens::component::navigation_drawer::ICON_LABEL_SPACE;
2671
2672    (label_start
2673        - navigation_drawer_menu_header_leading_space()
2674        - tokens::component::icon_button::CONTAINER_WIDTH)
2675        .max(0.0)
2676}
2677
2678fn navigation_drawer_badge_space() -> f32 {
2679    tokens::component::navigation_drawer::LABEL_BADGE_SPACE
2680}
2681
2682fn navigation_rail_expanded_icon_layer<'a, Message, Renderer>(
2683    icon: &'static str,
2684    progress: f32,
2685    height: f32,
2686    badge: Option<Badge>,
2687) -> Container<'a, Message, Theme, Renderer>
2688where
2689    Message: 'a,
2690    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2691    Font: Into<Renderer::Font>,
2692{
2693    let icon = destination_icon_anchor::<Message, Renderer>(
2694        icon,
2695        tokens::component::navigation_rail::ICON_SIZE,
2696        progress,
2697        badge,
2698        true,
2699    )
2700    .width(Length::Fixed(tokens::component::navigation_rail::ICON_SIZE))
2701    .height(Length::Fixed(height));
2702
2703    Container::new(icon)
2704        .width(Length::Fixed(navigation_rail_expanded_icon_anchor_width()))
2705        .height(Length::Fixed(height))
2706        .align_x(alignment::Horizontal::Right)
2707        .align_y(alignment::Vertical::Center)
2708}
2709
2710fn navigation_rail_expanded_collapsed_label<'a, Message, Renderer>(
2711    label: &'static str,
2712    progress: f32,
2713    alpha: f32,
2714) -> Container<'a, Message, Theme, Renderer>
2715where
2716    Message: 'a,
2717    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2718    Font: Into<Renderer::Font>,
2719{
2720    let alpha = alpha.clamp(0.0, 1.0);
2721    let scale = tokens::component::navigation_rail::LABEL_TEXT;
2722    let label = type_text(label, scale).style(move |theme| text::Style {
2723        color: Some(alpha_color(bar_or_rail_label_color(theme, progress), alpha)),
2724    });
2725
2726    Container::new(label)
2727        .padding(Padding {
2728            top: navigation_rail_collapsed_label_top_padding(),
2729            right: 0.0,
2730            bottom: 0.0,
2731            left: 0.0,
2732        })
2733        .align_x(alignment::Horizontal::Center)
2734}
2735
2736fn destination_icon_anchor<'a, Message, Renderer>(
2737    icon: &'static str,
2738    size: f32,
2739    progress: f32,
2740    badge: Option<Badge>,
2741    drawer: bool,
2742) -> Container<'a, Message, Theme, Renderer>
2743where
2744    Message: 'a,
2745    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2746    Font: Into<Renderer::Font>,
2747{
2748    let icon: Element<'a, Message, Theme, Renderer> =
2749        destination_icon::<Message, Renderer>(icon, size, progress, drawer).into();
2750    let anchor = if let Some(badge) = badge {
2751        badge_widget::badged_box(
2752            icon,
2753            destination_badge::<Message, Renderer>(badge),
2754            destination_badge_placement(badge),
2755        )
2756        .into()
2757    } else {
2758        icon
2759    };
2760
2761    Container::new(anchor)
2762        .align_x(alignment::Horizontal::Center)
2763        .align_y(alignment::Vertical::Center)
2764}
2765
2766fn destination_badge<'a, Message, Renderer>(badge: Badge) -> Element<'a, Message, Theme, Renderer>
2767where
2768    Message: 'a,
2769    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2770    Font: Into<Renderer::Font>,
2771{
2772    match badge {
2773        Badge::Small => badge_widget::small().into(),
2774        Badge::Large(label) => badge_widget::large(label).into(),
2775    }
2776}
2777
2778fn destination_badge_with_alpha<'a, Message, Renderer>(
2779    badge: Badge,
2780    alpha: f32,
2781) -> Element<'a, Message, Theme, Renderer>
2782where
2783    Message: 'a,
2784    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2785    Font: Into<Renderer::Font>,
2786{
2787    let alpha = alpha.clamp(0.0, 1.0);
2788
2789    match badge {
2790        Badge::Small => badge_widget::small()
2791            .style(move |theme| alpha_badge_style(theme, alpha))
2792            .into(),
2793        Badge::Large(label) => badge_widget::large(label)
2794            .style(move |theme| alpha_badge_style(theme, alpha))
2795            .into(),
2796    }
2797}
2798
2799fn alpha_badge_style(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
2800    let mut style = crate::style::badge::default(theme);
2801
2802    if let Some(Background::Color(color)) = style.background {
2803        style.background = Some(Background::Color(alpha_color(color, alpha)));
2804    }
2805
2806    style.text_color = style.text_color.map(|color| alpha_color(color, alpha));
2807    style
2808}
2809
2810fn destination_badge_placement(badge: Badge) -> badge_widget::BadgedBoxPlacement {
2811    match badge {
2812        Badge::Small => badge_widget::BadgedBoxPlacement::IconOnly,
2813        Badge::Large(_) => badge_widget::BadgedBoxPlacement::WithContent,
2814    }
2815}
2816
2817fn destination_icon<'a, Message, Renderer>(
2818    icon: &'static str,
2819    size: f32,
2820    progress: f32,
2821    drawer: bool,
2822) -> Stack<'a, Message, Theme, Renderer>
2823where
2824    Message: 'a,
2825    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2826    Font: Into<Renderer::Font>,
2827{
2828    let outline = fonts::icon(icon, size)
2829        .width(Length::Fixed(size))
2830        .height(Length::Fixed(size))
2831        .center()
2832        .style(move |theme| text::Style {
2833            color: Some(destination_icon_outline_color(theme, progress)),
2834        });
2835    let filled = fonts::filled_icon(icon, size)
2836        .width(Length::Fixed(size))
2837        .height(Length::Fixed(size))
2838        .center()
2839        .style(move |theme| text::Style {
2840            color: Some(destination_icon_filled_color(theme, progress, drawer)),
2841        });
2842
2843    Stack::new()
2844        .width(Length::Fixed(size))
2845        .height(Length::Fixed(size))
2846        .push(outline)
2847        .push(filled)
2848}
2849
2850fn destination_icon_outline_color(theme: &Theme, progress: f32) -> Color {
2851    alpha_color(
2852        theme.colors().surface.text_variant,
2853        1.0 - progress.clamp(0.0, 1.0),
2854    )
2855}
2856
2857fn destination_icon_filled_color(theme: &Theme, progress: f32, drawer: bool) -> Color {
2858    let color = if drawer {
2859        drawer_content_color(theme, 1.0)
2860    } else {
2861        bar_or_rail_icon_color(theme, 1.0)
2862    };
2863
2864    alpha_color(color, progress.clamp(0.0, 1.0))
2865}
2866
2867fn type_text<'a, Renderer>(
2868    content: &'static str,
2869    scale: tokens::typography::TypeScale,
2870) -> Text<'a, Theme, Renderer>
2871where
2872    Renderer: core_text::Renderer + 'a,
2873    Font: Into<Renderer::Font>,
2874{
2875    Text::new(content)
2876        .font(fonts::roboto_for_type_scale(scale))
2877        .size(scale.size)
2878        .line_height(LineHeight::Absolute(scale.line_height.into()))
2879}
2880
2881fn navigation_bar_container(theme: &Theme) -> iced_widget::container::Style {
2882    let colors = theme.colors();
2883
2884    iced_widget::container::Style {
2885        background: Some(Background::Color(colors.surface.color)),
2886        text_color: Some(colors.surface.text),
2887        border: border::rounded(tokens::shape::CORNER_NONE),
2888        shadow: shadow_from_level(
2889            tokens::component::navigation_bar::CONTAINER_ELEVATION_LEVEL,
2890            colors.shadow,
2891        ),
2892        ..iced_widget::container::Style::default()
2893    }
2894}
2895
2896fn navigation_rail_container(theme: &Theme) -> iced_widget::container::Style {
2897    let colors = theme.colors();
2898
2899    iced_widget::container::Style {
2900        background: Some(Background::Color(colors.surface.color)),
2901        text_color: Some(colors.surface.text),
2902        border: border::rounded(tokens::shape::CORNER_NONE),
2903        shadow: shadow_from_level(
2904            tokens::component::navigation_rail::CONTAINER_ELEVATION_LEVEL,
2905            colors.shadow,
2906        ),
2907        ..iced_widget::container::Style::default()
2908    }
2909}
2910
2911fn navigation_drawer_container(theme: &Theme) -> iced_widget::container::Style {
2912    let colors = theme.colors();
2913
2914    iced_widget::container::Style {
2915        background: Some(Background::Color(colors.surface.color)),
2916        text_color: Some(colors.surface.text),
2917        border: border::rounded(tokens::shape::CORNER_LARGE),
2918        shadow: shadow_from_level(
2919            tokens::component::navigation_drawer::STANDARD_CONTAINER_ELEVATION_LEVEL,
2920            colors.shadow,
2921        ),
2922        ..iced_widget::container::Style::default()
2923    }
2924}
2925
2926fn active_indicator(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
2927    let mut color = theme.colors().secondary.container;
2928    color.a *= alpha.clamp(0.0, 1.0);
2929
2930    iced_widget::container::Style {
2931        background: Some(Background::Color(color)),
2932        text_color: Some(theme.colors().secondary.container_text),
2933        border: border::rounded(tokens::shape::CORNER_FULL),
2934        ..iced_widget::container::Style::default()
2935    }
2936}
2937
2938fn headline_text_style(theme: &Theme) -> text::Style {
2939    text::Style {
2940        color: Some(theme.colors().surface.text_variant),
2941    }
2942}
2943
2944fn bar_or_rail_icon_color(theme: &Theme, progress: f32) -> Color {
2945    let colors = theme.colors();
2946
2947    mix(
2948        colors.surface.text_variant,
2949        colors.secondary.container_text,
2950        progress,
2951    )
2952}
2953
2954fn bar_or_rail_label_color(theme: &Theme, progress: f32) -> Color {
2955    let colors = theme.colors();
2956
2957    mix(colors.surface.text_variant, colors.surface.text, progress)
2958}
2959
2960fn drawer_content_color(theme: &Theme, progress: f32) -> Color {
2961    let colors = theme.colors();
2962
2963    mix(
2964        colors.surface.text_variant,
2965        colors.secondary.container_text,
2966        progress,
2967    )
2968}
2969
2970fn navigation_state_layer_color(theme: &Theme, layer: NavigationStateLayer) -> Color {
2971    let colors = theme.colors();
2972
2973    match layer {
2974        // AndroidX Material3 applies the selectable interaction to the whole item, but remaps the
2975        // ripple onto the active-indicator pill. Its ripple color comes from the navigation
2976        // container content color, not from the active icon color.
2977        NavigationStateLayer::BarOrRail => colors.surface.text,
2978        NavigationStateLayer::Drawer { progress } => mix(
2979            colors.surface.text,
2980            colors.secondary.container_text,
2981            progress,
2982        ),
2983    }
2984}
2985
2986#[cfg(test)]
2987#[path = "../../../tests/widget/component/navigation.rs"]
2988mod tests;