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 =
1643        single_line_type_text(headline, headline_scale).style(move |theme| text::Style {
1644            color: Some(alpha_color(
1645                theme.colors().surface.text_variant,
1646                metrics.label_alpha(),
1647            )),
1648        });
1649    let headline = Container::new(headline)
1650        .width(Length::Fill)
1651        .height(Length::Fixed(
1652            tokens::component::icon_button::CONTAINER_HEIGHT,
1653        ))
1654        .align_y(alignment::Vertical::Center)
1655        .clip(true);
1656    let content = Row::new()
1657        .width(Length::Fill)
1658        .height(Length::Fixed(
1659            tokens::component::icon_button::CONTAINER_HEIGHT,
1660        ))
1661        .spacing(metrics.header_title_spacing())
1662        .align_y(alignment::Vertical::Center)
1663        .push(navigation_menu_button(on_menu, metrics.progress()))
1664        .push(headline);
1665
1666    Container::new(content)
1667        .height(Length::Fixed(RailMetrics::header_slot_height()))
1668        .width(Length::Fill)
1669        .padding(Padding {
1670            top: 0.0,
1671            right: tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL,
1672            bottom: RailMetrics::header_bottom_padding(),
1673            left: metrics.header_leading_space(),
1674        })
1675        .align_y(alignment::Vertical::Center)
1676}
1677
1678fn expanded_rail_item<'a, Id, Message, Renderer, F>(
1679    destination: Destination<Id>,
1680    selection: Selection<Id>,
1681    on_select: F,
1682    metrics: ExpandedRailMetrics,
1683) -> Element<'a, Message, Theme, Renderer>
1684where
1685    Id: Copy + Eq + 'a,
1686    Message: Clone + 'a,
1687    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1688    Font: Into<Renderer::Font>,
1689    F: Fn(Id) -> Message + Clone + 'a,
1690{
1691    let size_progress = selection.size_progress(destination.id);
1692    let alpha_progress = selection.alpha_progress(destination.id);
1693    let indicator_width = metrics.indicator_width();
1694    let indicator_height = metrics.indicator_height();
1695    let vertical_inset = metrics.item_vertical_inset();
1696    let scale = tokens::component::navigation_drawer::LABEL_TEXT;
1697    let message = on_select(destination.id);
1698    let badge_on_icon = metrics.badge_uses_icon_anchor();
1699    let trailing_badge_alpha = metrics.trailing_badge_alpha();
1700    let collapsed_label_alpha = metrics.collapsed_label_alpha();
1701    let icon = expanded_rail_icon_layer(
1702        destination.icon,
1703        alpha_progress,
1704        indicator_height,
1705        badge_on_icon.then_some(destination.badge).flatten(),
1706    );
1707    let label = type_text(destination.label, scale).style(move |theme| text::Style {
1708        color: Some(alpha_color(
1709            drawer_content_color(theme, alpha_progress),
1710            metrics.label_alpha(),
1711        )),
1712    });
1713    let content = Row::new()
1714        .width(Length::Fill)
1715        .height(Length::Fixed(indicator_height))
1716        .align_y(alignment::Vertical::Center)
1717        .push(Container::new(label).width(Length::Fill));
1718    let content = if let Some(badge) = destination.badge.filter(|_| !badge_on_icon) {
1719        content
1720            .push(Space::new().width(Length::Fixed(DrawerMetrics::badge_space())))
1721            .push(destination_badge_with_alpha::<Message, Renderer>(
1722                badge,
1723                trailing_badge_alpha,
1724            ))
1725    } else {
1726        content
1727    };
1728    let content = Container::new(content)
1729        .width(Length::Fixed(indicator_width))
1730        .height(Length::Fixed(indicator_height))
1731        .padding(Padding {
1732            top: 0.0,
1733            right: tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_END,
1734            bottom: 0.0,
1735            left: metrics.label_leading_padding(),
1736        })
1737        .align_y(alignment::Vertical::Center);
1738    let expanded_indicator = Stack::new()
1739        .width(Length::Fixed(indicator_width))
1740        .height(Length::Fixed(indicator_height))
1741        .push(
1742            Space::new()
1743                .width(Length::Fixed(indicator_width))
1744                .height(Length::Fixed(indicator_height)),
1745        )
1746        .push(indicator_layer(
1747            indicator_width,
1748            indicator_height,
1749            size_progress,
1750            alpha_progress,
1751        ))
1752        .push(content)
1753        .push(icon);
1754    let collapsed_label = collapsed_rail_label::<Message, Renderer>(
1755        destination.label,
1756        alpha_progress,
1757        collapsed_label_alpha,
1758    )
1759    .width(Length::Fixed(RailMetrics::collapsed_label_width()))
1760    .height(Length::Fixed(RailMetrics::item_slot_height()));
1761    let item = Stack::new()
1762        .width(Length::Fixed(indicator_width))
1763        .height(Length::Fixed(RailMetrics::item_slot_height()))
1764        .push(
1765            Container::new(expanded_indicator)
1766                .width(Length::Fixed(indicator_width))
1767                .height(Length::Fixed(RailMetrics::item_slot_height()))
1768                .padding(Padding {
1769                    top: vertical_inset,
1770                    right: 0.0,
1771                    bottom: vertical_inset,
1772                    left: 0.0,
1773                })
1774                .align_y(alignment::Vertical::Top),
1775        )
1776        .push(collapsed_label);
1777
1778    press_surface(
1779        Container::new(item)
1780            .width(Length::Fixed(indicator_width))
1781            .height(Length::Fixed(RailMetrics::item_slot_height())),
1782        message,
1783        NavigationStateLayer::Drawer {
1784            progress: alpha_progress,
1785        },
1786        NavigationIndicatorPlacement::Inset {
1787            x: 0.0,
1788            y: vertical_inset,
1789            width: indicator_width,
1790            height: indicator_height,
1791        },
1792    )
1793    .into()
1794}
1795
1796fn drawer_menu_header<'a, Message, Renderer>(
1797    headline: &'static str,
1798    on_menu: Message,
1799) -> Container<'a, Message, Theme, Renderer>
1800where
1801    Message: Clone + 'a,
1802    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1803    Font: Into<Renderer::Font>,
1804{
1805    let headline_scale = tokens::component::navigation_drawer::HEADLINE_TEXT;
1806    let content = Row::new()
1807        .height(Length::Fixed(
1808            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1809        ))
1810        .spacing(DrawerMetrics::menu_header_title_spacing())
1811        .align_y(alignment::Vertical::Center)
1812        .push(navigation_menu_button(on_menu, 0.0))
1813        .push(type_text(headline, headline_scale).style(headline_text_style));
1814
1815    Container::new(content)
1816        .height(Length::Fixed(
1817            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1818        ))
1819        .padding(Padding {
1820            top: 0.0,
1821            right: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
1822                + tokens::component::navigation_drawer::ITEM_CONTENT_TRAILING_SPACE,
1823            bottom: 0.0,
1824            left: DrawerMetrics::menu_header_leading_space(),
1825        })
1826        .align_y(alignment::Vertical::Center)
1827}
1828
1829fn drawer_item<'a, Id, Message, Renderer, F>(
1830    destination: Destination<Id>,
1831    selection: Selection<Id>,
1832    on_select: F,
1833    indicator_width: f32,
1834) -> Element<'a, Message, Theme, Renderer>
1835where
1836    Id: Copy + Eq + 'a,
1837    Message: Clone + 'a,
1838    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1839    Font: Into<Renderer::Font>,
1840    F: Fn(Id) -> Message + Clone + 'a,
1841{
1842    let size_progress = selection.size_progress(destination.id);
1843    let alpha_progress = selection.alpha_progress(destination.id);
1844    let scale = tokens::component::navigation_drawer::LABEL_TEXT;
1845    let message = on_select(destination.id);
1846    let icon = destination_icon::<Message, Renderer>(
1847        destination.icon,
1848        tokens::component::navigation_drawer::ICON_SIZE,
1849        alpha_progress,
1850        true,
1851    );
1852    let label = type_text(destination.label, scale).style(move |theme| text::Style {
1853        color: Some(drawer_content_color(theme, alpha_progress)),
1854    });
1855    let content = Row::new()
1856        .width(Length::Fill)
1857        .height(Length::Fixed(
1858            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1859        ))
1860        .spacing(tokens::component::navigation_drawer::ICON_LABEL_SPACE)
1861        .align_y(alignment::Vertical::Center)
1862        .push(icon)
1863        .push(Container::new(label).width(Length::Fill));
1864    let content = if let Some(badge) = destination.badge {
1865        content
1866            .push(Space::new().width(Length::Fixed(DrawerMetrics::badge_space())))
1867            .push(destination_badge::<Message, Renderer>(badge))
1868    } else {
1869        content
1870    };
1871    let content = Container::new(content)
1872        .width(Length::Fixed(indicator_width))
1873        .height(Length::Fixed(
1874            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1875        ))
1876        .padding(Padding {
1877            top: 0.0,
1878            right: tokens::component::navigation_drawer::ITEM_CONTENT_TRAILING_SPACE,
1879            bottom: 0.0,
1880            left: tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE,
1881        })
1882        .align_y(alignment::Vertical::Center);
1883    let indicator = Stack::new()
1884        .width(Length::Fixed(indicator_width))
1885        .height(Length::Fixed(
1886            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1887        ))
1888        .push(
1889            Space::new()
1890                .width(Length::Fixed(indicator_width))
1891                .height(Length::Fixed(
1892                    tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1893                )),
1894        )
1895        .push(indicator_layer(
1896            indicator_width,
1897            tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1898            size_progress,
1899            alpha_progress,
1900        ))
1901        .push(content);
1902
1903    press_surface(
1904        indicator,
1905        message,
1906        NavigationStateLayer::Drawer {
1907            progress: alpha_progress,
1908        },
1909        NavigationIndicatorPlacement::Full,
1910    )
1911    .into()
1912}
1913
1914#[derive(Debug, Clone, Copy)]
1915struct IndicatorIconSpec {
1916    icon: &'static str,
1917    icon_size: f32,
1918    indicator_size: Size,
1919    size_progress: f32,
1920    alpha_progress: f32,
1921    badge: Option<Badge>,
1922    drawer: bool,
1923}
1924
1925fn indicator_icon_stack<'a, Message, Renderer>(
1926    spec: IndicatorIconSpec,
1927) -> Stack<'a, Message, Theme, Renderer>
1928where
1929    Message: Clone + 'a,
1930    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1931    Font: Into<Renderer::Font>,
1932{
1933    let indicator_width = spec.indicator_size.width;
1934    let indicator_height = spec.indicator_size.height;
1935
1936    Stack::new()
1937        .width(Length::Fixed(indicator_width))
1938        .height(Length::Fixed(indicator_height))
1939        .push(
1940            Space::new()
1941                .width(Length::Fixed(indicator_width))
1942                .height(Length::Fixed(indicator_height)),
1943        )
1944        .push(indicator_layer(
1945            indicator_width,
1946            indicator_height,
1947            spec.size_progress,
1948            spec.alpha_progress,
1949        ))
1950        .push(
1951            destination_icon_anchor::<Message, Renderer>(
1952                spec.icon,
1953                spec.icon_size,
1954                spec.alpha_progress,
1955                spec.badge,
1956                spec.drawer,
1957            )
1958            .width(Length::Fixed(indicator_width))
1959            .height(Length::Fixed(indicator_height)),
1960        )
1961}
1962
1963fn press_surface<'a, Message, Renderer>(
1964    content: impl Into<Element<'a, Message, Theme, Renderer>>,
1965    on_press: Message,
1966    layer: NavigationStateLayer,
1967    indicator: NavigationIndicatorPlacement,
1968) -> NavigationPressSurface<'a, Message, Renderer>
1969where
1970    Message: Clone + 'a,
1971    Renderer: geometry::Renderer + primitive::Renderer + 'a,
1972{
1973    NavigationPressSurface {
1974        content: content.into(),
1975        on_press,
1976        layer,
1977        indicator,
1978    }
1979}
1980
1981struct NavigationPressSurface<'a, Message, Renderer>
1982where
1983    Renderer: geometry::Renderer + primitive::Renderer,
1984{
1985    content: Element<'a, Message, Theme, Renderer>,
1986    on_press: Message,
1987    layer: NavigationStateLayer,
1988    indicator: NavigationIndicatorPlacement,
1989}
1990
1991#[derive(Debug, Clone, Copy)]
1992enum NavigationIndicatorPlacement {
1993    Full,
1994    TopCenter {
1995        top: f32,
1996        width: f32,
1997        height: f32,
1998    },
1999    Inset {
2000        x: f32,
2001        y: f32,
2002        width: f32,
2003        height: f32,
2004    },
2005}
2006
2007impl NavigationIndicatorPlacement {
2008    fn bounds(self, bounds: Rectangle) -> Rectangle {
2009        match self {
2010            Self::Full => bounds,
2011            Self::TopCenter { top, width, height } => Rectangle {
2012                x: bounds.x + (bounds.width - width) / 2.0,
2013                y: bounds.y + top,
2014                width,
2015                height,
2016            },
2017            Self::Inset {
2018                x,
2019                y,
2020                width,
2021                height,
2022            } => Rectangle {
2023                x: bounds.x + x,
2024                y: bounds.y + y,
2025                width,
2026                height,
2027            },
2028        }
2029    }
2030}
2031
2032#[derive(Debug)]
2033struct NavigationPressSurfaceState {
2034    is_hovered: bool,
2035    is_pressed: bool,
2036    state_layer_opacity: AnimatedScalar,
2037    ripples: PressRippleState,
2038    now: Option<Instant>,
2039}
2040
2041impl Default for NavigationPressSurfaceState {
2042    fn default() -> Self {
2043        Self {
2044            is_hovered: false,
2045            is_pressed: false,
2046            state_layer_opacity: AnimatedScalar::new(0.0),
2047            ripples: PressRippleState::default(),
2048            now: None,
2049        }
2050    }
2051}
2052
2053impl NavigationPressSurfaceState {
2054    fn sync_hover(&mut self, is_hovered: bool, now: Instant) -> bool {
2055        if self.is_hovered == is_hovered {
2056            return false;
2057        }
2058
2059        self.is_hovered = is_hovered;
2060
2061        if !self.is_pressed {
2062            if !is_hovered {
2063                self.clear_ripples();
2064            }
2065
2066            self.animate_to_interaction_target(now);
2067        }
2068
2069        true
2070    }
2071
2072    fn press(&mut self, origin: Point, now: Instant) {
2073        self.is_pressed = true;
2074        self.ripples.press(
2075            origin,
2076            now,
2077            RippleStart::Replace,
2078            RippleStyle::material_patterned(),
2079        );
2080        self.now = Some(now);
2081        self.animate_to_interaction_target(now);
2082    }
2083
2084    fn release(&mut self, is_hovered: bool, now: Instant) {
2085        self.release_with_hover(is_hovered, is_hovered, now);
2086    }
2087
2088    fn release_with_hover(&mut self, keep_ripple: bool, is_hovered: bool, now: Instant) {
2089        self.is_pressed = false;
2090        self.is_hovered = is_hovered;
2091
2092        if keep_ripple {
2093            self.ripples.release_replacing(now);
2094        } else {
2095            self.clear_ripples();
2096        }
2097
2098        self.now = Some(now);
2099        self.animate_to_interaction_target(now);
2100    }
2101
2102    fn snap_to_interaction_target(&mut self) {
2103        self.state_layer_opacity
2104            .snap_to(NavigationLayer::target(self.is_hovered, self.is_pressed));
2105    }
2106
2107    fn cancel(&mut self, now: Instant) {
2108        self.is_pressed = false;
2109        self.is_hovered = false;
2110        self.clear_ripples();
2111
2112        self.now = Some(now);
2113        self.animate_to_interaction_target(now);
2114    }
2115
2116    fn advance(&mut self, now: Instant) -> bool {
2117        self.now = Some(now);
2118        self.prune(now);
2119
2120        self.state_layer_opacity.advance(now) || self.has_visible_ripples(now)
2121    }
2122
2123    fn opacity(&self) -> f32 {
2124        NavigationLayer::opacity(self.state_layer_opacity.value)
2125    }
2126
2127    fn animate_to_interaction_target(&mut self, now: Instant) {
2128        self.state_layer_opacity.set_target(
2129            NavigationLayer::target(self.is_hovered, self.is_pressed),
2130            now,
2131            duration_ms(tokens::motion::DURATION_SHORT2_MS),
2132            tokens::motion::EASING_STANDARD,
2133        );
2134    }
2135
2136    fn clear_ripples(&mut self) {
2137        self.ripples.clear();
2138    }
2139
2140    fn prune(&mut self, now: Instant) {
2141        self.ripples.prune(now);
2142    }
2143
2144    fn has_visible_ripples(&self, now: Instant) -> bool {
2145        self.ripples.has_visible_ripples(now)
2146    }
2147}
2148
2149impl<Message, Renderer> Widget<Message, Theme, Renderer>
2150    for NavigationPressSurface<'_, Message, Renderer>
2151where
2152    Message: Clone,
2153    Renderer: geometry::Renderer + primitive::Renderer,
2154{
2155    fn tag(&self) -> tree::Tag {
2156        tree::Tag::of::<NavigationPressSurfaceState>()
2157    }
2158
2159    fn state(&self) -> tree::State {
2160        tree::State::new(NavigationPressSurfaceState::default())
2161    }
2162
2163    fn children(&self) -> Vec<Tree> {
2164        vec![Tree::new(&self.content)]
2165    }
2166
2167    fn diff(&self, tree: &mut Tree) {
2168        tree.diff_children(std::slice::from_ref(&self.content));
2169    }
2170
2171    fn size(&self) -> Size<Length> {
2172        self.content.as_widget().size()
2173    }
2174
2175    fn size_hint(&self) -> Size<Length> {
2176        self.content.as_widget().size_hint()
2177    }
2178
2179    fn layout(
2180        &mut self,
2181        tree: &mut Tree,
2182        renderer: &Renderer,
2183        limits: &layout::Limits,
2184    ) -> layout::Node {
2185        self.content
2186            .as_widget_mut()
2187            .layout(&mut tree.children[0], renderer, limits)
2188    }
2189
2190    fn operate(
2191        &mut self,
2192        tree: &mut Tree,
2193        layout: Layout<'_>,
2194        renderer: &Renderer,
2195        operation: &mut dyn Operation,
2196    ) {
2197        self.content
2198            .as_widget_mut()
2199            .operate(&mut tree.children[0], layout, renderer, operation);
2200    }
2201
2202    fn update(
2203        &mut self,
2204        tree: &mut Tree,
2205        event: &Event,
2206        layout: Layout<'_>,
2207        cursor: mouse::Cursor,
2208        renderer: &Renderer,
2209        clipboard: &mut dyn Clipboard,
2210        shell: &mut Shell<'_, Message>,
2211        viewport: &Rectangle,
2212    ) {
2213        self.content.as_widget_mut().update(
2214            &mut tree.children[0],
2215            event,
2216            layout,
2217            cursor,
2218            renderer,
2219            clipboard,
2220            shell,
2221            viewport,
2222        );
2223
2224        if shell.is_event_captured() {
2225            return;
2226        }
2227
2228        let state = tree.state.downcast_mut::<NavigationPressSurfaceState>();
2229        let now = match event {
2230            Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
2231            _ => None,
2232        };
2233        let is_touch_event = matches!(event, Event::Touch(_));
2234        let is_hovered = !is_touch_event && cursor.is_over(layout.bounds());
2235        let interaction = NavigationInteraction {
2236            event,
2237            cursor,
2238            is_hovered,
2239        };
2240        let pointer = NavigationPointer { event, cursor };
2241        let should_snap_initial_redraw_hover = interaction.should_snap_initial_redraw(state);
2242
2243        if interaction.should_sync_hover()
2244            && state.sync_hover(is_hovered, now.unwrap_or_else(Instant::now))
2245        {
2246            if should_snap_initial_redraw_hover {
2247                state.snap_to_interaction_target();
2248            }
2249
2250            shell.request_redraw();
2251        }
2252
2253        match event {
2254            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
2255            | Event::Touch(touch::Event::FingerPressed { .. })
2256                if pointer.is_over(layout.bounds()) =>
2257            {
2258                let indicator_bounds = self.indicator.bounds(layout.bounds());
2259
2260                if let Some(origin) = pointer.press_origin(indicator_bounds) {
2261                    state.press(origin, now.unwrap_or_else(Instant::now));
2262                    shell.request_redraw();
2263                    shell.capture_event();
2264                }
2265            }
2266            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
2267            | Event::Touch(touch::Event::FingerLifted { .. })
2268                if state.is_pressed =>
2269            {
2270                let is_released_over = pointer.is_over(layout.bounds());
2271                let is_touch_release = matches!(event, Event::Touch(_));
2272
2273                if is_touch_release {
2274                    state.release_with_hover(
2275                        is_released_over,
2276                        false,
2277                        now.unwrap_or_else(Instant::now),
2278                    );
2279                } else {
2280                    state.release(is_released_over, now.unwrap_or_else(Instant::now));
2281                }
2282                shell.request_redraw();
2283
2284                if is_released_over {
2285                    shell.publish(self.on_press.clone());
2286                }
2287
2288                shell.capture_event();
2289            }
2290            Event::Touch(touch::Event::FingerLost { .. }) if state.is_pressed => {
2291                state.cancel(now.unwrap_or_else(Instant::now));
2292                shell.request_redraw();
2293            }
2294            _ => {}
2295        }
2296
2297        if let Some(now) = now
2298            && state.advance(now)
2299        {
2300            shell.request_redraw();
2301        }
2302    }
2303
2304    fn mouse_interaction(
2305        &self,
2306        tree: &Tree,
2307        layout: Layout<'_>,
2308        cursor: mouse::Cursor,
2309        viewport: &Rectangle,
2310        renderer: &Renderer,
2311    ) -> mouse::Interaction {
2312        let content_interaction = self.content.as_widget().mouse_interaction(
2313            &tree.children[0],
2314            layout,
2315            cursor,
2316            viewport,
2317            renderer,
2318        );
2319
2320        if matches!(content_interaction, mouse::Interaction::None)
2321            && cursor.is_over(layout.bounds())
2322        {
2323            mouse::Interaction::Pointer
2324        } else {
2325            content_interaction
2326        }
2327    }
2328
2329    fn draw(
2330        &self,
2331        tree: &Tree,
2332        renderer: &mut Renderer,
2333        theme: &Theme,
2334        renderer_style: &renderer::Style,
2335        layout: Layout<'_>,
2336        cursor: mouse::Cursor,
2337        viewport: &Rectangle,
2338    ) {
2339        self.content.as_widget().draw(
2340            &tree.children[0],
2341            renderer,
2342            theme,
2343            renderer_style,
2344            layout,
2345            cursor,
2346            viewport,
2347        );
2348
2349        let state = tree.state.downcast_ref::<NavigationPressSurfaceState>();
2350        let indicator_bounds = self.indicator.bounds(layout.bounds());
2351        let now = state.now.unwrap_or_else(Instant::now);
2352        let opacity = NavigationDrawState {
2353            state,
2354            cursor,
2355            bounds: layout.bounds(),
2356        }
2357        .opacity();
2358        let layer_color = layer_color(theme, self.layer);
2359
2360        if opacity > 0.0 {
2361            renderer.fill_quad(
2362                renderer::Quad {
2363                    bounds: indicator_bounds,
2364                    border: border::rounded(tokens::shape::CORNER_FULL),
2365                    snap: cfg!(feature = "crisp"),
2366                    ..renderer::Quad::default()
2367                },
2368                state_layer(layer_color, opacity),
2369            );
2370        }
2371
2372        draw_ripples(
2373            renderer,
2374            indicator_bounds,
2375            &state.ripples,
2376            layer_color,
2377            RippleConfig::bounded(border::radius(tokens::shape::CORNER_FULL)),
2378            now,
2379        );
2380    }
2381
2382    fn overlay<'b>(
2383        &'b mut self,
2384        tree: &'b mut Tree,
2385        layout: Layout<'b>,
2386        renderer: &Renderer,
2387        viewport: &Rectangle,
2388        translation: Vector,
2389    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
2390        self.content.as_widget_mut().overlay(
2391            &mut tree.children[0],
2392            layout,
2393            renderer,
2394            viewport,
2395            translation,
2396        )
2397    }
2398}
2399
2400impl<'a, Message, Renderer> From<NavigationPressSurface<'a, Message, Renderer>>
2401    for Element<'a, Message, Theme, Renderer>
2402where
2403    Message: Clone + 'a,
2404    Renderer: geometry::Renderer + primitive::Renderer + 'a,
2405{
2406    fn from(surface: NavigationPressSurface<'a, Message, Renderer>) -> Self {
2407        Element::new(surface)
2408    }
2409}
2410
2411struct NavigationLayer;
2412
2413impl NavigationLayer {
2414    fn target(is_hovered: bool, _is_pressed: bool) -> f32 {
2415        if is_hovered {
2416            HOVERED_LAYER_OPACITY
2417        } else {
2418            0.0
2419        }
2420    }
2421
2422    fn opacity(interaction_opacity: f32) -> f32 {
2423        interaction_opacity
2424    }
2425}
2426
2427#[derive(Debug, Clone, Copy)]
2428struct NavigationInteraction<'a> {
2429    event: &'a Event,
2430    cursor: mouse::Cursor,
2431    is_hovered: bool,
2432}
2433
2434impl NavigationInteraction<'_> {
2435    fn should_sync_hover(self) -> bool {
2436        match self.event {
2437            Event::Window(window::Event::RedrawRequested(_)) => {
2438                !matches!(self.cursor, mouse::Cursor::Unavailable)
2439            }
2440            Event::Mouse(_) | Event::Touch(_) => true,
2441            _ => false,
2442        }
2443    }
2444
2445    fn should_snap_initial_redraw(self, state: &NavigationPressSurfaceState) -> bool {
2446        matches!(self.event, Event::Window(window::Event::RedrawRequested(_)))
2447            && state.now.is_none()
2448            && self.is_hovered
2449    }
2450}
2451
2452#[derive(Debug, Clone, Copy)]
2453struct NavigationDrawState<'a> {
2454    state: &'a NavigationPressSurfaceState,
2455    cursor: mouse::Cursor,
2456    bounds: Rectangle,
2457}
2458
2459impl NavigationDrawState<'_> {
2460    fn opacity(self) -> f32 {
2461        if self.cursor.is_over(self.bounds) && self.state.now.is_none() {
2462            NavigationLayer::opacity(NavigationLayer::target(true, false))
2463        } else {
2464            self.state.opacity()
2465        }
2466    }
2467}
2468
2469#[derive(Debug, Clone, Copy)]
2470struct NavigationPointer<'a> {
2471    event: &'a Event,
2472    cursor: mouse::Cursor,
2473}
2474
2475impl NavigationPointer<'_> {
2476    fn is_over(self, bounds: Rectangle) -> bool {
2477        if self.cursor.position().is_some() {
2478            return self.cursor.is_over(bounds);
2479        }
2480
2481        if self.cursor.is_levitating() {
2482            return false;
2483        }
2484
2485        self.position()
2486            .map(|position| bounds.contains(position))
2487            .unwrap_or_else(|| self.cursor.is_over(bounds))
2488    }
2489
2490    fn press_origin(self, indicator_bounds: Rectangle) -> Option<Point> {
2491        let position = self.cursor.position().or_else(|| self.position())?;
2492
2493        if self.cursor.is_levitating() {
2494            return None;
2495        }
2496
2497        Some(position - Vector::new(indicator_bounds.x, indicator_bounds.y))
2498    }
2499
2500    fn position(self) -> Option<Point> {
2501        match self.event {
2502            Event::Touch(touch::Event::FingerPressed { position, .. })
2503            | Event::Touch(touch::Event::FingerMoved { position, .. })
2504            | Event::Touch(touch::Event::FingerLifted { position, .. })
2505            | Event::Touch(touch::Event::FingerLost { position, .. }) => Some(*position),
2506            _ => None,
2507        }
2508    }
2509}
2510
2511fn indicator_layer<'a, Message, Renderer>(
2512    target_width: f32,
2513    height: f32,
2514    size_progress: f32,
2515    alpha_progress: f32,
2516) -> Container<'a, Message, Theme, Renderer>
2517where
2518    Message: 'a,
2519    Renderer: geometry::Renderer + primitive::Renderer + 'a,
2520{
2521    let indicator = Container::new(Space::new())
2522        .width(Length::Fixed(animated_indicator_width(
2523            target_width,
2524            size_progress,
2525        )))
2526        .height(Length::Fixed(height))
2527        .style(move |theme| active_indicator(theme, alpha_progress));
2528
2529    Container::new(indicator)
2530        .width(Length::Fixed(target_width))
2531        .height(Length::Fixed(height))
2532        .align_x(alignment::Horizontal::Center)
2533        .align_y(alignment::Vertical::Center)
2534}
2535
2536#[derive(Debug, Clone, Copy)]
2537enum NavigationStateLayer {
2538    BarOrRail,
2539    Drawer { progress: f32 },
2540}
2541
2542fn animated_indicator_width(target_width: f32, progress: f32) -> f32 {
2543    // AndroidX Material3 measures the selected indicator width from animation progress.
2544    target_width * progress.max(0.0)
2545}
2546
2547struct BarMetrics;
2548
2549impl BarMetrics {
2550    fn item_bottom_padding() -> f32 {
2551        let label = tokens::component::navigation_bar::LABEL_TEXT;
2552
2553        (tokens::component::navigation_bar::CONTAINER_HEIGHT
2554            - tokens::component::navigation_bar::INDICATOR_VERTICAL_OFFSET
2555            - tokens::component::navigation_bar::ACTIVE_INDICATOR_HEIGHT
2556            - tokens::component::navigation_bar::INDICATOR_TO_LABEL_PADDING
2557            - label.line_height)
2558            .max(0.0)
2559    }
2560}
2561
2562struct RailMetrics;
2563
2564impl RailMetrics {
2565    fn min_height(destination_count: usize, has_header: bool) -> f32 {
2566        let header_height = if has_header {
2567            Self::header_slot_height()
2568        } else {
2569            0.0
2570        };
2571        let child_count = destination_count + usize::from(has_header);
2572        let spacing_count = child_count.saturating_sub(1);
2573
2574        tokens::component::navigation_rail::CONTENT_TOP_MARGIN
2575            + header_height
2576            + destination_count as f32 * Self::item_slot_height()
2577            + spacing_count as f32 * tokens::component::navigation_rail::VERTICAL_PADDING
2578            + tokens::component::navigation_rail::VERTICAL_PADDING
2579    }
2580
2581    fn item_content_top_padding() -> f32 {
2582        tokens::component::navigation_rail::ITEM_TOP_PADDING
2583    }
2584
2585    fn header_bottom_padding() -> f32 {
2586        tokens::component::navigation_rail::HEADER_PADDING
2587    }
2588
2589    fn header_slot_height() -> f32 {
2590        tokens::component::icon_button::CONTAINER_HEIGHT + Self::header_bottom_padding()
2591    }
2592
2593    fn item_slot_height() -> f32 {
2594        tokens::component::navigation_rail::ITEM_HEIGHT
2595    }
2596
2597    fn collapsed_label_top_padding() -> f32 {
2598        Self::item_content_top_padding()
2599            + tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT
2600            + tokens::component::navigation_rail::ITEM_VERTICAL_PADDING
2601    }
2602
2603    fn collapsed_label_width() -> f32 {
2604        tokens::component::navigation_rail::ACTIVE_INDICATOR_WIDTH
2605    }
2606
2607    #[cfg(test)]
2608    fn collapsed_icon_center_x() -> f32 {
2609        tokens::component::navigation_rail::CONTAINER_WIDTH / 2.0
2610    }
2611
2612    #[cfg(test)]
2613    fn collapsed_icon_center_y() -> f32 {
2614        Self::item_content_top_padding()
2615            + tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT / 2.0
2616    }
2617
2618    #[cfg(test)]
2619    fn first_item_y_after_header() -> f32 {
2620        tokens::component::navigation_rail::CONTENT_TOP_MARGIN
2621            + Self::header_slot_height()
2622            + tokens::component::navigation_rail::VERTICAL_PADDING
2623    }
2624}
2625
2626#[derive(Debug, Clone, Copy)]
2627struct ExpandedRailMetrics {
2628    width: f32,
2629}
2630
2631impl ExpandedRailMetrics {
2632    fn new(width: f32) -> Self {
2633        Self {
2634            width: width.clamp(
2635                tokens::component::navigation_rail::CONTAINER_WIDTH,
2636                tokens::component::navigation_drawer::CONTAINER_WIDTH,
2637            ),
2638        }
2639    }
2640
2641    fn width(self) -> f32 {
2642        self.width
2643    }
2644
2645    fn indicator_width(self) -> f32 {
2646        (self.width
2647            - tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL * 2.0)
2648            .max(0.0)
2649    }
2650
2651    fn progress(self) -> f32 {
2652        let range = tokens::component::navigation_rail::EXPANDED_CONTAINER_WIDTH
2653            - tokens::component::navigation_rail::CONTAINER_WIDTH;
2654
2655        if range <= f32::EPSILON {
2656            1.0
2657        } else {
2658            ((self.width - tokens::component::navigation_rail::CONTAINER_WIDTH) / range)
2659                .clamp(0.0, 1.0)
2660        }
2661    }
2662
2663    fn label_alpha(self) -> f32 {
2664        ((self.progress() - 0.6) / 0.4).clamp(0.0, 1.0)
2665    }
2666
2667    fn item_vertical_inset(self) -> f32 {
2668        Self::item_vertical_inset_for(self.progress())
2669    }
2670
2671    fn item_vertical_inset_for(progress: f32) -> f32 {
2672        lerp(
2673            RailMetrics::item_content_top_padding(),
2674            Self::expanded_item_vertical_inset(),
2675            progress.clamp(0.0, 1.0),
2676        )
2677    }
2678
2679    fn indicator_height(self) -> f32 {
2680        Self::indicator_height_for(self.progress())
2681    }
2682
2683    fn indicator_height_for(progress: f32) -> f32 {
2684        lerp(
2685            tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT,
2686            tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_HEIGHT,
2687            progress.clamp(0.0, 1.0),
2688        )
2689    }
2690
2691    fn label_leading_padding(self) -> f32 {
2692        Self::icon_anchor_width() + tokens::component::navigation_rail::ICON_LABEL_HORIZONTAL_SPACE
2693    }
2694
2695    fn badge_uses_icon_anchor(self) -> bool {
2696        Self::badge_uses_icon_anchor_for(self.label_alpha())
2697    }
2698
2699    fn badge_uses_icon_anchor_for(label_alpha: f32) -> bool {
2700        label_alpha <= 0.0
2701    }
2702
2703    fn trailing_badge_alpha(self) -> f32 {
2704        Self::trailing_badge_alpha_for(self.label_alpha())
2705    }
2706
2707    fn trailing_badge_alpha_for(label_alpha: f32) -> f32 {
2708        label_alpha.clamp(0.0, 1.0)
2709    }
2710
2711    fn collapsed_label_alpha(self) -> f32 {
2712        Self::collapsed_label_alpha_for(self.label_alpha())
2713    }
2714
2715    fn collapsed_label_alpha_for(label_alpha: f32) -> f32 {
2716        (1.0 - label_alpha).clamp(0.0, 1.0)
2717    }
2718
2719    fn header_leading_space(self) -> f32 {
2720        tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2721            + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2722            - (tokens::component::icon_button::CONTAINER_WIDTH
2723                - tokens::component::navigation_rail::ICON_SIZE)
2724                / 2.0
2725    }
2726
2727    fn header_title_spacing(self) -> f32 {
2728        let label_start =
2729            tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2730                + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2731                + tokens::component::navigation_rail::ICON_SIZE
2732                + tokens::component::navigation_rail::ICON_LABEL_HORIZONTAL_SPACE;
2733
2734        (label_start
2735            - self.header_leading_space()
2736            - tokens::component::icon_button::CONTAINER_WIDTH)
2737            .max(0.0)
2738    }
2739
2740    #[cfg(test)]
2741    fn expanded_icon_center_x(self) -> f32 {
2742        tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2743            + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2744            + tokens::component::navigation_rail::ICON_SIZE / 2.0
2745    }
2746
2747    #[cfg(test)]
2748    fn expanded_icon_center_y(self) -> f32 {
2749        self.item_vertical_inset() + self.indicator_height() / 2.0
2750    }
2751
2752    fn icon_anchor_width() -> f32 {
2753        tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2754            + tokens::component::navigation_rail::ICON_SIZE
2755    }
2756
2757    fn expanded_item_vertical_inset() -> f32 {
2758        ((tokens::component::navigation_rail::ITEM_HEIGHT
2759            - tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_HEIGHT)
2760            / 2.0)
2761            .max(0.0)
2762    }
2763}
2764
2765pub fn expanded_rail_width(progress: f32) -> f32 {
2766    lerp(
2767        tokens::component::navigation_rail::CONTAINER_WIDTH,
2768        tokens::component::navigation_rail::EXPANDED_CONTAINER_WIDTH,
2769        progress.clamp(0.0, 1.0),
2770    )
2771}
2772
2773#[derive(Debug, Clone, Copy)]
2774struct DrawerMetrics {
2775    width: f32,
2776}
2777
2778impl DrawerMetrics {
2779    fn new(width: f32) -> Self {
2780        Self {
2781            width: width.clamp(
2782                tokens::component::navigation_drawer::MINIMUM_CONTAINER_WIDTH,
2783                tokens::component::navigation_drawer::CONTAINER_WIDTH,
2784            ),
2785        }
2786    }
2787
2788    fn width(self) -> f32 {
2789        self.width
2790    }
2791
2792    fn indicator_width(self) -> f32 {
2793        (self.width - tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING * 2.0).max(0.0)
2794    }
2795
2796    fn menu_header_leading_space() -> f32 {
2797        tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
2798            + tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE
2799            - (tokens::component::icon_button::CONTAINER_WIDTH
2800                - tokens::component::navigation_drawer::ICON_SIZE)
2801                / 2.0
2802    }
2803
2804    fn menu_header_title_spacing() -> f32 {
2805        let label_start = tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
2806            + tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE
2807            + tokens::component::navigation_drawer::ICON_SIZE
2808            + tokens::component::navigation_drawer::ICON_LABEL_SPACE;
2809
2810        (label_start
2811            - Self::menu_header_leading_space()
2812            - tokens::component::icon_button::CONTAINER_WIDTH)
2813            .max(0.0)
2814    }
2815
2816    fn badge_space() -> f32 {
2817        tokens::component::navigation_drawer::LABEL_BADGE_SPACE
2818    }
2819}
2820
2821fn expanded_rail_icon_layer<'a, Message, Renderer>(
2822    icon: &'static str,
2823    progress: f32,
2824    height: f32,
2825    badge: Option<Badge>,
2826) -> Container<'a, Message, Theme, Renderer>
2827where
2828    Message: 'a,
2829    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2830    Font: Into<Renderer::Font>,
2831{
2832    let icon = destination_icon_anchor::<Message, Renderer>(
2833        icon,
2834        tokens::component::navigation_rail::ICON_SIZE,
2835        progress,
2836        badge,
2837        true,
2838    )
2839    .width(Length::Fixed(tokens::component::navigation_rail::ICON_SIZE))
2840    .height(Length::Fixed(height));
2841
2842    Container::new(icon)
2843        .width(Length::Fixed(ExpandedRailMetrics::icon_anchor_width()))
2844        .height(Length::Fixed(height))
2845        .align_x(alignment::Horizontal::Right)
2846        .align_y(alignment::Vertical::Center)
2847}
2848
2849fn collapsed_rail_label<'a, Message, Renderer>(
2850    label: &'static str,
2851    progress: f32,
2852    alpha: f32,
2853) -> Container<'a, Message, Theme, Renderer>
2854where
2855    Message: 'a,
2856    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2857    Font: Into<Renderer::Font>,
2858{
2859    let alpha = alpha.clamp(0.0, 1.0);
2860    let scale = tokens::component::navigation_rail::LABEL_TEXT;
2861    let label = type_text(label, scale).style(move |theme| text::Style {
2862        color: Some(alpha_color(bar_or_rail_label_color(theme, progress), alpha)),
2863    });
2864
2865    Container::new(label)
2866        .padding(Padding {
2867            top: RailMetrics::collapsed_label_top_padding(),
2868            right: 0.0,
2869            bottom: 0.0,
2870            left: 0.0,
2871        })
2872        .align_x(alignment::Horizontal::Center)
2873}
2874
2875fn destination_icon_anchor<'a, Message, Renderer>(
2876    icon: &'static str,
2877    size: f32,
2878    progress: f32,
2879    badge: Option<Badge>,
2880    drawer: bool,
2881) -> Container<'a, Message, Theme, Renderer>
2882where
2883    Message: 'a,
2884    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2885    Font: Into<Renderer::Font>,
2886{
2887    let icon: Element<'a, Message, Theme, Renderer> =
2888        destination_icon::<Message, Renderer>(icon, size, progress, drawer).into();
2889    let anchor = if let Some(badge) = badge {
2890        badge_widget::badged_box(
2891            icon,
2892            destination_badge::<Message, Renderer>(badge),
2893            destination_badge_placement(badge),
2894        )
2895        .into()
2896    } else {
2897        icon
2898    };
2899
2900    Container::new(anchor)
2901        .align_x(alignment::Horizontal::Center)
2902        .align_y(alignment::Vertical::Center)
2903}
2904
2905fn destination_badge<'a, Message, Renderer>(badge: Badge) -> Element<'a, Message, Theme, Renderer>
2906where
2907    Message: 'a,
2908    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2909    Font: Into<Renderer::Font>,
2910{
2911    match badge {
2912        Badge::Small => badge_widget::small().into(),
2913        Badge::Large(label) => badge_widget::large(label).into(),
2914    }
2915}
2916
2917fn destination_badge_with_alpha<'a, Message, Renderer>(
2918    badge: Badge,
2919    alpha: f32,
2920) -> Element<'a, Message, Theme, Renderer>
2921where
2922    Message: 'a,
2923    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2924    Font: Into<Renderer::Font>,
2925{
2926    let alpha = alpha.clamp(0.0, 1.0);
2927
2928    match badge {
2929        Badge::Small => badge_widget::small()
2930            .style(move |theme| alpha_badge_style(theme, alpha))
2931            .into(),
2932        Badge::Large(label) => badge_widget::large(label)
2933            .style(move |theme| alpha_badge_style(theme, alpha))
2934            .into(),
2935    }
2936}
2937
2938fn alpha_badge_style(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
2939    let mut style = crate::style::badge::default(theme);
2940
2941    if let Some(Background::Color(color)) = style.background {
2942        style.background = Some(Background::Color(alpha_color(color, alpha)));
2943    }
2944
2945    style.text_color = style.text_color.map(|color| alpha_color(color, alpha));
2946    style
2947}
2948
2949fn destination_badge_placement(badge: Badge) -> badge_widget::BadgedBoxPlacement {
2950    match badge {
2951        Badge::Small => badge_widget::BadgedBoxPlacement::IconOnly,
2952        Badge::Large(_) => badge_widget::BadgedBoxPlacement::WithContent,
2953    }
2954}
2955
2956fn destination_icon<'a, Message, Renderer>(
2957    icon: &'static str,
2958    size: f32,
2959    progress: f32,
2960    drawer: bool,
2961) -> Stack<'a, Message, Theme, Renderer>
2962where
2963    Message: 'a,
2964    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2965    Font: Into<Renderer::Font>,
2966{
2967    let outline = fonts::icon(icon, size)
2968        .width(Length::Fixed(size))
2969        .height(Length::Fixed(size))
2970        .center()
2971        .style(move |theme| text::Style {
2972            color: Some(destination_icon_outline_color(theme, progress)),
2973        });
2974    let filled = fonts::filled_icon(icon, size)
2975        .width(Length::Fixed(size))
2976        .height(Length::Fixed(size))
2977        .center()
2978        .style(move |theme| text::Style {
2979            color: Some(destination_icon_filled_color(theme, progress, drawer)),
2980        });
2981
2982    Stack::new()
2983        .width(Length::Fixed(size))
2984        .height(Length::Fixed(size))
2985        .push(outline)
2986        .push(filled)
2987}
2988
2989fn destination_icon_outline_color(theme: &Theme, progress: f32) -> Color {
2990    alpha_color(
2991        theme.colors().surface.text_variant,
2992        1.0 - progress.clamp(0.0, 1.0),
2993    )
2994}
2995
2996fn destination_icon_filled_color(theme: &Theme, progress: f32, drawer: bool) -> Color {
2997    let color = if drawer {
2998        drawer_content_color(theme, 1.0)
2999    } else {
3000        bar_or_rail_icon_color(theme, 1.0)
3001    };
3002
3003    alpha_color(color, progress.clamp(0.0, 1.0))
3004}
3005
3006fn type_text<'a, Renderer>(
3007    content: &'static str,
3008    scale: tokens::typography::TypeScale,
3009) -> Text<'a, Theme, Renderer>
3010where
3011    Renderer: core_text::Renderer + 'a,
3012    Font: Into<Renderer::Font>,
3013{
3014    Text::new(content)
3015        .font(fonts::roboto_for_type_scale(scale))
3016        .size(scale.size)
3017        .line_height(LineHeight::Absolute(scale.line_height.into()))
3018}
3019
3020fn single_line_type_text<'a, Renderer>(
3021    content: &'static str,
3022    scale: tokens::typography::TypeScale,
3023) -> Text<'a, Theme, Renderer>
3024where
3025    Renderer: core_text::Renderer + 'a,
3026    Font: Into<Renderer::Font>,
3027{
3028    type_text(content, scale).wrapping(text::Wrapping::None)
3029}
3030
3031fn bar_container(theme: &Theme) -> iced_widget::container::Style {
3032    let colors = theme.colors();
3033
3034    iced_widget::container::Style {
3035        background: Some(Background::Color(colors.surface.color)),
3036        text_color: Some(colors.surface.text),
3037        border: border::rounded(tokens::shape::CORNER_NONE),
3038        shadow: shadow_from_level(
3039            tokens::component::navigation_bar::CONTAINER_ELEVATION_LEVEL,
3040            colors.shadow,
3041        ),
3042        ..iced_widget::container::Style::default()
3043    }
3044}
3045
3046fn rail_container(theme: &Theme) -> iced_widget::container::Style {
3047    let colors = theme.colors();
3048
3049    iced_widget::container::Style {
3050        background: Some(Background::Color(colors.surface.color)),
3051        text_color: Some(colors.surface.text),
3052        border: border::rounded(tokens::shape::CORNER_NONE),
3053        shadow: shadow_from_level(
3054            tokens::component::navigation_rail::CONTAINER_ELEVATION_LEVEL,
3055            colors.shadow,
3056        ),
3057        ..iced_widget::container::Style::default()
3058    }
3059}
3060
3061fn drawer_container(theme: &Theme) -> iced_widget::container::Style {
3062    let colors = theme.colors();
3063
3064    iced_widget::container::Style {
3065        background: Some(Background::Color(colors.surface.color)),
3066        text_color: Some(colors.surface.text),
3067        border: border::rounded(tokens::shape::CORNER_LARGE),
3068        shadow: shadow_from_level(
3069            tokens::component::navigation_drawer::STANDARD_CONTAINER_ELEVATION_LEVEL,
3070            colors.shadow,
3071        ),
3072        ..iced_widget::container::Style::default()
3073    }
3074}
3075
3076fn active_indicator(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
3077    let mut color = theme.colors().secondary.container;
3078    color.a *= alpha.clamp(0.0, 1.0);
3079
3080    iced_widget::container::Style {
3081        background: Some(Background::Color(color)),
3082        text_color: Some(theme.colors().secondary.container_text),
3083        border: border::rounded(tokens::shape::CORNER_FULL),
3084        ..iced_widget::container::Style::default()
3085    }
3086}
3087
3088fn headline_text_style(theme: &Theme) -> text::Style {
3089    text::Style {
3090        color: Some(theme.colors().surface.text_variant),
3091    }
3092}
3093
3094fn bar_or_rail_icon_color(theme: &Theme, progress: f32) -> Color {
3095    let colors = theme.colors();
3096
3097    mix(
3098        colors.surface.text_variant,
3099        colors.secondary.container_text,
3100        progress,
3101    )
3102}
3103
3104fn bar_or_rail_label_color(theme: &Theme, progress: f32) -> Color {
3105    let colors = theme.colors();
3106
3107    mix(colors.surface.text_variant, colors.surface.text, progress)
3108}
3109
3110fn drawer_content_color(theme: &Theme, progress: f32) -> Color {
3111    let colors = theme.colors();
3112
3113    mix(
3114        colors.surface.text_variant,
3115        colors.secondary.container_text,
3116        progress,
3117    )
3118}
3119
3120fn layer_color(theme: &Theme, layer: NavigationStateLayer) -> Color {
3121    let colors = theme.colors();
3122
3123    match layer {
3124        // AndroidX Material3 applies the selectable interaction to the whole item, but remaps the
3125        // ripple onto the active-indicator pill. Its ripple color comes from the navigation
3126        // container content color, not from the active icon color.
3127        NavigationStateLayer::BarOrRail => colors.surface.text,
3128        NavigationStateLayer::Drawer { progress } => mix(
3129            colors.surface.text,
3130            colors.secondary.container_text,
3131            progress,
3132        ),
3133    }
3134}
3135
3136#[cfg(test)]
3137#[path = "../../../tests/widget/component/navigation.rs"]
3138mod tests;