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 disabled: 0.0,
597 })
598 .shadow(config.shadow_elevation, 0.0)
599 .padding_values(config.content_padding)
600 .on_key_event({
601 let s = state.clone();
602 move |ev| {
603 if ev.key == Key::Escape && s.is_active() {
604 s.deactivate();
605 true
606 } else {
607 false
608 }
609 }
610 })
611 .on_focus_changed({
612 let s = state.clone();
613 move |focused| {
614 if focused {
615 s.activate();
616 }
617 }
618 })
619 .semantics(Semantics {
620 role: Role::TextField,
621 label: Some("Search".into()),
622 focused: state.is_active(),
623 enabled: true,
624 selectable_group: false,
625 })
626 .background(colors.container_color)
627 .clip_rounded(config.shape_radius)
628 .then(track_collapsed_layout(&state));
629
630 bar_m = apply_tonal_elevation(bar_m, config.tonal_elevation, colors.container_color);
631
632 Box(bar_m).child(
633 Row(Modifier::new()
634 .fill_max_size()
635 .align_items(AlignItems::CENTER))
636 .child((
637 leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
638 Box(Modifier::new().width(8.0).fill_max_height()),
639 input_field,
640 trailing_icon.unwrap_or(Box(Modifier::new())),
641 )),
642 )
643}
644
645
646pub fn SearchBarWithContent(
653 input_field: View,
654 expanded: bool,
655 on_expanded_change: Rc<dyn Fn(bool)>,
656 modifier: Modifier,
657 leading_icon: Option<View>,
658 trailing_icon: Option<View>,
659 config: SearchBarConfig,
660 content: View,
661) -> View {
662 let th = theme();
663 let width = animate_f32(
664 "sbwc_w",
665 if expanded {
666 config.expanded_width
667 } else {
668 config.collapsed_width
669 },
670 theme().motion.expand,
671 );
672
673 let bar_bg = if expanded {
674 config.colors.active_container_color
675 } else {
676 config.colors.container_color
677 };
678 let shape = if expanded {
679 config.active_shape_radius
680 } else {
681 config.shape_radius
682 };
683
684 let mut bar_m = modifier
685 .clone()
686 .width(width)
687 .min_width(config.min_width)
688 .max_width(config.max_width)
689 .height(config.height)
690 .shadow(config.shadow_elevation, 0.0)
691 .padding_values(config.content_padding)
692 .on_key_event({
693 let cb = on_expanded_change.clone();
694 move |ev| {
695 if ev.key == Key::Escape {
696 cb(false);
697 true
698 } else {
699 false
700 }
701 }
702 })
703 .background(bar_bg)
704 .clip_rounded(shape);
705
706 bar_m = apply_tonal_elevation(bar_m, config.tonal_elevation, bar_bg);
707
708 let content_alpha = animate_f32("sbwc_a", if expanded { 1.0 } else { 0.0 }, th.motion.color);
710
711 let bar = Box(bar_m).child(
712 Row(Modifier::new()
713 .fill_max_size()
714 .align_items(AlignItems::CENTER))
715 .child((
716 leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
717 Box(Modifier::new().width(8.0).fill_max_height()),
718 input_field,
719 trailing_icon.unwrap_or(Box(Modifier::new())),
720 )),
721 );
722
723 let show_content = expanded || content_alpha > 0.01;
724 if show_content || expanded {
725 Column(modifier).child((
726 bar,
727 Box(Modifier::new()
728 .width(width)
729 .max_height(SearchBarDefaults::DOCKED_HEIGHT)
730 .alpha(content_alpha)
731 .background(config.colors.container_color)
732 .clip_rounded(th.shapes.extra_small))
733 .child(content),
734 ))
735 } else {
736 bar
737 }
738}
739
740pub fn DockedSearchBar(
745 input_field: View,
746 expanded: bool,
747 on_expanded_change: Option<Rc<dyn Fn(bool)>>,
748 modifier: Modifier,
749 leading_icon: Option<View>,
750 config: SearchBarConfig,
751 content: View,
752) -> View {
753 let th = theme();
754 let active = expanded;
755 let colors = config.colors;
756
757 let content_target = if expanded {
758 get_window_container_height() * 2.0 / 3.0
759 } else {
760 0.0
761 };
762 let content_height = animate_f32("docked_sh", content_target, theme().motion.expand);
763 let content_alpha = animate_f32(
764 "docked_sa",
765 if expanded { 1.0 } else { 0.0 },
766 theme().motion.color,
767 );
768 let bar_bg = if active {
769 colors.active_container_color
770 } else {
771 colors.container_color
772 };
773
774 let clear_btn = if active {
775 Box(Modifier::new().size(24.0, 24.0).clickable().on_click({
776 let cb = on_expanded_change.clone();
777 move || {
778 if let Some(ref cb) = cb {
779 cb(false);
780 }
781 }
782 }))
783 .child(Text("✕").size(16.0).color(colors.placeholder_color))
784 } else {
785 Box(Modifier::new())
786 };
787
788 let mut bar_m = modifier
789 .z_index(1.0)
790 .min_width(SearchBarDefaults::MIN_WIDTH)
791 .height(config.height)
792 .state_elevation(StateElevation {
793 default: if active {
794 th.elevation.level3
795 } else {
796 config.tonal_elevation
797 },
798 hovered: th.elevation.level2,
799 pressed: th.elevation.level3,
800 disabled: 0.0,
801 })
802 .shadow(config.shadow_elevation, 0.0)
803 .padding_values(config.content_padding)
804 .on_key_event({
805 let cb = on_expanded_change.clone();
806 move |ev| {
807 if ev.key == Key::Escape {
808 if let Some(ref cb) = cb {
809 cb(false);
810 }
811 true
812 } else {
813 false
814 }
815 }
816 })
817 .background(bar_bg)
818 .clip_rounded(config.shape_radius);
819
820 bar_m = apply_tonal_elevation(bar_m, config.tonal_elevation, bar_bg);
821
822 let bar = Box(bar_m).child(
823 Row(Modifier::new()
824 .fill_max_size()
825 .align_items(AlignItems::CENTER))
826 .child((
827 leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
828 Box(Modifier::new().width(12.0).fill_max_height()),
829 input_field,
830 clear_btn,
831 )),
832 );
833
834 let show_content = expanded || content_height > 1.0;
835 if show_content {
836 Column(Modifier::new().min_width(SearchBarDefaults::MIN_WIDTH)).child((
837 bar,
838 Box(Modifier::new()
839 .min_width(SearchBarDefaults::MIN_WIDTH)
840 .height(content_height)
841 .alpha(content_alpha)
842 .clip_rounded(th.shapes.small)
843 .background(colors.container_color)
844 .state_elevation(StateElevation {
845 default: th.elevation.level3,
846 hovered: th.elevation.level3,
847 pressed: th.elevation.level3,
848 disabled: 0.0,
849 }))
850 .child(
851 Column(Modifier::new().min_width(SearchBarDefaults::MIN_WIDTH)).child((
852 Box(Modifier::new()
853 .min_width(SearchBarDefaults::MIN_WIDTH)
854 .height(1.0)
855 .background(colors.divider_color)),
856 content,
857 )),
858 ),
859 ))
860 } else {
861 bar
862 }
863}
864
865pub fn set_window_container_height(h: f32) {
869 repose_core::locals::set_window_container_height(h);
870}
871
872fn get_window_container_height() -> f32 {
873 repose_core::locals::get_window_container_height()
874}
875
876pub fn set_window_container_width(w: f32) {
878 repose_core::locals::set_window_container_width(w);
879}
880
881fn get_window_container_width() -> f32 {
882 repose_core::locals::get_window_container_width()
883}
884
885pub fn ExpandedFullScreenSearchBar(
889 state: Rc<SearchBarState>,
890 overlay: OverlayHandle,
891 input_field: View,
892 modifier: Modifier,
893 config: ExpandedFullScreenSearchBarConfig,
894 content: View,
895) -> View {
896 state.expands_to_full_screen.set(true);
898
899 let overlay_id = remember_with_key("efs_oid", || signal(0u64));
900 let current_content = remember_state_with_key("efs_cc", || Box(Modifier::new()));
901 *current_content.borrow_mut() = content;
902
903 let progress = state.progress();
904 let _content_alpha = state.content_progress();
905
906 let expanded = state.is_expanded();
907 let visible = expanded || progress > 0.01;
908
909 if visible {
910 if overlay_id.get() == 0 {
911 let input_fr = FocusRequester::new();
912 let builder: Rc<dyn Fn() -> View> = Rc::new({
913 let state = state.clone();
914 let modifier = modifier.clone();
915 let input_field = input_field.clone();
916 let current_content = current_content.clone();
917 let config = config.clone();
918 let input_fr = input_fr.clone();
919 move || {
920 let progress = state.progress();
921 let content_alpha = state.content_progress();
922 let alpha = progress.clamp(0.0, 1.0);
923 let c_alpha = content_alpha.clamp(0.0, 1.0);
924 let th = theme();
925 let content = current_content.borrow().clone();
926
927 let inp = Box(Modifier::new().focus_requester(input_fr.clone()))
929 .child(input_field.clone());
930 input_fr.request_focus();
931
932 let header = Box(modifier
933 .clone()
934 .fill_max_width()
935 .height(SearchBarDefaults::HEIGHT)
936 .padding_values(PaddingValues {
937 left: 16.0,
938 right: 16.0,
939 top: 0.0,
940 bottom: 0.0,
941 })
942 .background(config.colors.container_color)
943 .alpha(alpha))
944 .child(inp);
945
946 let body = Box(Modifier::new()
947 .fill_max_width()
948 .flex_grow(1.0)
949 .alpha(c_alpha)
950 .background(th.surface))
951 .child(content);
952
953 let insets = config.window_insets;
954 let full = Column(Modifier::new().fill_max_size().padding_values(
955 PaddingValues {
956 left: insets.left,
957 right: insets.right,
958 top: insets.top,
959 bottom: insets.bottom,
960 },
961 ))
962 .child((header, body));
963
964 let scrim = Box(Modifier::new()
965 .fill_max_size()
966 .background(config.scrim_color.with_alpha((85.0 * alpha) as u8))
967 .on_click({
968 let s = state.clone();
969 move || s.collapse()
970 }));
971
972 ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, full))
973 }
974 });
975
976 let id = overlay.show_entry(builder, 900.0, false);
977 overlay_id.set(id);
978 }
979 } else {
980 let prev = overlay_id.get();
981 if prev != 0 {
982 let _ = overlay.dismiss(prev);
983 overlay_id.set(0);
984 }
985 }
986
987 Box(Modifier::new())
988}
989
990pub fn ExpandedDockedSearchBar(
994 state: Rc<SearchBarState>,
995 overlay: OverlayHandle,
996 input_field: View,
997 modifier: Modifier,
998 config: ExpandedDockedSearchBarConfig,
999 content: View,
1000) -> View {
1001 state.expands_to_full_screen.set(false);
1003
1004 let overlay_id = remember_with_key("eds_oid", || signal(0u64));
1005 let current_content = remember_state_with_key("eds_cc", || Box(Modifier::new()));
1006 *current_content.borrow_mut() = content;
1007
1008 let progress = state.progress();
1009 let _content_alpha = state.content_progress();
1010 let expanded = state.is_expanded();
1011 let visible = expanded || progress > 0.01;
1012
1013 if visible {
1014 if overlay_id.get() == 0 {
1015 let input_fr = FocusRequester::new();
1016 let builder: Rc<dyn Fn() -> View> = Rc::new({
1017 let state = state.clone();
1018 let modifier = modifier.clone();
1019 let input_field = input_field.clone();
1020 let current_content = current_content.clone();
1021 let config = config.clone();
1022 let input_fr = input_fr.clone();
1023 move || {
1024 let progress = state.progress();
1025 let content_alpha = state.content_progress();
1026 let alpha = progress.clamp(0.0, 1.0);
1027 let c_alpha = content_alpha.clamp(0.0, 1.0);
1028 let th = theme();
1029 let content = current_content.borrow().clone();
1030 let (_cx, _cy, _cw, _ch) = state.collapsed_layout_rect.get();
1031
1032 let inp = Box(Modifier::new().focus_requester(input_fr.clone()))
1033 .child(input_field.clone());
1034 input_fr.request_focus();
1035
1036 let header = Box(modifier
1037 .clone()
1038 .fill_max_width()
1039 .height(SearchBarDefaults::HEIGHT)
1040 .alpha(alpha)
1041 .background(config.colors.container_color)
1042 .clip_rounded(config.shape_radius)
1043 .state_elevation(StateElevation {
1044 default: th.elevation.level3,
1045 hovered: th.elevation.level2,
1046 pressed: th.elevation.level3,
1047 disabled: 0.0,
1048 }))
1049 .child(inp);
1050
1051 let dropdown = Box(Modifier::new()
1052 .fill_max_width()
1053 .max_height(get_window_container_height() * 2.0 / 3.0)
1054 .alpha(c_alpha)
1055 .clip_rounded(config.dropdown_shape_radius)
1056 .background(config.colors.container_color)
1057 .state_elevation(StateElevation {
1058 default: th.elevation.level3,
1059 hovered: th.elevation.level3,
1060 pressed: th.elevation.level3,
1061 disabled: 0.0,
1062 }))
1063 .child(
1064 Column(Modifier::new().fill_max_width()).child((
1065 Box(Modifier::new()
1066 .fill_max_width()
1067 .height(1.0)
1068 .background(config.colors.divider_color)),
1069 content,
1070 )),
1071 );
1072
1073 let col = Column(Modifier::new().fill_max_width().padding_values(
1074 PaddingValues {
1075 left: _cx.max(16.0),
1076 right: 16.0,
1077 top: _cy + _ch + config.dropdown_gap_size,
1078 bottom: 0.0,
1079 },
1080 ))
1081 .child((header, dropdown));
1082
1083 let scrim = Box(Modifier::new()
1084 .fill_max_size()
1085 .background(config.dropdown_scrim_color)
1086 .on_click({
1087 let s = state.clone();
1088 move || s.collapse()
1089 }));
1090
1091 ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, col))
1092 }
1093 });
1094
1095 let id = overlay.show_entry(builder, 900.0, false);
1096 overlay_id.set(id);
1097 }
1098 } else {
1099 let prev = overlay_id.get();
1100 if prev != 0 {
1101 let _ = overlay.dismiss(prev);
1102 overlay_id.set(0);
1103 }
1104 }
1105
1106 Box(Modifier::new())
1107}
1108
1109pub fn AppBarWithSearch(
1113 state: Rc<SearchBarState>,
1114 input_field: View,
1115 navigation_icon: Option<View>,
1116 actions: Option<Vec<View>>,
1117 config: AppBarWithSearchConfig,
1118) -> View {
1119 let bg = config.colors.search_bar_container(config.scroll_fraction);
1120 let app_bar_bg = config.colors.app_bar_container(config.scroll_fraction);
1121
1122 let insets = config.window_insets;
1123
1124 let is_container_transparent = app_bar_bg.3 == 0;
1126 let tonal_elevation = if is_container_transparent {
1127 0.0
1128 } else {
1129 config.tonal_elevation
1130 };
1131 let shadow_elevation = if is_container_transparent {
1132 0.0
1133 } else {
1134 config.shadow_elevation
1135 };
1136
1137 let hide_collapsed = state.expands_to_full_screen.get() && state.is_expanded();
1139 let collapsed_alpha = if hide_collapsed { 0.0 } else { 1.0 };
1140
1141 let bar_m = Modifier::new()
1142 .fill_max_width()
1143 .height(config.height + insets.top)
1144 .translate(0.0, config.scroll_offset)
1145 .background(app_bar_bg)
1146 .semantics(Semantics::new(Role::Container).with_selectable_group());
1147
1148 let row = Row(Modifier::new()
1149 .fill_max_size()
1150 .align_items(AlignItems::CENTER)
1151 .padding_values(PaddingValues {
1152 left: config.content_padding.left + insets.left,
1153 right: config.content_padding.right + insets.right,
1154 top: insets.top,
1155 bottom: 0.0,
1156 }))
1157 .child({
1158 let mut children: Vec<View> = Vec::new();
1159 if let Some(nav) = navigation_icon {
1160 children.push(nav);
1161 children.push(Box(Modifier::new().width(4.0)));
1162 }
1163 let sb_colors = &config.colors.search_bar_colors;
1165 let collapsed_bar = SearchBar(
1166 state.clone(),
1167 input_field,
1168 Modifier::new().flex_grow(1.0).alpha(collapsed_alpha),
1169 None,
1170 None,
1171 SearchBarConfig {
1172 height: config.height - 8.0,
1173 shape_radius: config.shape_radius,
1174 colors: SearchBarColors {
1175 container_color: bg,
1176 active_container_color: bg,
1177 divider_color: sb_colors.divider_color,
1178 content_color: sb_colors.content_color,
1179 placeholder_color: sb_colors.placeholder_color,
1180 scrim_color: sb_colors.scrim_color,
1181 },
1182 tonal_elevation,
1183 shadow_elevation,
1184 ..Default::default()
1185 },
1186 );
1187 children.push(Box(Modifier::new().flex_grow(1.0)).child(collapsed_bar));
1188 if let Some(acts) = actions {
1189 children.push(Spacer());
1190 for a in acts {
1191 children.push(a);
1192 }
1193 }
1194 children
1195 });
1196
1197 Box(bar_m.shadow(shadow_elevation, 0.0)).child(row)
1198}