Skip to main content

material_ui_rs/widget/component/
navigation.rs

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