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<crate::navrail::DestinationBadge>,
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 self.badge == Some(crate::navrail::DestinationBadge::Dot) {
319 let size = theme.widgets.badge.small_size.unwrap_or(6.0);
320 let right = (pill.x + pill.width) - BADGE_TRAIL;
321 let at = Rect::new(
322 right - size,
323 inner.y + (inner.height - size) * 0.5,
324 size,
325 size,
326 );
327 let fill = theme
328 .widgets
329 .badge
330 .background_color
331 .unwrap_or(theme.scheme.error);
332 scene.draw_rect(at, fill.fade(o), size * 0.5, 0.0, Color::TRANSPARENT);
333 }
334 let count = match self.badge {
335 Some(crate::navrail::DestinationBadge::Count(count)) if count > 0 => Some(count),
336 _ => None,
337 };
338 if let Some(count) = count {
339 let text = crate::badge::count_label(count, 999);
341 let badge_s = badge_style(self.badge_text_style, Some(theme));
342 let m = frus_text::measure_resolved(&text, &badge_s);
343 let right = (pill.x + pill.width) - BADGE_TRAIL;
344 scene.text(
345 Point::new(right - m.width, inner.y + (inner.height - m.height) * 0.5),
346 text,
347 &badge_s,
348 ink.fade(o),
349 );
350 }
351 }
352
353 fn on_click(&self) -> Option<Msg> {
355 (!self.disabled).then(|| self.message.clone())
356 }
357
358 fn focusable(&self) -> bool {
360 !self.disabled
361 }
362
363 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
371 let words = crate::localizations::of();
372 let semantics = frus_core::SemanticsProperties::new(frus_core::Role::Tab)
373 .label(format!(
374 "{}, {}",
375 self.label,
376 words.tab_label(self.index + 1, self.count)
377 ))
378 .toggled(self.selected);
379 Some(if self.disabled {
380 semantics.disabled(true)
381 } else {
382 semantics.clickable()
383 })
384 }
385}
386
387pub struct NavigationDrawer<Msg = crate::callback::Callback> {
389 selected: usize,
390 on_select: Box<dyn Fn(usize) -> Msg>,
391 entries: RefCell<Vec<Entry<Msg>>>,
393 header: RefCell<Option<Box<dyn Widget<Msg>>>>,
394 footer: RefCell<Option<Box<dyn Widget<Msg>>>>,
395 width: Option<f32>,
396 background: Option<Color>,
397 elevation: Option<f32>,
398 shadow_color: Option<Color>,
399 indicator_color: Option<Color>,
400 indicator_shape: Option<ShapeBorder>,
401 indicator_size: Option<(f32, f32)>,
402 tile_height: Option<f32>,
403 tile_padding: Option<Insets>,
404 icon_size: Option<f32>,
405 label_text_style: Option<TextStyle>,
406 badge_text_style: Option<TextStyle>,
407 built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
408}
409
410impl<Msg: Clone + 'static> NavigationDrawer<Msg> {
411 pub fn new(selected: usize, on_select: impl Fn(usize) -> Msg + 'static) -> Self {
418 Self {
419 selected,
420 on_select: Box::new(on_select),
421 entries: RefCell::new(Vec::new()),
422 header: RefCell::new(None),
423 footer: RefCell::new(None),
424 width: None,
425 background: None,
426 elevation: None,
427 shadow_color: None,
428 indicator_color: None,
429 indicator_shape: None,
430 indicator_size: None,
431 tile_height: None,
432 tile_padding: None,
433 icon_size: None,
434 label_text_style: None,
435 badge_text_style: None,
436 built: OnceCell::new(),
437 }
438 }
439
440 pub fn item(self, icon: impl Into<DestinationIcon>, label: impl Into<String>) -> Self {
442 self.destination(NavigationDestination::new(icon, label))
443 }
444
445 pub(crate) fn destination(mut self, destination: NavigationDestination) -> Self {
448 self.entries
449 .borrow_mut()
450 .push(Entry::Stop(Box::new(destination)));
451 self.rebuild();
452 self
453 }
454
455 pub fn child(self, child: impl Widget<Msg> + 'static) -> Self {
461 self.child_boxed(Box::new(child))
462 }
463
464 pub fn child_boxed(mut self, child: Box<dyn Widget<Msg>>) -> Self {
466 self.entries.borrow_mut().push(Entry::Other(child));
467 self.rebuild();
468 self
469 }
470
471 #[must_use]
489 pub fn destinations(
490 mut self,
491 destinations: impl IntoIterator<Item = NavigationDestination>,
492 ) -> Self {
493 self.entries
494 .borrow_mut()
495 .extend(destinations.into_iter().map(|d| Entry::Stop(Box::new(d))));
496 self.rebuild();
497 self
498 }
499
500 pub fn badge(self, count: u32) -> Self {
502 self.decorate(|last| last.badge = Some(crate::navrail::DestinationBadge::Count(count)))
503 }
504
505 pub fn badge_dot(self) -> Self {
507 self.decorate(|last| last.badge = Some(crate::navrail::DestinationBadge::Dot))
508 }
509 #[must_use]
515 pub fn tooltip(self, message: impl Into<String>) -> Self {
516 let message = message.into();
517 self.decorate(move |last| last.tooltip = Some(message))
518 }
519
520 pub fn selected_icon(self, icon: impl Into<DestinationIcon>) -> Self {
523 let icon = icon.into();
524 self.decorate(move |last| last.selected_icon = Some(icon))
525 }
526
527 pub fn disabled(self) -> Self {
529 self.decorate(|last| last.disabled = true)
530 }
531
532 pub fn tile_color(self, color: Color) -> Self {
535 self.decorate(move |last| last.background = Some(color))
536 }
537
538 pub fn destination_indicator_color(self, color: Color) -> Self {
540 self.decorate(move |last| last.indicator_color = Some(color))
541 }
542
543 pub fn destination_indicator_shape(self, shape: ShapeBorder) -> Self {
545 self.decorate(move |last| last.indicator_shape = Some(shape))
546 }
547
548 pub fn destination_overlay_color(self, overlay: WidgetStateProperty<Color>) -> Self {
551 self.decorate(move |last| last.overlay_color = Some(overlay))
552 }
553
554 fn decorate(mut self, f: impl FnOnce(&mut NavigationDestination)) -> Self {
556 {
557 let mut entries = self.entries.borrow_mut();
558 if let Some(Entry::Stop(last)) = entries
559 .iter_mut()
560 .rev()
561 .find(|e| matches!(e, Entry::Stop(_)))
562 {
563 f(last);
564 }
565 }
566 self.rebuild();
567 self
568 }
569
570 pub fn header(self, header: impl Widget<Msg> + 'static) -> Self {
573 self.header_boxed(Box::new(header))
574 }
575
576 pub fn header_boxed(mut self, header: Box<dyn Widget<Msg>>) -> Self {
578 *self.header.borrow_mut() = Some(header);
579 self.rebuild();
580 self
581 }
582
583 pub fn footer(self, footer: impl Widget<Msg> + 'static) -> Self {
586 self.footer_boxed(Box::new(footer))
587 }
588
589 pub fn footer_boxed(mut self, footer: Box<dyn Widget<Msg>>) -> Self {
591 *self.footer.borrow_mut() = Some(footer);
592 self.rebuild();
593 self
594 }
595
596 pub fn width(mut self, width: f32) -> Self {
600 self.width = Some(width);
601 self
602 }
603
604 pub fn background_color(mut self, color: Color) -> Self {
607 self.background = Some(color);
608 self.rebuild();
609 self
610 }
611
612 pub fn elevation(mut self, elevation: f32) -> Self {
615 self.elevation = Some(elevation);
616 self
617 }
618
619 pub fn shadow_color(mut self, color: Color) -> Self {
623 self.shadow_color = Some(color);
624 self
625 }
626
627 pub fn indicator_color(mut self, color: Color) -> Self {
630 self.indicator_color = Some(color);
631 self.rebuild();
632 self
633 }
634
635 pub fn indicator_shape(mut self, shape: ShapeBorder) -> Self {
637 self.indicator_shape = Some(shape);
638 self.rebuild();
639 self
640 }
641
642 pub fn indicator_size(mut self, width: f32, height: f32) -> Self {
645 self.indicator_size = Some((width, height));
646 self.rebuild();
647 self
648 }
649
650 pub fn tile_height(mut self, height: f32) -> Self {
652 self.tile_height = Some(height);
653 self.rebuild();
654 self
655 }
656
657 pub fn tile_padding(mut self, padding: Insets) -> Self {
660 self.tile_padding = Some(padding);
661 self.rebuild();
662 self
663 }
664
665 pub fn icon_size(mut self, size: f32) -> Self {
667 self.icon_size = Some(size);
668 self.rebuild();
669 self
670 }
671
672 pub fn label_text_style(mut self, style: TextStyle) -> Self {
674 self.label_text_style = Some(style);
675 self.rebuild();
676 self
677 }
678
679 pub fn badge_text_style(mut self, style: TextStyle) -> Self {
681 self.badge_text_style = Some(style);
682 self.rebuild();
683 self
684 }
685
686 fn rebuild(&mut self) {
689 self.built.take();
690 }
691
692 fn assemble(&self) -> Vec<Box<dyn Widget<Msg>>> {
699 let entries = std::mem::take(&mut *self.entries.borrow_mut());
700 let count = entries
701 .iter()
702 .filter(|e| matches!(e, Entry::Stop(_)))
703 .count();
704
705 let mut list = Flex::<Msg>::column();
706 let mut index = 0;
707 for entry in entries {
708 match entry {
709 Entry::Other(child) => list = list.child_boxed(child),
710 Entry::Stop(stop) => {
711 let selected = index == self.selected;
712 let tooltip = stop.tooltip.clone();
713 let tile = DrawerTile {
714 icon: stop.glyph(selected).clone(),
715 label: stop.label.clone(),
716 badge: stop.badge,
717 selected,
718 disabled: stop.disabled,
719 index,
720 count,
721 indicator_color: stop.indicator_color.or(self.indicator_color),
722 indicator_shape: stop.indicator_shape.or(self.indicator_shape),
723 overlay_color: stop.overlay_color.clone(),
724 ground: self.background,
725 background: stop.background,
726 tile_height: self.tile_height,
727 tile_padding: self.tile_padding,
728 indicator_size: self.indicator_size,
729 icon_size: self.icon_size,
730 label_text_style: self.label_text_style,
731 badge_text_style: self.badge_text_style,
732 message: (self.on_select)(index),
733 };
734 list = match tooltip {
737 Some(message) => list.child(crate::Tooltip::new(message).child(tile)),
738 None => list.child(tile),
739 };
740 index += 1;
741 }
742 }
743 }
744
745 let mut out: Vec<Box<dyn Widget<Msg>>> = Vec::new();
746 if let Some(header) = self.header.borrow_mut().take() {
747 out.push(header);
748 }
749 out.push(Box::new(
750 SingleChildScrollView::<Msg>::new().flex(1.0).child(list),
751 ));
752 if let Some(footer) = self.footer.borrow_mut().take() {
753 out.push(footer);
754 }
755 out
756 }
757}
758
759impl<Msg: Clone + 'static> Widget<Msg> for NavigationDrawer<Msg> {
760 fn style(&self) -> Style {
766 let safe = crate::MediaQuery::of().padding;
767 Style {
768 width: self.width.map_or(Dimension::Auto, Dimension::Length),
769 flex_direction: FlexDirection::Column,
770 padding: Insets::new(safe.top, 0.0, 0.0, 0.0),
771 ..Default::default()
772 }
773 }
774
775 fn fill_axes(&self, _theme: &Theme) -> FillAxes {
777 if self.width.is_some() {
778 FillAxes::HEIGHT
779 } else {
780 FillAxes::BOTH
781 }
782 }
783
784 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
785 self.built.get_or_init(|| self.assemble())
786 }
787
788 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
789 let o = status.opacity;
790 let t = &theme.widgets.nav_drawer;
791 let depth = self.elevation.or(t.elevation).unwrap_or(ELEVATION);
792 let shadow = self
796 .shadow_color
797 .or(t.shadow_color)
798 .unwrap_or(Color::TRANSPARENT);
799 if depth > 0.0 && shadow.a > 0.0 {
800 frus_core::paint_elevation(
802 scene,
803 bounds,
804 frus_core::BorderRadius::ZERO,
805 depth,
806 shadow.fade(o),
807 );
808 }
809 let fill = self
810 .background
811 .or(t.background_color)
812 .unwrap_or(theme.scheme.surface_container_low);
813 scene.fill_rect(bounds, fill.fade(o));
814 }
815
816 fn on_click(&self) -> Option<Msg> {
817 None
818 }
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824 use crate::divider::Divider;
825 use crate::text::Text;
826
827 #[derive(Clone, Debug, PartialEq)]
828 enum Msg {
829 Go(usize),
830 }
831
832 fn drawer() -> NavigationDrawer<Msg> {
835 NavigationDrawer::new(1, Msg::Go)
836 .item("I", "Inbox")
837 .item("S", "Starred")
838 .child(Divider::new())
839 .item("T", "Trash")
840 }
841
842 fn tiles(drawer: &NavigationDrawer<Msg>) -> Vec<&dyn Widget<Msg>> {
846 fn walk<'a>(node: &'a dyn Widget<Msg>, out: &mut Vec<&'a dyn Widget<Msg>>) {
847 if node.debug_name() == "DrawerTile" {
848 out.push(node);
849 }
850 for child in node.children() {
851 walk(&**child, out);
852 }
853 }
854 let mut out = Vec::new();
855 for child in Widget::<Msg>::children(drawer) {
856 walk(&**child, &mut out);
857 }
858 out
859 }
860
861 fn scene_of(widget: &dyn Widget<Msg>, bounds: Rect, theme: &Theme) -> Scene {
862 let mut scene = Scene::new();
863 widget.paint(
864 bounds,
865 Status {
866 opacity: 1.0,
867 ..Default::default()
868 },
869 theme,
870 &mut scene,
871 );
872 scene
873 }
874
875 fn texts(scene: &Scene) -> Vec<(String, Color)> {
876 scene
877 .primitives()
878 .iter()
879 .filter_map(|p| match p {
880 frus_core::Primitive::Text { text, color, .. } => Some((text.clone(), *color)),
881 _ => None,
882 })
883 .collect()
884 }
885
886 #[test]
893 fn a_rule_between_destinations_takes_no_number() {
894 let drawer = drawer();
895 let tiles = tiles(&drawer);
896 assert_eq!(
897 tiles.len(),
898 3,
899 "three destinations, and the rule is not one"
900 );
901 assert_eq!(
902 tiles
903 .iter()
904 .filter_map(|t| t.on_click())
905 .collect::<Vec<_>>(),
906 vec![Msg::Go(0), Msg::Go(1), Msg::Go(2)],
907 "the third destination is 2, not 3"
908 );
909 assert_eq!(
910 tiles
911 .iter()
912 .map(|t| t.semantics().expect("announced").toggled)
913 .collect::<Vec<_>>(),
914 vec![
915 frus_core::Toggled::False,
916 frus_core::Toggled::True,
917 frus_core::Toggled::False
918 ],
919 "and the middle one is the live one"
920 );
921 }
922
923 #[test]
925 fn a_reader_is_told_which_of_how_many() {
926 let drawer = drawer();
927 let heard: Vec<String> = tiles(&drawer)
928 .iter()
929 .filter_map(|t| t.semantics()?.label)
930 .collect();
931 assert_eq!(
932 heard,
933 vec![
934 "Inbox, Tab 1 of 3".to_string(),
935 "Starred, Tab 2 of 3".to_string(),
936 "Trash, Tab 3 of 3".to_string(),
937 ],
938 "the rule between them is not a destination, so it is not counted either"
939 );
940 }
941
942 fn lone_tile(selected: bool) -> DrawerTile<Msg> {
944 DrawerTile {
945 icon: "I".into(),
946 label: "Inbox".into(),
947 badge: None,
948 selected,
949 disabled: false,
950 index: 0,
951 count: 1,
952 indicator_color: None,
953 indicator_shape: None,
954 overlay_color: None,
955 ground: None,
956 background: None,
957 tile_height: None,
958 tile_padding: None,
959 indicator_size: None,
960 icon_size: None,
961 label_text_style: None,
962 badge_text_style: None,
963 message: Msg::Go(0),
964 }
965 }
966
967 #[test]
972 fn the_pill_never_grows_past_the_room_the_tile_has() {
973 let theme = Theme::default();
974 let tile = lone_tile(true);
975 let pill_width = |width: f32| {
976 let scene = scene_of(&tile, Rect::new(0.0, 0.0, width, TILE_HEIGHT), &theme);
977 scene
978 .primitives()
979 .iter()
980 .find_map(|p| match p {
981 frus_core::Primitive::Rect { rect, .. } => Some(rect.width),
982 _ => None,
983 })
984 .expect("a selected destination has an indicator")
985 };
986
987 assert_eq!(
988 pill_width(304.0),
989 304.0 - 24.0,
990 "in a 304-wide panel the pill takes what is left after the padding"
991 );
992 assert_eq!(
993 pill_width(600.0),
994 INDICATOR_WIDTH,
995 "and stops at its declared width once there is room for it"
996 );
997 }
998
999 #[test]
1003 fn a_selected_row_paints_its_glyph_and_its_label_the_same() {
1004 let theme = Theme::default();
1005 let scene = scene_of(
1006 &lone_tile(true),
1007 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
1008 &theme,
1009 );
1010 let painted = texts(&scene);
1011 assert_eq!(painted.len(), 2, "a glyph and a label");
1012 assert_eq!(
1013 painted[0].1, painted[1].1,
1014 "the glyph and the label stand on the same indicator"
1015 );
1016 assert_eq!(painted[0].1, theme.scheme.on_secondary_container);
1017
1018 let scene = scene_of(
1019 &lone_tile(false),
1020 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
1021 &theme,
1022 );
1023 assert_eq!(texts(&scene)[0].1, theme.scheme.on_surface_variant);
1024 }
1025
1026 #[test]
1029 fn a_destination_that_cannot_be_reached_answers_nowhere() {
1030 let drawer = NavigationDrawer::new(0, Msg::Go)
1031 .item("I", "Inbox")
1032 .item("A", "Archive")
1033 .disabled();
1034 let tiles = tiles(&drawer);
1035 let off = *tiles.last().expect("two of them");
1036 assert_eq!(off.on_click(), None, "the tap goes nowhere");
1037 assert!(!off.focusable(), "and the tab skips it");
1038 let semantics = off.semantics().expect("it is still announced");
1039 assert!(semantics.disabled, "and it is announced as unavailable");
1040 assert!(!semantics.clickable);
1041 assert_eq!(
1042 tiles[0].on_click(),
1043 Some(Msg::Go(0)),
1044 "its neighbour is untouched"
1045 );
1046 }
1047
1048 #[test]
1051 fn a_destination_that_cannot_be_reached_does_not_light_up() {
1052 let theme = Theme::default();
1053 let mut tile = lone_tile(false);
1054 tile.disabled = true;
1055 let mut scene = Scene::new();
1056 Widget::<Msg>::paint(
1057 &tile,
1058 Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT),
1059 Status {
1060 opacity: 1.0,
1061 hover_progress: 1.0,
1062 ..Default::default()
1063 },
1064 &theme,
1065 &mut scene,
1066 );
1067 assert!(
1068 !scene
1069 .primitives()
1070 .iter()
1071 .any(|p| matches!(p, frus_core::Primitive::Rect { .. })),
1072 "an unreachable destination has nothing to say to a pointer resting on it"
1073 );
1074 }
1075
1076 #[test]
1079 fn the_two_ends_do_not_scroll() {
1080 let drawer = drawer()
1081 .header(Text::new("Mailbox"))
1082 .footer(Text::new("v1"));
1083 let children = Widget::<Msg>::children(&drawer);
1084 assert_eq!(children.len(), 3, "header, list, footer");
1085 assert_eq!(
1086 children[1].debug_name(),
1087 "SingleChildScrollView",
1088 "and only the middle one scrolls"
1089 );
1090 }
1091
1092 #[test]
1096 fn saying_it_afterwards_says_it() {
1097 let theme = Theme::default();
1098 let ink = Color::rgb8(200, 30, 30);
1099 let before = NavigationDrawer::new(0, Msg::Go)
1100 .indicator_color(ink)
1101 .item("I", "Inbox");
1102 let after = NavigationDrawer::new(0, Msg::Go)
1103 .item("I", "Inbox")
1104 .indicator_color(ink);
1105 let fill = |drawer: &NavigationDrawer<Msg>| {
1106 let found = tiles(drawer);
1107 let scene = scene_of(found[0], Rect::new(0.0, 0.0, 360.0, TILE_HEIGHT), &theme);
1108 scene
1109 .primitives()
1110 .iter()
1111 .find_map(|p| match p {
1112 frus_core::Primitive::Rect { color, .. } => Some(*color),
1113 _ => None,
1114 })
1115 .expect("the selected destination has an indicator")
1116 };
1117 assert_eq!(fill(&before), ink);
1118 assert_eq!(fill(&after), ink, "the order the builders were called in");
1119 }
1120
1121 #[test]
1124 fn a_row_grows_before_it_clips() {
1125 let theme = Theme::default();
1126 let plain_drawer = drawer();
1127 let plain = tiles(&plain_drawer)[0].style_themed(&theme).height;
1128 assert_eq!(plain, Dimension::Length(TILE_HEIGHT));
1129
1130 let big = drawer().label_text_style(TextStyle::new(48.0));
1131 let grown = tiles(&big)[0].style_themed(&theme).height;
1132 assert!(
1133 matches!(grown, Dimension::Length(h) if h > TILE_HEIGHT),
1134 "the row makes room for the type rather than cutting it: {grown:?}"
1135 );
1136 }
1137
1138 #[test]
1141 fn the_panel_stands_a_rung_above_a_rail() {
1142 let theme = Theme::default();
1143 let scene = scene_of(&drawer(), Rect::new(0.0, 0.0, 304.0, 600.0), &theme);
1144 let surface = scene
1145 .primitives()
1146 .iter()
1147 .rev()
1148 .find_map(|p| match p {
1149 frus_core::Primitive::Rect { color, blur, .. } if *blur == 0.0 => Some(*color),
1150 _ => None,
1151 })
1152 .expect("the panel paints one");
1153 assert_eq!(surface, theme.scheme.surface_container_low);
1154 assert_ne!(
1155 theme.scheme.surface_container_low, theme.scheme.surface,
1156 "the two rungs have to be tellable apart for that to mean anything"
1157 );
1158 }
1159
1160 #[test]
1165 fn a_drawer_casts_no_shadow_until_a_colour_is_named() {
1166 let bounds = Rect::new(0.0, 0.0, 304.0, 600.0);
1167 let shadows = |drawer: &NavigationDrawer<Msg>, theme: &Theme| {
1168 crate::shadowprobe::colour(scene_of(drawer, bounds, theme).primitives())
1169 };
1170 let theme = Theme::default();
1171 assert!(shadows(&drawer(), &theme).is_none(), "none by default");
1172
1173 let named = Color::rgba(0.0, 0.0, 0.0, 0.25);
1174 assert_eq!(shadows(&drawer().shadow_color(named), &theme), Some(named));
1175 let mut themed = Theme::default();
1176 themed.widgets.nav_drawer.shadow_color = Some(named);
1177 assert_eq!(shadows(&drawer(), &themed), Some(named), "or on the theme");
1178 assert!(
1179 shadows(&drawer().shadow_color(named).elevation(0.0), &theme).is_none(),
1180 "and a flat drawer has no height to cast it from"
1181 );
1182 }
1183}