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