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]
42#[allow(non_snake_case)]
43pub fn LiquidSearchField(modifier: Modifier, state: TextFieldState, spec: LiquidSearchFieldSpec) {
44 let colors = liquid_colors();
45 let typography = liquid_typography();
46 let foreground = spec.foreground.unwrap_or(colors.label);
47 let secondary_foreground = spec.foreground.unwrap_or(colors.secondary_label);
48
49 let base = if spec.on_glass {
50 let glass = spec
51 .foreground
52 .map(|color| {
53 spec.glass
54 .clone()
55 .adaptive_frost(color, spec.glass.adaptive_frost)
56 })
57 .unwrap_or_else(|| spec.glass.clone());
58 Modifier::empty().glass_effect(glass)
59 } else {
60 let fill = colors.fill;
61 Modifier::empty()
62 .rounded_corners(999.0)
63 .draw_behind(move |scope| {
64 scope.draw_round_rect(
65 cranpose_ui_graphics::Brush::solid(fill),
66 cranpose_ui_graphics::CornerRadii::uniform(999.0),
67 );
68 })
69 };
70
71 let placeholder = spec.placeholder.clone();
72 Box(
73 base.then(modifier).padding_symmetric(14.0, 0.0),
74 BoxSpec::default().content_alignment(Alignment::new(
75 cranpose_ui_layout::HorizontalAlignment::Start,
76 cranpose_ui_layout::VerticalAlignment::CenterVertically,
77 )),
78 move || {
79 let placeholder = placeholder.clone();
80 let body = typography.body.clone();
81 Row(
82 Modifier::empty().fill_max_width(),
83 RowSpec::default().vertical_alignment(VerticalAlignment::CenterVertically),
84 move || {
85 crate::icons::Icon(crate::icons::SEARCH, None, 18.0, secondary_foreground);
86 Box(Modifier::empty().width(8.0), BoxSpec::default(), || {});
87
88 let field_style = TextStyle {
89 span_style: SpanStyle {
90 color: Some(foreground),
91 ..body.span_style.clone()
92 },
93 ..body.clone()
94 };
95 let placeholder_style = TextStyle {
96 span_style: SpanStyle {
97 color: Some(secondary_foreground),
98 ..body.span_style.clone()
99 },
100 ..body.clone()
101 };
102 let placeholder = placeholder.clone();
103 Box(
104 Modifier::empty().weight(1.0),
105 BoxSpec::default(),
106 move || {
107 let placeholder = placeholder.clone();
108 let placeholder_style = placeholder_style.clone();
109 let field_name = placeholder.clone();
110 BasicTextFieldDecorated(
111 state,
112 Modifier::empty()
113 .fill_max_width()
114 .padding_symmetric(0.0, 9.0)
115 .role(cranpose_ui::SemanticsWidgetRole::SearchField)
116 .content_description(field_name),
117 BasicTextFieldOptions {
118 text_style: field_style.clone(),
119 ..BasicTextFieldOptions::default()
120 },
121 move |inner| {
122 let empty = state.text().is_empty();
123 let placeholder = placeholder.clone();
124 let placeholder_style = placeholder_style.clone();
125 Box(
126 Modifier::empty().fill_max_width(),
127 BoxSpec::default().content_alignment(Alignment::new(
128 cranpose_ui_layout::HorizontalAlignment::Start,
129 cranpose_ui_layout::VerticalAlignment::CenterVertically,
130 )),
131 move || {
132 if empty {
133 Text(
134 placeholder.clone(),
135 Modifier::empty(),
136 placeholder_style.clone(),
137 );
138 }
139 inner.inner_text_field();
140 },
141 )
142 },
143 );
144 },
145 );
146 },
147 );
148 },
149 );
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn search_spec_defaults_to_a_search_label_and_glass() {
158 let spec = LiquidSearchFieldSpec::default();
159 assert_eq!(spec.placeholder, "Search");
160 assert!(spec.on_glass);
161 assert_eq!(spec.glass, Glass::regular());
162 assert_eq!(spec.foreground, None);
163 }
164}
165
166#[composable]
168#[allow(non_snake_case)]
169pub fn SearchBar(modifier: Modifier, state: TextFieldState, placeholder: impl Into<String>) {
170 LiquidSearchField(
171 modifier,
172 state,
173 LiquidSearchFieldSpec {
174 placeholder: placeholder.into(),
175 ..Default::default()
176 },
177 );
178}
179
180#[composable]
182#[allow(non_snake_case)]
183pub fn SearchField(modifier: Modifier, state: TextFieldState, placeholder: impl Into<String>) {
184 SearchBar(modifier, state, placeholder);
185}