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