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