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