1use std::cell::{OnceCell, RefCell};
37use std::rc::Rc;
38
39use frus_core::{BorderRadius, BorderSide, Color, Insets, Rect, Scene, ShapeBorder, TextStyle};
40use frus_layout::{Dimension, FlexDirection, Style};
41
42use crate::flex::Flex;
43use crate::interaction::Status;
44use crate::portal::Placement;
45use crate::theme::Theme;
46use crate::widget::Widget;
47use crate::widgettheme::resolve_shape;
48
49pub const SEARCH_VIEW_RADIUS: f32 = 28.0;
52pub const SEARCH_VIEW_MIN_WIDTH: f32 = 360.0;
54pub const SEARCH_VIEW_MIN_HEIGHT: f32 = 240.0;
56pub const SEARCH_VIEW_FULL_SCREEN_HEADER: f32 = 72.0;
62const ELEVATION: f32 = 6.0;
64const BAR_PADDING_X: f32 = 8.0;
66
67const fn full_screen_by_default(platform: frus_core::TargetPlatform) -> bool {
76 use frus_core::TargetPlatform as P;
77 match platform {
78 P::Ios | P::Android | P::Fuchsia => true,
79 P::MacOs | P::Linux | P::Windows => false,
80 }
81}
82
83pub struct SearchAnchor<Msg = crate::callback::Callback> {
85 open: bool,
86 query: String,
87 hint: Option<String>,
88 view_hint: Option<String>,
89 anchor: RefCell<Option<Box<dyn Widget<Msg>>>>,
90 suggestions: RefCell<Vec<Box<dyn Widget<Msg>>>>,
91 view_leading: RefCell<Option<Box<dyn Widget<Msg>>>>,
92 view_trailing: RefCell<Vec<Box<dyn Widget<Msg>>>>,
93 on_open: Option<Msg>,
94 on_close: Option<Msg>,
95 on_clear: Option<Msg>,
96 on_input: Option<Rc<dyn Fn(String) -> Msg>>,
97 on_submit: Option<Msg>,
98 enabled: bool,
99 full_screen: Option<bool>,
100 background: Option<Color>,
101 elevation: Option<f32>,
102 shape: Option<ShapeBorder>,
103 radius: Option<BorderRadius>,
104 side: Option<BorderSide>,
105 divider_color: Option<Color>,
106 header_height: Option<f32>,
107 header_text_style: Option<TextStyle>,
108 header_hint_style: Option<TextStyle>,
109 min_width: Option<f32>,
110 min_height: Option<f32>,
111 bar_padding: Option<Insets>,
112 built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
113}
114
115impl<Msg: Clone + 'static> SearchAnchor<Msg> {
116 pub fn new(open: bool, query: impl Into<String>) -> Self {
118 Self {
119 open,
120 query: query.into(),
121 hint: None,
122 view_hint: None,
123 anchor: RefCell::new(None),
124 suggestions: RefCell::new(Vec::new()),
125 view_leading: RefCell::new(None),
126 view_trailing: RefCell::new(Vec::new()),
127 on_open: None,
128 on_close: None,
129 on_clear: None,
130 on_input: None,
131 on_submit: None,
132 enabled: true,
133 full_screen: None,
134 background: None,
135 elevation: None,
136 shape: None,
137 radius: None,
138 side: None,
139 divider_color: None,
140 header_height: None,
141 header_text_style: None,
142 header_hint_style: None,
143 min_width: None,
144 min_height: None,
145 bar_padding: None,
146 built: OnceCell::new(),
147 }
148 }
149
150 pub fn hint(mut self, hint: impl Into<String>) -> Self {
152 self.hint = Some(hint.into());
153 self.rebuild();
154 self
155 }
156
157 pub fn view_hint(mut self, hint: impl Into<String>) -> Self {
161 self.view_hint = Some(hint.into());
162 self.rebuild();
163 self
164 }
165
166 pub fn anchor(self, anchor: impl Widget<Msg> + 'static) -> Self {
171 self.anchor_boxed(Box::new(anchor))
172 }
173
174 pub fn anchor_boxed(mut self, anchor: Box<dyn Widget<Msg>>) -> Self {
176 *self.anchor.borrow_mut() = Some(anchor);
177 self.rebuild();
178 self
179 }
180
181 pub fn suggestion(self, suggestion: impl Widget<Msg> + 'static) -> Self {
186 self.suggestion_boxed(Box::new(suggestion))
187 }
188
189 pub fn suggestion_boxed(mut self, suggestion: Box<dyn Widget<Msg>>) -> Self {
191 self.suggestions.borrow_mut().push(suggestion);
192 self.rebuild();
193 self
194 }
195
196 pub fn view_leading(self, leading: impl Widget<Msg> + 'static) -> Self {
199 self.view_leading_boxed(Box::new(leading))
200 }
201
202 pub fn view_leading_boxed(mut self, leading: Box<dyn Widget<Msg>>) -> Self {
204 *self.view_leading.borrow_mut() = Some(leading);
205 self.rebuild();
206 self
207 }
208
209 pub fn view_trailing(self, trailing: impl Widget<Msg> + 'static) -> Self {
212 self.view_trailing_boxed(Box::new(trailing))
213 }
214
215 pub fn view_trailing_boxed(mut self, trailing: Box<dyn Widget<Msg>>) -> Self {
217 self.view_trailing.borrow_mut().push(trailing);
218 self.rebuild();
219 self
220 }
221
222 pub fn on_open(mut self, message: impl Into<Msg>) -> Self {
225 self.on_open = Some(message.into());
226 self.rebuild();
227 self
228 }
229
230 pub fn on_close(mut self, message: impl Into<Msg>) -> Self {
232 self.on_close = Some(message.into());
233 self.rebuild();
234 self
235 }
236
237 pub fn on_clear(mut self, message: impl Into<Msg>) -> Self {
241 self.on_clear = Some(message.into());
242 self.rebuild();
243 self
244 }
245
246 pub fn on_input<R: crate::callback::IntoMsg<Msg>>(
248 mut self,
249 on_input: impl Fn(String) -> R + 'static,
250 ) -> Self {
251 self.on_input = Some(Rc::new(crate::callback::handler1(on_input)));
252 self.rebuild();
253 self
254 }
255
256 pub fn on_submit(mut self, message: impl Into<Msg>) -> Self {
258 self.on_submit = Some(message.into());
259 self.rebuild();
260 self
261 }
262
263 pub fn enabled(mut self, enabled: bool) -> Self {
265 self.enabled = enabled;
266 self.rebuild();
267 self
268 }
269
270 pub fn full_screen(mut self, full_screen: bool) -> Self {
278 self.full_screen = Some(full_screen);
279 self.rebuild();
280 self
281 }
282
283 pub fn view_background_color(mut self, color: Color) -> Self {
287 self.background = Some(color);
288 self.rebuild();
289 self
290 }
291
292 pub fn view_elevation(mut self, elevation: f32) -> Self {
294 self.elevation = Some(elevation);
295 self
296 }
297
298 pub fn view_shape(mut self, shape: ShapeBorder) -> Self {
301 self.shape = Some(shape);
302 self
303 }
304
305 pub fn view_radius(mut self, radius: impl Into<BorderRadius>) -> Self {
307 self.radius = Some(radius.into());
308 self
309 }
310
311 pub fn view_side(mut self, width: f32, color: Color) -> Self {
313 self.side = Some(BorderSide { width, color });
314 self
315 }
316
317 pub fn divider_color(mut self, color: Color) -> Self {
320 self.divider_color = Some(color);
321 self.rebuild();
322 self
323 }
324
325 pub fn header_height(mut self, height: f32) -> Self {
328 self.header_height = Some(height);
329 self.rebuild();
330 self
331 }
332
333 pub fn header_text_style(mut self, style: TextStyle) -> Self {
335 self.header_text_style = Some(style);
336 self.rebuild();
337 self
338 }
339
340 pub fn header_hint_style(mut self, style: TextStyle) -> Self {
342 self.header_hint_style = Some(style);
343 self.rebuild();
344 self
345 }
346
347 pub fn view_min_width(mut self, width: f32) -> Self {
349 self.min_width = Some(width);
350 self.rebuild();
351 self
352 }
353
354 pub fn view_min_height(mut self, height: f32) -> Self {
356 self.min_height = Some(height);
357 self.rebuild();
358 self
359 }
360
361 pub fn view_bar_padding(mut self, padding: Insets) -> Self {
363 self.bar_padding = Some(padding);
364 self.rebuild();
365 self
366 }
367
368 fn rebuild(&mut self) {
369 self.built.take();
370 }
371
372 fn is_full_screen(&self, theme: &Theme) -> bool {
374 self.full_screen
375 .unwrap_or(full_screen_by_default(theme.platform))
376 }
377
378 fn view_outline(&self, theme: &Theme) -> ShapeBorder {
382 let t = &theme.widgets.search_view;
383 let shape = resolve_shape(
384 self.shape,
385 t.shape,
386 self.radius.or(t.radius),
387 if self.is_full_screen(theme) {
388 ShapeBorder::rounded(BorderRadius::ZERO)
389 } else {
390 ShapeBorder::rounded(BorderRadius::uniform(SEARCH_VIEW_RADIUS))
391 },
392 );
393 match self.side.or(t.side) {
394 Some(side) => shape.with_side(side),
395 None => shape,
396 }
397 }
398
399 fn header(&self, theme: &Theme) -> f32 {
401 self.header_height
402 .or(theme.widgets.search_view.header_height)
403 .unwrap_or(if self.is_full_screen(theme) {
404 SEARCH_VIEW_FULL_SCREEN_HEADER
405 } else {
406 crate::SEARCH_BAR_HEIGHT
407 })
408 }
409
410 fn build_anchor(&self) -> Box<dyn Widget<Msg>> {
412 if let Some(anchor) = self.anchor.borrow_mut().take() {
413 return anchor;
414 }
415 let mut bar = crate::SearchBar::<Msg>::new(self.query.clone())
416 .enabled(self.enabled)
417 .read_only(true);
422 if let Some(hint) = &self.hint {
423 bar = bar.hint(hint.clone());
424 }
425 if let Some(message) = &self.on_open {
426 bar = bar.on_tap(message.clone());
427 }
428 Box::new(bar)
429 }
430
431 fn build_header(&self, theme: &Theme) -> Box<dyn Widget<Msg>> {
433 let words = crate::localizations::of();
434 let room = self
435 .bar_padding
436 .or(theme.widgets.search_view.bar_padding)
437 .unwrap_or(Insets::new(0.0, BAR_PADDING_X, 0.0, BAR_PADDING_X));
438 let mut header = crate::SearchBar::<Msg>::new(self.query.clone())
439 .background_color(Color::TRANSPARENT)
444 .elevation(0.0)
445 .min_width(0.0)
446 .height(self.header(theme))
447 .padding(room);
448 if let Some(hint) = self.view_hint.as_ref().or(self.hint.as_ref()) {
449 header = header.hint(hint.clone());
450 }
451 if let Some(style) = self
452 .header_text_style
453 .or(theme.widgets.search_view.header_text_style)
454 {
455 header = header.text_style(style);
456 }
457 if let Some(style) = self
458 .header_hint_style
459 .or(theme.widgets.search_view.header_hint_style)
460 {
461 header = header.hint_style(style);
462 }
463 header = match self.view_leading.borrow_mut().take() {
464 Some(leading) => header.leading_boxed(leading),
465 None => {
466 let mut back = crate::BackButton::<Msg>::new().icon_size(20.0);
467 if let Some(message) = &self.on_close {
468 back = back.on_press(message.clone());
469 }
470 header.leading(back)
471 }
472 };
473 let told = std::mem::take(&mut *self.view_trailing.borrow_mut());
474 if told.is_empty() {
475 if !self.query.is_empty() {
478 if let Some(message) = &self.on_clear {
479 header = header.trailing(
480 crate::IconButton::new(crate::Icons::CLOSE)
481 .icon_size(20.0)
482 .label(words.clear_button_label())
483 .on_press(message.clone()),
484 );
485 }
486 }
487 } else {
488 for trailing in told {
489 header = header.trailing_boxed(trailing);
490 }
491 }
492 Box::new(header)
493 }
494
495 fn build_view(&self, theme: &Theme) -> Box<dyn Widget<Msg>> {
497 let t = &theme.widgets.search_view;
498 let full = self.is_full_screen(theme);
499 let screen = crate::MediaQuery::of().size;
500
501 let mut column = Flex::<Msg>::column()
507 .flex(1.0)
508 .child_boxed(self.build_header(theme));
509 column = column.child(
510 crate::Divider::new().height(1.0).color(
511 self.divider_color
512 .or(t.divider_color)
513 .unwrap_or(theme.scheme.outline),
514 ),
515 );
516 let mut list = Flex::<Msg>::column();
517 for suggestion in std::mem::take(&mut *self.suggestions.borrow_mut()) {
518 list = list.child_boxed(suggestion);
519 }
520 column = column.child(
521 crate::SingleChildScrollView::<Msg>::new()
522 .flex(1.0)
523 .child(list),
524 );
525
526 Box::new(SearchView {
527 children: vec![Box::new(column)],
528 full,
529 screen: (screen.width, screen.height),
530 min_width: self
531 .min_width
532 .or(t.min_width)
533 .unwrap_or(SEARCH_VIEW_MIN_WIDTH),
534 min_height: self
535 .min_height
536 .or(t.min_height)
537 .unwrap_or(SEARCH_VIEW_MIN_HEIGHT),
538 background: self
539 .background
540 .or(t.background_color)
541 .unwrap_or(theme.scheme.surface_container_high),
542 elevation: self.elevation.or(t.elevation).unwrap_or(ELEVATION),
543 shape: self.view_outline(theme),
544 })
545 }
546
547 fn assemble(&self, theme: &Theme) -> Vec<Box<dyn Widget<Msg>>> {
549 let anchor = self.build_anchor();
550 let mut portal = crate::OverlayPortal::<Msg>::new_boxed(anchor);
551 if self.open && self.enabled {
552 let placement = if self.is_full_screen(theme) {
556 Placement::Center
557 } else {
558 Placement::Below
559 };
560 portal = portal.overlay_boxed(self.build_view(theme), placement);
561 if let Some(message) = &self.on_close {
562 portal = portal.dismiss(message.clone());
563 }
564 }
565 vec![Box::new(portal)]
566 }
567}
568
569impl<Msg: Clone + 'static> Widget<Msg> for SearchAnchor<Msg> {
570 fn style(&self) -> Style {
571 Style {
572 flex_direction: FlexDirection::Column,
573 ..Default::default()
574 }
575 }
576
577 fn build_themed(&self, theme: &Theme) {
578 let _ = self.built.set(self.assemble(theme));
579 }
580
581 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
582 self.built.get_or_init(|| self.assemble(&Theme::default()))
583 }
584
585 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
586
587 fn on_click(&self) -> Option<Msg> {
588 None
589 }
590}
591
592struct SearchView<Msg> {
598 children: Vec<Box<dyn Widget<Msg>>>,
599 full: bool,
600 screen: (f32, f32),
602 min_width: f32,
603 min_height: f32,
604 background: Color,
605 elevation: f32,
606 shape: ShapeBorder,
607}
608
609impl<Msg: Clone> Widget<Msg> for SearchView<Msg> {
610 fn style(&self) -> Style {
617 if self.full {
618 Style {
619 width: Dimension::Length(self.screen.0),
620 height: Dimension::Length(self.screen.1),
621 flex_direction: FlexDirection::Column,
622 ..Default::default()
623 }
624 } else {
625 Style {
626 min_width: Dimension::Length(self.min_width),
627 min_height: Dimension::Length(self.min_height),
628 flex_direction: FlexDirection::Column,
629 ..Default::default()
630 }
631 }
632 }
633
634 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
635 &self.children
636 }
637
638 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
639 let o = status.opacity;
640 if self.elevation > 0.0 {
641 frus_core::paint_elevation(
643 scene,
644 bounds,
645 self.shape
648 .as_rounded(bounds)
649 .map(|(_, radius)| radius)
650 .unwrap_or(BorderRadius::ZERO),
651 self.elevation,
652 theme.scheme.shadow.fade(o),
653 );
654 }
655 scene.draw_shape(bounds, self.shape, self.background.fade(o));
656 }
657
658 fn on_click(&self) -> Option<Msg> {
659 None
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666
667 #[test]
671 fn the_view_takes_the_screen_where_the_theme_s_platform_does() {
672 use frus_core::TargetPlatform as P;
673 for platform in P::ALL {
674 let theme = Theme::default().with_platform(platform);
675 let phone = matches!(platform, P::Ios | P::Android | P::Fuchsia);
676 let anchor = SearchAnchor::<()>::new(true, "");
677 assert_eq!(anchor.is_full_screen(&theme), phone, "{platform}");
678 let told = SearchAnchor::<()>::new(true, "").full_screen(!phone);
679 assert_eq!(told.is_full_screen(&theme), !phone, "told, {platform}");
680 }
681 }
682 use frus_core::Primitive;
683
684 #[derive(Clone, Debug, PartialEq)]
685 enum Msg {
686 Open,
687 Close,
688 Clear,
689 Query(String),
690 }
691
692 fn anchor(open: bool, query: &str) -> SearchAnchor<Msg> {
693 SearchAnchor::new(open, query)
694 .hint("Search mail")
695 .on_open(Msg::Open)
696 .on_close(Msg::Close)
697 .on_input(Msg::Query)
698 }
699
700 fn portal(anchor: &SearchAnchor<Msg>, theme: &Theme) -> Vec<String> {
702 anchor.build_themed(theme);
703 let built = Widget::<Msg>::children(anchor);
704 assert_eq!(built.len(), 1, "one portal");
705 built[0]
706 .children()
707 .iter()
708 .map(|child| child.debug_name().to_string())
709 .collect()
710 }
711
712 fn find<'a>(node: &'a dyn Widget<Msg>, name: &str, out: &mut Vec<&'a dyn Widget<Msg>>) {
714 if node.debug_name() == name {
715 out.push(node);
716 }
717 for child in node.children() {
718 find(&**child, name, out);
719 }
720 }
721
722 fn all<'a>(
723 anchor: &'a SearchAnchor<Msg>,
724 name: &str,
725 theme: &Theme,
726 ) -> Vec<&'a dyn Widget<Msg>> {
727 anchor.build_themed(theme);
728 let mut out = Vec::new();
729 for child in Widget::<Msg>::children(anchor) {
730 find(&**child, name, &mut out);
731 }
732 out
733 }
734
735 #[test]
738 fn the_view_is_there_when_the_application_says_it_is() {
739 let theme = Theme::default();
740 assert_eq!(portal(&anchor(false, ""), &theme), vec!["SearchBar"]);
741 assert_eq!(
742 portal(&anchor(true, ""), &theme),
743 vec!["SearchBar", "SearchView"],
744 );
745 assert_eq!(
746 portal(&anchor(true, "").enabled(false), &theme),
747 vec!["SearchBar"],
748 "and an anchor that cannot be used opens nothing, whatever the flag says"
749 );
750 }
751
752 #[test]
756 fn the_anchors_own_bar_is_a_tap_target() {
757 let theme = Theme::default();
758 let shut = anchor(false, "");
759 let bars = all(&shut, "SearchBar", &theme);
760 assert_eq!(bars.len(), 1);
761 assert_eq!(
762 bars[0].on_click(),
763 Some(Msg::Open),
764 "pressing it opens the view"
765 );
766 }
767
768 #[test]
771 fn the_clear_cross_comes_and_goes_with_the_query() {
772 let theme = Theme::default();
773 let empty = anchor(true, "").on_clear(Msg::Clear);
774 assert_eq!(
775 all(&empty, "IconButton", &theme).len(),
776 0,
777 "nothing to clear, nothing to press"
778 );
779
780 let typed = anchor(true, "lisbon").on_clear(Msg::Clear);
781 let crosses = all(&typed, "IconButton", &theme);
782 assert_eq!(crosses.len(), 1);
783 assert_eq!(crosses[0].on_click(), Some(Msg::Clear));
784
785 let nowhere = anchor(true, "lisbon");
786 assert_eq!(
787 all(&nowhere, "IconButton", &theme).len(),
788 0,
789 "and a cross with nowhere to say so is not offered either"
790 );
791 }
792
793 #[test]
796 fn the_screen_is_square_and_the_panel_is_not() {
797 let theme = Theme::default();
798 assert_eq!(
799 anchor(true, "").full_screen(true).view_outline(&theme),
800 ShapeBorder::rounded(BorderRadius::ZERO)
801 );
802 assert_eq!(
803 anchor(true, "").full_screen(false).view_outline(&theme),
804 ShapeBorder::rounded(BorderRadius::uniform(SEARCH_VIEW_RADIUS))
805 );
806 }
807
808 #[test]
810 fn the_screens_header_is_the_taller_one() {
811 let theme = Theme::default();
812 assert_eq!(
813 anchor(true, "").full_screen(true).header(&theme),
814 SEARCH_VIEW_FULL_SCREEN_HEADER
815 );
816 assert_eq!(
817 anchor(true, "").full_screen(false).header(&theme),
818 crate::SEARCH_BAR_HEIGHT
819 );
820 assert_eq!(
821 anchor(true, "").header_height(40.0).header(&theme),
822 40.0,
823 "and a caller still has the last word"
824 );
825 }
826
827 #[test]
830 fn the_panel_has_a_floor_and_the_screen_has_a_size() {
831 let panel = SearchView::<Msg> {
832 children: Vec::new(),
833 full: false,
834 screen: (390.0, 844.0),
835 min_width: SEARCH_VIEW_MIN_WIDTH,
836 min_height: SEARCH_VIEW_MIN_HEIGHT,
837 background: Color::BLACK,
838 elevation: 0.0,
839 shape: ShapeBorder::stadium(),
840 };
841 let style = Widget::<Msg>::style(&panel);
842 assert_eq!(style.min_width, Dimension::Length(SEARCH_VIEW_MIN_WIDTH));
843 assert_eq!(style.min_height, Dimension::Length(SEARCH_VIEW_MIN_HEIGHT));
844 assert_eq!(style.width, Dimension::Auto, "and no width of its own");
845
846 let screen = SearchView::<Msg> {
847 full: true,
848 ..panel
849 };
850 let style = Widget::<Msg>::style(&screen);
851 assert_eq!(style.width, Dimension::Length(390.0));
852 assert_eq!(style.height, Dimension::Length(844.0));
853 }
854
855 #[test]
859 fn a_bar_with_no_surface_does_not_light() {
860 let theme = Theme::default();
861 let hovered = Status {
862 opacity: 1.0,
863 hover_progress: 1.0,
864 ..Default::default()
865 };
866 let fill = |bar: &crate::SearchBar<Msg>, status: Status| {
867 let mut scene = Scene::new();
868 bar.paint(Rect::new(0.0, 0.0, 400.0, 56.0), status, &theme, &mut scene);
869 scene
870 .primitives()
871 .iter()
872 .rev()
873 .find_map(|p| match p {
874 Primitive::Rect { color, blur, .. } if *blur == 0.0 => Some(*color),
875 _ => None,
876 })
877 .expect("it paints its pill")
878 };
879 let bare = crate::SearchBar::<Msg>::new("").background_color(Color::TRANSPARENT);
880 assert_eq!(
881 fill(&bare, hovered),
882 fill(
883 &bare,
884 Status {
885 opacity: 1.0,
886 ..Default::default()
887 }
888 ),
889 "there is no ground to light"
890 );
891
892 let solid = crate::SearchBar::<Msg>::new("");
893 assert_ne!(
894 fill(
895 &solid,
896 Status {
897 opacity: 1.0,
898 ..Default::default()
899 }
900 ),
901 fill(&solid, hovered),
902 "and one with a surface still answers a pointer"
903 );
904 }
905
906 #[test]
909 fn the_views_hint_falls_back_to_the_bars() {
910 let theme = Theme::default();
911 let plain = anchor(true, "");
912 plain.build_themed(&theme);
913 let heard: Vec<String> = all(&plain, "SearchBar", &theme)
914 .iter()
915 .filter_map(|bar| bar.semantics()?.label)
916 .collect();
917 assert_eq!(heard.len(), 2, "the bar and the view's header");
918 assert!(heard.iter().all(|hint| hint == "Search mail"));
919
920 let told = anchor(true, "").view_hint("Search all mail");
921 let heard: Vec<String> = all(&told, "SearchBar", &theme)
922 .iter()
923 .filter_map(|bar| bar.semantics()?.label)
924 .collect();
925 assert_eq!(heard, vec!["Search mail", "Search all mail"]);
926 }
927
928 #[test]
930 fn saying_it_afterwards_says_it() {
931 let theme = Theme::default();
932 let after = SearchAnchor::<Msg>::new(true, "")
933 .on_close(Msg::Close)
934 .full_screen(true)
935 .hint("Search");
936 assert_eq!(portal(&after, &theme).len(), 2);
937 assert_eq!(after.header(&theme), SEARCH_VIEW_FULL_SCREEN_HEADER);
938 }
939}