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().size(24.0, 24.0).clickable().on_click({
773 let cb = on_expanded_change.clone();
774 move || {
775 if let Some(ref cb) = cb {
776 cb(false);
777 }
778 }
779 }))
780 .child(Text("✕").size(16.0).color(colors.placeholder_color))
781 } else {
782 Box(Modifier::new())
783 };
784
785 let mut bar_m = modifier
786 .z_index(1.0)
787 .min_width(SearchBarDefaults::MIN_WIDTH)
788 .height(config.height)
789 .state_elevation(StateElevation {
790 default: if active {
791 th.elevation.level3
792 } else {
793 config.tonal_elevation
794 },
795 hovered: th.elevation.level2,
796 focused: th.elevation.level2,
797 pressed: th.elevation.level3,
798 dragged: th.elevation.level3,
799 disabled: 0.0,
800 })
801 .shadow(config.shadow_elevation, 0.0)
802 .padding_values(config.content_padding)
803 .on_key_event({
804 let cb = on_expanded_change.clone();
805 move |ev| {
806 if ev.key == Key::Escape {
807 if let Some(ref cb) = cb {
808 cb(false);
809 }
810 true
811 } else {
812 false
813 }
814 }
815 })
816 .background(bar_bg)
817 .clip_rounded(config.shape_radius);
818
819 bar_m = apply_tonal_elevation(bar_m, config.tonal_elevation, bar_bg);
820
821 let bar = Box(bar_m).child(
822 Row(Modifier::new()
823 .fill_max_size()
824 .align_items(AlignItems::CENTER))
825 .child((
826 leading_icon.unwrap_or(Box(Modifier::new().size(24.0, 24.0))),
827 Box(Modifier::new().width(12.0).fill_max_height()),
828 input_field,
829 clear_btn,
830 )),
831 );
832
833 let show_content = expanded || content_height > 1.0;
834 if show_content {
835 Column(Modifier::new().min_width(SearchBarDefaults::MIN_WIDTH)).child((
836 bar,
837 Box(Modifier::new()
838 .min_width(SearchBarDefaults::MIN_WIDTH)
839 .height(content_height)
840 .alpha(content_alpha)
841 .clip_rounded(th.shapes.small)
842 .background(colors.container_color)
843 .state_elevation(StateElevation {
844 default: th.elevation.level3,
845 hovered: th.elevation.level3,
846 focused: th.elevation.level3,
847 pressed: th.elevation.level3,
848 dragged: th.elevation.level3,
849 disabled: 0.0,
850 }))
851 .child(
852 Column(Modifier::new().min_width(SearchBarDefaults::MIN_WIDTH)).child((
853 Box(Modifier::new()
854 .min_width(SearchBarDefaults::MIN_WIDTH)
855 .height(1.0)
856 .background(colors.divider_color)),
857 content,
858 )),
859 ),
860 ))
861 } else {
862 bar
863 }
864}
865
866pub fn set_window_container_height(h: f32) {
870 repose_core::locals::set_window_container_height(h);
871}
872
873fn get_window_container_height() -> f32 {
874 repose_core::locals::get_window_container_height()
875}
876
877pub fn set_window_container_width(w: f32) {
879 repose_core::locals::set_window_container_width(w);
880}
881
882fn get_window_container_width() -> f32 {
883 repose_core::locals::get_window_container_width()
884}
885
886pub fn ExpandedFullScreenSearchBar(
890 state: Rc<SearchBarState>,
891 overlay: OverlayHandle,
892 input_field: View,
893 modifier: Modifier,
894 config: ExpandedFullScreenSearchBarConfig,
895 content: View,
896) -> View {
897 state.expands_to_full_screen.set(true);
899
900 let overlay_id = remember_with_key("efs_oid", || signal(0u64));
901 let current_content = remember_state_with_key("efs_cc", || Box(Modifier::new()));
902 *current_content.borrow_mut() = content;
903
904 let progress = state.progress();
905 let _content_alpha = state.content_progress();
906
907 let expanded = state.is_expanded();
908 let visible = expanded || progress > 0.01;
909
910 if visible {
911 if overlay_id.get() == 0 {
912 let input_fr = FocusRequester::new();
913 let focus_requested = Rc::new(Cell::new(false));
914 let builder: Rc<dyn Fn() -> View> = Rc::new({
915 let state = state.clone();
916 let modifier = modifier.clone();
917 let input_field = input_field.clone();
918 let current_content = current_content.clone();
919 let config = config.clone();
920 let input_fr = input_fr.clone();
921 let focus_requested = focus_requested.clone();
922 move || {
923 let progress = state.progress();
924 let content_alpha = state.content_progress();
925 let alpha = progress.clamp(0.0, 1.0);
926 let c_alpha = content_alpha.clamp(0.0, 1.0);
927 let th = theme();
928 let content = current_content.borrow().clone();
929
930 let inp = Box(Modifier::new().focus_requester(input_fr.clone()))
932 .child(input_field.clone());
933 if !focus_requested.get() {
934 focus_requested.set(true);
935 input_fr.request_focus();
936 }
937
938 let header = Box(modifier
939 .clone()
940 .fill_max_width()
941 .height(SearchBarDefaults::HEIGHT)
942 .padding_values(PaddingValues {
943 left: 16.0,
944 right: 16.0,
945 top: 0.0,
946 bottom: 0.0,
947 })
948 .background(config.colors.container_color)
949 .alpha(alpha))
950 .child(inp);
951
952 let body = Box(Modifier::new()
953 .fill_max_width()
954 .flex_grow(1.0)
955 .alpha(c_alpha)
956 .background(th.surface))
957 .child(content);
958
959 let insets = config.window_insets;
960 let full = Column(Modifier::new().fill_max_size().padding_values(
961 PaddingValues {
962 left: insets.left,
963 right: insets.right,
964 top: insets.top,
965 bottom: insets.bottom,
966 },
967 ))
968 .child((header, body));
969
970 let scrim = Box(Modifier::new()
971 .fill_max_size()
972 .background(config.scrim_color.with_alpha((85.0 * alpha) as u8))
973 .on_click({
974 let s = state.clone();
975 move || s.collapse()
976 }));
977
978 ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, full))
979 }
980 });
981
982 let id = overlay.show_entry(builder, 900.0, false);
983 overlay_id.set(id);
984 }
985 } else {
986 let prev = overlay_id.get();
987 if prev != 0 {
988 let _ = overlay.dismiss(prev);
989 overlay_id.set(0);
990 }
991 }
992
993 Box(Modifier::new())
994}
995
996pub fn ExpandedDockedSearchBar(
1000 state: Rc<SearchBarState>,
1001 overlay: OverlayHandle,
1002 input_field: View,
1003 modifier: Modifier,
1004 config: ExpandedDockedSearchBarConfig,
1005 content: View,
1006) -> View {
1007 state.expands_to_full_screen.set(false);
1009
1010 let overlay_id = remember_with_key("eds_oid", || signal(0u64));
1011 let current_content = remember_state_with_key("eds_cc", || Box(Modifier::new()));
1012 *current_content.borrow_mut() = content;
1013
1014 let progress = state.progress();
1015 let _content_alpha = state.content_progress();
1016 let expanded = state.is_expanded();
1017 let visible = expanded || progress > 0.01;
1018
1019 if visible {
1020 if overlay_id.get() == 0 {
1021 let input_fr = FocusRequester::new();
1022 let focus_requested = Rc::new(Cell::new(false));
1023 let builder: Rc<dyn Fn() -> View> = Rc::new({
1024 let state = state.clone();
1025 let modifier = modifier.clone();
1026 let input_field = input_field.clone();
1027 let current_content = current_content.clone();
1028 let config = config.clone();
1029 let input_fr = input_fr.clone();
1030 let focus_requested = focus_requested.clone();
1031 move || {
1032 let progress = state.progress();
1033 let content_alpha = state.content_progress();
1034 let alpha = progress.clamp(0.0, 1.0);
1035 let c_alpha = content_alpha.clamp(0.0, 1.0);
1036 let th = theme();
1037 let content = current_content.borrow().clone();
1038 let (_cx, _cy, _cw, _ch) = state.collapsed_layout_rect.get();
1039
1040 let inp = Box(Modifier::new().focus_requester(input_fr.clone()))
1041 .child(input_field.clone());
1042 if !focus_requested.get() {
1043 focus_requested.set(true);
1044 input_fr.request_focus();
1045 }
1046
1047 let header = Box(modifier
1048 .clone()
1049 .fill_max_width()
1050 .height(SearchBarDefaults::HEIGHT)
1051 .alpha(alpha)
1052 .background(config.colors.container_color)
1053 .clip_rounded(config.shape_radius)
1054 .state_elevation(StateElevation {
1055 default: th.elevation.level3,
1056 hovered: th.elevation.level2,
1057 focused: th.elevation.level2,
1058 pressed: th.elevation.level3,
1059 dragged: th.elevation.level3,
1060 disabled: 0.0,
1061 }))
1062 .child(inp);
1063
1064 let dropdown = Box(Modifier::new()
1065 .fill_max_width()
1066 .max_height(get_window_container_height() * 2.0 / 3.0)
1067 .alpha(c_alpha)
1068 .clip_rounded(config.dropdown_shape_radius)
1069 .background(config.colors.container_color)
1070 .state_elevation(StateElevation {
1071 default: th.elevation.level3,
1072 hovered: th.elevation.level3,
1073 focused: th.elevation.level3,
1074 pressed: th.elevation.level3,
1075 dragged: th.elevation.level3,
1076 disabled: 0.0,
1077 }))
1078 .child(
1079 Column(Modifier::new().fill_max_width()).child((
1080 Box(Modifier::new()
1081 .fill_max_width()
1082 .height(1.0)
1083 .background(config.colors.divider_color)),
1084 content,
1085 )),
1086 );
1087
1088 let docked_width = _cw.max(SearchBarDefaults::MIN_WIDTH);
1089 let popup_left = _cx;
1090 let popup_top = _cy + _ch + config.dropdown_gap_size;
1091
1092 let col = Column(Modifier::new().fill_max_width()).child((header, dropdown));
1093
1094 let positioned = Box(Modifier::new()
1095 .absolute()
1096 .offset(Some(popup_left), Some(popup_top), None, None)
1097 .width(docked_width))
1098 .child(col);
1099
1100 let scrim = Box(Modifier::new()
1101 .fill_max_size()
1102 .background(config.dropdown_scrim_color)
1103 .on_click({
1104 let s = state.clone();
1105 move || s.collapse()
1106 }));
1107
1108 ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, positioned))
1109 }
1110 });
1111
1112 let id = overlay.show_entry(builder, 900.0, false);
1113 overlay_id.set(id);
1114 }
1115 } else {
1116 let prev = overlay_id.get();
1117 if prev != 0 {
1118 let _ = overlay.dismiss(prev);
1119 overlay_id.set(0);
1120 }
1121 }
1122
1123 Box(Modifier::new())
1124}
1125
1126pub fn AppBarWithSearch(
1130 state: Rc<SearchBarState>,
1131 input_field: View,
1132 navigation_icon: Option<View>,
1133 actions: Option<Vec<View>>,
1134 config: AppBarWithSearchConfig,
1135) -> View {
1136 let bg = config.colors.search_bar_container(config.scroll_fraction);
1137 let app_bar_bg = config.colors.app_bar_container(config.scroll_fraction);
1138
1139 let insets = config.window_insets;
1140
1141 let is_container_transparent = app_bar_bg.3 == 0;
1143 let tonal_elevation = if is_container_transparent {
1144 0.0
1145 } else {
1146 config.tonal_elevation
1147 };
1148 let shadow_elevation = if is_container_transparent {
1149 0.0
1150 } else {
1151 config.shadow_elevation
1152 };
1153
1154 let hide_collapsed = state.expands_to_full_screen.get() && state.is_expanded();
1156 let collapsed_alpha = if hide_collapsed { 0.0 } else { 1.0 };
1157
1158 let bar_m = Modifier::new()
1159 .fill_max_width()
1160 .height(config.height + insets.top)
1161 .translate(0.0, config.scroll_offset)
1162 .background(app_bar_bg)
1163 .semantics(Semantics::new(Role::Container).with_selectable_group());
1164
1165 let row = Row(Modifier::new()
1166 .fill_max_size()
1167 .align_items(AlignItems::CENTER)
1168 .padding_values(PaddingValues {
1169 left: config.content_padding.left + insets.left,
1170 right: config.content_padding.right + insets.right,
1171 top: insets.top,
1172 bottom: 0.0,
1173 }))
1174 .child({
1175 let mut children: Vec<View> = Vec::new();
1176 if let Some(nav) = navigation_icon {
1177 children.push(nav);
1178 children.push(Box(Modifier::new().width(4.0)));
1179 }
1180 let sb_colors = &config.colors.search_bar_colors;
1182 let collapsed_bar = SearchBar(
1183 state.clone(),
1184 input_field,
1185 Modifier::new().flex_grow(1.0).alpha(collapsed_alpha),
1186 None,
1187 None,
1188 SearchBarConfig {
1189 height: config.height - 8.0,
1190 shape_radius: config.shape_radius,
1191 colors: SearchBarColors {
1192 container_color: bg,
1193 active_container_color: bg,
1194 divider_color: sb_colors.divider_color,
1195 content_color: sb_colors.content_color,
1196 placeholder_color: sb_colors.placeholder_color,
1197 scrim_color: sb_colors.scrim_color,
1198 },
1199 tonal_elevation,
1200 shadow_elevation,
1201 ..Default::default()
1202 },
1203 );
1204 children.push(Box(Modifier::new().flex_grow(1.0)).child(collapsed_bar));
1205 if let Some(acts) = actions {
1206 children.push(Spacer());
1207 for a in acts {
1208 children.push(a);
1209 }
1210 }
1211 children
1212 });
1213
1214 Box(bar_m.shadow(shadow_elevation, 0.0)).child(row)
1215}