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