cranpose_liquid/widgets/
search_field.rs1use cranpose_foundation::text::TextFieldState;
2use cranpose_macros::composable;
3use cranpose_ui::{
4 Modifier,
5 text::{SpanStyle, TextStyle},
6 widgets::{BasicTextFieldDecorated, BasicTextFieldOptions, Box, BoxSpec, Row, RowSpec, Text},
7};
8use cranpose_ui_graphics::Color;
9use cranpose_ui_layout::{Alignment, VerticalAlignment};
10
11use crate::{
12 material::{Glass, LiquidModifierExt},
13 theme::{liquid_colors, liquid_typography},
14};
15
16#[derive(Clone, Debug, PartialEq)]
18pub struct LiquidSearchFieldSpec {
19 pub placeholder: String,
20 pub on_glass: bool,
22 pub glass: Glass,
24 pub foreground: Option<Color>,
27}
28
29impl Default for LiquidSearchFieldSpec {
30 fn default() -> Self {
31 Self {
32 placeholder: "Search".to_string(),
33 on_glass: true,
34 glass: Glass::regular(),
35 foreground: None,
36 }
37 }
38}
39
40#[composable]
42pub fn LiquidSearchField(modifier: Modifier, state: TextFieldState, spec: LiquidSearchFieldSpec) {
43 let colors = liquid_colors();
44 let typography = liquid_typography();
45 let foreground = spec.foreground.unwrap_or(colors.label);
46 let secondary_foreground = spec.foreground.unwrap_or(colors.secondary_label);
47
48 let base = if spec.on_glass {
49 let glass = spec
50 .foreground
51 .map(|color| {
52 spec.glass
53 .clone()
54 .adaptive_frost(color, spec.glass.adaptive_frost)
55 })
56 .unwrap_or_else(|| spec.glass.clone());
57 Modifier::empty().glass_effect(glass)
58 } else {
59 let fill = colors.fill;
60 Modifier::empty()
61 .rounded_corners(999.0)
62 .draw_behind(move |scope| {
63 scope.draw_round_rect(
64 cranpose_ui_graphics::Brush::solid(fill),
65 cranpose_ui_graphics::CornerRadii::uniform(999.0),
66 );
67 })
68 };
69
70 let placeholder = spec.placeholder;
71 Box(
72 base.then(modifier).padding_symmetric(14.0, 0.0),
73 BoxSpec::default().content_alignment(Alignment::new(
74 cranpose_ui_layout::HorizontalAlignment::Start,
75 cranpose_ui_layout::VerticalAlignment::CenterVertically,
76 )),
77 move || {
78 let placeholder = placeholder.clone();
79 let body = typography.body.clone();
80 Row(
81 Modifier::empty().fill_max_width(),
82 RowSpec::default().vertical_alignment(VerticalAlignment::CenterVertically),
83 move || {
84 crate::icons::Icon(crate::icons::SEARCH, None, 18.0, secondary_foreground);
85 Box(Modifier::empty().width(8.0), BoxSpec::default(), || {});
86
87 let field_style = TextStyle {
88 span_style: SpanStyle {
89 color: Some(foreground),
90 ..body.span_style.clone()
91 },
92 ..body.clone()
93 };
94 let placeholder_style = TextStyle {
95 span_style: SpanStyle {
96 color: Some(secondary_foreground),
97 ..body.span_style.clone()
98 },
99 ..body.clone()
100 };
101 let placeholder = placeholder.clone();
102 Box(
103 Modifier::empty().weight(1.0),
104 BoxSpec::default(),
105 move || {
106 let placeholder = placeholder.clone();
107 let placeholder_style = placeholder_style.clone();
108 let field_name = placeholder.clone();
109 BasicTextFieldDecorated(
110 state,
111 Modifier::empty()
112 .fill_max_width()
113 .padding_symmetric(0.0, 9.0)
114 .role(cranpose_ui::SemanticsWidgetRole::SearchField)
115 .content_description(field_name),
116 BasicTextFieldOptions {
117 text_style: field_style.clone(),
118 ..BasicTextFieldOptions::default()
119 },
120 move |inner| {
121 let empty = state.text().is_empty();
122 let placeholder = placeholder.clone();
123 let placeholder_style = placeholder_style.clone();
124 Box(
125 Modifier::empty().fill_max_width(),
126 BoxSpec::default().content_alignment(Alignment::new(
127 cranpose_ui_layout::HorizontalAlignment::Start,
128 cranpose_ui_layout::VerticalAlignment::CenterVertically,
129 )),
130 move || {
131 if empty {
132 Text(
133 placeholder.clone(),
134 Modifier::empty().hide_from_accessibility(),
135 placeholder_style.clone(),
136 );
137 }
138 inner.inner_text_field();
139 },
140 )
141 },
142 );
143 },
144 );
145 },
146 );
147 },
148 );
149}
150
151#[cfg(test)]
152#[path = "tests/search_field_tests.rs"]
153mod tests;
154
155#[composable]
157pub fn SearchBar(modifier: Modifier, state: TextFieldState, placeholder: impl Into<String>) {
158 LiquidSearchField(
159 modifier,
160 state,
161 LiquidSearchFieldSpec {
162 placeholder: placeholder.into(),
163 ..Default::default()
164 },
165 );
166}
167
168#[composable]
170pub fn SearchField(modifier: Modifier, state: TextFieldState, placeholder: impl Into<String>) {
171 SearchBar(modifier, state, placeholder);
172}