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 frus_core::paint_elevation(
778 scene,
779 bounds,
780 frus_core::BorderRadius::ZERO,
781 depth,
782 shadow.fade(o),
783 );
784 }
785 let fill = self
786 .background
787 .or(t.background_color)
788 .unwrap_or(theme.scheme.surface_container_low);
789 scene.fill_rect(bounds, fill.fade(o));
790 }
791
792 fn on_click(&self) -> Option<Msg> {
793 None
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use crate::divider::Divider;
801 use crate::text::Text;
802
803 #[derive(Clone, Debug, PartialEq)]
804 enum Msg {
805 Go(usize),
806 }
807
808 fn drawer() -> NavigationDrawer<Msg> {
811 NavigationDrawer::new(1, Msg::Go)
812 .item("I", "Inbox")
813 .item("S", "Starred")
814 .child(Divider::new())
815 .item("T", "Trash")
816 }
817
818 fn tiles(drawer: &NavigationDrawer<Msg>) -> Vec<&dyn Widget<Msg>> {
822 fn walk<'a>(node: &'a dyn Widget<Msg>, out: &mut Vec<&'a dyn Widget<Msg>>) {
823 if node.debug_name() == "DrawerTile" {
824 out.push(node);
825 }
826 for child in node.children() {
827 walk(&**child, out);
828 }
829 }
830 let mut out = Vec::new();
831 for child in Widget::<Msg>::children(drawer) {
832 walk(&**child, &mut out);
833 }
834 out
835 }
836
837 fn scene_of(widget: &dyn Widget<Msg>, bounds: Rect, theme: &Theme) -> Scene {
838 let mut scene = Scene::new();
839 widget.paint(
840 bounds,
841 Status {
842 opacity: 1.0,
843 ..Default::default()
844 },
845 theme,
846 &mut scene,
847 );
848 scene
849 }
850
851 fn texts(scene: &Scene) -> Vec<(String, Color)> {
852 scene
853 .primitives()
854 .iter()
855 .filter_map(|p| match p {
856 frus_core::Primitive::Text { text, color, .. } => Some((text.clone(), *color)),
857 _ => None,
858 })
859 .collect()
860 }
861
862 #[test]
869 fn a_rule_between_destinations_takes_no_number() {
870 let drawer = drawer();
871 let tiles = tiles(&drawer);
872 assert_eq!(
873 tiles.len(),
874 3,
875 "three destinations, and the rule is not one"
876 );
877 assert_eq!(
878 tiles
879 .iter()
880 .filter_map(|t| t.on_click())
881 .collect::<Vec<_>>(),
882 vec![Msg::Go(0), Msg::Go(1), Msg::Go(2)],
883 "the third destination is 2, not 3"
884 );
885 assert_eq!(
886 tiles
887 .iter()
888 .map(|t| t.semantics().expect("announced").toggled)
889 .collect::<Vec<_>>(),
890 vec![
891 frus_core::Toggled::False,
892 frus_core::Toggled::True,
893 frus_core::Toggled::False
894 ],
895 "and the middle one is the live one"
896 );
897 }
898
899 #[test]
901 fn a_reader_is_told_which_of_how_many() {
902 let drawer = drawer();
903 let heard: Vec<String> = tiles(&drawer)
904 .iter()
905 .filter_map(|t| t.semantics()?.label)
906 .collect();
907 assert_eq!(
908 heard,
909 vec![
910 "Inbox, Tab 1 of 3".to_string(),
911 "Starred, Tab 2 of 3".to_string(),
912 "Trash, Tab 3 of 3".to_string(),
913 ],
914 "the rule between them is not a destination, so it is not counted either"
915 );
916 }
917
918 fn lone_tile(selected: bool) -> DrawerTile<Msg> {
920 DrawerTile {
921 icon: "I".into(),
922 label: "Inbox".into(),
923 badge: None,
924 selected,
925 disabled: false,
926 index: 0,
927 count: 1,
928 indicator_color: None,
929 indicator_shape: None,
930 overlay_color: None,
931 ground: None,
932 background: None,
933 tile_height: None,
934 tile_padding: None,
935 indicator_size: None,
936 icon_size: None,
937 label_text_style: None,
938 badge_text_style: None,
939 message: Msg::Go(0),
940 }
941 }
942
943 #[test]
948 fn the_pill_never_grows_past_the_room_the_tile_has() {
949 let theme = Theme::default();
950 let tile = lone_tile(true);
951 let pill_width = |width: f32| {
952 let scene = scene_of(&tile, Rect::new(0.0, 0.0, width, TILE_HEIGHT), &theme);
953 scene
954 .primitives()
955 .iter()
956 .find_map(|p| match p {
957 frus_core::Primitive::Rect { rect, .. } => Some(rect.width),
958 _ => None,
959 })
960 .expect("a selected destination has an indicator")
961 };
962
963 assert_eq!(
964 pill_width(304.0),
965 304.0 - 24.0,
966 "in a 304-wide panel the pill takes what is left after the padding"
967 );
968 assert_eq!(
969 pill_width(600.0),
970 INDICATOR_WIDTH,
971 "and stops at its declared width once there is room for it"
972 );
973 }
974
975 #[test]
979 fn a_selected_row_paints_its_glyph_and_its_label_the_same() {
980 let theme = Theme::default();
981 let scene = scene_of(
982 &lone_tile(true),
983 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
984 &theme,
985 );
986 let painted = texts(&scene);
987 assert_eq!(painted.len(), 2, "a glyph and a label");
988 assert_eq!(
989 painted[0].1, painted[1].1,
990 "the glyph and the label stand on the same indicator"
991 );
992 assert_eq!(painted[0].1, theme.scheme.on_secondary_container);
993
994 let scene = scene_of(
995 &lone_tile(false),
996 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
997 &theme,
998 );
999 assert_eq!(texts(&scene)[0].1, theme.scheme.on_surface_variant);
1000 }
1001
1002 #[test]
1005 fn a_destination_that_cannot_be_reached_answers_nowhere() {
1006 let drawer = NavigationDrawer::new(0, Msg::Go)
1007 .item("I", "Inbox")
1008 .item("A", "Archive")
1009 .disabled();
1010 let tiles = tiles(&drawer);
1011 let off = *tiles.last().expect("two of them");
1012 assert_eq!(off.on_click(), None, "the tap goes nowhere");
1013 assert!(!off.focusable(), "and the tab skips it");
1014 let semantics = off.semantics().expect("it is still announced");
1015 assert!(semantics.disabled, "and it is announced as unavailable");
1016 assert!(!semantics.clickable);
1017 assert_eq!(
1018 tiles[0].on_click(),
1019 Some(Msg::Go(0)),
1020 "its neighbour is untouched"
1021 );
1022 }
1023
1024 #[test]
1027 fn a_destination_that_cannot_be_reached_does_not_light_up() {
1028 let theme = Theme::default();
1029 let mut tile = lone_tile(false);
1030 tile.disabled = true;
1031 let mut scene = Scene::new();
1032 Widget::<Msg>::paint(
1033 &tile,
1034 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
1035 Status {
1036 opacity: 1.0,
1037 hover_progress: 1.0,
1038 ..Default::default()
1039 },
1040 &theme,
1041 &mut scene,
1042 );
1043 assert!(
1044 !scene
1045 .primitives()
1046 .iter()
1047 .any(|p| matches!(p, frus_core::Primitive::Rect { .. })),
1048 "an unreachable destination has nothing to say to a pointer resting on it"
1049 );
1050 }
1051
1052 #[test]
1055 fn the_two_ends_do_not_scroll() {
1056 let drawer = drawer()
1057 .header(Text::new("Mailbox"))
1058 .footer(Text::new("v1"));
1059 let children = Widget::<Msg>::children(&drawer);
1060 assert_eq!(children.len(), 3, "header, list, footer");
1061 assert_eq!(
1062 children[1].debug_name(),
1063 "SingleChildScrollView",
1064 "and only the middle one scrolls"
1065 );
1066 }
1067
1068 #[test]
1072 fn saying_it_afterwards_says_it() {
1073 let theme = Theme::default();
1074 let ink = Color::rgb8(200, 30, 30);
1075 let before = NavigationDrawer::new(0, Msg::Go)
1076 .indicator_color(ink)
1077 .item("I", "Inbox");
1078 let after = NavigationDrawer::new(0, Msg::Go)
1079 .item("I", "Inbox")
1080 .indicator_color(ink);
1081 let fill = |drawer: &NavigationDrawer<Msg>| {
1082 let found = tiles(drawer);
1083 let scene = scene_of(found[0], Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT), &theme);
1084 scene
1085 .primitives()
1086 .iter()
1087 .find_map(|p| match p {
1088 frus_core::Primitive::Rect { color, .. } => Some(*color),
1089 _ => None,
1090 })
1091 .expect("the selected destination has an indicator")
1092 };
1093 assert_eq!(fill(&before), ink);
1094 assert_eq!(fill(&after), ink, "the order the builders were called in");
1095 }
1096
1097 #[test]
1100 fn a_row_grows_before_it_clips() {
1101 let theme = Theme::default();
1102 let plain_drawer = drawer();
1103 let plain = tiles(&plain_drawer)[0].style_themed(&theme).height;
1104 assert_eq!(plain, Dimension::Length(TILE_HEIGHT));
1105
1106 let big = drawer().label_text_style(TextStyle::new(48.0));
1107 let grown = tiles(&big)[0].style_themed(&theme).height;
1108 assert!(
1109 matches!(grown, Dimension::Length(h) if h > TILE_HEIGHT),
1110 "the row makes room for the type rather than cutting it: {grown:?}"
1111 );
1112 }
1113
1114 #[test]
1117 fn the_panel_stands_a_rung_above_a_rail() {
1118 let theme = Theme::default();
1119 let scene = scene_of(&drawer(), Rect::new(0.0, 0.0, 304.0, 600.0), &theme);
1120 let surface = scene
1121 .primitives()
1122 .iter()
1123 .rev()
1124 .find_map(|p| match p {
1125 frus_core::Primitive::Rect { color, blur, .. } if *blur == 0.0 => Some(*color),
1126 _ => None,
1127 })
1128 .expect("the panel paints one");
1129 assert_eq!(surface, theme.scheme.surface_container_low);
1130 assert_ne!(
1131 theme.scheme.surface_container_low, theme.scheme.surface,
1132 "the two rungs have to be tellable apart for that to mean anything"
1133 );
1134 }
1135
1136 #[test]
1141 fn a_drawer_casts_no_shadow_until_a_colour_is_named() {
1142 let bounds = Rect::new(0.0, 0.0, 304.0, 600.0);
1143 let shadows = |drawer: &NavigationDrawer<Msg>, theme: &Theme| {
1144 crate::shadowprobe::colour(scene_of(drawer, bounds, theme).primitives())
1145 };
1146 let theme = Theme::default();
1147 assert!(shadows(&drawer(), &theme).is_none(), "none by default");
1148
1149 let named = Color::rgba(0.0, 0.0, 0.0, 0.25);
1150 assert_eq!(shadows(&drawer().shadow_color(named), &theme), Some(named));
1151 let mut themed = Theme::default();
1152 themed.widgets.nav_drawer.shadow_color = Some(named);
1153 assert_eq!(shadows(&drawer(), &themed), Some(named), "or on the theme");
1154 assert!(
1155 shadows(&drawer().shadow_color(named).elevation(0.0), &theme).is_none(),
1156 "and a flat drawer has no height to cast it from"
1157 );
1158 }
1159}