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: Option<String>,
21 pub on_glass: bool,
23 pub glass: Option<Glass>,
25 pub foreground: Option<Color>,
28}
29
30impl Default for LiquidSearchFieldSpec {
31 fn default() -> Self {
32 Self {
33 placeholder: None,
34 on_glass: true,
35 glass: None,
36 foreground: None,
37 }
38 }
39}
40
41#[composable]
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 placeholder_color = spec.foreground.unwrap_or_else(|| {
48 colors
49 .label
50 .with_alpha(if colors.is_dark { 0.35 } else { 0.40 })
51 });
52
53 let base = if spec.on_glass {
54 let mut glass = spec
55 .glass
56 .unwrap_or_else(|| super::glass_surface::control_surface_material(foreground));
57 if let Some(color) = spec.foreground {
58 let frost = glass.adaptive_frost;
59 glass = glass.adaptive_frost(color, frost);
60 }
61 Modifier::empty().glass_effect(glass)
62 } else {
63 let fill = colors.fill;
64 Modifier::empty()
65 .rounded_corners(999.0)
66 .draw_behind(move |scope| {
67 scope.draw_round_rect(
68 cranpose_ui_graphics::Brush::solid(fill),
69 cranpose_ui_graphics::CornerRadii::uniform(999.0),
70 );
71 })
72 };
73
74 let body = typography.body;
75 let field_style = TextStyle {
76 span_style: SpanStyle {
77 color: Some(foreground),
78 ..body.span_style.clone()
79 },
80 ..body.clone()
81 };
82 let placeholder_style = TextStyle {
83 span_style: SpanStyle {
84 color: Some(placeholder_color),
85 ..body.span_style.clone()
86 },
87 ..body
88 };
89 let placeholder = spec
90 .placeholder
91 .unwrap_or_else(|| cranpose_ui::UiString::Search.resolve());
92 BasicTextFieldDecorated(
93 state,
94 modifier
95 .then(base)
96 .height_in(44.0, f32::INFINITY)
97 .padding_symmetric(11.0, 0.0)
98 .role(cranpose_ui::SemanticsWidgetRole::SearchField)
99 .content_description(placeholder.clone()),
100 BasicTextFieldOptions {
101 text_style: field_style,
102 ..BasicTextFieldOptions::default()
103 },
104 move |inner| {
105 let placeholder = placeholder.clone();
106 let placeholder_style = placeholder_style.clone();
107 Row(
108 Modifier::empty().fill_max_width(),
109 RowSpec::default().vertical_alignment(VerticalAlignment::CenterVertically),
110 move || {
111 crate::icons::Icon(crate::icons::SEARCH, None, 24.0, foreground);
112 Box(Modifier::empty().width(5.0), BoxSpec::default(), || {});
113 let placeholder = placeholder.clone();
114 let placeholder_style = placeholder_style.clone();
115 let inner = inner.clone();
116 Box(
117 Modifier::empty().weight(1.0).padding_symmetric(0.0, 9.0),
118 BoxSpec {
119 content_alignment: Alignment::new(
120 cranpose_ui_layout::HorizontalAlignment::Start,
121 cranpose_ui_layout::VerticalAlignment::CenterVertically,
122 ),
123 propagate_min_constraints: true,
124 },
125 move || {
126 if state.text().is_empty() {
127 Text(
128 placeholder.clone(),
129 Modifier::empty().hide_from_accessibility(),
130 placeholder_style.clone(),
131 );
132 }
133 inner.inner_text_field();
134 },
135 );
136 },
137 );
138 },
139 );
140}
141
142#[cfg(test)]
143#[path = "tests/search_field_tests.rs"]
144mod tests;
145
146#[composable]
148pub fn SearchBar(modifier: Modifier, state: TextFieldState, placeholder: impl Into<String>) {
149 LiquidSearchField(
150 modifier,
151 state,
152 LiquidSearchFieldSpec {
153 placeholder: Some(placeholder.into()),
154 ..Default::default()
155 },
156 );
157}
158
159#[composable]
161pub fn SearchField(modifier: Modifier, state: TextFieldState, placeholder: impl Into<String>) {
162 SearchBar(modifier, state, placeholder);
163}