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, 9.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, 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 BasicTextFieldDecorated(
110 state,
111 Modifier::empty().fill_max_width(),
112 BasicTextFieldOptions {
113 text_style: field_style.clone(),
114 ..BasicTextFieldOptions::default()
115 },
116 move |inner| {
117 let empty = state.text().is_empty();
118 let placeholder = placeholder.clone();
119 let placeholder_style = placeholder_style.clone();
120 Box(
121 Modifier::empty().fill_max_width(),
122 BoxSpec::default().content_alignment(Alignment::new(
123 cranpose_ui_layout::HorizontalAlignment::Start,
124 cranpose_ui_layout::VerticalAlignment::CenterVertically,
125 )),
126 move || {
127 if empty {
128 Text(
129 placeholder.clone(),
130 Modifier::empty(),
131 placeholder_style.clone(),
132 );
133 }
134 inner.inner_text_field();
135 },
136 )
137 },
138 );
139 },
140 );
141 },
142 );
143 },
144 );
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn search_spec_defaults_to_a_search_label_and_glass() {
153 let spec = LiquidSearchFieldSpec::default();
154 assert_eq!(spec.placeholder, "Search");
155 assert!(spec.on_glass);
156 assert_eq!(spec.glass, Glass::regular());
157 assert_eq!(spec.foreground, None);
158 }
159}
160
161#[composable]
163#[allow(non_snake_case)]
164pub fn SearchBar(modifier: Modifier, state: TextFieldState, placeholder: impl Into<String>) {
165 LiquidSearchField(
166 modifier,
167 state,
168 LiquidSearchFieldSpec {
169 placeholder: placeholder.into(),
170 ..Default::default()
171 },
172 );
173}
174
175#[composable]
177#[allow(non_snake_case)]
178pub fn SearchField(modifier: Modifier, state: TextFieldState, placeholder: impl Into<String>) {
179 SearchBar(modifier, state, placeholder);
180}