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