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