1use std::rc::Rc;
17
18use frus_core::{
19 BorderRadius, Color, Insets, Path, Point, Rect, ResolvedTextStyle, Scene, ShapeBorder,
20 TextStyle,
21};
22use frus_layout::{Align, Dimension, FlexDirection, Style};
23
24use crate::disabled::{disabled_container, disabled_content};
25use crate::icons::Icons;
26use crate::interaction::Status;
27use crate::menu::{Panel, PanelKind, PanelStyle};
28use crate::portal::Placement;
29use crate::theme::Theme;
30use crate::transparent::Shared;
31use crate::widget::Widget;
32
33const DEFAULT_WIDTH: f32 = 240.0;
34const ROW_H: f32 = 40.0;
35const PAD_X: f32 = 12.0;
36
37pub(crate) fn label_style(over: Option<TextStyle>, theme: Option<&Theme>) -> ResolvedTextStyle {
44 over.or(theme.and_then(|t| t.widgets.dropdown.text_style))
45 .unwrap_or_else(|| crate::theme::type_scale(theme).title_medium)
46 .resolved()
47}
48
49const TICK: f32 = 18.0;
53
54struct Row<Msg> {
56 label: Option<String>,
59 children: Vec<Box<dyn Widget<Msg>>>,
61 width: f32,
62 is_header: bool,
63 selected: bool,
65 enabled: bool,
67 text_style: Option<TextStyle>,
68 kind: PanelKind,
71 menu_background: Option<Color>,
72 on_click: Option<Msg>,
73}
74
75impl<Msg> Row<Msg> {
76 fn sizing(&self, theme: Option<&Theme>) -> Style {
77 let line = frus_text::line_box(ROW_H, &label_style(self.text_style, theme), 0.0);
78 if self.children.is_empty() {
79 return Style {
80 width: Dimension::Length(self.width),
81 height: Dimension::Length(line),
82 ..Default::default()
83 };
84 }
85 Style {
86 width: Dimension::Length(self.width),
87 height: Dimension::Auto,
90 min_height: Dimension::Length(line),
91 flex_direction: FlexDirection::Row,
92 align: Align::Center,
93 padding: Insets::new(0.0, PAD_X + TICK, 0.0, PAD_X),
96 ..Default::default()
97 }
98 }
99}
100
101impl<Msg: Clone> Widget<Msg> for Row<Msg> {
102 fn style(&self) -> Style {
103 self.sizing(None)
104 }
105
106 fn style_themed(&self, theme: &Theme) -> Style {
107 self.sizing(Some(theme))
108 }
109
110 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
111 &self.children
112 }
113
114 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
115 let o = status.opacity;
116 if self.is_header {
117 let base = theme.scheme.surface_container;
122 let bg = if self.enabled {
123 theme.state_layer(base, theme.on_surface, &status)
124 } else {
125 base
126 };
127 let outline = if self.enabled {
128 theme.border
129 } else {
130 disabled_container(theme)
131 };
132 scene.draw_rect(bounds, bg.fade(o), theme.radius, 1.0, outline.fade(o));
133 } else {
134 let panel = PanelStyle {
140 background: self.menu_background,
141 ..PanelStyle::default()
142 }
143 .background(self.kind, theme);
144 let base = if self.selected && self.enabled {
145 panel.lerp(theme.primary, 0.14)
146 } else {
147 panel
148 };
149 let bg = if self.enabled {
150 theme.state_layer(base, theme.on_surface, &status)
151 } else {
152 base
153 };
154 if bg != panel {
155 scene.fill_rect(bounds, bg.fade(o));
156 }
157 }
158
159 let ink = if self.enabled {
160 theme.on_surface
161 } else {
162 disabled_content(theme)
163 };
164 let style = label_style(self.text_style, Some(theme));
165 let ty = bounds.y + (bounds.height - style.line_height()) * 0.5;
166 if let Some(label) = &self.label {
167 scene.text(
168 Point::new(bounds.x + PAD_X, ty),
169 label.clone(),
170 &style,
171 ink.fade(o),
172 );
173 }
174
175 if self.is_header {
176 let cx = bounds.x + self.width - PAD_X - 4.0;
178 let cy = bounds.y + bounds.height * 0.5;
179 let (w, h) = (5.0, 3.0);
180 let tri = Path::new()
181 .move_to(Point::new(cx - w, cy - h))
182 .line_to(Point::new(cx + w, cy - h))
183 .line_to(Point::new(cx, cy + h))
184 .close();
185 let chevron = if self.enabled {
186 theme.muted
187 } else {
188 disabled_content(theme)
189 };
190 scene.fill_path(&tri, chevron.fade(o));
191 } else if self.selected {
192 let size = 18.0;
194 let x = bounds.x + self.width - PAD_X - size;
195 let y = bounds.y + (bounds.height - size) * 0.5;
196 let path = Icons::CHECK.placed(size, x, y, theme.direction);
197 let check = if self.enabled {
200 theme.primary
201 } else {
202 disabled_content(theme)
203 };
204 scene.fill_path(&path, check.fade(o));
205 }
206 }
207
208 fn on_click(&self) -> Option<Msg> {
209 if !self.enabled {
210 return None;
211 }
212 self.on_click.clone()
213 }
214
215 fn focusable(&self) -> bool {
216 self.enabled && self.on_click.is_some()
217 }
218
219 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
220 let semantics = frus_core::SemanticsProperties::new(frus_core::Role::Button);
227 let semantics = match &self.label {
228 Some(label) => semantics.label(label.clone()),
229 None if !self.is_header => return None,
230 None => semantics,
231 };
232 let semantics = if self.is_header {
233 semantics
234 } else {
235 semantics.toggled(self.selected)
236 };
237 Some(if self.enabled {
238 semantics.clickable()
239 } else {
240 semantics.disabled(true)
241 })
242 }
243}
244
245pub struct DropdownOption<Msg = crate::callback::Callback> {
273 label: Option<String>,
274 child: Option<Rc<dyn Widget<Msg>>>,
275 enabled: bool,
276}
277
278impl<Msg> DropdownOption<Msg> {
279 pub fn new(label: impl Into<String>) -> Self {
281 Self {
282 label: Some(label.into()),
283 child: None,
284 enabled: true,
285 }
286 }
287
288 pub fn widget(child: impl Widget<Msg> + 'static) -> Self {
294 Self {
295 label: None,
296 child: Some(Rc::new(child)),
297 enabled: true,
298 }
299 }
300
301 #[must_use]
308 pub fn enabled(mut self, enabled: bool) -> Self {
309 self.enabled = enabled;
310 self
311 }
312
313 pub(crate) fn label(&self) -> Option<&str> {
315 self.label.as_deref()
316 }
317
318 pub(crate) fn is_enabled(&self) -> bool {
319 self.enabled
320 }
321}
322
323pub(crate) fn option_row<Msg: Clone + 'static>(
329 option: &DropdownOption<Msg>,
330 width: f32,
331 selected: bool,
332 enabled: bool,
333 text_style: Option<TextStyle>,
334 panel: (PanelKind, Option<Color>),
335 on_click: Option<Msg>,
336) -> impl Widget<Msg> {
337 Row {
338 label: option.label.clone(),
339 children: option
340 .child
341 .clone()
342 .map(|inner| Box::new(Shared::new(inner)) as Box<dyn Widget<Msg>>)
343 .into_iter()
344 .collect(),
345 width,
346 is_header: false,
347 selected,
348 enabled: enabled && option.enabled,
349 text_style,
350 kind: panel.0,
351 menu_background: panel.1,
352 on_click,
353 }
354}
355
356pub struct DropdownButton<Msg = crate::callback::Callback> {
358 header_label: String,
359 on_toggle: Msg,
360 width: f32,
361 selected: Option<usize>,
362 open: bool,
363 enabled: bool,
364 options: Vec<DropdownOption<Msg>>,
365 text_style: Option<TextStyle>,
366 look: PanelStyle,
368 on_select: Option<Box<dyn Fn(usize) -> Msg>>,
369 children: Vec<Box<dyn Widget<Msg>>>,
370}
371
372impl<Msg: Clone + 'static> DropdownButton<Msg> {
373 pub fn new(selected_label: impl Into<String>, on_toggle: Msg) -> Self {
375 let mut dropdown = Self {
376 header_label: selected_label.into(),
377 on_toggle,
378 width: DEFAULT_WIDTH,
379 selected: None,
380 open: false,
381 enabled: true,
382 options: Vec::new(),
383 text_style: None,
384 look: PanelStyle::default(),
385 on_select: None,
386 children: Vec::new(),
387 };
388 dropdown.rebuild();
389 dropdown
390 }
391
392 #[must_use]
394 pub fn text_style(mut self, style: TextStyle) -> Self {
395 self.text_style = Some(style);
396 self.rebuild();
397 self
398 }
399
400 #[must_use]
409 pub fn menu_background(mut self, color: Color) -> Self {
410 self.look.background = Some(color);
411 self.rebuild();
412 self
413 }
414
415 #[must_use]
417 pub fn menu_shape(mut self, shape: ShapeBorder) -> Self {
418 self.look.shape = Some(shape);
419 self.rebuild();
420 self
421 }
422
423 #[must_use]
425 pub fn menu_radius(self, radius: impl Into<BorderRadius>) -> Self {
426 self.menu_shape(ShapeBorder::rounded(radius.into()))
427 }
428
429 #[must_use]
431 pub fn menu_elevation(mut self, elevation: f32) -> Self {
432 self.look.elevation = Some(elevation);
433 self.rebuild();
434 self
435 }
436
437 #[must_use]
440 pub fn menu_shadow_color(mut self, color: Color) -> Self {
441 self.look.shadow_color = Some(color);
442 self.rebuild();
443 self
444 }
445
446 #[must_use]
448 pub fn menu_padding(mut self, padding: Insets) -> Self {
449 self.look.padding = Some(padding);
450 self.rebuild();
451 self
452 }
453
454 pub fn enabled(mut self, enabled: bool) -> Self {
463 self.enabled = enabled;
464 self.rebuild();
465 self
466 }
467
468 pub fn width(mut self, width: f32) -> Self {
470 self.width = width;
471 self.rebuild();
472 self
473 }
474
475 pub fn selected(mut self, index: usize) -> Self {
477 self.selected = Some(index);
478 self.rebuild();
479 self
480 }
481
482 pub fn options(
485 self,
486 open: bool,
487 labels: &[&str],
488 on_select: impl Fn(usize) -> Msg + 'static,
489 ) -> Self {
490 let options = labels.iter().map(|s| DropdownOption::new(*s)).collect();
491 self.options_widgets(open, options, on_select)
492 }
493
494 pub fn options_widgets(
500 mut self,
501 open: bool,
502 options: Vec<DropdownOption<Msg>>,
503 on_select: impl Fn(usize) -> Msg + 'static,
504 ) -> Self {
505 self.open = open;
506 self.options = options;
507 self.on_select = Some(Box::new(on_select));
508 self.rebuild();
509 self
510 }
511
512 fn rebuild(&mut self) {
514 let header = Row {
515 label: Some(self.header_label.clone()),
516 children: Vec::new(),
517 width: self.width,
518 is_header: true,
519 selected: false,
520 enabled: self.enabled,
521 text_style: self.text_style,
522 kind: PanelKind::Dropdown,
523 menu_background: None,
524 on_click: Some(self.on_toggle.clone()),
525 };
526 self.children = vec![Box::new(header)];
527
528 if self.open && self.enabled && !self.options.is_empty() {
529 let mut rows: Vec<Box<dyn Widget<Msg>>> = Vec::with_capacity(self.options.len());
532 for (index, option) in self.options.iter().enumerate() {
533 let on_click = self
534 .on_select
535 .as_ref()
536 .filter(|_| self.enabled && option.enabled)
537 .map(|f| f(index));
538 rows.push(Box::new(option_row(
539 option,
540 self.width,
541 self.selected == Some(index),
542 self.enabled,
543 self.text_style,
544 (PanelKind::Dropdown, self.look.background),
545 on_click,
546 )));
547 }
548 self.children
549 .push(Box::new(Panel::new(PanelKind::Dropdown, self.look, rows)));
550 }
551 }
552}
553
554impl<Msg: Clone> Widget<Msg> for DropdownButton<Msg> {
555 fn style(&self) -> Style {
556 Style {
557 flex_direction: FlexDirection::Column,
558 ..Default::default()
559 }
560 }
561
562 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
563 &self.children
564 }
565
566 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
567
568 fn on_click(&self) -> Option<Msg> {
569 None
570 }
571
572 fn overlay(&self) -> Option<(&dyn Widget<Msg>, Placement)> {
573 self.children
574 .get(1)
575 .map(|menu| (menu.as_ref(), Placement::Below))
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::{build_ui, Runtime, Size};
583 use frus_core::Primitive;
584
585 #[derive(Clone, Debug, PartialEq)]
586 enum Msg {
587 Toggle,
588 Select(usize),
589 }
590
591 #[test]
598 fn a_choice_can_be_a_widget_the_caller_drew() {
599 let mark = frus_core::Color::rgb(0.9, 0.1, 0.1);
600 let open = DropdownButton::new("Pick", Msg::Toggle)
601 .width(200.0)
602 .options_widgets(
603 true,
604 vec![
605 DropdownOption::widget(
606 crate::Container::<Msg>::new()
607 .width(40.0)
608 .height(60.0)
609 .color(mark),
610 ),
611 DropdownOption::new("B"),
612 ],
613 Msg::Select,
614 );
615 let (menu, _) = Widget::<Msg>::overlay(&open).unwrap();
616 let ui = build_ui(
617 menu,
618 Size::new(220.0, 200.0),
619 &Runtime::default(),
620 &Theme::default(),
621 );
622 let drawn = rects(ui.scene())
623 .into_iter()
624 .find(|(_, color)| *color == mark)
625 .expect("the caller's own widget is drawn");
626 assert_eq!(drawn.0.x, PAD_X, "it starts where the words would have");
627 assert_eq!(drawn.0.height, 60.0, "and keeps the height it asked for");
628 }
629
630 #[test]
636 fn one_choice_can_be_unavailable_while_the_list_is_not() {
637 let open = DropdownButton::new("Pick", Msg::Toggle).options_widgets(
638 true,
639 vec![
640 DropdownOption::new("A"),
641 DropdownOption::new("B").enabled(false),
642 ],
643 Msg::Select,
644 );
645 let menu = &Widget::<Msg>::children(&open)[1];
646 assert_eq!(menu.children()[0].on_click(), Some(Msg::Select(0)));
647 assert_eq!(
648 menu.children()[1].on_click(),
649 None,
650 "the choice that said it was unavailable answers nothing"
651 );
652 assert!(
653 !menu.children()[1].focusable(),
654 "and does not stop the keyboard on the way past"
655 );
656 let announced = menu.children()[1].semantics().expect("still announced");
657 assert!(announced.disabled, "as unavailable");
658 assert_eq!(announced.label.as_deref(), Some("B"), "and by name");
659 }
660
661 fn rects(scene: &frus_core::Scene) -> Vec<(Rect, frus_core::Color)> {
663 fn walk(primitives: &[Primitive], out: &mut Vec<(Rect, frus_core::Color)>) {
664 for p in primitives {
665 match p {
666 Primitive::Rect { rect, color, .. } => out.push((*rect, *color)),
667 Primitive::Layer { primitives, .. } => walk(primitives, out),
668 _ => {}
669 }
670 }
671 }
672 let mut out = Vec::new();
673 walk(scene.primitives(), &mut out);
674 out
675 }
676
677 #[test]
678 fn closed_has_no_overlay_open_floats_options() {
679 let closed =
680 DropdownButton::new("Pick one", Msg::Toggle).options(false, &["A", "B"], Msg::Select);
681 assert!(
682 Widget::<Msg>::overlay(&closed).is_none(),
683 "closed: no overlay"
684 );
685
686 let open =
687 DropdownButton::new("Pick one", Msg::Toggle).options(true, &["A", "B"], Msg::Select);
688 assert!(
689 Widget::<Msg>::overlay(&open).is_some(),
690 "open: a floating menu"
691 );
692 let menu = &Widget::<Msg>::children(&open)[1];
693 assert_eq!(menu.children().len(), 2);
694 assert_eq!(menu.children()[1].on_click(), Some(Msg::Select(1)));
695 }
696
697 #[test]
698 fn header_and_options_are_keyboard_focusable() {
699 let open = DropdownButton::new("Pick", Msg::Toggle).options(true, &["A", "B"], Msg::Select);
700 assert!(Widget::<Msg>::children(&open)[0].focusable());
702 let menu = &Widget::<Msg>::children(&open)[1];
703 assert!(menu.children()[0].focusable() && menu.children()[1].focusable());
704 }
705
706 #[test]
707 fn selected_option_is_highlighted_and_checked() {
708 let open = DropdownButton::new("Pick", Msg::Toggle)
709 .selected(1)
710 .options(true, &["A", "B"], Msg::Select)
711 .width(200.0);
712 let (menu, _) = Widget::<Msg>::overlay(&open).unwrap();
714 let ui = build_ui(
715 menu,
716 Size::new(220.0, 120.0),
717 &Runtime::default(),
718 &Theme::default(),
719 );
720 let theme = Theme::default();
721 let has_check = ui
723 .scene()
724 .primitives()
725 .iter()
726 .any(|p| matches!(p, Primitive::Path { .. }));
727 assert!(has_check, "the selected option is ticked");
728 let sel = theme.scheme.surface_container.lerp(theme.primary, 0.14);
730 let has_tint = ui.scene().primitives().iter().any(|p| {
731 matches!(
732 p,
733 Primitive::Rect { color, .. } if color.fade(1.0) == sel.fade(1.0)
734 )
735 });
736 assert!(has_tint, "the selected option is highlighted");
737 }
738
739 #[test]
740 fn a_disabled_list_is_inert_and_cannot_be_open() {
741 let dead = DropdownButton::new("Option B", Msg::Toggle)
742 .selected(1)
743 .options(true, &["A", "B"], Msg::Select)
744 .enabled(false);
745 assert!(Widget::<Msg>::overlay(&dead).is_none(), "no floating menu");
748 assert_eq!(Widget::<Msg>::children(&dead).len(), 1, "header only");
749
750 let header = &Widget::<Msg>::children(&dead)[0];
751 assert_eq!(header.on_click(), None, "the header takes no press");
752 assert!(!header.focusable(), "and no focus");
753 let semantics = header.semantics().expect("still announced");
754 assert!(semantics.disabled, "announced as unavailable");
755 assert_eq!(
756 semantics.label.as_deref(),
757 Some("Option B"),
758 "and still says which option is current"
759 );
760 }
761
762 #[test]
763 fn a_live_list_is_untouched_by_it() {
764 let live = DropdownButton::new("Pick one", Msg::Toggle)
765 .options(true, &["A", "B"], Msg::Select)
766 .enabled(true);
767 assert!(Widget::<Msg>::overlay(&live).is_some());
768 assert_eq!(
769 Widget::<Msg>::children(&live)[0].on_click(),
770 Some(Msg::Toggle)
771 );
772 }
773
774 use crate::menu::probe::{blurred, colour_of, crisp, height_of};
775
776 fn open_list() -> DropdownButton<Msg> {
778 DropdownButton::new("Pick", Msg::Toggle)
779 .width(200.0)
780 .options(true, &["A", "B", "C"], Msg::Select)
781 }
782
783 fn frame(list: &DropdownButton<Msg>, theme: &Theme) -> crate::Ui<Msg> {
784 build_ui(list, Size::new(400.0, 400.0), &Runtime::default(), theme)
785 }
786
787 #[test]
796 fn the_options_float_on_one_panel_and_draw_no_box_of_their_own() {
797 let theme = Theme::default();
798 let ui = frame(&open_list(), &theme);
799 let painted = crisp(ui.scene());
800 assert_eq!(painted.len(), 2, "the header and one panel: {painted:#?}");
801 let (header, panel) = (painted[0], painted[1]);
802 assert_eq!(header.border_width, 1.0, "the header keeps its outline");
803 let row = frus_text::line_box(ROW_H, &label_style(None, Some(&theme)), 0.0);
804 assert_eq!(panel.color, theme.scheme.surface_container);
805 assert_eq!(panel.border_width, 0.0, "the panel has no outline");
806 assert!(panel.radius != BorderRadius::ZERO, "and is rounded");
807 assert_eq!(panel.rect.y, header.rect.y + row + 4.0, "under the header");
810 assert_eq!(panel.rect.width, 200.0);
811 assert_eq!(
812 panel.rect.height,
813 3.0 * row + 16.0,
814 "no gutters between rows"
815 );
816 }
817
818 #[test]
821 fn the_list_casts_a_shadow_eight_high() {
822 let theme = Theme::default();
823 let ui = frame(&open_list(), &theme);
824 let panel = crisp(ui.scene())[1];
825 let shadows = blurred(ui.scene());
826 assert_eq!(
827 shadows.len(),
828 3,
829 "one shadow, under the panel: {shadows:#?}"
830 );
831 assert_eq!(height_of(ui.scene()), 8.0);
832 assert_eq!(
833 shadows[1].rect.y + shadows[1].rect.height * 0.5,
834 panel.rect.y + panel.rect.height * 0.5 + 8.0,
835 "the second shadow drops by the height"
836 );
837 assert_eq!(colour_of(ui.scene()), Some(theme.scheme.shadow));
838
839 let closed = DropdownButton::new("Pick", Msg::Toggle).options(false, &["A"], Msg::Select);
840 assert!(
841 blurred(frame(&closed, &theme).scene()).is_empty(),
842 "and a shut list casts nothing"
843 );
844 }
845
846 #[test]
850 fn the_panel_answers_to_its_theme_and_to_its_caller() {
851 let mut theme = Theme::default();
852 let (surface, shade) = (
853 frus_core::Color::rgb(0.2, 0.4, 0.6),
854 frus_core::Color::rgba(0.0, 0.0, 0.5, 0.5),
855 );
856 theme.widgets.dropdown.menu_background = Some(surface);
857 theme.widgets.dropdown.menu_radius = Some(3.0);
858 theme.widgets.dropdown.menu_elevation = Some(2.0);
859 theme.widgets.dropdown.menu_shadow_color = Some(shade);
860 theme.widgets.dropdown.menu_padding = Some(Insets::new(20.0, 0.0, 20.0, 0.0));
861 let row = frus_text::line_box(ROW_H, &label_style(None, Some(&theme)), 0.0);
862
863 let ui = frame(&open_list().selected(0), &theme);
864 let painted = crisp(ui.scene());
865 let panel = *painted
866 .iter()
867 .find(|r| r.color == surface)
868 .expect("the theme's surface");
869 assert_eq!(panel.radius, BorderRadius::uniform(3.0));
870 assert_eq!(panel.rect.height, 3.0 * row + 40.0);
871 assert!(
872 painted
873 .iter()
874 .any(|r| r.color == surface.lerp(theme.primary, 0.14)),
875 "the selected option is tinted from the theme's surface: {painted:#?}"
876 );
877 assert_eq!(
878 (height_of(ui.scene()), colour_of(ui.scene())),
879 (2.0, Some(shade))
880 );
881
882 let own = frus_core::Color::rgb(0.9, 0.9, 0.1);
883 let own_shade = frus_core::Color::rgba(0.5, 0.0, 0.0, 0.4);
884 let told = open_list()
885 .menu_background(own)
886 .menu_radius(9.0)
887 .menu_elevation(1.0)
888 .menu_shadow_color(own_shade)
889 .menu_padding(Insets::ZERO)
890 .selected(0);
891 let ui = frame(&told, &theme);
892 let painted = crisp(ui.scene());
893 let panel = *painted
894 .iter()
895 .find(|r| r.color == own)
896 .expect("the caller's surface");
897 assert_eq!(panel.radius, BorderRadius::uniform(9.0));
898 assert_eq!(panel.rect.height, 3.0 * row);
899 assert!(painted
900 .iter()
901 .any(|r| r.color == own.lerp(theme.primary, 0.14)));
902 assert_eq!(
903 (height_of(ui.scene()), colour_of(ui.scene())),
904 (1.0, Some(own_shade))
905 );
906 }
907
908 #[test]
911 fn a_transparent_shadow_or_a_flat_list_casts_nothing() {
912 let theme = Theme::default();
913 let clear = open_list().menu_shadow_color(frus_core::Color::TRANSPARENT);
914 assert!(blurred(frame(&clear, &theme).scene()).is_empty());
915 let flat = open_list().menu_elevation(0.0);
916 assert!(blurred(frame(&flat, &theme).scene()).is_empty());
917
918 let mut themed = Theme::default();
919 themed.widgets.dropdown.menu_shadow_color = Some(frus_core::Color::TRANSPARENT);
920 assert!(blurred(frame(&open_list(), &themed).scene()).is_empty());
921 }
922
923 #[test]
927 fn the_options_still_answer_inside_the_panel() {
928 let theme = Theme::default();
929 let ui = frame(&open_list(), &theme);
930 let (header, panel) = {
931 let painted = crisp(ui.scene());
932 (painted[0].rect, painted[1].rect)
933 };
934 let row = frus_text::line_box(ROW_H, &label_style(None, Some(&theme)), 0.0);
935 let press = |y: f32| {
936 ui.hit(frus_core::Point::new(100.0, y))
937 .and_then(|id| ui.msg_for(id))
938 };
939 assert_eq!(press(panel.y + 8.0 + row * 1.5), Some(Msg::Select(1)));
940 assert_eq!(press(panel.y + 4.0), None, "the room above the first row");
941 assert_eq!(press(header.y + header.height * 0.5), Some(Msg::Toggle));
942 let list = open_list();
943 let menu = &Widget::<Msg>::children(&list)[1];
944 assert!(menu.children().iter().all(|row| row.focusable()));
945 }
946}