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