1#![allow(non_snake_case)]
2
3use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5
6use repose_core::NestedScrollConnection;
7use repose_core::animation::AnimationSpec;
8use repose_core::text::ImeAction;
9use repose_core::*;
10use repose_ui::{
11 BasicTextField, Box, Column, Row, Spacer, Text, TextFieldState, TextStyle,
12 ViewExt, ZStack,
13 anim::animate_f32,
14 overlay::OverlayHandle,
15};
16
17use super::*;
18use super::app_bar::WindowInsets;
19use super::util::apply_tonal_elevation;
20
21use super::util::lerp_color;
22#[derive(Clone, Copy, Debug)]
24pub struct SearchBarColors {
25 pub container_color: Color,
26 pub active_container_color: Color,
27 pub divider_color: Color,
28 pub content_color: Color,
29 pub placeholder_color: Color,
30 pub scrim_color: Color,
31}
32
33impl SearchBarColors {
34 pub fn container(&self, active: bool) -> Color {
35 if active {
36 self.active_container_color
37 } else {
38 self.container_color
39 }
40 }
41}
42
43impl Default for SearchBarColors {
44 fn default() -> Self {
45 Self {
46 container_color: SearchBarDefaults::container_color(),
47 active_container_color: SearchBarDefaults::active_container_color(),
48 divider_color: SearchBarDefaults::divider_color(),
49 content_color: SearchBarDefaults::content_color(),
50 placeholder_color: SearchBarDefaults::placeholder_color(),
51 scrim_color: SearchBarDefaults::scrim_color(),
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug)]
58pub struct AppBarWithSearchColors {
59 pub search_bar_colors: SearchBarColors,
60 pub scrolled_search_bar_container_color: Color,
61 pub app_bar_container_color: Color,
62 pub scrolled_app_bar_container_color: Color,
63 pub navigation_icon_content_color: Color,
64 pub action_icon_content_color: Color,
65}
66
67impl AppBarWithSearchColors {
68 pub fn search_bar_container(&self, scroll_fraction: f32) -> Color {
69 lerp_color(
70 self.search_bar_colors.container_color,
71 self.scrolled_search_bar_container_color,
72 scroll_fraction.clamp(0.0, 1.0),
73 )
74 }
75 pub fn app_bar_container(&self, scroll_fraction: f32) -> Color {
76 lerp_color(
77 self.app_bar_container_color,
78 self.scrolled_app_bar_container_color,
79 scroll_fraction.clamp(0.0, 1.0),
80 )
81 }
82}
83
84impl Default for AppBarWithSearchColors {
85 fn default() -> Self {
86 Self {
87 search_bar_colors: SearchBarColors::default(),
88 scrolled_search_bar_container_color: SearchBarDefaults::scrolled_container_color(),
89 app_bar_container_color: SearchBarDefaults::app_bar_container_color(),
90 scrolled_app_bar_container_color: SearchBarDefaults::scrolled_app_bar_container_color(),
91 navigation_icon_content_color: SearchBarDefaults::navigation_icon_content_color(),
92 action_icon_content_color: SearchBarDefaults::action_icon_content_color(),
93 }
94 }
95}
96
97#[derive(Clone, Debug)]
99pub struct SearchBarConfig {
100 pub modifier: Modifier,
101 pub colors: SearchBarColors,
102 pub height: f32,
103 pub shape_radius: f32,
104 pub active_shape_radius: f32,
105 pub expanded_width: f32,
106 pub collapsed_width: f32,
107 pub tonal_elevation: f32,
108 pub shadow_elevation: f32,
109 pub window_insets: WindowInsets,
110 pub content_padding: PaddingValues,
111 pub min_width: f32,
112 pub max_width: f32,
113}
114
115impl Default for SearchBarConfig {
116 fn default() -> Self {
117 Self {
118 modifier: Modifier::new(),
119 colors: SearchBarColors::default(),
120 height: SearchBarDefaults::HEIGHT,
121 shape_radius: SearchBarDefaults::SHAPE_RADIUS,
122 active_shape_radius: SearchBarDefaults::ACTIVE_SHAPE_RADIUS,
123 expanded_width: SearchBarDefaults::EXPANDED_WIDTH,
124 collapsed_width: SearchBarDefaults::COLLAPSED_WIDTH,
125 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
126 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
127 window_insets: WindowInsets::default(),
128 content_padding: SearchBarDefaults::CONTENT_PADDING,
129 min_width: SearchBarDefaults::MIN_WIDTH,
130 max_width: SearchBarDefaults::MAX_WIDTH,
131 }
132 }
133}
134
135#[derive(Clone, Debug)]
137pub struct ExpandedFullScreenSearchBarConfig {
138 pub modifier: Modifier,
139 pub colors: SearchBarColors,
140 pub collapsed_shape_radius: f32,
141 pub tonal_elevation: f32,
142 pub shadow_elevation: f32,
143 pub window_insets: WindowInsets,
144 pub scrim_color: Color,
145}
146
147impl Default for ExpandedFullScreenSearchBarConfig {
148 fn default() -> Self {
149 Self {
150 modifier: Modifier::new(),
151 colors: SearchBarColors::default(),
152 collapsed_shape_radius: SearchBarDefaults::SHAPE_RADIUS,
153 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
154 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
155 window_insets: WindowInsets::default(),
156 scrim_color: SearchBarDefaults::scrim_color(),
157 }
158 }
159}
160
161#[derive(Clone, Debug)]
163pub struct ExpandedDockedSearchBarConfig {
164 pub modifier: Modifier,
165 pub colors: SearchBarColors,
166 pub shape_radius: f32,
167 pub dropdown_shape_radius: f32,
168 pub dropdown_gap_size: f32,
169 pub dropdown_scrim_color: Color,
170 pub tonal_elevation: f32,
171 pub shadow_elevation: f32,
172}
173
174impl Default for ExpandedDockedSearchBarConfig {
175 fn default() -> Self {
176 Self {
177 modifier: Modifier::new(),
178 colors: SearchBarColors::default(),
179 shape_radius: SearchBarDefaults::DOCKED_SHAPE_RADIUS,
180 dropdown_shape_radius: SearchBarDefaults::DROPDOWN_SHAPE_RADIUS,
181 dropdown_gap_size: SearchBarDefaults::DROPDOWN_GAP_SIZE,
182 dropdown_scrim_color: SearchBarDefaults::dropdown_scrim_color(),
183 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
184 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
185 }
186 }
187}
188
189#[derive(Clone, Debug)]
191pub struct AppBarWithSearchConfig {
192 pub modifier: Modifier,
193 pub colors: AppBarWithSearchColors,
194 pub height: f32,
195 pub shape_radius: f32,
196 pub tonal_elevation: f32,
197 pub shadow_elevation: f32,
198 pub content_padding: PaddingValues,
199 pub window_insets: WindowInsets,
200 pub scroll_fraction: f32,
201 pub scroll_offset: f32,
202}
203
204impl Default for AppBarWithSearchConfig {
205 fn default() -> Self {
206 Self {
207 modifier: Modifier::new(),
208 colors: AppBarWithSearchColors::default(),
209 height: SearchBarDefaults::HEIGHT,
210 shape_radius: SearchBarDefaults::SHAPE_RADIUS,
211 tonal_elevation: SearchBarDefaults::TONAL_ELEVATION,
212 shadow_elevation: SearchBarDefaults::SHADOW_ELEVATION,
213 content_padding: SearchBarDefaults::CONTENT_PADDING,
214 window_insets: WindowInsets::default(),
215 scroll_fraction: 0.0,
216 scroll_offset: 0.0,
217 }
218 }
219}
220
221pub struct SearchBarScrollBehavior {
223 pub collapsed_offset: Signal<f32>,
224 pub height: f32,
225 pub collapsed_height: f32,
226 _pending: Rc<Cell<f32>>,
227}
228
229impl SearchBarScrollBehavior {
230 pub fn new(height: f32, collapsed_height: f32) -> Self {
231 Self {
232 collapsed_offset: signal(0.0),
233 height,
234 collapsed_height,
235 _pending: Rc::new(Cell::new(0.0)),
236 }
237 }
238
239 pub fn offset(&self) -> f32 {
240 self.collapsed_offset.get()
241 }
242
243 pub fn nested_scroll_connection(&self) -> NestedScrollConnection {
244 let offset = self.collapsed_offset.clone();
245 let max_offset = self.height - self.collapsed_height;
246 NestedScrollConnection::new().on_pre_scroll(move |delta: Vec2, _source| {
247 let cur = offset.get();
248 let new = (cur - delta.y).clamp(-max_offset, 0.0);
249 let consumed = cur - new;
250 offset.set(new);
251 request_frame();
252 Vec2 {
253 x: 0.0,
254 y: consumed,
255 }
256 })
257 }
258}
259
260#[derive(Clone, Copy, Debug, PartialEq)]
262pub enum SearchBarValue {
263 Collapsed,
264 Expanded,
265}
266
267pub struct SearchBarState {
270 pub query: Signal<String>,
271 pub expanded: Signal<bool>,
272 pub active: Signal<bool>,
273 pub expands_to_full_screen: Signal<bool>,
276 anim: Rc<RefCell<AnimatedValue<f32>>>,
278 content_anim: Rc<RefCell<AnimatedValue<f32>>>,
280 pub collapsed_layout_rect: Signal<(f32, f32, f32, f32)>,
283}
284
285impl Default for SearchBarState {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291impl SearchBarState {
292 pub fn new() -> Self {
293 Self {
294 query: signal(String::new()),
295 expanded: signal(false),
296 active: signal(false),
297 expands_to_full_screen: signal(false),
298 anim: Rc::new(RefCell::new(AnimatedValue::new(
299 0.0,
300 AnimationSpec::spring_gentle(),
301 ))),
302 content_anim: Rc::new(RefCell::new(AnimatedValue::new(
303 0.0,
304 AnimationSpec::spring_gentle(),
305 ))),
306 collapsed_layout_rect: signal((0.0, 0.0, 0.0, 0.0)),
307 }
308 }
309
310 pub fn query(&self) -> String {
311 self.query.get()
312 }
313
314 pub fn set_query(&self, q: impl Into<String>) {
315 self.query.set(q.into());
316 }
317
318 pub fn is_expanded(&self) -> bool {
319 self.expanded.get()
320 }
321
322 pub fn expand(&self) {
323 self.expanded.set(true);
324 self.anim.borrow_mut().set_target(1.0);
325 self.content_anim.borrow_mut().set_target(1.0);
326 request_frame();
327 }
328
329 pub fn collapse(&self) {
330 self.expanded.set(false);
331 self.active.set(false);
332 self.content_anim.borrow_mut().set_target(0.0);
334 self.anim.borrow_mut().set_target(0.0);
335 request_frame();
336 }
337
338 pub fn is_active(&self) -> bool {
339 self.active.get()
340 }
341
342 pub fn activate(&self) {
343 self.active.set(true);
344 self.expanded.set(true);
345 self.anim.borrow_mut().set_target(1.0);
346 self.content_anim.borrow_mut().set_target(1.0);
347 request_frame();
348 }
349
350 pub fn deactivate(&self) {
351 if self.expanded.get() {
352 self.expanded.set(false);
353 self.content_anim.borrow_mut().set_target(0.0);
354 self.anim.borrow_mut().set_target(0.0);
355 }
356 self.active.set(false);
357 FocusManager::new(vec![], None).clear_focus(false);
358 request_frame();
359 }
360
361 pub fn progress(&self) -> f32 {
364 let mut a = self.anim.borrow_mut();
365 let still = a.update();
366 if still {
367 request_frame();
368 }
369 a.get().clamp(0.0, 1.0)
370 }
371
372 pub fn content_progress(&self) -> f32 {
374 let mut a = self.content_anim.borrow_mut();
375 let still = a.update();
376 if still {
377 request_frame();
378 }
379 a.get().clamp(0.0, 1.0)
380 }
381
382 pub fn is_animating(&self) -> bool {
384 self.anim.borrow().is_animating() || self.content_anim.borrow().is_animating()
385 }
386
387 pub fn current_value(&self) -> SearchBarValue {
389 if *self.anim.borrow().get() <= 0.02 {
390 SearchBarValue::Collapsed
391 } else {
392 SearchBarValue::Expanded
393 }
394 }
395
396 pub fn snap_to(&self, fraction: f32) {
398 self.anim.borrow_mut().snap_to(fraction.clamp(0.0, 1.0));
399 request_frame();
400 }
401}
402
403#[derive(Clone)]
404pub struct SearchBarInputFieldConfig {
405 pub state: Option<Rc<SearchBarState>>,
406 pub on_search: Option<Rc<dyn Fn(String)>>,
407 pub enabled: bool,
408 pub text_color: Color,
409 pub placeholder_color: Color,
410 pub leading_icon: Option<View>,
411 pub trailing_icon: Option<View>,
412 pub interaction_source: Option<MutableInteractionSource>,
413}
414
415impl Default for SearchBarInputFieldConfig {
416 fn default() -> Self {
417 let th = theme();
418 Self {
419 state: None,
420 on_search: None,
421 enabled: true,
422 text_color: th.on_surface,
423 placeholder_color: th.on_surface_variant,
424 leading_icon: None,
425 trailing_icon: None,
426 interaction_source: None,
427 }
428 }
429}
430
431pub fn SearchBarInputField(
436 placeholder: String,
437 query: String,
438 on_query_change: Rc<dyn Fn(String)>,
439 expanded: bool,
440 config: SearchBarInputFieldConfig,
441) -> View {
442 let source: Rc<MutableInteractionSource> = config
443 .interaction_source
444 .clone()
445 .map(Rc::new)
446 .unwrap_or_else(|| Rc::new(MutableInteractionSource::new()));
447 let focused = source.source().collect_is_focused();
448 let state = config.state;
449 let enabled = config.enabled;
450
451 let mut input_m = Modifier::new()
452 .flex_grow(1.0)
453 .padding(4.0)
454 .required_width_in(SearchBarDefaults::MIN_WIDTH, SearchBarDefaults::MAX_WIDTH)
455 .required_height_in(SearchBarDefaults::HEIGHT, SearchBarDefaults::HEIGHT)
456 .interaction_source(&*source)
457 .semantics(Semantics {
458 role: Role::TextField,
459 label: Some("Search".into()),
460 focused: expanded || focused,
461 enabled,
462 selectable_group: false,
463 })
464 .on_key_event({
465 let s = state.clone();
466 move |ev| {
467 if ev.key == Key::Escape {
468 if let Some(ref s) = s {
469 if s.is_active() {
470 s.deactivate();
471 }
472 }
473 true
474 } else if ev.key == Key::ArrowDown || ev.key == Key::ArrowUp {
475 if let Some(ref s) = s {
476 if !s.is_expanded() {
477 s.activate();
478 }
479 }
480 true
481 } else {
482 false
483 }
484 }
485 });
486 if let Some(ref s) = state {
487 let s2 = s.clone();
488 input_m = input_m.on_focus_changed(move |focused| {
489 if focused {
490 s2.activate();
491 }
492 });
493 }
494
495 let on_qc = on_query_change.clone();
496 let on_s = config.on_search.clone();
497
498 let read_only = !expanded;
500
501 let display_color = if query.is_empty() {
502 config.placeholder_color
503 } else {
504 config.text_color
505 };
506
507 let tf_state = remember_with_key("SearchBarInputField_tf_state", || {
508 RefCell::new(TextFieldState::new())
509 });
510 if tf_state.borrow().text != query {
511 tf_state.borrow_mut().text = query.clone();
512 }
513
514 let mut row_children: Vec<View> = Vec::new();
516 if let Some(icon) = config.leading_icon {
517 row_children.push(icon);
518 }
519 let on_qc2 = on_qc.clone();
520 row_children.push(
521 BasicTextField(
522 tf_state.clone(),
523 input_m,
524 placeholder,
525 repose_ui::TextFieldConfig {
526 on_change: Some(Rc::new(move |text| on_qc2(text))),
527 on_submit: on_s.clone(),
528 enabled,
529 read_only,
530 line_limits: TextFieldLineLimits::SingleLine,
531 keyboard_options: KeyboardOptions {
532 ime_action: ImeAction::Search,
533 ..KeyboardOptions::DEFAULT
534 },
535 ..Default::default()
536 },
537 )
538 .color(display_color)
539 .size(repose_core::locals::theme().typography.body_large),
540 );
541 if let Some(icon) = config.trailing_icon {
542 row_children.push(icon);
543 }
544
545 if row_children.len() == 1 {
546 row_children.into_iter().next().unwrap()
547 } else {
548 Row(Modifier::new()
549 .fill_max_width()
550 .align_items(AlignItems::CENTER))
551 .child(row_children)
552 }
553}
554
555
556fn track_collapsed_layout(state: &Rc<SearchBarState>) -> Modifier {
559 let s = state.clone();
560 Modifier::new().on_globally_positioned(move |rect| {
561 s.collapsed_layout_rect
562 .set((rect.x, rect.y, rect.w, rect.h));
563 })
564}
565
566
567pub fn SearchBar(
579 state: Rc<SearchBarState>,
580 input_field: View,
581 modifier: Modifier,
582 leading_icon: Option<View>,
583 trailing_icon: Option<View>,
584 config: SearchBarConfig,
585) -> View {
586 let th = theme();
587 let colors = config.colors;
588
589 let mut bar_m = modifier
590 .fill_max_width()
591 .height(config.height)
592 .state_elevation(StateElevation {
593 default: config.tonal_elevation,
594 hovered: th.elevation.level2,
595 pressed: th.elevation.level3,
596 dragged: th.elevation.level3,
597 disabled: 0.0,
598 })
599 .shadow(config.shadow_elevation, 0.0)
600 .padding_values(config.content_padding)
601 .on_key_event({
602 let s = state.clone();
603 move |ev| {
604 if ev.key == Key::Escape && s.is_active() {
605 s.deactivate();
606 true
607 } else {
608 false
609 }
610 }
611 })
612 .on_focus_changed({
613 let s = state.clone();
614 move |focused| {
615 if focused {
616 s.activate();
617 }
618 }
619 })
620 .semantics(Semantics {
621 role: Role::TextField,
622 label: Some("Search".into()),
623 focused: state.is_active(),
624 enabled: true,
625 selectable_group: false,
626 })
627 .background(colors.container_color)
628 .clip_rounded(config.shape_radius)
629 .then(track_collapsed_layout(&state));
630
631 bar_m = apply_tonal_elevation(bar_m, config.tonal_elevation, colors.container_color);
632
633 Box(bar_m).child(
634 Row(Modifier::new()
635 .fill_max_size()
636 .align_items(AlignItems::CENTER))
637 .child((
638 leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
639 Box(Modifier::new().width(8.0).fill_max_height()),
640 input_field,
641 trailing_icon.unwrap_or(Box(Modifier::new())),
642 )),
643 )
644}
645
646
647pub fn SearchBarWithContent(
654 input_field: View,
655 expanded: bool,
656 on_expanded_change: Rc<dyn Fn(bool)>,
657 modifier: Modifier,
658 leading_icon: Option<View>,
659 trailing_icon: Option<View>,
660 config: SearchBarConfig,
661 content: View,
662) -> View {
663 let th = theme();
664 let width = animate_f32(
665 "sbwc_w",
666 if expanded {
667 config.expanded_width
668 } else {
669 config.collapsed_width
670 },
671 theme().motion.expand,
672 );
673
674 let bar_bg = if expanded {
675 config.colors.active_container_color
676 } else {
677 config.colors.container_color
678 };
679 let shape = if expanded {
680 config.active_shape_radius
681 } else {
682 config.shape_radius
683 };
684
685 let mut bar_m = modifier
686 .clone()
687 .width(width)
688 .min_width(config.min_width)
689 .max_width(config.max_width)
690 .height(config.height)
691 .shadow(config.shadow_elevation, 0.0)
692 .padding_values(config.content_padding)
693 .on_key_event({
694 let cb = on_expanded_change.clone();
695 move |ev| {
696 if ev.key == Key::Escape {
697 cb(false);
698 true
699 } else {
700 false
701 }
702 }
703 })
704 .background(bar_bg)
705 .clip_rounded(shape);
706
707 bar_m = apply_tonal_elevation(bar_m, config.tonal_elevation, bar_bg);
708
709 let content_alpha = animate_f32("sbwc_a", if expanded { 1.0 } else { 0.0 }, th.motion.color);
711
712 let bar = Box(bar_m).child(
713 Row(Modifier::new()
714 .fill_max_size()
715 .align_items(AlignItems::CENTER))
716 .child((
717 leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
718 Box(Modifier::new().width(8.0).fill_max_height()),
719 input_field,
720 trailing_icon.unwrap_or(Box(Modifier::new())),
721 )),
722 );
723
724 let show_content = expanded || content_alpha > 0.01;
725 if show_content || expanded {
726 Column(modifier).child((
727 bar,
728 Box(Modifier::new()
729 .width(width)
730 .max_height(SearchBarDefaults::DOCKED_HEIGHT)
731 .alpha(content_alpha)
732 .background(config.colors.container_color)
733 .clip_rounded(th.shapes.extra_small))
734 .child(content),
735 ))
736 } else {
737 bar
738 }
739}
740
741pub fn DockedSearchBar(
746 input_field: View,
747 expanded: bool,
748 on_expanded_change: Option<Rc<dyn Fn(bool)>>,
749 modifier: Modifier,
750 leading_icon: Option<View>,
751 config: SearchBarConfig,
752 content: View,
753) -> View {
754 let th = theme();
755 let active = expanded;
756 let colors = config.colors;
757
758 let content_target = if expanded {
759 get_window_container_height() * 2.0 / 3.0
760 } else {
761 0.0
762 };
763 let content_height = animate_f32("docked_sh", content_target, theme().motion.expand);
764 let content_alpha = animate_f32(
765 "docked_sa",
766 if expanded { 1.0 } else { 0.0 },
767 theme().motion.color,
768 );
769 let bar_bg = if active {
770 colors.active_container_color
771 } else {
772 colors.container_color
773 };
774
775 let clear_btn = if active {
776 Box(Modifier::new().size(24.0, 24.0).clickable().on_click({
777 let cb = on_expanded_change.clone();
778 move || {
779 if let Some(ref cb) = cb {
780 cb(false);
781 }
782 }
783 }))
784 .child(Text("✕").size(16.0).color(colors.placeholder_color))
785 } else {
786 Box(Modifier::new())
787 };
788
789 let mut bar_m = modifier
790 .z_index(1.0)
791 .min_width(SearchBarDefaults::MIN_WIDTH)
792 .height(config.height)
793 .state_elevation(StateElevation {
794 default: if active {
795 th.elevation.level3
796 } else {
797 config.tonal_elevation
798 },
799 hovered: th.elevation.level2,
800 pressed: th.elevation.level3,
801 dragged: th.elevation.level3,
802 disabled: 0.0,
803 })
804 .shadow(config.shadow_elevation, 0.0)
805 .padding_values(config.content_padding)
806 .on_key_event({
807 let cb = on_expanded_change.clone();
808 move |ev| {
809 if ev.key == Key::Escape {
810 if let Some(ref cb) = cb {
811 cb(false);
812 }
813 true
814 } else {
815 false
816 }
817 }
818 })
819 .background(bar_bg)
820 .clip_rounded(config.shape_radius);
821
822 bar_m = apply_tonal_elevation(bar_m, config.tonal_elevation, bar_bg);
823
824 let bar = Box(bar_m).child(
825 Row(Modifier::new()
826 .fill_max_size()
827 .align_items(AlignItems::CENTER))
828 .child((
829 leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
830 Box(Modifier::new().width(12.0).fill_max_height()),
831 input_field,
832 clear_btn,
833 )),
834 );
835
836 let show_content = expanded || content_height > 1.0;
837 if show_content {
838 Column(Modifier::new().min_width(SearchBarDefaults::MIN_WIDTH)).child((
839 bar,
840 Box(Modifier::new()
841 .min_width(SearchBarDefaults::MIN_WIDTH)
842 .height(content_height)
843 .alpha(content_alpha)
844 .clip_rounded(th.shapes.small)
845 .background(colors.container_color)
846 .state_elevation(StateElevation {
847 default: th.elevation.level3,
848 hovered: th.elevation.level3,
849 pressed: th.elevation.level3,
850 dragged: th.elevation.level3,
851 disabled: 0.0,
852 }))
853 .child(
854 Column(Modifier::new().min_width(SearchBarDefaults::MIN_WIDTH)).child((
855 Box(Modifier::new()
856 .min_width(SearchBarDefaults::MIN_WIDTH)
857 .height(1.0)
858 .background(colors.divider_color)),
859 content,
860 )),
861 ),
862 ))
863 } else {
864 bar
865 }
866}
867
868pub fn set_window_container_height(h: f32) {
872 repose_core::locals::set_window_container_height(h);
873}
874
875fn get_window_container_height() -> f32 {
876 repose_core::locals::get_window_container_height()
877}
878
879pub fn set_window_container_width(w: f32) {
881 repose_core::locals::set_window_container_width(w);
882}
883
884fn get_window_container_width() -> f32 {
885 repose_core::locals::get_window_container_width()
886}
887
888pub fn ExpandedFullScreenSearchBar(
892 state: Rc<SearchBarState>,
893 overlay: OverlayHandle,
894 input_field: View,
895 modifier: Modifier,
896 config: ExpandedFullScreenSearchBarConfig,
897 content: View,
898) -> View {
899 state.expands_to_full_screen.set(true);
901
902 let overlay_id = remember_with_key("efs_oid", || signal(0u64));
903 let current_content = remember_state_with_key("efs_cc", || Box(Modifier::new()));
904 *current_content.borrow_mut() = content;
905
906 let progress = state.progress();
907 let _content_alpha = state.content_progress();
908
909 let expanded = state.is_expanded();
910 let visible = expanded || progress > 0.01;
911
912 if visible {
913 if overlay_id.get() == 0 {
914 let input_fr = FocusRequester::new();
915 let focus_requested = Rc::new(Cell::new(false));
916 let builder: Rc<dyn Fn() -> View> = Rc::new({
917 let state = state.clone();
918 let modifier = modifier.clone();
919 let input_field = input_field.clone();
920 let current_content = current_content.clone();
921 let config = config.clone();
922 let input_fr = input_fr.clone();
923 let focus_requested = focus_requested.clone();
924 move || {
925 let progress = state.progress();
926 let content_alpha = state.content_progress();
927 let alpha = progress.clamp(0.0, 1.0);
928 let c_alpha = content_alpha.clamp(0.0, 1.0);
929 let th = theme();
930 let content = current_content.borrow().clone();
931
932 let inp = Box(Modifier::new().focus_requester(input_fr.clone()))
934 .child(input_field.clone());
935 if !focus_requested.get() {
936 focus_requested.set(true);
937 input_fr.request_focus();
938 }
939
940 let header = Box(modifier
941 .clone()
942 .fill_max_width()
943 .height(SearchBarDefaults::HEIGHT)
944 .padding_values(PaddingValues {
945 left: 16.0,
946 right: 16.0,
947 top: 0.0,
948 bottom: 0.0,
949 })
950 .background(config.colors.container_color)
951 .alpha(alpha))
952 .child(inp);
953
954 let body = Box(Modifier::new()
955 .fill_max_width()
956 .flex_grow(1.0)
957 .alpha(c_alpha)
958 .background(th.surface))
959 .child(content);
960
961 let insets = config.window_insets;
962 let full = Column(Modifier::new().fill_max_size().padding_values(
963 PaddingValues {
964 left: insets.left,
965 right: insets.right,
966 top: insets.top,
967 bottom: insets.bottom,
968 },
969 ))
970 .child((header, body));
971
972 let scrim = Box(Modifier::new()
973 .fill_max_size()
974 .background(config.scrim_color.with_alpha((85.0 * alpha) as u8))
975 .on_click({
976 let s = state.clone();
977 move || s.collapse()
978 }));
979
980 ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, full))
981 }
982 });
983
984 let id = overlay.show_entry(builder, 900.0, false);
985 overlay_id.set(id);
986 }
987 } else {
988 let prev = overlay_id.get();
989 if prev != 0 {
990 let _ = overlay.dismiss(prev);
991 overlay_id.set(0);
992 }
993 }
994
995 Box(Modifier::new())
996}
997
998pub fn ExpandedDockedSearchBar(
1002 state: Rc<SearchBarState>,
1003 overlay: OverlayHandle,
1004 input_field: View,
1005 modifier: Modifier,
1006 config: ExpandedDockedSearchBarConfig,
1007 content: View,
1008) -> View {
1009 state.expands_to_full_screen.set(false);
1011
1012 let overlay_id = remember_with_key("eds_oid", || signal(0u64));
1013 let current_content = remember_state_with_key("eds_cc", || Box(Modifier::new()));
1014 *current_content.borrow_mut() = content;
1015
1016 let progress = state.progress();
1017 let _content_alpha = state.content_progress();
1018 let expanded = state.is_expanded();
1019 let visible = expanded || progress > 0.01;
1020
1021 if visible {
1022 if overlay_id.get() == 0 {
1023 let input_fr = FocusRequester::new();
1024 let focus_requested = Rc::new(Cell::new(false));
1025 let builder: Rc<dyn Fn() -> View> = Rc::new({
1026 let state = state.clone();
1027 let modifier = modifier.clone();
1028 let input_field = input_field.clone();
1029 let current_content = current_content.clone();
1030 let config = config.clone();
1031 let input_fr = input_fr.clone();
1032 let focus_requested = focus_requested.clone();
1033 move || {
1034 let progress = state.progress();
1035 let content_alpha = state.content_progress();
1036 let alpha = progress.clamp(0.0, 1.0);
1037 let c_alpha = content_alpha.clamp(0.0, 1.0);
1038 let th = theme();
1039 let content = current_content.borrow().clone();
1040 let (_cx, _cy, _cw, _ch) = state.collapsed_layout_rect.get();
1041
1042 let inp = Box(Modifier::new().focus_requester(input_fr.clone()))
1043 .child(input_field.clone());
1044 if !focus_requested.get() {
1045 focus_requested.set(true);
1046 input_fr.request_focus();
1047 }
1048
1049 let header = Box(modifier
1050 .clone()
1051 .fill_max_width()
1052 .height(SearchBarDefaults::HEIGHT)
1053 .alpha(alpha)
1054 .background(config.colors.container_color)
1055 .clip_rounded(config.shape_radius)
1056 .state_elevation(StateElevation {
1057 default: th.elevation.level3,
1058 hovered: th.elevation.level2,
1059 pressed: th.elevation.level3,
1060 dragged: th.elevation.level3,
1061 disabled: 0.0,
1062 }))
1063 .child(inp);
1064
1065 let dropdown = Box(Modifier::new()
1066 .fill_max_width()
1067 .max_height(get_window_container_height() * 2.0 / 3.0)
1068 .alpha(c_alpha)
1069 .clip_rounded(config.dropdown_shape_radius)
1070 .background(config.colors.container_color)
1071 .state_elevation(StateElevation {
1072 default: th.elevation.level3,
1073 hovered: th.elevation.level3,
1074 pressed: th.elevation.level3,
1075 dragged: th.elevation.level3,
1076 disabled: 0.0,
1077 }))
1078 .child(
1079 Column(Modifier::new().fill_max_width()).child((
1080 Box(Modifier::new()
1081 .fill_max_width()
1082 .height(1.0)
1083 .background(config.colors.divider_color)),
1084 content,
1085 )),
1086 );
1087
1088 let docked_width = _cw.max(SearchBarDefaults::MIN_WIDTH);
1089 let popup_left = _cx;
1090 let popup_top = _cy + _ch + config.dropdown_gap_size;
1091
1092 let col = Column(Modifier::new().fill_max_width()).child((header, dropdown));
1093
1094 let positioned = Box(Modifier::new()
1095 .absolute()
1096 .offset(Some(popup_left), Some(popup_top), None, None)
1097 .width(docked_width))
1098 .child(col);
1099
1100 let scrim = Box(Modifier::new()
1101 .fill_max_size()
1102 .background(config.dropdown_scrim_color)
1103 .on_click({
1104 let s = state.clone();
1105 move || s.collapse()
1106 }));
1107
1108 ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, positioned))
1109 }
1110 });
1111
1112 let id = overlay.show_entry(builder, 900.0, false);
1113 overlay_id.set(id);
1114 }
1115 } else {
1116 let prev = overlay_id.get();
1117 if prev != 0 {
1118 let _ = overlay.dismiss(prev);
1119 overlay_id.set(0);
1120 }
1121 }
1122
1123 Box(Modifier::new())
1124}
1125
1126pub fn AppBarWithSearch(
1130 state: Rc<SearchBarState>,
1131 input_field: View,
1132 navigation_icon: Option<View>,
1133 actions: Option<Vec<View>>,
1134 config: AppBarWithSearchConfig,
1135) -> View {
1136 let bg = config.colors.search_bar_container(config.scroll_fraction);
1137 let app_bar_bg = config.colors.app_bar_container(config.scroll_fraction);
1138
1139 let insets = config.window_insets;
1140
1141 let is_container_transparent = app_bar_bg.3 == 0;
1143 let tonal_elevation = if is_container_transparent {
1144 0.0
1145 } else {
1146 config.tonal_elevation
1147 };
1148 let shadow_elevation = if is_container_transparent {
1149 0.0
1150 } else {
1151 config.shadow_elevation
1152 };
1153
1154 let hide_collapsed = state.expands_to_full_screen.get() && state.is_expanded();
1156 let collapsed_alpha = if hide_collapsed { 0.0 } else { 1.0 };
1157
1158 let bar_m = Modifier::new()
1159 .fill_max_width()
1160 .height(config.height + insets.top)
1161 .translate(0.0, config.scroll_offset)
1162 .background(app_bar_bg)
1163 .semantics(Semantics::new(Role::Container).with_selectable_group());
1164
1165 let row = Row(Modifier::new()
1166 .fill_max_size()
1167 .align_items(AlignItems::CENTER)
1168 .padding_values(PaddingValues {
1169 left: config.content_padding.left + insets.left,
1170 right: config.content_padding.right + insets.right,
1171 top: insets.top,
1172 bottom: 0.0,
1173 }))
1174 .child({
1175 let mut children: Vec<View> = Vec::new();
1176 if let Some(nav) = navigation_icon {
1177 children.push(nav);
1178 children.push(Box(Modifier::new().width(4.0)));
1179 }
1180 let sb_colors = &config.colors.search_bar_colors;
1182 let collapsed_bar = SearchBar(
1183 state.clone(),
1184 input_field,
1185 Modifier::new().flex_grow(1.0).alpha(collapsed_alpha),
1186 None,
1187 None,
1188 SearchBarConfig {
1189 height: config.height - 8.0,
1190 shape_radius: config.shape_radius,
1191 colors: SearchBarColors {
1192 container_color: bg,
1193 active_container_color: bg,
1194 divider_color: sb_colors.divider_color,
1195 content_color: sb_colors.content_color,
1196 placeholder_color: sb_colors.placeholder_color,
1197 scrim_color: sb_colors.scrim_color,
1198 },
1199 tonal_elevation,
1200 shadow_elevation,
1201 ..Default::default()
1202 },
1203 );
1204 children.push(Box(Modifier::new().flex_grow(1.0)).child(collapsed_bar));
1205 if let Some(acts) = actions {
1206 children.push(Spacer());
1207 for a in acts {
1208 children.push(a);
1209 }
1210 }
1211 children
1212 });
1213
1214 Box(bar_m.shadow(shadow_elevation, 0.0)).child(row)
1215}