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