1use std::cell::{OnceCell, RefCell};
40
41use frus_core::{Color, Insets, Point, Rect, ResolvedTextStyle, Scene, ShapeBorder, TextStyle};
42use frus_layout::{Dimension, FlexDirection, Style};
43
44use crate::disabled::disabled_content;
45use crate::flex::Flex;
46use crate::interaction::Status;
47use crate::navrail::{DestinationIcon, NavigationDestination};
48use crate::scroll::SingleChildScrollView;
49use crate::theme::Theme;
50use crate::widget::{FillAxes, Widget};
51use crate::widgetstate::WidgetStateProperty;
52
53const TILE_HEIGHT: f32 = 56.0;
55const TILE_PADDING: Insets = Insets::new(0.0, 12.0, 0.0, 12.0);
57const INDICATOR_WIDTH: f32 = 336.0;
60const INDICATOR_HEIGHT: f32 = 56.0;
61const ICON_LEAD: f32 = 16.0;
63const ICON_GAP: f32 = 12.0;
65const ICON_SIZE: f32 = 24.0;
67const BADGE_TRAIL: f32 = 24.0;
69const ELEVATION: f32 = 1.0;
71
72fn label_style(over: Option<TextStyle>, theme: Option<&Theme>) -> ResolvedTextStyle {
75 over.or(theme.and_then(|t| t.widgets.nav_drawer.label_text_style))
76 .unwrap_or_else(|| crate::theme::type_scale(theme).label_large)
77 .resolved()
78}
79
80fn badge_style(over: Option<TextStyle>, theme: Option<&Theme>) -> ResolvedTextStyle {
82 over.or(theme.and_then(|t| t.widgets.nav_drawer.badge_text_style))
83 .unwrap_or_else(|| crate::theme::type_scale(theme).label_small)
84 .resolved()
85}
86
87enum Entry<Msg> {
94 Stop(Box<NavigationDestination>),
99 Other(Box<dyn Widget<Msg>>),
101}
102
103struct DrawerTile<Msg> {
112 icon: DestinationIcon,
113 label: String,
114 badge: Option<u32>,
115 selected: bool,
116 disabled: bool,
117 index: usize,
119 count: usize,
120 indicator_color: Option<Color>,
122 indicator_shape: Option<ShapeBorder>,
123 overlay_color: Option<WidgetStateProperty<Color>>,
124 ground: Option<Color>,
127 background: Option<Color>,
130 tile_height: Option<f32>,
131 tile_padding: Option<Insets>,
132 indicator_size: Option<(f32, f32)>,
133 icon_size: Option<f32>,
134 label_text_style: Option<TextStyle>,
135 badge_text_style: Option<TextStyle>,
136 message: Msg,
137}
138
139impl<Msg> DrawerTile<Msg> {
140 fn padding(&self, theme: Option<&Theme>) -> Insets {
142 self.tile_padding
143 .or(theme.and_then(|t| t.widgets.nav_drawer.tile_padding))
144 .unwrap_or(TILE_PADDING)
145 }
146
147 fn glyph_size(&self, theme: Option<&Theme>) -> f32 {
149 self.icon_size
150 .or(theme.and_then(|t| t.widgets.nav_drawer.icon_size))
151 .unwrap_or(ICON_SIZE)
152 }
153
154 fn sizing(&self, theme: Option<&Theme>) -> Style {
158 let pad = self.padding(theme);
159 let floor = self
160 .tile_height
161 .or(theme.and_then(|t| t.widgets.nav_drawer.tile_height))
162 .unwrap_or(TILE_HEIGHT);
163 let icon = frus_text::line_height(self.glyph_size(theme));
164 let label = label_style(self.label_text_style, theme).line_height();
165 Style {
166 height: Dimension::Length(floor.max(icon.max(label) + 8.0) + pad.top + pad.bottom),
167 ..Default::default()
168 }
169 }
170}
171
172impl<Msg: Clone> Widget<Msg> for DrawerTile<Msg> {
173 fn style(&self) -> Style {
174 self.sizing(None)
175 }
176
177 fn style_themed(&self, theme: &Theme) -> Style {
178 self.sizing(Some(theme))
179 }
180
181 fn fill_axes(&self, _theme: &Theme) -> FillAxes {
185 FillAxes::WIDTH
186 }
187
188 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
189 &[]
190 }
191
192 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
193 let o = status.opacity;
194 let t = &theme.widgets.nav_drawer;
195 let pad = self.padding(Some(theme));
196
197 if let Some(background) = self.background {
201 scene.fill_rect(bounds, background.fade(o));
202 }
203
204 let inner = Rect::new(
205 bounds.x + pad.left,
206 bounds.y + pad.top,
207 (bounds.width - pad.left - pad.right).max(0.0),
208 (bounds.height - pad.top - pad.bottom).max(0.0),
209 );
210
211 let (want_w, want_h) = self
217 .indicator_size
218 .or(t.indicator_size)
219 .unwrap_or((INDICATOR_WIDTH, INDICATOR_HEIGHT));
220 let ind_w = want_w.min(inner.width);
221 let ind_h = want_h.min(inner.height);
222 let pill = Rect::new(
223 inner.x + (inner.width - ind_w) * 0.5,
224 inner.y + (inner.height - ind_h) * 0.5,
225 ind_w,
226 ind_h,
227 );
228
229 let ground = self
232 .background
233 .or(self.ground)
234 .or(t.background_color)
235 .unwrap_or(theme.scheme.surface_container_low);
236 let indicator = self
237 .indicator_color
238 .or(t.indicator_color)
239 .unwrap_or(theme.scheme.secondary_container);
240 let base = if self.selected { indicator } else { ground };
241
242 let states = status
243 .states()
244 .set(crate::WidgetState::Selected, self.selected)
245 .set(crate::WidgetState::Disabled, self.disabled);
246 let told = self
250 .overlay_color
251 .as_ref()
252 .and_then(|own| own.resolve(states))
253 .or_else(|| t.overlay_color.as_ref().and_then(|w| w.resolve(states)));
254 let fill = if self.disabled {
255 base
256 } else if let Some(overlay) = told {
257 base.lerp(overlay.fade(1.0), overlay.a)
258 } else {
259 theme.state_layer(base, theme.scheme.primary, &status)
260 };
261 if self.selected || fill != base {
262 let shape = self
263 .indicator_shape
264 .or(t.indicator_shape)
265 .unwrap_or_else(ShapeBorder::stadium);
266 scene.draw_shape(pill, shape, fill.fade(o));
267 }
268
269 let ink = if self.disabled {
275 disabled_content(theme)
276 } else if self.selected {
277 t.selected_color
278 .unwrap_or(theme.scheme.on_secondary_container)
279 } else {
280 t.unselected_color
281 .unwrap_or(theme.scheme.on_surface_variant)
282 };
283
284 let size = self.glyph_size(Some(theme));
285 let label_s = label_style(self.label_text_style, Some(theme));
286 let icon_m = self.icon.measure(size);
287 let label_m = frus_text::measure_resolved(&self.label, &label_s);
288
289 let x = inner.x + ICON_LEAD;
295 self.icon.paint(
296 size,
297 x,
298 inner.y + (inner.height - icon_m.height) * 0.5,
299 ink.fade(o),
300 theme,
301 scene,
302 );
303 scene.text(
304 Point::new(
305 x + size + ICON_GAP,
306 inner.y + (inner.height - label_m.height) * 0.5,
307 ),
308 self.label.clone(),
309 &label_s,
310 ink.fade(o),
311 );
312
313 if let Some(count) = self.badge.filter(|&n| n > 0) {
317 let text = if count > 99 {
318 "99+".to_string()
319 } else {
320 count.to_string()
321 };
322 let badge_s = badge_style(self.badge_text_style, Some(theme));
323 let m = frus_text::measure_resolved(&text, &badge_s);
324 let right = (pill.x + pill.width) - BADGE_TRAIL;
325 scene.text(
326 Point::new(right - m.width, inner.y + (inner.height - m.height) * 0.5),
327 text,
328 &badge_s,
329 ink.fade(o),
330 );
331 }
332 }
333
334 fn on_click(&self) -> Option<Msg> {
336 (!self.disabled).then(|| self.message.clone())
337 }
338
339 fn focusable(&self) -> bool {
341 !self.disabled
342 }
343
344 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
352 let words = crate::localizations::of();
353 let semantics = frus_core::SemanticsProperties::new(frus_core::Role::Tab)
354 .label(format!(
355 "{}, {}",
356 self.label,
357 words.tab_label(self.index + 1, self.count)
358 ))
359 .toggled(self.selected);
360 Some(if self.disabled {
361 semantics.disabled(true)
362 } else {
363 semantics.clickable()
364 })
365 }
366}
367
368pub struct NavigationDrawer<Msg = crate::callback::Callback> {
370 selected: usize,
371 on_select: Box<dyn Fn(usize) -> Msg>,
372 entries: RefCell<Vec<Entry<Msg>>>,
374 header: RefCell<Option<Box<dyn Widget<Msg>>>>,
375 footer: RefCell<Option<Box<dyn Widget<Msg>>>>,
376 width: Option<f32>,
377 background: Option<Color>,
378 elevation: Option<f32>,
379 shadow_color: Option<Color>,
380 indicator_color: Option<Color>,
381 indicator_shape: Option<ShapeBorder>,
382 indicator_size: Option<(f32, f32)>,
383 tile_height: Option<f32>,
384 tile_padding: Option<Insets>,
385 icon_size: Option<f32>,
386 label_text_style: Option<TextStyle>,
387 badge_text_style: Option<TextStyle>,
388 built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
389}
390
391impl<Msg: Clone + 'static> NavigationDrawer<Msg> {
392 pub fn new(selected: usize, on_select: impl Fn(usize) -> Msg + 'static) -> Self {
399 Self {
400 selected,
401 on_select: Box::new(on_select),
402 entries: RefCell::new(Vec::new()),
403 header: RefCell::new(None),
404 footer: RefCell::new(None),
405 width: None,
406 background: None,
407 elevation: None,
408 shadow_color: None,
409 indicator_color: None,
410 indicator_shape: None,
411 indicator_size: None,
412 tile_height: None,
413 tile_padding: None,
414 icon_size: None,
415 label_text_style: None,
416 badge_text_style: None,
417 built: OnceCell::new(),
418 }
419 }
420
421 pub fn item(self, icon: impl Into<DestinationIcon>, label: impl Into<String>) -> Self {
423 self.destination(NavigationDestination::new(icon, label))
424 }
425
426 pub(crate) fn destination(mut self, destination: NavigationDestination) -> Self {
429 self.entries
430 .borrow_mut()
431 .push(Entry::Stop(Box::new(destination)));
432 self.rebuild();
433 self
434 }
435
436 pub fn child(self, child: impl Widget<Msg> + 'static) -> Self {
442 self.child_boxed(Box::new(child))
443 }
444
445 pub fn child_boxed(mut self, child: Box<dyn Widget<Msg>>) -> Self {
447 self.entries.borrow_mut().push(Entry::Other(child));
448 self.rebuild();
449 self
450 }
451
452 #[must_use]
470 pub fn destinations(
471 mut self,
472 destinations: impl IntoIterator<Item = NavigationDestination>,
473 ) -> Self {
474 self.entries
475 .borrow_mut()
476 .extend(destinations.into_iter().map(|d| Entry::Stop(Box::new(d))));
477 self.rebuild();
478 self
479 }
480
481 pub fn badge(self, count: u32) -> Self {
483 self.decorate(|last| last.badge = Some(count))
484 }
485 #[must_use]
491 pub fn tooltip(self, message: impl Into<String>) -> Self {
492 let message = message.into();
493 self.decorate(move |last| last.tooltip = Some(message))
494 }
495
496 pub fn selected_icon(self, icon: impl Into<DestinationIcon>) -> Self {
499 let icon = icon.into();
500 self.decorate(move |last| last.selected_icon = Some(icon))
501 }
502
503 pub fn disabled(self) -> Self {
505 self.decorate(|last| last.disabled = true)
506 }
507
508 pub fn tile_color(self, color: Color) -> Self {
511 self.decorate(move |last| last.background = Some(color))
512 }
513
514 pub fn destination_indicator_color(self, color: Color) -> Self {
516 self.decorate(move |last| last.indicator_color = Some(color))
517 }
518
519 pub fn destination_indicator_shape(self, shape: ShapeBorder) -> Self {
521 self.decorate(move |last| last.indicator_shape = Some(shape))
522 }
523
524 pub fn destination_overlay_color(self, overlay: WidgetStateProperty<Color>) -> Self {
527 self.decorate(move |last| last.overlay_color = Some(overlay))
528 }
529
530 fn decorate(mut self, f: impl FnOnce(&mut NavigationDestination)) -> Self {
532 {
533 let mut entries = self.entries.borrow_mut();
534 if let Some(Entry::Stop(last)) = entries
535 .iter_mut()
536 .rev()
537 .find(|e| matches!(e, Entry::Stop(_)))
538 {
539 f(last);
540 }
541 }
542 self.rebuild();
543 self
544 }
545
546 pub fn header(self, header: impl Widget<Msg> + 'static) -> Self {
549 self.header_boxed(Box::new(header))
550 }
551
552 pub fn header_boxed(mut self, header: Box<dyn Widget<Msg>>) -> Self {
554 *self.header.borrow_mut() = Some(header);
555 self.rebuild();
556 self
557 }
558
559 pub fn footer(self, footer: impl Widget<Msg> + 'static) -> Self {
562 self.footer_boxed(Box::new(footer))
563 }
564
565 pub fn footer_boxed(mut self, footer: Box<dyn Widget<Msg>>) -> Self {
567 *self.footer.borrow_mut() = Some(footer);
568 self.rebuild();
569 self
570 }
571
572 pub fn width(mut self, width: f32) -> Self {
576 self.width = Some(width);
577 self
578 }
579
580 pub fn background_color(mut self, color: Color) -> Self {
583 self.background = Some(color);
584 self.rebuild();
585 self
586 }
587
588 pub fn elevation(mut self, elevation: f32) -> Self {
591 self.elevation = Some(elevation);
592 self
593 }
594
595 pub fn shadow_color(mut self, color: Color) -> Self {
599 self.shadow_color = Some(color);
600 self
601 }
602
603 pub fn indicator_color(mut self, color: Color) -> Self {
606 self.indicator_color = Some(color);
607 self.rebuild();
608 self
609 }
610
611 pub fn indicator_shape(mut self, shape: ShapeBorder) -> Self {
613 self.indicator_shape = Some(shape);
614 self.rebuild();
615 self
616 }
617
618 pub fn indicator_size(mut self, width: f32, height: f32) -> Self {
621 self.indicator_size = Some((width, height));
622 self.rebuild();
623 self
624 }
625
626 pub fn tile_height(mut self, height: f32) -> Self {
628 self.tile_height = Some(height);
629 self.rebuild();
630 self
631 }
632
633 pub fn tile_padding(mut self, padding: Insets) -> Self {
636 self.tile_padding = Some(padding);
637 self.rebuild();
638 self
639 }
640
641 pub fn icon_size(mut self, size: f32) -> Self {
643 self.icon_size = Some(size);
644 self.rebuild();
645 self
646 }
647
648 pub fn label_text_style(mut self, style: TextStyle) -> Self {
650 self.label_text_style = Some(style);
651 self.rebuild();
652 self
653 }
654
655 pub fn badge_text_style(mut self, style: TextStyle) -> Self {
657 self.badge_text_style = Some(style);
658 self.rebuild();
659 self
660 }
661
662 fn rebuild(&mut self) {
665 self.built.take();
666 }
667
668 fn assemble(&self) -> Vec<Box<dyn Widget<Msg>>> {
675 let entries = std::mem::take(&mut *self.entries.borrow_mut());
676 let count = entries
677 .iter()
678 .filter(|e| matches!(e, Entry::Stop(_)))
679 .count();
680
681 let mut list = Flex::<Msg>::column();
682 let mut index = 0;
683 for entry in entries {
684 match entry {
685 Entry::Other(child) => list = list.child_boxed(child),
686 Entry::Stop(stop) => {
687 let selected = index == self.selected;
688 let tooltip = stop.tooltip.clone();
689 let tile = DrawerTile {
690 icon: stop.glyph(selected).clone(),
691 label: stop.label.clone(),
692 badge: stop.badge,
693 selected,
694 disabled: stop.disabled,
695 index,
696 count,
697 indicator_color: stop.indicator_color.or(self.indicator_color),
698 indicator_shape: stop.indicator_shape.or(self.indicator_shape),
699 overlay_color: stop.overlay_color.clone(),
700 ground: self.background,
701 background: stop.background,
702 tile_height: self.tile_height,
703 tile_padding: self.tile_padding,
704 indicator_size: self.indicator_size,
705 icon_size: self.icon_size,
706 label_text_style: self.label_text_style,
707 badge_text_style: self.badge_text_style,
708 message: (self.on_select)(index),
709 };
710 list = match tooltip {
713 Some(message) => list.child(crate::Tooltip::new(message).child(tile)),
714 None => list.child(tile),
715 };
716 index += 1;
717 }
718 }
719 }
720
721 let mut out: Vec<Box<dyn Widget<Msg>>> = Vec::new();
722 if let Some(header) = self.header.borrow_mut().take() {
723 out.push(header);
724 }
725 out.push(Box::new(
726 SingleChildScrollView::<Msg>::new().flex(1.0).child(list),
727 ));
728 if let Some(footer) = self.footer.borrow_mut().take() {
729 out.push(footer);
730 }
731 out
732 }
733}
734
735impl<Msg: Clone + 'static> Widget<Msg> for NavigationDrawer<Msg> {
736 fn style(&self) -> Style {
742 let safe = crate::MediaQuery::of().padding;
743 Style {
744 width: self.width.map_or(Dimension::Auto, Dimension::Length),
745 flex_direction: FlexDirection::Column,
746 padding: Insets::new(safe.top, 0.0, 0.0, 0.0),
747 ..Default::default()
748 }
749 }
750
751 fn fill_axes(&self, _theme: &Theme) -> FillAxes {
753 if self.width.is_some() {
754 FillAxes::HEIGHT
755 } else {
756 FillAxes::BOTH
757 }
758 }
759
760 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
761 self.built.get_or_init(|| self.assemble())
762 }
763
764 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
765 let o = status.opacity;
766 let t = &theme.widgets.nav_drawer;
767 let depth = self.elevation.or(t.elevation).unwrap_or(ELEVATION);
768 let shadow = self
772 .shadow_color
773 .or(t.shadow_color)
774 .unwrap_or(Color::TRANSPARENT);
775 if depth > 0.0 && shadow.a > 0.0 {
776 let blur = depth * 4.0 + 8.0;
777 scene.shadow(
778 Rect::new(
779 bounds.x - blur,
780 bounds.y + depth * 2.0 - blur,
781 bounds.width + 2.0 * blur,
782 bounds.height + 2.0 * blur,
783 ),
784 shadow.fade(o),
785 frus_core::BorderRadius::uniform(blur),
786 blur,
787 );
788 }
789 let fill = self
790 .background
791 .or(t.background_color)
792 .unwrap_or(theme.scheme.surface_container_low);
793 scene.fill_rect(bounds, fill.fade(o));
794 }
795
796 fn on_click(&self) -> Option<Msg> {
797 None
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use crate::divider::Divider;
805 use crate::text::Text;
806
807 #[derive(Clone, Debug, PartialEq)]
808 enum Msg {
809 Go(usize),
810 }
811
812 fn drawer() -> NavigationDrawer<Msg> {
815 NavigationDrawer::new(1, Msg::Go)
816 .item("I", "Inbox")
817 .item("S", "Starred")
818 .child(Divider::new())
819 .item("T", "Trash")
820 }
821
822 fn tiles(drawer: &NavigationDrawer<Msg>) -> Vec<&dyn Widget<Msg>> {
826 fn walk<'a>(node: &'a dyn Widget<Msg>, out: &mut Vec<&'a dyn Widget<Msg>>) {
827 if node.debug_name() == "DrawerTile" {
828 out.push(node);
829 }
830 for child in node.children() {
831 walk(&**child, out);
832 }
833 }
834 let mut out = Vec::new();
835 for child in Widget::<Msg>::children(drawer) {
836 walk(&**child, &mut out);
837 }
838 out
839 }
840
841 fn scene_of(widget: &dyn Widget<Msg>, bounds: Rect, theme: &Theme) -> Scene {
842 let mut scene = Scene::new();
843 widget.paint(
844 bounds,
845 Status {
846 opacity: 1.0,
847 ..Default::default()
848 },
849 theme,
850 &mut scene,
851 );
852 scene
853 }
854
855 fn texts(scene: &Scene) -> Vec<(String, Color)> {
856 scene
857 .primitives()
858 .iter()
859 .filter_map(|p| match p {
860 frus_core::Primitive::Text { text, color, .. } => Some((text.clone(), *color)),
861 _ => None,
862 })
863 .collect()
864 }
865
866 #[test]
873 fn a_rule_between_destinations_takes_no_number() {
874 let drawer = drawer();
875 let tiles = tiles(&drawer);
876 assert_eq!(
877 tiles.len(),
878 3,
879 "three destinations, and the rule is not one"
880 );
881 assert_eq!(
882 tiles
883 .iter()
884 .filter_map(|t| t.on_click())
885 .collect::<Vec<_>>(),
886 vec![Msg::Go(0), Msg::Go(1), Msg::Go(2)],
887 "the third destination is 2, not 3"
888 );
889 assert_eq!(
890 tiles
891 .iter()
892 .map(|t| t.semantics().expect("announced").toggled)
893 .collect::<Vec<_>>(),
894 vec![
895 frus_core::Toggled::False,
896 frus_core::Toggled::True,
897 frus_core::Toggled::False
898 ],
899 "and the middle one is the live one"
900 );
901 }
902
903 #[test]
905 fn a_reader_is_told_which_of_how_many() {
906 let drawer = drawer();
907 let heard: Vec<String> = tiles(&drawer)
908 .iter()
909 .filter_map(|t| t.semantics()?.label)
910 .collect();
911 assert_eq!(
912 heard,
913 vec![
914 "Inbox, Tab 1 of 3".to_string(),
915 "Starred, Tab 2 of 3".to_string(),
916 "Trash, Tab 3 of 3".to_string(),
917 ],
918 "the rule between them is not a destination, so it is not counted either"
919 );
920 }
921
922 fn lone_tile(selected: bool) -> DrawerTile<Msg> {
924 DrawerTile {
925 icon: "I".into(),
926 label: "Inbox".into(),
927 badge: None,
928 selected,
929 disabled: false,
930 index: 0,
931 count: 1,
932 indicator_color: None,
933 indicator_shape: None,
934 overlay_color: None,
935 ground: None,
936 background: None,
937 tile_height: None,
938 tile_padding: None,
939 indicator_size: None,
940 icon_size: None,
941 label_text_style: None,
942 badge_text_style: None,
943 message: Msg::Go(0),
944 }
945 }
946
947 #[test]
952 fn the_pill_never_grows_past_the_room_the_tile_has() {
953 let theme = Theme::default();
954 let tile = lone_tile(true);
955 let pill_width = |width: f32| {
956 let scene = scene_of(&tile, Rect::new(0.0, 0.0, width, TILE_HEIGHT), &theme);
957 scene
958 .primitives()
959 .iter()
960 .find_map(|p| match p {
961 frus_core::Primitive::Rect { rect, .. } => Some(rect.width),
962 _ => None,
963 })
964 .expect("a selected destination has an indicator")
965 };
966
967 assert_eq!(
968 pill_width(304.0),
969 304.0 - 24.0,
970 "in a 304-wide panel the pill takes what is left after the padding"
971 );
972 assert_eq!(
973 pill_width(600.0),
974 INDICATOR_WIDTH,
975 "and stops at its declared width once there is room for it"
976 );
977 }
978
979 #[test]
983 fn a_selected_row_paints_its_glyph_and_its_label_the_same() {
984 let theme = Theme::default();
985 let scene = scene_of(
986 &lone_tile(true),
987 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
988 &theme,
989 );
990 let painted = texts(&scene);
991 assert_eq!(painted.len(), 2, "a glyph and a label");
992 assert_eq!(
993 painted[0].1, painted[1].1,
994 "the glyph and the label stand on the same indicator"
995 );
996 assert_eq!(painted[0].1, theme.scheme.on_secondary_container);
997
998 let scene = scene_of(
999 &lone_tile(false),
1000 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
1001 &theme,
1002 );
1003 assert_eq!(texts(&scene)[0].1, theme.scheme.on_surface_variant);
1004 }
1005
1006 #[test]
1009 fn a_destination_that_cannot_be_reached_answers_nowhere() {
1010 let drawer = NavigationDrawer::new(0, Msg::Go)
1011 .item("I", "Inbox")
1012 .item("A", "Archive")
1013 .disabled();
1014 let tiles = tiles(&drawer);
1015 let off = *tiles.last().expect("two of them");
1016 assert_eq!(off.on_click(), None, "the tap goes nowhere");
1017 assert!(!off.focusable(), "and the tab skips it");
1018 let semantics = off.semantics().expect("it is still announced");
1019 assert!(semantics.disabled, "and it is announced as unavailable");
1020 assert!(!semantics.clickable);
1021 assert_eq!(
1022 tiles[0].on_click(),
1023 Some(Msg::Go(0)),
1024 "its neighbour is untouched"
1025 );
1026 }
1027
1028 #[test]
1031 fn a_destination_that_cannot_be_reached_does_not_light_up() {
1032 let theme = Theme::default();
1033 let mut tile = lone_tile(false);
1034 tile.disabled = true;
1035 let mut scene = Scene::new();
1036 Widget::<Msg>::paint(
1037 &tile,
1038 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
1039 Status {
1040 opacity: 1.0,
1041 hover_progress: 1.0,
1042 ..Default::default()
1043 },
1044 &theme,
1045 &mut scene,
1046 );
1047 assert!(
1048 !scene
1049 .primitives()
1050 .iter()
1051 .any(|p| matches!(p, frus_core::Primitive::Rect { .. })),
1052 "an unreachable destination has nothing to say to a pointer resting on it"
1053 );
1054 }
1055
1056 #[test]
1059 fn the_two_ends_do_not_scroll() {
1060 let drawer = drawer()
1061 .header(Text::new("Mailbox"))
1062 .footer(Text::new("v1"));
1063 let children = Widget::<Msg>::children(&drawer);
1064 assert_eq!(children.len(), 3, "header, list, footer");
1065 assert_eq!(
1066 children[1].debug_name(),
1067 "SingleChildScrollView",
1068 "and only the middle one scrolls"
1069 );
1070 }
1071
1072 #[test]
1076 fn saying_it_afterwards_says_it() {
1077 let theme = Theme::default();
1078 let ink = Color::rgb8(200, 30, 30);
1079 let before = NavigationDrawer::new(0, Msg::Go)
1080 .indicator_color(ink)
1081 .item("I", "Inbox");
1082 let after = NavigationDrawer::new(0, Msg::Go)
1083 .item("I", "Inbox")
1084 .indicator_color(ink);
1085 let fill = |drawer: &NavigationDrawer<Msg>| {
1086 let found = tiles(drawer);
1087 let scene = scene_of(found[0], Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT), &theme);
1088 scene
1089 .primitives()
1090 .iter()
1091 .find_map(|p| match p {
1092 frus_core::Primitive::Rect { color, .. } => Some(*color),
1093 _ => None,
1094 })
1095 .expect("the selected destination has an indicator")
1096 };
1097 assert_eq!(fill(&before), ink);
1098 assert_eq!(fill(&after), ink, "the order the builders were called in");
1099 }
1100
1101 #[test]
1104 fn a_row_grows_before_it_clips() {
1105 let theme = Theme::default();
1106 let plain_drawer = drawer();
1107 let plain = tiles(&plain_drawer)[0].style_themed(&theme).height;
1108 assert_eq!(plain, Dimension::Length(TILE_HEIGHT));
1109
1110 let big = drawer().label_text_style(TextStyle::new(48.0));
1111 let grown = tiles(&big)[0].style_themed(&theme).height;
1112 assert!(
1113 matches!(grown, Dimension::Length(h) if h > TILE_HEIGHT),
1114 "the row makes room for the type rather than cutting it: {grown:?}"
1115 );
1116 }
1117
1118 #[test]
1121 fn the_panel_stands_a_rung_above_a_rail() {
1122 let theme = Theme::default();
1123 let scene = scene_of(&drawer(), Rect::new(0.0, 0.0, 304.0, 600.0), &theme);
1124 let surface = scene
1125 .primitives()
1126 .iter()
1127 .rev()
1128 .find_map(|p| match p {
1129 frus_core::Primitive::Rect { color, blur, .. } if *blur == 0.0 => Some(*color),
1130 _ => None,
1131 })
1132 .expect("the panel paints one");
1133 assert_eq!(surface, theme.scheme.surface_container_low);
1134 assert_ne!(
1135 theme.scheme.surface_container_low, theme.scheme.surface,
1136 "the two rungs have to be tellable apart for that to mean anything"
1137 );
1138 }
1139
1140 #[test]
1145 fn a_drawer_casts_no_shadow_until_a_colour_is_named() {
1146 let bounds = Rect::new(0.0, 0.0, 304.0, 600.0);
1147 let shadows = |drawer: &NavigationDrawer<Msg>, theme: &Theme| {
1148 scene_of(drawer, bounds, theme)
1149 .primitives()
1150 .iter()
1151 .filter_map(|p| match p {
1152 frus_core::Primitive::Rect { color, blur, .. } if *blur > 0.0 => Some(*color),
1153 _ => None,
1154 })
1155 .collect::<Vec<_>>()
1156 };
1157 let theme = Theme::default();
1158 assert!(shadows(&drawer(), &theme).is_empty(), "none by default");
1159
1160 let named = Color::rgba(0.0, 0.0, 0.0, 0.25);
1161 assert_eq!(shadows(&drawer().shadow_color(named), &theme), vec![named]);
1162 let mut themed = Theme::default();
1163 themed.widgets.nav_drawer.shadow_color = Some(named);
1164 assert_eq!(shadows(&drawer(), &themed), vec![named], "or on the theme");
1165 assert!(
1166 shadows(&drawer().shadow_color(named).elevation(0.0), &theme).is_empty(),
1167 "and a flat drawer has no height to cast it from"
1168 );
1169 }
1170}