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, navigation_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, navigation_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 navigation_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 navigation_rail_min_height(destination_count: usize, has_header: bool) -> f32 {
488 let header_height = if has_header {
489 navigation_rail_header_slot_height()
490 } else {
491 0.0
492 };
493 let child_count = destination_count + usize::from(has_header);
494 let spacing_count = child_count.saturating_sub(1);
495
496 tokens::component::navigation_rail::CONTENT_TOP_MARGIN
497 + header_height
498 + destination_count as f32 * navigation_rail_item_slot_height()
499 + spacing_count as f32 * tokens::component::navigation_rail::VERTICAL_PADDING
500 + tokens::component::navigation_rail::VERTICAL_PADDING
501}
502
503pub fn suite<'a, Id>(
508 destinations: &'a [Destination<Id>],
509 state: &'a NavigationState<Id>,
510) -> Suite<'a, Id> {
511 Suite::new(destinations, state)
512}
513
514#[derive(Debug, Clone, Copy)]
516pub struct Suite<'a, Id> {
517 destinations: &'a [Destination<Id>],
518 state: &'a NavigationState<Id>,
519 layout: AdaptiveLayout,
520}
521
522impl<'a, Id> Suite<'a, Id> {
523 pub fn new(destinations: &'a [Destination<Id>], state: &'a NavigationState<Id>) -> Self {
529 Self {
530 destinations,
531 state,
532 layout: AdaptiveLayout::NavigationRail,
533 }
534 }
535
536 pub fn layout(mut self, layout: AdaptiveLayout) -> Self {
538 self.layout = layout;
539 self
540 }
541
542 pub fn window_size(mut self, size: Size) -> Self {
544 self.layout = adaptive_layout(size.width, size.height);
545 self
546 }
547
548 pub fn dimensions(mut self, width: f32, height: f32) -> Self {
550 self.layout = adaptive_layout(width, height);
551 self
552 }
553
554 pub fn with_menu<Message>(
556 self,
557 headline: &'static str,
558 on_menu: Message,
559 ) -> SuiteWithMenu<'a, Id, Message> {
560 SuiteWithMenu {
561 suite: self,
562 headline,
563 on_menu,
564 }
565 }
566
567 pub fn view<Message, Renderer, F>(
569 self,
570 on_select: F,
571 content: impl Into<Element<'a, Message, Theme, Renderer>>,
572 ) -> Element<'a, Message, Theme, Renderer>
573 where
574 Id: Copy + Eq + 'a,
575 Message: Clone + 'a,
576 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
577 Font: Into<Renderer::Font>,
578 F: Fn(Id) -> Message + Clone + 'a,
579 {
580 navigation_suite_for_layout(
581 self.layout,
582 self.destinations,
583 self.state.selection(),
584 on_select,
585 content,
586 )
587 }
588}
589
590#[derive(Debug, Clone, Copy)]
592pub struct SuiteWithMenu<'a, Id, Message> {
593 suite: Suite<'a, Id>,
594 headline: &'static str,
595 on_menu: Message,
596}
597
598impl<'a, Id, Message> SuiteWithMenu<'a, Id, Message> {
599 pub fn layout(mut self, layout: AdaptiveLayout) -> Self {
601 self.suite = self.suite.layout(layout);
602 self
603 }
604
605 pub fn window_size(mut self, size: Size) -> Self {
607 self.suite = self.suite.window_size(size);
608 self
609 }
610
611 pub fn dimensions(mut self, width: f32, height: f32) -> Self {
613 self.suite = self.suite.dimensions(width, height);
614 self
615 }
616
617 pub fn view<Renderer, F>(
619 self,
620 on_select: F,
621 content: impl Into<Element<'a, Message, Theme, Renderer>>,
622 ) -> Element<'a, Message, Theme, Renderer>
623 where
624 Id: Copy + Eq + 'a,
625 Message: Clone + 'a,
626 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
627 Font: Into<Renderer::Font>,
628 F: Fn(Id) -> Message + Clone + 'a,
629 {
630 navigation_suite_for_layout_with_menu(
631 self.headline,
632 self.suite.layout,
633 self.suite.destinations,
634 self.suite.state,
635 on_select,
636 self.on_menu,
637 content,
638 )
639 }
640}
641
642pub fn navigation_suite<'a, Id, Message, Renderer, F>(
643 width: f32,
644 height: f32,
645 destinations: &'a [Destination<Id>],
646 selection: Selection<Id>,
647 on_select: F,
648 content: impl Into<Element<'a, Message, Theme, Renderer>>,
649) -> Element<'a, Message, Theme, Renderer>
650where
651 Id: Copy + Eq + 'a,
652 Message: Clone + 'a,
653 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
654 Font: Into<Renderer::Font>,
655 F: Fn(Id) -> Message + Clone + 'a,
656{
657 navigation_suite_for_layout(
658 adaptive_layout(width, height),
659 destinations,
660 selection,
661 on_select,
662 content,
663 )
664}
665
666pub fn navigation_suite_for_layout<'a, Id, Message, Renderer, F>(
667 layout: AdaptiveLayout,
668 destinations: &'a [Destination<Id>],
669 selection: Selection<Id>,
670 on_select: F,
671 content: impl Into<Element<'a, Message, Theme, Renderer>>,
672) -> Element<'a, Message, Theme, Renderer>
673where
674 Id: Copy + Eq + 'a,
675 Message: Clone + 'a,
676 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
677 Font: Into<Renderer::Font>,
678 F: Fn(Id) -> Message + Clone + 'a,
679{
680 let content = content.into();
681
682 match layout {
683 AdaptiveLayout::NavigationBar => Column::new()
684 .width(Length::Fill)
685 .height(Length::Fill)
686 .push(content)
687 .push(navigation_bar(destinations, selection, on_select))
688 .into(),
689 AdaptiveLayout::NavigationRail => Row::new()
690 .width(Length::Fill)
691 .height(Length::Fill)
692 .push(navigation_rail(destinations, selection, on_select))
693 .push(content)
694 .into(),
695 }
696}
697
698pub fn navigation_suite_with_menu<'a, Id, Message, Renderer, F>(
699 headline: &'static str,
700 width: f32,
701 height: f32,
702 destinations: &'a [Destination<Id>],
703 state: &NavigationState<Id>,
704 on_select: F,
705 on_menu: Message,
706 content: impl Into<Element<'a, Message, Theme, Renderer>>,
707) -> Element<'a, Message, Theme, Renderer>
708where
709 Id: Copy + Eq + 'a,
710 Message: Clone + 'a,
711 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
712 Font: Into<Renderer::Font>,
713 F: Fn(Id) -> Message + Clone + 'a,
714{
715 navigation_suite_for_layout_with_menu(
716 headline,
717 adaptive_layout(width, height),
718 destinations,
719 state,
720 on_select,
721 on_menu,
722 content,
723 )
724}
725
726pub fn navigation_suite_for_layout_with_menu<'a, Id, Message, Renderer, F>(
727 headline: &'static str,
728 layout: AdaptiveLayout,
729 destinations: &'a [Destination<Id>],
730 state: &NavigationState<Id>,
731 on_select: F,
732 on_menu: Message,
733 content: impl Into<Element<'a, Message, Theme, Renderer>>,
734) -> Element<'a, Message, Theme, Renderer>
735where
736 Id: Copy + Eq + 'a,
737 Message: Clone + 'a,
738 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
739 Font: Into<Renderer::Font>,
740 F: Fn(Id) -> Message + Clone + 'a,
741{
742 let menu_progress = state.menu_progress();
743 let content = content.into();
744 let selection = state.selection();
745
746 match layout {
747 AdaptiveLayout::NavigationBar => Column::new()
748 .width(Length::Fill)
749 .height(Length::Fill)
750 .push(content)
751 .push(navigation_bar(destinations, selection, on_select))
752 .into(),
753 AdaptiveLayout::NavigationRail => Row::new()
754 .width(Length::Fill)
755 .height(Length::Fill)
756 .push(if state.is_menu_visible() {
757 navigation_rail_expanded_with_menu_at_width(
758 headline,
759 destinations,
760 selection,
761 on_select,
762 on_menu,
763 navigation_rail_expanded_width_for_progress(menu_progress),
764 )
765 } else {
766 navigation_rail_with_menu_at_progress(
767 destinations,
768 selection,
769 on_select,
770 on_menu,
771 menu_progress,
772 )
773 })
774 .push(content)
775 .into(),
776 }
777}
778
779pub fn navigation_bar<'a, Id, Message, Renderer, F>(
780 destinations: &'a [Destination<Id>],
781 selection: Selection<Id>,
782 on_select: F,
783) -> Container<'a, Message, Theme, Renderer>
784where
785 Id: Copy + Eq + 'a,
786 Message: Clone + 'a,
787 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
788 Font: Into<Renderer::Font>,
789 F: Fn(Id) -> Message + Clone + 'a,
790{
791 let mut items = Row::new()
792 .width(Length::Fill)
793 .height(Length::Fixed(
794 tokens::component::navigation_bar::CONTAINER_HEIGHT,
795 ))
796 .spacing(tokens::component::navigation_bar::ITEM_HORIZONTAL_PADDING);
797
798 for destination in destinations {
799 items = items.push(
800 Container::new(navigation_bar_item(
801 *destination,
802 selection,
803 on_select.clone(),
804 ))
805 .width(Length::FillPortion(1)),
806 );
807 }
808
809 Container::new(items)
810 .width(Length::Fill)
811 .height(Length::Fixed(
812 tokens::component::navigation_bar::CONTAINER_HEIGHT,
813 ))
814 .padding(Padding {
815 top: 0.0,
816 right: tokens::component::navigation_bar::ITEM_HORIZONTAL_PADDING,
817 bottom: 0.0,
818 left: tokens::component::navigation_bar::ITEM_HORIZONTAL_PADDING,
819 })
820 .style(navigation_bar_container)
821}
822
823pub fn navigation_rail<'a, Id, Message, Renderer, F>(
824 destinations: &'a [Destination<Id>],
825 selection: Selection<Id>,
826 on_select: F,
827) -> Container<'a, Message, Theme, Renderer>
828where
829 Id: Copy + Eq + 'a,
830 Message: Clone + 'a,
831 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
832 Font: Into<Renderer::Font>,
833 F: Fn(Id) -> Message + Clone + 'a,
834{
835 navigation_rail_with_optional_header(destinations, selection, on_select, None)
836}
837
838pub fn navigation_rail_fitting_content<'a, Id, Message, Renderer, F>(
839 destinations: &'a [Destination<Id>],
840 selection: Selection<Id>,
841 on_select: F,
842) -> Container<'a, Message, Theme, Renderer>
843where
844 Id: Copy + Eq + 'a,
845 Message: Clone + 'a,
846 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
847 Font: Into<Renderer::Font>,
848 F: Fn(Id) -> Message + Clone + 'a,
849{
850 navigation_rail(destinations, selection, on_select).height(Length::Fixed(
851 navigation_rail_min_height(destinations.len(), false),
852 ))
853}
854
855pub fn navigation_rail_with_header<'a, Id, Message, Renderer, F>(
856 destinations: &'a [Destination<Id>],
857 selection: Selection<Id>,
858 on_select: F,
859 header: impl Into<Element<'a, Message, Theme, Renderer>>,
860) -> Container<'a, Message, Theme, Renderer>
861where
862 Id: Copy + Eq + 'a,
863 Message: Clone + 'a,
864 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
865 Font: Into<Renderer::Font>,
866 F: Fn(Id) -> Message + Clone + 'a,
867{
868 navigation_rail_with_optional_header(destinations, selection, on_select, Some(header.into()))
869}
870
871pub fn navigation_rail_with_header_fitting_content<'a, Id, Message, Renderer, F>(
872 destinations: &'a [Destination<Id>],
873 selection: Selection<Id>,
874 on_select: F,
875 header: impl Into<Element<'a, Message, Theme, Renderer>>,
876) -> Container<'a, Message, Theme, Renderer>
877where
878 Id: Copy + Eq + 'a,
879 Message: Clone + 'a,
880 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
881 Font: Into<Renderer::Font>,
882 F: Fn(Id) -> Message + Clone + 'a,
883{
884 navigation_rail_with_header(destinations, selection, on_select, header).height(Length::Fixed(
885 navigation_rail_min_height(destinations.len(), true),
886 ))
887}
888
889pub fn navigation_rail_with_menu<'a, Id, Message, Renderer, F>(
890 destinations: &'a [Destination<Id>],
891 selection: Selection<Id>,
892 on_select: F,
893 on_menu: Message,
894) -> Container<'a, Message, Theme, Renderer>
895where
896 Id: Copy + Eq + 'a,
897 Message: Clone + 'a,
898 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
899 Font: Into<Renderer::Font>,
900 F: Fn(Id) -> Message + Clone + 'a,
901{
902 navigation_rail_with_header(
903 destinations,
904 selection,
905 on_select,
906 navigation_menu_button(on_menu, 0.0),
907 )
908}
909
910fn navigation_rail_with_menu_at_progress<'a, Id, Message, Renderer, F>(
911 destinations: &'a [Destination<Id>],
912 selection: Selection<Id>,
913 on_select: F,
914 on_menu: Message,
915 menu_progress: f32,
916) -> Container<'a, Message, Theme, Renderer>
917where
918 Id: Copy + Eq + 'a,
919 Message: Clone + 'a,
920 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
921 Font: Into<Renderer::Font>,
922 F: Fn(Id) -> Message + Clone + 'a,
923{
924 navigation_rail_with_header(
925 destinations,
926 selection,
927 on_select,
928 navigation_menu_button(on_menu, menu_progress),
929 )
930}
931
932pub fn navigation_rail_with_menu_fitting_content<'a, Id, Message, Renderer, F>(
933 destinations: &'a [Destination<Id>],
934 selection: Selection<Id>,
935 on_select: F,
936 on_menu: Message,
937) -> Container<'a, Message, Theme, Renderer>
938where
939 Id: Copy + Eq + 'a,
940 Message: Clone + 'a,
941 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
942 Font: Into<Renderer::Font>,
943 F: Fn(Id) -> Message + Clone + 'a,
944{
945 navigation_rail_with_menu(destinations, selection, on_select, on_menu).height(Length::Fixed(
946 navigation_rail_min_height(destinations.len(), true),
947 ))
948}
949
950pub fn navigation_rail_expanded_with_menu<'a, Id, Message, Renderer, F>(
951 headline: &'static str,
952 destinations: &'a [Destination<Id>],
953 selection: Selection<Id>,
954 on_select: F,
955 on_menu: Message,
956) -> Container<'a, Message, Theme, Renderer>
957where
958 Id: Copy + Eq + 'a,
959 Message: Clone + 'a,
960 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
961 Font: Into<Renderer::Font>,
962 F: Fn(Id) -> Message + Clone + 'a,
963{
964 navigation_rail_expanded_with_menu_at_width(
965 headline,
966 destinations,
967 selection,
968 on_select,
969 on_menu,
970 tokens::component::navigation_rail::EXPANDED_CONTAINER_WIDTH,
971 )
972}
973
974pub fn navigation_rail_expanded_with_menu_fitting_content<'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 navigation_rail_expanded_with_menu(headline, destinations, selection, on_select, on_menu)
989 .height(Length::Fixed(navigation_rail_min_height(
990 destinations.len(),
991 true,
992 )))
993}
994
995pub fn navigation_rail_expanded_with_menu_at_width<'a, Id, Message, Renderer, F>(
996 headline: &'static str,
997 destinations: &'a [Destination<Id>],
998 selection: Selection<Id>,
999 on_select: F,
1000 on_menu: Message,
1001 width: f32,
1002) -> Container<'a, Message, Theme, Renderer>
1003where
1004 Id: Copy + Eq + 'a,
1005 Message: Clone + 'a,
1006 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1007 Font: Into<Renderer::Font>,
1008 F: Fn(Id) -> Message + Clone + 'a,
1009{
1010 let width = navigation_rail_expanded_container_width(width);
1011 let indicator_width = navigation_rail_expanded_indicator_width(width);
1012 let expansion_progress = navigation_rail_expanded_progress_for_width(width);
1013 let label_alpha = navigation_rail_expanded_label_alpha_for_width(width);
1014 let mut items = Column::new()
1015 .width(Length::Fixed(width))
1016 .height(Length::Fill)
1017 .spacing(tokens::component::navigation_rail::VERTICAL_PADDING)
1018 .align_x(alignment::Horizontal::Center)
1019 .push(navigation_rail_expanded_header(
1020 headline,
1021 on_menu,
1022 expansion_progress,
1023 label_alpha,
1024 ));
1025
1026 for destination in destinations {
1027 items = items.push(navigation_rail_expanded_item(
1028 *destination,
1029 selection,
1030 on_select.clone(),
1031 indicator_width,
1032 expansion_progress,
1033 label_alpha,
1034 ));
1035 }
1036
1037 Container::new(items)
1038 .width(Length::Fixed(width))
1039 .height(Length::Fill)
1040 .padding(Padding {
1041 top: tokens::component::navigation_rail::CONTENT_TOP_MARGIN,
1042 right: 0.0,
1043 bottom: tokens::component::navigation_rail::VERTICAL_PADDING,
1044 left: 0.0,
1045 })
1046 .style(navigation_rail_container)
1047}
1048
1049fn navigation_rail_with_optional_header<'a, Id, Message, Renderer, F>(
1050 destinations: &'a [Destination<Id>],
1051 selection: Selection<Id>,
1052 on_select: F,
1053 header: Option<Element<'a, Message, Theme, Renderer>>,
1054) -> Container<'a, Message, Theme, Renderer>
1055where
1056 Id: Copy + Eq + 'a,
1057 Message: Clone + 'a,
1058 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1059 Font: Into<Renderer::Font>,
1060 F: Fn(Id) -> Message + Clone + 'a,
1061{
1062 let mut items = Column::new()
1063 .width(Length::Fixed(
1064 tokens::component::navigation_rail::CONTAINER_WIDTH,
1065 ))
1066 .height(Length::Fill)
1067 .spacing(tokens::component::navigation_rail::VERTICAL_PADDING)
1068 .align_x(alignment::Horizontal::Center);
1069
1070 if let Some(header) = header {
1071 items = items.push(navigation_rail_header(header));
1072 }
1073
1074 for destination in destinations {
1075 items = items.push(navigation_rail_item(
1076 *destination,
1077 selection,
1078 on_select.clone(),
1079 ));
1080 }
1081
1082 Container::new(items)
1083 .width(Length::Fixed(
1084 tokens::component::navigation_rail::CONTAINER_WIDTH,
1085 ))
1086 .height(Length::Fill)
1087 .padding(Padding {
1088 top: tokens::component::navigation_rail::CONTENT_TOP_MARGIN,
1089 right: 0.0,
1090 bottom: tokens::component::navigation_rail::VERTICAL_PADDING,
1091 left: 0.0,
1092 })
1093 .style(navigation_rail_container)
1094}
1095
1096pub fn navigation_drawer<'a, Id, Message, Renderer, F>(
1097 headline: &'static str,
1098 destinations: &'a [Destination<Id>],
1099 selection: Selection<Id>,
1100 on_select: F,
1101) -> Container<'a, Message, Theme, Renderer>
1102where
1103 Id: Copy + Eq + 'a,
1104 Message: Clone + 'a,
1105 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1106 Font: Into<Renderer::Font>,
1107 F: Fn(Id) -> Message + Clone + 'a,
1108{
1109 navigation_drawer_at_width(
1110 headline,
1111 destinations,
1112 selection,
1113 on_select,
1114 tokens::component::navigation_drawer::CONTAINER_WIDTH,
1115 )
1116}
1117
1118pub fn navigation_drawer_at_width<'a, Id, Message, Renderer, F>(
1119 headline: &'static str,
1120 destinations: &'a [Destination<Id>],
1121 selection: Selection<Id>,
1122 on_select: F,
1123 width: f32,
1124) -> Container<'a, Message, Theme, Renderer>
1125where
1126 Id: Copy + Eq + 'a,
1127 Message: Clone + 'a,
1128 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1129 Font: Into<Renderer::Font>,
1130 F: Fn(Id) -> Message + Clone + 'a,
1131{
1132 navigation_drawer_with_optional_header(
1133 headline,
1134 destinations,
1135 selection,
1136 on_select,
1137 width,
1138 None,
1139 )
1140}
1141
1142pub fn navigation_drawer_with_menu<'a, Id, Message, Renderer, F>(
1143 headline: &'static str,
1144 destinations: &'a [Destination<Id>],
1145 selection: Selection<Id>,
1146 on_select: F,
1147 on_menu: Message,
1148) -> Container<'a, Message, Theme, Renderer>
1149where
1150 Id: Copy + Eq + 'a,
1151 Message: Clone + 'a,
1152 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1153 Font: Into<Renderer::Font>,
1154 F: Fn(Id) -> Message + Clone + 'a,
1155{
1156 navigation_drawer_with_menu_at_width(
1157 headline,
1158 destinations,
1159 selection,
1160 on_select,
1161 on_menu,
1162 tokens::component::navigation_drawer::CONTAINER_WIDTH,
1163 )
1164}
1165
1166pub fn navigation_drawer_with_menu_at_width<'a, Id, Message, Renderer, F>(
1167 headline: &'static str,
1168 destinations: &'a [Destination<Id>],
1169 selection: Selection<Id>,
1170 on_select: F,
1171 on_menu: Message,
1172 width: f32,
1173) -> Container<'a, Message, Theme, Renderer>
1174where
1175 Id: Copy + Eq + 'a,
1176 Message: Clone + 'a,
1177 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1178 Font: Into<Renderer::Font>,
1179 F: Fn(Id) -> Message + Clone + 'a,
1180{
1181 navigation_drawer_with_optional_header(
1182 headline,
1183 destinations,
1184 selection,
1185 on_select,
1186 width,
1187 Some(navigation_drawer_menu_header(headline, on_menu).into()),
1188 )
1189}
1190
1191fn navigation_drawer_with_optional_header<'a, Id, Message, Renderer, F>(
1192 headline: &'static str,
1193 destinations: &'a [Destination<Id>],
1194 selection: Selection<Id>,
1195 on_select: F,
1196 width: f32,
1197 header: Option<Element<'a, Message, Theme, Renderer>>,
1198) -> Container<'a, Message, Theme, Renderer>
1199where
1200 Id: Copy + Eq + 'a,
1201 Message: Clone + 'a,
1202 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1203 Font: Into<Renderer::Font>,
1204 F: Fn(Id) -> Message + Clone + 'a,
1205{
1206 let container_width = navigation_drawer_container_width(width);
1207 let indicator_width = navigation_drawer_indicator_width(container_width);
1208 let headline_scale = tokens::component::navigation_drawer::HEADLINE_TEXT;
1209 let mut items = Column::new()
1210 .width(Length::Fixed(container_width))
1211 .height(Length::Fill)
1212 .spacing(0);
1213
1214 if let Some(header) = header {
1215 items = items.push(header);
1216 } else {
1217 items = items.push(
1218 Container::new(type_text(headline, headline_scale).style(headline_text_style))
1219 .height(Length::Fixed(
1220 tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1221 ))
1222 .padding(Padding {
1223 top: 0.0,
1224 right: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
1225 + tokens::component::navigation_drawer::ITEM_CONTENT_TRAILING_SPACE,
1226 bottom: 0.0,
1227 left: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
1228 + tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE,
1229 })
1230 .align_y(alignment::Vertical::Center),
1231 );
1232 }
1233
1234 for destination in destinations {
1235 items = items.push(navigation_drawer_item(
1236 *destination,
1237 selection,
1238 on_select.clone(),
1239 indicator_width,
1240 ));
1241 }
1242
1243 Container::new(items)
1244 .width(Length::Fixed(container_width))
1245 .height(Length::Fill)
1246 .padding(Padding {
1247 top: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING,
1248 right: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING,
1249 bottom: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING,
1250 left: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING,
1251 })
1252 .style(navigation_drawer_container)
1253}
1254
1255pub fn navigation_drawer_width_for_progress(progress: f32) -> f32 {
1256 let progress = progress.clamp(0.0, 1.0);
1257
1258 if progress <= f32::EPSILON {
1259 0.0
1260 } else {
1261 lerp(
1262 tokens::component::navigation_drawer::MINIMUM_CONTAINER_WIDTH,
1263 tokens::component::navigation_drawer::CONTAINER_WIDTH,
1264 progress,
1265 )
1266 }
1267}
1268
1269fn navigation_bar_item<'a, Id, Message, Renderer, F>(
1270 destination: Destination<Id>,
1271 selection: Selection<Id>,
1272 on_select: F,
1273) -> Element<'a, Message, Theme, Renderer>
1274where
1275 Id: Copy + Eq + 'a,
1276 Message: Clone + 'a,
1277 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1278 Font: Into<Renderer::Font>,
1279 F: Fn(Id) -> Message + Clone + 'a,
1280{
1281 let size_progress = selection.size_progress(destination.id);
1282 let alpha_progress = selection.alpha_progress(destination.id);
1283 let scale = tokens::component::navigation_bar::LABEL_TEXT;
1284 let message = on_select(destination.id);
1285 let indicator = indicator_icon_stack(
1286 destination.icon,
1287 tokens::component::navigation_bar::ICON_SIZE,
1288 tokens::component::navigation_bar::ACTIVE_INDICATOR_WIDTH,
1289 tokens::component::navigation_bar::ACTIVE_INDICATOR_HEIGHT,
1290 size_progress,
1291 alpha_progress,
1292 destination.badge,
1293 false,
1294 );
1295 let label = type_text(destination.label, scale).style(move |theme| text::Style {
1296 color: Some(bar_or_rail_label_color(theme, alpha_progress)),
1297 });
1298 let content = Column::new()
1299 .width(Length::Fill)
1300 .spacing(tokens::component::navigation_bar::INDICATOR_TO_LABEL_PADDING)
1301 .align_x(alignment::Horizontal::Center)
1302 .push(indicator)
1303 .push(label);
1304
1305 navigation_press_surface(
1306 Container::new(content)
1307 .width(Length::Fill)
1308 .height(Length::Fixed(
1309 tokens::component::navigation_bar::CONTAINER_HEIGHT,
1310 ))
1311 .padding(Padding {
1312 top: tokens::component::navigation_bar::INDICATOR_VERTICAL_OFFSET,
1313 right: 0.0,
1314 bottom: navigation_bar_item_bottom_padding(),
1315 left: 0.0,
1316 })
1317 .align_y(alignment::Vertical::Center),
1318 message,
1319 NavigationStateLayer::BarOrRail,
1320 NavigationIndicatorPlacement::TopCenter {
1321 top: tokens::component::navigation_bar::INDICATOR_VERTICAL_OFFSET,
1322 width: tokens::component::navigation_bar::ACTIVE_INDICATOR_WIDTH,
1323 height: tokens::component::navigation_bar::ACTIVE_INDICATOR_HEIGHT,
1324 },
1325 )
1326 .into()
1327}
1328
1329fn navigation_rail_item<'a, Id, Message, Renderer, F>(
1330 destination: Destination<Id>,
1331 selection: Selection<Id>,
1332 on_select: F,
1333) -> Element<'a, Message, Theme, Renderer>
1334where
1335 Id: Copy + Eq + 'a,
1336 Message: Clone + 'a,
1337 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1338 Font: Into<Renderer::Font>,
1339 F: Fn(Id) -> Message + Clone + 'a,
1340{
1341 let size_progress = selection.size_progress(destination.id);
1342 let alpha_progress = selection.alpha_progress(destination.id);
1343 let scale = tokens::component::navigation_rail::LABEL_TEXT;
1344 let message = on_select(destination.id);
1345 let indicator = indicator_icon_stack(
1346 destination.icon,
1347 tokens::component::navigation_rail::ICON_SIZE,
1348 tokens::component::navigation_rail::ACTIVE_INDICATOR_WIDTH,
1349 tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT,
1350 size_progress,
1351 alpha_progress,
1352 destination.badge,
1353 false,
1354 );
1355 let label = type_text(destination.label, scale).style(move |theme| text::Style {
1356 color: Some(bar_or_rail_label_color(theme, alpha_progress)),
1357 });
1358 let content = Column::new()
1359 .width(Length::Fixed(
1360 tokens::component::navigation_rail::ITEM_WIDTH,
1361 ))
1362 .spacing(tokens::component::navigation_rail::ITEM_VERTICAL_PADDING)
1363 .align_x(alignment::Horizontal::Center)
1364 .push(indicator)
1365 .push(label);
1366
1367 navigation_press_surface(
1368 Container::new(content)
1369 .width(Length::Fixed(
1370 tokens::component::navigation_rail::ITEM_WIDTH,
1371 ))
1372 .height(Length::Fixed(
1373 tokens::component::navigation_rail::ITEM_HEIGHT,
1374 ))
1375 .padding(Padding {
1376 top: navigation_rail_item_content_top_padding(),
1377 right: 0.0,
1378 bottom: 0.0,
1379 left: 0.0,
1380 }),
1381 message,
1382 NavigationStateLayer::BarOrRail,
1383 NavigationIndicatorPlacement::TopCenter {
1384 top: navigation_rail_item_content_top_padding(),
1385 width: tokens::component::navigation_rail::ACTIVE_INDICATOR_WIDTH,
1386 height: tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT,
1387 },
1388 )
1389 .into()
1390}
1391
1392fn navigation_rail_header<'a, Message, Renderer>(
1393 header: Element<'a, Message, Theme, Renderer>,
1394) -> Container<'a, Message, Theme, Renderer>
1395where
1396 Message: 'a,
1397 Renderer: geometry::Renderer + primitive::Renderer + 'a,
1398{
1399 Container::new(header)
1400 .width(Length::Fixed(
1401 tokens::component::navigation_rail::CONTAINER_WIDTH,
1402 ))
1403 .padding(Padding {
1404 top: 0.0,
1405 right: 0.0,
1406 bottom: navigation_rail_header_bottom_padding(),
1407 left: 0.0,
1408 })
1409 .align_x(alignment::Horizontal::Center)
1410}
1411
1412fn navigation_menu_button<'a, Message, Renderer>(
1413 on_press: Message,
1414 progress: f32,
1415) -> Button<'a, Message, Renderer>
1416where
1417 Message: Clone + 'a,
1418 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1419 Font: Into<Renderer::Font>,
1420{
1421 let icon = Canvas::new(NavigationMenuIcon { progress })
1422 .width(Length::Fixed(tokens::component::icon_button::ICON_SIZE))
1423 .height(Length::Fixed(tokens::component::icon_button::ICON_SIZE));
1424
1425 Button::new(
1426 Container::new(icon)
1427 .center_x(Length::Fixed(
1428 tokens::component::icon_button::CONTAINER_WIDTH,
1429 ))
1430 .center_y(Length::Fixed(
1431 tokens::component::icon_button::CONTAINER_HEIGHT,
1432 )),
1433 )
1434 .width(Length::Fixed(
1435 tokens::component::icon_button::CONTAINER_WIDTH,
1436 ))
1437 .height(Length::Fixed(
1438 tokens::component::icon_button::CONTAINER_HEIGHT,
1439 ))
1440 .padding(Padding::ZERO)
1441 .style(button_style::icon)
1442 .on_press(on_press)
1443}
1444
1445#[derive(Debug, Clone, Copy)]
1446struct NavigationMenuIcon {
1447 progress: f32,
1448}
1449
1450impl<Message, Renderer> canvas::Program<Message, Theme, Renderer> for NavigationMenuIcon
1451where
1452 Renderer: geometry::Renderer,
1453{
1454 type State = ();
1455
1456 fn draw(
1457 &self,
1458 _state: &Self::State,
1459 renderer: &Renderer,
1460 theme: &Theme,
1461 bounds: Rectangle,
1462 _cursor: mouse::Cursor,
1463 ) -> Vec<canvas::Geometry<Renderer>> {
1464 let size = bounds.width.min(bounds.height);
1465
1466 if size <= 0.0 {
1467 return Vec::new();
1468 }
1469
1470 let mut frame = canvas::Frame::new(renderer, bounds.size());
1471 let offset = Vector::new((bounds.width - size) / 2.0, (bounds.height - size) / 2.0);
1472 let center = Point::new(bounds.width / 2.0, bounds.height / 2.0);
1473 let stroke = Stroke::default()
1474 .with_width(navigation_menu_icon_stroke_width(size))
1475 .with_color(theme.colors().surface.text_variant)
1476 .with_line_cap(LineCap::Round);
1477
1478 frame.with_save(|frame| {
1479 frame.translate(Vector::new(center.x, center.y));
1480 frame.rotate(navigation_menu_icon_rotation_radians(self.progress));
1481 frame.translate(Vector::new(-center.x, -center.y));
1482
1483 for (from, to) in navigation_menu_icon_segments(self.progress, size) {
1484 frame.stroke(
1485 &Path::line(
1486 Point::new(from.x + offset.x, from.y + offset.y),
1487 Point::new(to.x + offset.x, to.y + offset.y),
1488 ),
1489 stroke,
1490 );
1491 }
1492 });
1493
1494 vec![frame.into_geometry()]
1495 }
1496}
1497
1498fn navigation_menu_icon_rotation_radians(progress: f32) -> f32 {
1499 PI * progress.clamp(0.0, 1.0)
1500}
1501
1502fn navigation_menu_icon_segments(progress: f32, size: f32) -> [(Point, Point); 3] {
1503 let progress = progress.clamp(0.0, 1.0);
1504
1505 [
1506 (
1507 navigation_menu_icon_point(
1508 lerp(
1509 NAVIGATION_MENU_ICON_START_X,
1510 NAVIGATION_MENU_ICON_CENTER_X,
1511 progress,
1512 ),
1513 lerp(
1514 NAVIGATION_MENU_ICON_TOP_Y,
1515 NAVIGATION_MENU_ICON_ARROW_TOP_Y,
1516 progress,
1517 ),
1518 size,
1519 ),
1520 navigation_menu_icon_point(
1521 NAVIGATION_MENU_ICON_END_X,
1522 lerp(
1523 NAVIGATION_MENU_ICON_TOP_Y,
1524 NAVIGATION_MENU_ICON_CENTER_Y,
1525 progress,
1526 ),
1527 size,
1528 ),
1529 ),
1530 (
1531 navigation_menu_icon_point(
1532 NAVIGATION_MENU_ICON_START_X,
1533 NAVIGATION_MENU_ICON_CENTER_Y,
1534 size,
1535 ),
1536 navigation_menu_icon_point(
1537 NAVIGATION_MENU_ICON_END_X,
1538 NAVIGATION_MENU_ICON_CENTER_Y,
1539 size,
1540 ),
1541 ),
1542 (
1543 navigation_menu_icon_point(
1544 lerp(
1545 NAVIGATION_MENU_ICON_START_X,
1546 NAVIGATION_MENU_ICON_CENTER_X,
1547 progress,
1548 ),
1549 lerp(
1550 NAVIGATION_MENU_ICON_BOTTOM_Y,
1551 NAVIGATION_MENU_ICON_ARROW_BOTTOM_Y,
1552 progress,
1553 ),
1554 size,
1555 ),
1556 navigation_menu_icon_point(
1557 NAVIGATION_MENU_ICON_END_X,
1558 lerp(
1559 NAVIGATION_MENU_ICON_BOTTOM_Y,
1560 NAVIGATION_MENU_ICON_CENTER_Y,
1561 progress,
1562 ),
1563 size,
1564 ),
1565 ),
1566 ]
1567}
1568
1569fn navigation_menu_icon_point(x: f32, y: f32, size: f32) -> Point {
1570 Point::new(
1571 x / NAVIGATION_MENU_ICON_VIEWPORT_SIZE * size,
1572 y / NAVIGATION_MENU_ICON_VIEWPORT_SIZE * size,
1573 )
1574}
1575
1576fn navigation_menu_icon_stroke_width(size: f32) -> f32 {
1577 NAVIGATION_MENU_ICON_STROKE_WIDTH / NAVIGATION_MENU_ICON_VIEWPORT_SIZE * size
1578}
1579
1580fn navigation_rail_expanded_header<'a, Message, Renderer>(
1581 headline: &'static str,
1582 on_menu: Message,
1583 menu_progress: f32,
1584 label_alpha: f32,
1585) -> Container<'a, Message, Theme, Renderer>
1586where
1587 Message: Clone + 'a,
1588 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1589 Font: Into<Renderer::Font>,
1590{
1591 let headline_scale = tokens::component::navigation_drawer::HEADLINE_TEXT;
1592 let headline = type_text(headline, headline_scale).style(move |theme| text::Style {
1593 color: Some(alpha_color(
1594 theme.colors().surface.text_variant,
1595 label_alpha,
1596 )),
1597 });
1598 let content = Row::new()
1599 .height(Length::Fixed(
1600 tokens::component::icon_button::CONTAINER_HEIGHT,
1601 ))
1602 .spacing(navigation_rail_expanded_header_title_spacing())
1603 .align_y(alignment::Vertical::Center)
1604 .push(navigation_menu_button(on_menu, menu_progress))
1605 .push(headline);
1606
1607 Container::new(content)
1608 .height(Length::Fixed(navigation_rail_header_slot_height()))
1609 .width(Length::Fill)
1610 .padding(Padding {
1611 top: 0.0,
1612 right: tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL,
1613 bottom: navigation_rail_header_bottom_padding(),
1614 left: navigation_rail_expanded_header_leading_space(),
1615 })
1616 .align_y(alignment::Vertical::Center)
1617}
1618
1619fn navigation_rail_expanded_item<'a, Id, Message, Renderer, F>(
1620 destination: Destination<Id>,
1621 selection: Selection<Id>,
1622 on_select: F,
1623 indicator_width: f32,
1624 expansion_progress: f32,
1625 label_alpha: f32,
1626) -> Element<'a, Message, Theme, Renderer>
1627where
1628 Id: Copy + Eq + 'a,
1629 Message: Clone + 'a,
1630 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1631 Font: Into<Renderer::Font>,
1632 F: Fn(Id) -> Message + Clone + 'a,
1633{
1634 let size_progress = selection.size_progress(destination.id);
1635 let alpha_progress = selection.alpha_progress(destination.id);
1636 let indicator_height =
1637 navigation_rail_expanded_indicator_height_for_progress(expansion_progress);
1638 let vertical_inset =
1639 navigation_rail_expanded_item_vertical_inset_for_progress(expansion_progress);
1640 let scale = tokens::component::navigation_drawer::LABEL_TEXT;
1641 let message = on_select(destination.id);
1642 let badge_on_icon = navigation_rail_expanded_badge_uses_icon_anchor(label_alpha);
1643 let trailing_badge_alpha = navigation_rail_expanded_trailing_badge_alpha(label_alpha);
1644 let collapsed_label_alpha = navigation_rail_expanded_collapsed_label_alpha(label_alpha);
1645 let icon = navigation_rail_expanded_icon_layer(
1646 destination.icon,
1647 alpha_progress,
1648 indicator_height,
1649 badge_on_icon.then_some(destination.badge).flatten(),
1650 );
1651 let label = type_text(destination.label, scale).style(move |theme| text::Style {
1652 color: Some(alpha_color(
1653 drawer_content_color(theme, alpha_progress),
1654 label_alpha,
1655 )),
1656 });
1657 let content = Row::new()
1658 .width(Length::Fill)
1659 .height(Length::Fixed(indicator_height))
1660 .align_y(alignment::Vertical::Center)
1661 .push(Container::new(label).width(Length::Fill));
1662 let content = if let Some(badge) = destination.badge.filter(|_| !badge_on_icon) {
1663 content
1664 .push(Space::new().width(Length::Fixed(navigation_drawer_badge_space())))
1665 .push(destination_badge_with_alpha::<Message, Renderer>(
1666 badge,
1667 trailing_badge_alpha,
1668 ))
1669 } else {
1670 content
1671 };
1672 let content = Container::new(content)
1673 .width(Length::Fixed(indicator_width))
1674 .height(Length::Fixed(indicator_height))
1675 .padding(Padding {
1676 top: 0.0,
1677 right: tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_END,
1678 bottom: 0.0,
1679 left: navigation_rail_expanded_label_leading_padding(),
1680 })
1681 .align_y(alignment::Vertical::Center);
1682 let expanded_indicator = Stack::new()
1683 .width(Length::Fixed(indicator_width))
1684 .height(Length::Fixed(indicator_height))
1685 .push(
1686 Space::new()
1687 .width(Length::Fixed(indicator_width))
1688 .height(Length::Fixed(indicator_height)),
1689 )
1690 .push(indicator_layer(
1691 indicator_width,
1692 indicator_height,
1693 size_progress,
1694 alpha_progress,
1695 ))
1696 .push(content)
1697 .push(icon);
1698 let collapsed_label = navigation_rail_expanded_collapsed_label::<Message, Renderer>(
1699 destination.label,
1700 alpha_progress,
1701 collapsed_label_alpha,
1702 )
1703 .width(Length::Fixed(navigation_rail_collapsed_label_width()))
1704 .height(Length::Fixed(navigation_rail_item_slot_height()));
1705 let item = Stack::new()
1706 .width(Length::Fixed(indicator_width))
1707 .height(Length::Fixed(navigation_rail_item_slot_height()))
1708 .push(
1709 Container::new(expanded_indicator)
1710 .width(Length::Fixed(indicator_width))
1711 .height(Length::Fixed(navigation_rail_item_slot_height()))
1712 .padding(Padding {
1713 top: vertical_inset,
1714 right: 0.0,
1715 bottom: vertical_inset,
1716 left: 0.0,
1717 })
1718 .align_y(alignment::Vertical::Top),
1719 )
1720 .push(collapsed_label);
1721
1722 navigation_press_surface(
1723 Container::new(item)
1724 .width(Length::Fixed(indicator_width))
1725 .height(Length::Fixed(navigation_rail_item_slot_height())),
1726 message,
1727 NavigationStateLayer::Drawer {
1728 progress: alpha_progress,
1729 },
1730 NavigationIndicatorPlacement::Inset {
1731 x: 0.0,
1732 y: vertical_inset,
1733 width: indicator_width,
1734 height: indicator_height,
1735 },
1736 )
1737 .into()
1738}
1739
1740fn navigation_drawer_menu_header<'a, Message, Renderer>(
1741 headline: &'static str,
1742 on_menu: Message,
1743) -> Container<'a, Message, Theme, Renderer>
1744where
1745 Message: Clone + 'a,
1746 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1747 Font: Into<Renderer::Font>,
1748{
1749 let headline_scale = tokens::component::navigation_drawer::HEADLINE_TEXT;
1750 let content = Row::new()
1751 .height(Length::Fixed(
1752 tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1753 ))
1754 .spacing(navigation_drawer_menu_header_title_spacing())
1755 .align_y(alignment::Vertical::Center)
1756 .push(navigation_menu_button(on_menu, 0.0))
1757 .push(type_text(headline, headline_scale).style(headline_text_style));
1758
1759 Container::new(content)
1760 .height(Length::Fixed(
1761 tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1762 ))
1763 .padding(Padding {
1764 top: 0.0,
1765 right: tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
1766 + tokens::component::navigation_drawer::ITEM_CONTENT_TRAILING_SPACE,
1767 bottom: 0.0,
1768 left: navigation_drawer_menu_header_leading_space(),
1769 })
1770 .align_y(alignment::Vertical::Center)
1771}
1772
1773fn navigation_drawer_item<'a, Id, Message, Renderer, F>(
1774 destination: Destination<Id>,
1775 selection: Selection<Id>,
1776 on_select: F,
1777 indicator_width: f32,
1778) -> Element<'a, Message, Theme, Renderer>
1779where
1780 Id: Copy + Eq + 'a,
1781 Message: Clone + 'a,
1782 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1783 Font: Into<Renderer::Font>,
1784 F: Fn(Id) -> Message + Clone + 'a,
1785{
1786 let size_progress = selection.size_progress(destination.id);
1787 let alpha_progress = selection.alpha_progress(destination.id);
1788 let scale = tokens::component::navigation_drawer::LABEL_TEXT;
1789 let message = on_select(destination.id);
1790 let icon = destination_icon::<Message, Renderer>(
1791 destination.icon,
1792 tokens::component::navigation_drawer::ICON_SIZE,
1793 alpha_progress,
1794 true,
1795 );
1796 let label = type_text(destination.label, scale).style(move |theme| text::Style {
1797 color: Some(drawer_content_color(theme, alpha_progress)),
1798 });
1799 let content = Row::new()
1800 .width(Length::Fill)
1801 .height(Length::Fixed(
1802 tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1803 ))
1804 .spacing(tokens::component::navigation_drawer::ICON_LABEL_SPACE)
1805 .align_y(alignment::Vertical::Center)
1806 .push(icon)
1807 .push(Container::new(label).width(Length::Fill));
1808 let content = if let Some(badge) = destination.badge {
1809 content
1810 .push(Space::new().width(Length::Fixed(navigation_drawer_badge_space())))
1811 .push(destination_badge::<Message, Renderer>(badge))
1812 } else {
1813 content
1814 };
1815 let content = Container::new(content)
1816 .width(Length::Fixed(indicator_width))
1817 .height(Length::Fixed(
1818 tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1819 ))
1820 .padding(Padding {
1821 top: 0.0,
1822 right: tokens::component::navigation_drawer::ITEM_CONTENT_TRAILING_SPACE,
1823 bottom: 0.0,
1824 left: tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE,
1825 })
1826 .align_y(alignment::Vertical::Center);
1827 let indicator = Stack::new()
1828 .width(Length::Fixed(indicator_width))
1829 .height(Length::Fixed(
1830 tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1831 ))
1832 .push(
1833 Space::new()
1834 .width(Length::Fixed(indicator_width))
1835 .height(Length::Fixed(
1836 tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1837 )),
1838 )
1839 .push(indicator_layer(
1840 indicator_width,
1841 tokens::component::navigation_drawer::ACTIVE_INDICATOR_HEIGHT,
1842 size_progress,
1843 alpha_progress,
1844 ))
1845 .push(content);
1846
1847 navigation_press_surface(
1848 indicator,
1849 message,
1850 NavigationStateLayer::Drawer {
1851 progress: alpha_progress,
1852 },
1853 NavigationIndicatorPlacement::Full,
1854 )
1855 .into()
1856}
1857
1858fn indicator_icon_stack<'a, Message, Renderer>(
1859 icon: &'static str,
1860 icon_size: f32,
1861 indicator_width: f32,
1862 indicator_height: f32,
1863 size_progress: f32,
1864 alpha_progress: f32,
1865 badge: Option<Badge>,
1866 drawer: bool,
1867) -> Stack<'a, Message, Theme, Renderer>
1868where
1869 Message: Clone + 'a,
1870 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
1871 Font: Into<Renderer::Font>,
1872{
1873 Stack::new()
1874 .width(Length::Fixed(indicator_width))
1875 .height(Length::Fixed(indicator_height))
1876 .push(
1877 Space::new()
1878 .width(Length::Fixed(indicator_width))
1879 .height(Length::Fixed(indicator_height)),
1880 )
1881 .push(indicator_layer(
1882 indicator_width,
1883 indicator_height,
1884 size_progress,
1885 alpha_progress,
1886 ))
1887 .push(
1888 destination_icon_anchor::<Message, Renderer>(
1889 icon,
1890 icon_size,
1891 alpha_progress,
1892 badge,
1893 drawer,
1894 )
1895 .width(Length::Fixed(indicator_width))
1896 .height(Length::Fixed(indicator_height)),
1897 )
1898}
1899
1900fn navigation_press_surface<'a, Message, Renderer>(
1901 content: impl Into<Element<'a, Message, Theme, Renderer>>,
1902 on_press: Message,
1903 layer: NavigationStateLayer,
1904 indicator: NavigationIndicatorPlacement,
1905) -> NavigationPressSurface<'a, Message, Renderer>
1906where
1907 Message: Clone + 'a,
1908 Renderer: geometry::Renderer + primitive::Renderer + 'a,
1909{
1910 NavigationPressSurface {
1911 content: content.into(),
1912 on_press,
1913 layer,
1914 indicator,
1915 }
1916}
1917
1918struct NavigationPressSurface<'a, Message, Renderer>
1919where
1920 Renderer: geometry::Renderer + primitive::Renderer,
1921{
1922 content: Element<'a, Message, Theme, Renderer>,
1923 on_press: Message,
1924 layer: NavigationStateLayer,
1925 indicator: NavigationIndicatorPlacement,
1926}
1927
1928#[derive(Debug, Clone, Copy)]
1929enum NavigationIndicatorPlacement {
1930 Full,
1931 TopCenter {
1932 top: f32,
1933 width: f32,
1934 height: f32,
1935 },
1936 Inset {
1937 x: f32,
1938 y: f32,
1939 width: f32,
1940 height: f32,
1941 },
1942}
1943
1944impl NavigationIndicatorPlacement {
1945 fn bounds(self, bounds: Rectangle) -> Rectangle {
1946 match self {
1947 Self::Full => bounds,
1948 Self::TopCenter { top, width, height } => Rectangle {
1949 x: bounds.x + (bounds.width - width) / 2.0,
1950 y: bounds.y + top,
1951 width,
1952 height,
1953 },
1954 Self::Inset {
1955 x,
1956 y,
1957 width,
1958 height,
1959 } => Rectangle {
1960 x: bounds.x + x,
1961 y: bounds.y + y,
1962 width,
1963 height,
1964 },
1965 }
1966 }
1967}
1968
1969#[derive(Debug)]
1970struct NavigationPressSurfaceState {
1971 is_hovered: bool,
1972 is_pressed: bool,
1973 state_layer_opacity: AnimatedScalar,
1974 ripples: PressRippleState,
1975 now: Option<Instant>,
1976}
1977
1978impl Default for NavigationPressSurfaceState {
1979 fn default() -> Self {
1980 Self {
1981 is_hovered: false,
1982 is_pressed: false,
1983 state_layer_opacity: AnimatedScalar::new(0.0),
1984 ripples: PressRippleState::default(),
1985 now: None,
1986 }
1987 }
1988}
1989
1990impl NavigationPressSurfaceState {
1991 fn sync_hover(&mut self, is_hovered: bool, now: Instant) -> bool {
1992 if self.is_hovered == is_hovered {
1993 return false;
1994 }
1995
1996 self.is_hovered = is_hovered;
1997
1998 if !self.is_pressed {
1999 if !is_hovered {
2000 self.clear_ripples();
2001 }
2002
2003 self.animate_to_interaction_target(now);
2004 }
2005
2006 true
2007 }
2008
2009 fn press(&mut self, origin: Point, now: Instant) {
2010 self.is_pressed = true;
2011 self.ripples.press(
2012 origin,
2013 now,
2014 RippleStart::Replace,
2015 RippleStyle::material_patterned(),
2016 );
2017 self.now = Some(now);
2018 self.animate_to_interaction_target(now);
2019 }
2020
2021 fn release(&mut self, is_hovered: bool, now: Instant) {
2022 self.release_with_hover(is_hovered, is_hovered, now);
2023 }
2024
2025 fn release_with_hover(&mut self, keep_ripple: bool, is_hovered: bool, now: Instant) {
2026 self.is_pressed = false;
2027 self.is_hovered = is_hovered;
2028
2029 if keep_ripple {
2030 self.ripples.release_replacing(now);
2031 } else {
2032 self.clear_ripples();
2033 }
2034
2035 self.now = Some(now);
2036 self.animate_to_interaction_target(now);
2037 }
2038
2039 fn snap_to_interaction_target(&mut self) {
2040 self.state_layer_opacity
2041 .snap_to(navigation_interaction_state_layer_target(
2042 self.is_hovered,
2043 self.is_pressed,
2044 ));
2045 }
2046
2047 fn cancel(&mut self, now: Instant) {
2048 self.is_pressed = false;
2049 self.is_hovered = false;
2050 self.clear_ripples();
2051
2052 self.now = Some(now);
2053 self.animate_to_interaction_target(now);
2054 }
2055
2056 fn advance(&mut self, now: Instant) -> bool {
2057 self.now = Some(now);
2058 self.prune(now);
2059
2060 self.state_layer_opacity.advance(now) || self.has_visible_ripples(now)
2061 }
2062
2063 fn opacity(&self) -> f32 {
2064 navigation_surface_state_layer_opacity_from_interaction(self.state_layer_opacity.value)
2065 }
2066
2067 fn animate_to_interaction_target(&mut self, now: Instant) {
2068 self.state_layer_opacity.set_target(
2069 navigation_interaction_state_layer_target(self.is_hovered, self.is_pressed),
2070 now,
2071 duration_ms(tokens::motion::DURATION_SHORT2_MS),
2072 tokens::motion::EASING_STANDARD,
2073 );
2074 }
2075
2076 fn clear_ripples(&mut self) {
2077 self.ripples.clear();
2078 }
2079
2080 fn prune(&mut self, now: Instant) {
2081 self.ripples.prune(now);
2082 }
2083
2084 fn has_visible_ripples(&self, now: Instant) -> bool {
2085 self.ripples.has_visible_ripples(now)
2086 }
2087}
2088
2089impl<Message, Renderer> Widget<Message, Theme, Renderer>
2090 for NavigationPressSurface<'_, Message, Renderer>
2091where
2092 Message: Clone,
2093 Renderer: geometry::Renderer + primitive::Renderer,
2094{
2095 fn tag(&self) -> tree::Tag {
2096 tree::Tag::of::<NavigationPressSurfaceState>()
2097 }
2098
2099 fn state(&self) -> tree::State {
2100 tree::State::new(NavigationPressSurfaceState::default())
2101 }
2102
2103 fn children(&self) -> Vec<Tree> {
2104 vec![Tree::new(&self.content)]
2105 }
2106
2107 fn diff(&self, tree: &mut Tree) {
2108 tree.diff_children(std::slice::from_ref(&self.content));
2109 }
2110
2111 fn size(&self) -> Size<Length> {
2112 self.content.as_widget().size()
2113 }
2114
2115 fn size_hint(&self) -> Size<Length> {
2116 self.content.as_widget().size_hint()
2117 }
2118
2119 fn layout(
2120 &mut self,
2121 tree: &mut Tree,
2122 renderer: &Renderer,
2123 limits: &layout::Limits,
2124 ) -> layout::Node {
2125 self.content
2126 .as_widget_mut()
2127 .layout(&mut tree.children[0], renderer, limits)
2128 }
2129
2130 fn operate(
2131 &mut self,
2132 tree: &mut Tree,
2133 layout: Layout<'_>,
2134 renderer: &Renderer,
2135 operation: &mut dyn Operation,
2136 ) {
2137 self.content
2138 .as_widget_mut()
2139 .operate(&mut tree.children[0], layout, renderer, operation);
2140 }
2141
2142 fn update(
2143 &mut self,
2144 tree: &mut Tree,
2145 event: &Event,
2146 layout: Layout<'_>,
2147 cursor: mouse::Cursor,
2148 renderer: &Renderer,
2149 clipboard: &mut dyn Clipboard,
2150 shell: &mut Shell<'_, Message>,
2151 viewport: &Rectangle,
2152 ) {
2153 self.content.as_widget_mut().update(
2154 &mut tree.children[0],
2155 event,
2156 layout,
2157 cursor,
2158 renderer,
2159 clipboard,
2160 shell,
2161 viewport,
2162 );
2163
2164 if shell.is_event_captured() {
2165 return;
2166 }
2167
2168 let state = tree.state.downcast_mut::<NavigationPressSurfaceState>();
2169 let now = match event {
2170 Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
2171 _ => None,
2172 };
2173 let is_touch_event = matches!(event, Event::Touch(_));
2174 let is_hovered = !is_touch_event && cursor.is_over(layout.bounds());
2175 let should_snap_initial_redraw_hover =
2176 navigation_should_snap_initial_redraw_hover(event, state, is_hovered);
2177
2178 if navigation_should_sync_hover(event, cursor) {
2179 if state.sync_hover(is_hovered, now.unwrap_or_else(Instant::now)) {
2180 if should_snap_initial_redraw_hover {
2181 state.snap_to_interaction_target();
2182 }
2183
2184 shell.request_redraw();
2185 }
2186 }
2187
2188 match event {
2189 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
2190 | Event::Touch(touch::Event::FingerPressed { .. }) => {
2191 if navigation_event_is_over(event, layout.bounds(), cursor) {
2192 let indicator_bounds = self.indicator.bounds(layout.bounds());
2193
2194 if let Some(origin) = navigation_press_origin(event, indicator_bounds, cursor) {
2195 state.press(origin, now.unwrap_or_else(Instant::now));
2196 shell.request_redraw();
2197 shell.capture_event();
2198 }
2199 }
2200 }
2201 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
2202 | Event::Touch(touch::Event::FingerLifted { .. }) => {
2203 if state.is_pressed {
2204 let is_released_over = navigation_event_is_over(event, layout.bounds(), cursor);
2205 let is_touch_release = matches!(event, Event::Touch(_));
2206
2207 if is_touch_release {
2208 state.release_with_hover(
2209 is_released_over,
2210 false,
2211 now.unwrap_or_else(Instant::now),
2212 );
2213 } else {
2214 state.release(is_released_over, now.unwrap_or_else(Instant::now));
2215 }
2216 shell.request_redraw();
2217
2218 if is_released_over {
2219 shell.publish(self.on_press.clone());
2220 }
2221
2222 shell.capture_event();
2223 }
2224 }
2225 Event::Touch(touch::Event::FingerLost { .. }) => {
2226 if state.is_pressed {
2227 state.cancel(now.unwrap_or_else(Instant::now));
2228 shell.request_redraw();
2229 }
2230 }
2231 _ => {}
2232 }
2233
2234 if let Some(now) = now {
2235 if state.advance(now) {
2236 shell.request_redraw();
2237 }
2238 }
2239 }
2240
2241 fn mouse_interaction(
2242 &self,
2243 tree: &Tree,
2244 layout: Layout<'_>,
2245 cursor: mouse::Cursor,
2246 viewport: &Rectangle,
2247 renderer: &Renderer,
2248 ) -> mouse::Interaction {
2249 let content_interaction = self.content.as_widget().mouse_interaction(
2250 &tree.children[0],
2251 layout,
2252 cursor,
2253 viewport,
2254 renderer,
2255 );
2256
2257 if matches!(content_interaction, mouse::Interaction::None)
2258 && cursor.is_over(layout.bounds())
2259 {
2260 mouse::Interaction::Pointer
2261 } else {
2262 content_interaction
2263 }
2264 }
2265
2266 fn draw(
2267 &self,
2268 tree: &Tree,
2269 renderer: &mut Renderer,
2270 theme: &Theme,
2271 renderer_style: &renderer::Style,
2272 layout: Layout<'_>,
2273 cursor: mouse::Cursor,
2274 viewport: &Rectangle,
2275 ) {
2276 self.content.as_widget().draw(
2277 &tree.children[0],
2278 renderer,
2279 theme,
2280 renderer_style,
2281 layout,
2282 cursor,
2283 viewport,
2284 );
2285
2286 let state = tree.state.downcast_ref::<NavigationPressSurfaceState>();
2287 let indicator_bounds = self.indicator.bounds(layout.bounds());
2288 let now = state.now.unwrap_or_else(Instant::now);
2289 let opacity = navigation_press_surface_opacity_for_draw(state, cursor, layout.bounds());
2290 let layer_color = navigation_state_layer_color(theme, self.layer);
2291
2292 if opacity > 0.0 {
2293 renderer.fill_quad(
2294 renderer::Quad {
2295 bounds: indicator_bounds,
2296 border: border::rounded(tokens::shape::CORNER_FULL),
2297 snap: cfg!(feature = "crisp"),
2298 ..renderer::Quad::default()
2299 },
2300 state_layer(layer_color, opacity),
2301 );
2302 }
2303
2304 draw_ripples(
2305 renderer,
2306 indicator_bounds,
2307 &state.ripples,
2308 layer_color,
2309 RippleConfig::bounded(border::radius(tokens::shape::CORNER_FULL)),
2310 now,
2311 );
2312 }
2313
2314 fn overlay<'b>(
2315 &'b mut self,
2316 tree: &'b mut Tree,
2317 layout: Layout<'b>,
2318 renderer: &Renderer,
2319 viewport: &Rectangle,
2320 translation: Vector,
2321 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
2322 self.content.as_widget_mut().overlay(
2323 &mut tree.children[0],
2324 layout,
2325 renderer,
2326 viewport,
2327 translation,
2328 )
2329 }
2330}
2331
2332impl<'a, Message, Renderer> From<NavigationPressSurface<'a, Message, Renderer>>
2333 for Element<'a, Message, Theme, Renderer>
2334where
2335 Message: Clone + 'a,
2336 Renderer: geometry::Renderer + primitive::Renderer + 'a,
2337{
2338 fn from(surface: NavigationPressSurface<'a, Message, Renderer>) -> Self {
2339 Element::new(surface)
2340 }
2341}
2342
2343#[cfg(test)]
2344fn navigation_surface_state_layer_opacity(is_hovered: bool, is_pressed: bool) -> f32 {
2345 navigation_surface_state_layer_opacity_from_interaction(
2346 navigation_interaction_state_layer_target(is_hovered, is_pressed),
2347 )
2348}
2349
2350fn navigation_interaction_state_layer_target(is_hovered: bool, _is_pressed: bool) -> f32 {
2351 if is_hovered {
2352 HOVERED_LAYER_OPACITY
2353 } else {
2354 0.0
2355 }
2356}
2357
2358fn navigation_should_sync_hover(event: &Event, cursor: mouse::Cursor) -> bool {
2359 match event {
2360 Event::Window(window::Event::RedrawRequested(_)) => {
2361 !matches!(cursor, mouse::Cursor::Unavailable)
2362 }
2363 Event::Mouse(_) | Event::Touch(_) => true,
2364 _ => false,
2365 }
2366}
2367
2368fn navigation_should_snap_initial_redraw_hover(
2369 event: &Event,
2370 state: &NavigationPressSurfaceState,
2371 is_hovered: bool,
2372) -> bool {
2373 matches!(event, Event::Window(window::Event::RedrawRequested(_)))
2374 && state.now.is_none()
2375 && is_hovered
2376}
2377
2378fn navigation_press_surface_opacity_for_draw(
2379 state: &NavigationPressSurfaceState,
2380 cursor: mouse::Cursor,
2381 bounds: Rectangle,
2382) -> f32 {
2383 if cursor.is_over(bounds) && state.now.is_none() {
2384 navigation_surface_state_layer_opacity_from_interaction(
2385 navigation_interaction_state_layer_target(true, false),
2386 )
2387 } else {
2388 state.opacity()
2389 }
2390}
2391
2392fn navigation_surface_state_layer_opacity_from_interaction(interaction_opacity: f32) -> f32 {
2393 interaction_opacity
2394}
2395
2396fn navigation_press_origin(
2397 event: &Event,
2398 indicator_bounds: Rectangle,
2399 cursor: mouse::Cursor,
2400) -> Option<Point> {
2401 let position = cursor
2402 .position()
2403 .or_else(|| navigation_event_position(event))?;
2404
2405 if cursor.is_levitating() {
2406 return None;
2407 }
2408
2409 Some(position - Vector::new(indicator_bounds.x, indicator_bounds.y))
2410}
2411
2412fn navigation_event_is_over(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> bool {
2413 if cursor.position().is_some() {
2414 return cursor.is_over(bounds);
2415 }
2416
2417 if cursor.is_levitating() {
2418 return false;
2419 }
2420
2421 navigation_event_position(event)
2422 .map(|position| bounds.contains(position))
2423 .unwrap_or_else(|| cursor.is_over(bounds))
2424}
2425
2426fn navigation_event_position(event: &Event) -> Option<Point> {
2427 match event {
2428 Event::Touch(touch::Event::FingerPressed { position, .. })
2429 | Event::Touch(touch::Event::FingerMoved { position, .. })
2430 | Event::Touch(touch::Event::FingerLifted { position, .. })
2431 | Event::Touch(touch::Event::FingerLost { position, .. }) => Some(*position),
2432 _ => None,
2433 }
2434}
2435
2436fn indicator_layer<'a, Message, Renderer>(
2437 target_width: f32,
2438 height: f32,
2439 size_progress: f32,
2440 alpha_progress: f32,
2441) -> Container<'a, Message, Theme, Renderer>
2442where
2443 Message: 'a,
2444 Renderer: geometry::Renderer + primitive::Renderer + 'a,
2445{
2446 let indicator = Container::new(Space::new())
2447 .width(Length::Fixed(animated_indicator_width(
2448 target_width,
2449 size_progress,
2450 )))
2451 .height(Length::Fixed(height))
2452 .style(move |theme| active_indicator(theme, alpha_progress));
2453
2454 Container::new(indicator)
2455 .width(Length::Fixed(target_width))
2456 .height(Length::Fixed(height))
2457 .align_x(alignment::Horizontal::Center)
2458 .align_y(alignment::Vertical::Center)
2459}
2460
2461#[derive(Debug, Clone, Copy)]
2462enum NavigationStateLayer {
2463 BarOrRail,
2464 Drawer { progress: f32 },
2465}
2466
2467fn animated_indicator_width(target_width: f32, progress: f32) -> f32 {
2468 target_width * progress.max(0.0)
2470}
2471
2472fn navigation_bar_item_bottom_padding() -> f32 {
2473 let label = tokens::component::navigation_bar::LABEL_TEXT;
2474
2475 (tokens::component::navigation_bar::CONTAINER_HEIGHT
2476 - tokens::component::navigation_bar::INDICATOR_VERTICAL_OFFSET
2477 - tokens::component::navigation_bar::ACTIVE_INDICATOR_HEIGHT
2478 - tokens::component::navigation_bar::INDICATOR_TO_LABEL_PADDING
2479 - label.line_height)
2480 .max(0.0)
2481}
2482
2483fn navigation_rail_item_content_top_padding() -> f32 {
2484 tokens::component::navigation_rail::ITEM_TOP_PADDING
2485}
2486
2487fn navigation_rail_header_bottom_padding() -> f32 {
2488 tokens::component::navigation_rail::HEADER_PADDING
2489}
2490
2491fn navigation_rail_header_slot_height() -> f32 {
2492 tokens::component::icon_button::CONTAINER_HEIGHT + navigation_rail_header_bottom_padding()
2493}
2494
2495fn navigation_rail_item_slot_height() -> f32 {
2496 tokens::component::navigation_rail::ITEM_HEIGHT
2497}
2498
2499fn navigation_rail_expanded_item_vertical_inset() -> f32 {
2500 ((tokens::component::navigation_rail::ITEM_HEIGHT
2501 - tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_HEIGHT)
2502 / 2.0)
2503 .max(0.0)
2504}
2505
2506fn navigation_rail_expanded_indicator_height_for_progress(progress: f32) -> f32 {
2507 lerp(
2508 tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT,
2509 tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_HEIGHT,
2510 progress.clamp(0.0, 1.0),
2511 )
2512}
2513
2514fn navigation_rail_expanded_item_vertical_inset_for_progress(progress: f32) -> f32 {
2515 lerp(
2516 navigation_rail_item_content_top_padding(),
2517 navigation_rail_expanded_item_vertical_inset(),
2518 progress.clamp(0.0, 1.0),
2519 )
2520}
2521
2522fn navigation_rail_expanded_icon_anchor_width() -> f32 {
2523 tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2524 + tokens::component::navigation_rail::ICON_SIZE
2525}
2526
2527fn navigation_rail_expanded_label_leading_padding() -> f32 {
2528 navigation_rail_expanded_icon_anchor_width()
2529 + tokens::component::navigation_rail::ICON_LABEL_HORIZONTAL_SPACE
2530}
2531
2532fn navigation_rail_collapsed_label_top_padding() -> f32 {
2533 navigation_rail_item_content_top_padding()
2534 + tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT
2535 + tokens::component::navigation_rail::ITEM_VERTICAL_PADDING
2536}
2537
2538fn navigation_rail_collapsed_label_width() -> f32 {
2539 tokens::component::navigation_rail::ACTIVE_INDICATOR_WIDTH
2540}
2541
2542#[cfg(test)]
2543fn navigation_rail_collapsed_icon_center_x() -> f32 {
2544 tokens::component::navigation_rail::CONTAINER_WIDTH / 2.0
2545}
2546
2547#[cfg(test)]
2548fn navigation_rail_expanded_icon_center_x() -> f32 {
2549 tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2550 + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2551 + tokens::component::navigation_rail::ICON_SIZE / 2.0
2552}
2553
2554#[cfg(test)]
2555fn navigation_rail_collapsed_icon_center_y() -> f32 {
2556 navigation_rail_item_content_top_padding()
2557 + tokens::component::navigation_rail::ACTIVE_INDICATOR_HEIGHT / 2.0
2558}
2559
2560#[cfg(test)]
2561fn navigation_rail_expanded_icon_center_y_for_progress(progress: f32) -> f32 {
2562 navigation_rail_expanded_item_vertical_inset_for_progress(progress)
2563 + navigation_rail_expanded_indicator_height_for_progress(progress) / 2.0
2564}
2565
2566#[cfg(test)]
2567fn navigation_rail_first_item_y_after_header() -> f32 {
2568 tokens::component::navigation_rail::CONTENT_TOP_MARGIN
2569 + navigation_rail_header_slot_height()
2570 + tokens::component::navigation_rail::VERTICAL_PADDING
2571}
2572
2573fn navigation_rail_expanded_container_width(width: f32) -> f32 {
2574 width.clamp(
2575 tokens::component::navigation_rail::CONTAINER_WIDTH,
2576 tokens::component::navigation_drawer::CONTAINER_WIDTH,
2577 )
2578}
2579
2580fn navigation_rail_expanded_indicator_width(container_width: f32) -> f32 {
2581 (container_width
2582 - tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL * 2.0)
2583 .max(0.0)
2584}
2585
2586pub fn navigation_rail_expanded_width_for_progress(progress: f32) -> f32 {
2587 lerp(
2588 tokens::component::navigation_rail::CONTAINER_WIDTH,
2589 tokens::component::navigation_rail::EXPANDED_CONTAINER_WIDTH,
2590 progress.clamp(0.0, 1.0),
2591 )
2592}
2593
2594fn navigation_rail_expanded_progress_for_width(width: f32) -> f32 {
2595 let range = tokens::component::navigation_rail::EXPANDED_CONTAINER_WIDTH
2596 - tokens::component::navigation_rail::CONTAINER_WIDTH;
2597
2598 if range <= f32::EPSILON {
2599 1.0
2600 } else {
2601 ((navigation_rail_expanded_container_width(width)
2602 - tokens::component::navigation_rail::CONTAINER_WIDTH)
2603 / range)
2604 .clamp(0.0, 1.0)
2605 }
2606}
2607
2608fn navigation_rail_expanded_label_alpha_for_width(width: f32) -> f32 {
2609 let progress = navigation_rail_expanded_progress_for_width(width);
2610
2611 ((progress - 0.6) / 0.4).clamp(0.0, 1.0)
2612}
2613
2614fn navigation_rail_expanded_badge_uses_icon_anchor(label_alpha: f32) -> bool {
2615 label_alpha <= 0.0
2616}
2617
2618fn navigation_rail_expanded_trailing_badge_alpha(label_alpha: f32) -> f32 {
2619 label_alpha.clamp(0.0, 1.0)
2620}
2621
2622fn navigation_rail_expanded_collapsed_label_alpha(label_alpha: f32) -> f32 {
2623 (1.0 - label_alpha).clamp(0.0, 1.0)
2624}
2625
2626fn navigation_rail_expanded_header_leading_space() -> f32 {
2627 tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2628 + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2629 - (tokens::component::icon_button::CONTAINER_WIDTH
2630 - tokens::component::navigation_rail::ICON_SIZE)
2631 / 2.0
2632}
2633
2634fn navigation_rail_expanded_header_title_spacing() -> f32 {
2635 let label_start =
2636 tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_MARGIN_HORIZONTAL
2637 + tokens::component::navigation_rail::EXPANDED_ACTIVE_INDICATOR_PADDING_START
2638 + tokens::component::navigation_rail::ICON_SIZE
2639 + tokens::component::navigation_rail::ICON_LABEL_HORIZONTAL_SPACE;
2640
2641 (label_start
2642 - navigation_rail_expanded_header_leading_space()
2643 - tokens::component::icon_button::CONTAINER_WIDTH)
2644 .max(0.0)
2645}
2646
2647fn navigation_drawer_container_width(width: f32) -> f32 {
2648 width.clamp(
2649 tokens::component::navigation_drawer::MINIMUM_CONTAINER_WIDTH,
2650 tokens::component::navigation_drawer::CONTAINER_WIDTH,
2651 )
2652}
2653
2654fn navigation_drawer_indicator_width(container_width: f32) -> f32 {
2655 (container_width - tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING * 2.0).max(0.0)
2656}
2657
2658fn navigation_drawer_menu_header_leading_space() -> f32 {
2659 tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
2660 + tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE
2661 - (tokens::component::icon_button::CONTAINER_WIDTH
2662 - tokens::component::navigation_drawer::ICON_SIZE)
2663 / 2.0
2664}
2665
2666fn navigation_drawer_menu_header_title_spacing() -> f32 {
2667 let label_start = tokens::component::navigation_drawer::ITEM_HORIZONTAL_PADDING
2668 + tokens::component::navigation_drawer::ITEM_CONTENT_LEADING_SPACE
2669 + tokens::component::navigation_drawer::ICON_SIZE
2670 + tokens::component::navigation_drawer::ICON_LABEL_SPACE;
2671
2672 (label_start
2673 - navigation_drawer_menu_header_leading_space()
2674 - tokens::component::icon_button::CONTAINER_WIDTH)
2675 .max(0.0)
2676}
2677
2678fn navigation_drawer_badge_space() -> f32 {
2679 tokens::component::navigation_drawer::LABEL_BADGE_SPACE
2680}
2681
2682fn navigation_rail_expanded_icon_layer<'a, Message, Renderer>(
2683 icon: &'static str,
2684 progress: f32,
2685 height: f32,
2686 badge: Option<Badge>,
2687) -> Container<'a, Message, Theme, Renderer>
2688where
2689 Message: 'a,
2690 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2691 Font: Into<Renderer::Font>,
2692{
2693 let icon = destination_icon_anchor::<Message, Renderer>(
2694 icon,
2695 tokens::component::navigation_rail::ICON_SIZE,
2696 progress,
2697 badge,
2698 true,
2699 )
2700 .width(Length::Fixed(tokens::component::navigation_rail::ICON_SIZE))
2701 .height(Length::Fixed(height));
2702
2703 Container::new(icon)
2704 .width(Length::Fixed(navigation_rail_expanded_icon_anchor_width()))
2705 .height(Length::Fixed(height))
2706 .align_x(alignment::Horizontal::Right)
2707 .align_y(alignment::Vertical::Center)
2708}
2709
2710fn navigation_rail_expanded_collapsed_label<'a, Message, Renderer>(
2711 label: &'static str,
2712 progress: f32,
2713 alpha: f32,
2714) -> Container<'a, Message, Theme, Renderer>
2715where
2716 Message: 'a,
2717 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2718 Font: Into<Renderer::Font>,
2719{
2720 let alpha = alpha.clamp(0.0, 1.0);
2721 let scale = tokens::component::navigation_rail::LABEL_TEXT;
2722 let label = type_text(label, scale).style(move |theme| text::Style {
2723 color: Some(alpha_color(bar_or_rail_label_color(theme, progress), alpha)),
2724 });
2725
2726 Container::new(label)
2727 .padding(Padding {
2728 top: navigation_rail_collapsed_label_top_padding(),
2729 right: 0.0,
2730 bottom: 0.0,
2731 left: 0.0,
2732 })
2733 .align_x(alignment::Horizontal::Center)
2734}
2735
2736fn destination_icon_anchor<'a, Message, Renderer>(
2737 icon: &'static str,
2738 size: f32,
2739 progress: f32,
2740 badge: Option<Badge>,
2741 drawer: bool,
2742) -> Container<'a, Message, Theme, Renderer>
2743where
2744 Message: 'a,
2745 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2746 Font: Into<Renderer::Font>,
2747{
2748 let icon: Element<'a, Message, Theme, Renderer> =
2749 destination_icon::<Message, Renderer>(icon, size, progress, drawer).into();
2750 let anchor = if let Some(badge) = badge {
2751 badge_widget::badged_box(
2752 icon,
2753 destination_badge::<Message, Renderer>(badge),
2754 destination_badge_placement(badge),
2755 )
2756 .into()
2757 } else {
2758 icon
2759 };
2760
2761 Container::new(anchor)
2762 .align_x(alignment::Horizontal::Center)
2763 .align_y(alignment::Vertical::Center)
2764}
2765
2766fn destination_badge<'a, Message, Renderer>(badge: Badge) -> Element<'a, Message, Theme, Renderer>
2767where
2768 Message: 'a,
2769 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2770 Font: Into<Renderer::Font>,
2771{
2772 match badge {
2773 Badge::Small => badge_widget::small().into(),
2774 Badge::Large(label) => badge_widget::large(label).into(),
2775 }
2776}
2777
2778fn destination_badge_with_alpha<'a, Message, Renderer>(
2779 badge: Badge,
2780 alpha: f32,
2781) -> Element<'a, Message, Theme, Renderer>
2782where
2783 Message: 'a,
2784 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2785 Font: Into<Renderer::Font>,
2786{
2787 let alpha = alpha.clamp(0.0, 1.0);
2788
2789 match badge {
2790 Badge::Small => badge_widget::small()
2791 .style(move |theme| alpha_badge_style(theme, alpha))
2792 .into(),
2793 Badge::Large(label) => badge_widget::large(label)
2794 .style(move |theme| alpha_badge_style(theme, alpha))
2795 .into(),
2796 }
2797}
2798
2799fn alpha_badge_style(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
2800 let mut style = crate::style::badge::default(theme);
2801
2802 if let Some(Background::Color(color)) = style.background {
2803 style.background = Some(Background::Color(alpha_color(color, alpha)));
2804 }
2805
2806 style.text_color = style.text_color.map(|color| alpha_color(color, alpha));
2807 style
2808}
2809
2810fn destination_badge_placement(badge: Badge) -> badge_widget::BadgedBoxPlacement {
2811 match badge {
2812 Badge::Small => badge_widget::BadgedBoxPlacement::IconOnly,
2813 Badge::Large(_) => badge_widget::BadgedBoxPlacement::WithContent,
2814 }
2815}
2816
2817fn destination_icon<'a, Message, Renderer>(
2818 icon: &'static str,
2819 size: f32,
2820 progress: f32,
2821 drawer: bool,
2822) -> Stack<'a, Message, Theme, Renderer>
2823where
2824 Message: 'a,
2825 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2826 Font: Into<Renderer::Font>,
2827{
2828 let outline = fonts::icon(icon, size)
2829 .width(Length::Fixed(size))
2830 .height(Length::Fixed(size))
2831 .center()
2832 .style(move |theme| text::Style {
2833 color: Some(destination_icon_outline_color(theme, progress)),
2834 });
2835 let filled = fonts::filled_icon(icon, size)
2836 .width(Length::Fixed(size))
2837 .height(Length::Fixed(size))
2838 .center()
2839 .style(move |theme| text::Style {
2840 color: Some(destination_icon_filled_color(theme, progress, drawer)),
2841 });
2842
2843 Stack::new()
2844 .width(Length::Fixed(size))
2845 .height(Length::Fixed(size))
2846 .push(outline)
2847 .push(filled)
2848}
2849
2850fn destination_icon_outline_color(theme: &Theme, progress: f32) -> Color {
2851 alpha_color(
2852 theme.colors().surface.text_variant,
2853 1.0 - progress.clamp(0.0, 1.0),
2854 )
2855}
2856
2857fn destination_icon_filled_color(theme: &Theme, progress: f32, drawer: bool) -> Color {
2858 let color = if drawer {
2859 drawer_content_color(theme, 1.0)
2860 } else {
2861 bar_or_rail_icon_color(theme, 1.0)
2862 };
2863
2864 alpha_color(color, progress.clamp(0.0, 1.0))
2865}
2866
2867fn type_text<'a, Renderer>(
2868 content: &'static str,
2869 scale: tokens::typography::TypeScale,
2870) -> Text<'a, Theme, Renderer>
2871where
2872 Renderer: core_text::Renderer + 'a,
2873 Font: Into<Renderer::Font>,
2874{
2875 Text::new(content)
2876 .font(fonts::roboto_for_type_scale(scale))
2877 .size(scale.size)
2878 .line_height(LineHeight::Absolute(scale.line_height.into()))
2879}
2880
2881fn navigation_bar_container(theme: &Theme) -> iced_widget::container::Style {
2882 let colors = theme.colors();
2883
2884 iced_widget::container::Style {
2885 background: Some(Background::Color(colors.surface.color)),
2886 text_color: Some(colors.surface.text),
2887 border: border::rounded(tokens::shape::CORNER_NONE),
2888 shadow: shadow_from_level(
2889 tokens::component::navigation_bar::CONTAINER_ELEVATION_LEVEL,
2890 colors.shadow,
2891 ),
2892 ..iced_widget::container::Style::default()
2893 }
2894}
2895
2896fn navigation_rail_container(theme: &Theme) -> iced_widget::container::Style {
2897 let colors = theme.colors();
2898
2899 iced_widget::container::Style {
2900 background: Some(Background::Color(colors.surface.color)),
2901 text_color: Some(colors.surface.text),
2902 border: border::rounded(tokens::shape::CORNER_NONE),
2903 shadow: shadow_from_level(
2904 tokens::component::navigation_rail::CONTAINER_ELEVATION_LEVEL,
2905 colors.shadow,
2906 ),
2907 ..iced_widget::container::Style::default()
2908 }
2909}
2910
2911fn navigation_drawer_container(theme: &Theme) -> iced_widget::container::Style {
2912 let colors = theme.colors();
2913
2914 iced_widget::container::Style {
2915 background: Some(Background::Color(colors.surface.color)),
2916 text_color: Some(colors.surface.text),
2917 border: border::rounded(tokens::shape::CORNER_LARGE),
2918 shadow: shadow_from_level(
2919 tokens::component::navigation_drawer::STANDARD_CONTAINER_ELEVATION_LEVEL,
2920 colors.shadow,
2921 ),
2922 ..iced_widget::container::Style::default()
2923 }
2924}
2925
2926fn active_indicator(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
2927 let mut color = theme.colors().secondary.container;
2928 color.a *= alpha.clamp(0.0, 1.0);
2929
2930 iced_widget::container::Style {
2931 background: Some(Background::Color(color)),
2932 text_color: Some(theme.colors().secondary.container_text),
2933 border: border::rounded(tokens::shape::CORNER_FULL),
2934 ..iced_widget::container::Style::default()
2935 }
2936}
2937
2938fn headline_text_style(theme: &Theme) -> text::Style {
2939 text::Style {
2940 color: Some(theme.colors().surface.text_variant),
2941 }
2942}
2943
2944fn bar_or_rail_icon_color(theme: &Theme, progress: f32) -> Color {
2945 let colors = theme.colors();
2946
2947 mix(
2948 colors.surface.text_variant,
2949 colors.secondary.container_text,
2950 progress,
2951 )
2952}
2953
2954fn bar_or_rail_label_color(theme: &Theme, progress: f32) -> Color {
2955 let colors = theme.colors();
2956
2957 mix(colors.surface.text_variant, colors.surface.text, progress)
2958}
2959
2960fn drawer_content_color(theme: &Theme, progress: f32) -> Color {
2961 let colors = theme.colors();
2962
2963 mix(
2964 colors.surface.text_variant,
2965 colors.secondary.container_text,
2966 progress,
2967 )
2968}
2969
2970fn navigation_state_layer_color(theme: &Theme, layer: NavigationStateLayer) -> Color {
2971 let colors = theme.colors();
2972
2973 match layer {
2974 NavigationStateLayer::BarOrRail => colors.surface.text,
2978 NavigationStateLayer::Drawer { progress } => mix(
2979 colors.surface.text,
2980 colors.secondary.container_text,
2981 progress,
2982 ),
2983 }
2984}
2985
2986#[cfg(test)]
2987#[path = "../../../tests/widget/component/navigation.rs"]
2988mod tests;