Skip to main content

cranpose_liquid/widgets/
search_field.rs

1//! A pill search field on glass.
2
3use crate::material::Glass;
4use crate::material::LiquidModifierExt;
5use crate::theme::{liquid_colors, liquid_typography};
6use cranpose_foundation::text::TextFieldState;
7use cranpose_macros::composable;
8use cranpose_ui::text::{SpanStyle, TextStyle};
9use cranpose_ui::widgets::{BasicTextField, Box, BoxSpec, Row, RowSpec, Text};
10use cranpose_ui::Modifier;
11use cranpose_ui_layout::{Alignment, VerticalAlignment};
12
13/// Configuration for [`LiquidSearchField`].
14#[derive(Clone, Debug, PartialEq)]
15pub struct LiquidSearchFieldSpec {
16    pub placeholder: String,
17    /// Render on glass (floating) instead of the flat fill (inline in lists).
18    pub on_glass: bool,
19}
20
21impl Default for LiquidSearchFieldSpec {
22    fn default() -> Self {
23        Self {
24            placeholder: "Search".to_string(),
25            on_glass: true,
26        }
27    }
28}
29
30/// A capsule search field: magnifier icon and editable text.
31#[composable]
32#[allow(non_snake_case)]
33pub fn LiquidSearchField(modifier: Modifier, state: TextFieldState, spec: LiquidSearchFieldSpec) {
34    let colors = liquid_colors();
35    let typography = liquid_typography();
36
37    let base = if spec.on_glass {
38        Modifier::empty().glass_effect(Glass::regular())
39    } else {
40        let fill = colors.fill;
41        Modifier::empty()
42            .rounded_corners(999.0)
43            .draw_behind(move |scope| {
44                scope.draw_round_rect(
45                    cranpose_ui_graphics::Brush::solid(fill),
46                    cranpose_ui_graphics::CornerRadii::uniform(999.0),
47                );
48            })
49    };
50
51    let placeholder = spec.placeholder.clone();
52    Box(
53        base.then(modifier).padding_symmetric(14.0, 9.0),
54        BoxSpec::default().content_alignment(Alignment::new(
55            cranpose_ui_layout::HorizontalAlignment::Start,
56            cranpose_ui_layout::VerticalAlignment::CenterVertically,
57        )),
58        move || {
59            let placeholder = placeholder.clone();
60            let typography = typography.clone();
61            Row(
62                Modifier::empty().fill_max_width(),
63                RowSpec::default().vertical_alignment(VerticalAlignment::CenterVertically),
64                move || {
65                    crate::icons::Icon(crate::icons::SEARCH, 18.0, colors.secondary_label);
66                    Box(Modifier::empty().width(8.0), BoxSpec::default(), || {});
67
68                    let is_empty = state.text().is_empty();
69                    let field_style = TextStyle {
70                        span_style: SpanStyle {
71                            color: Some(colors.label),
72                            ..typography.body.span_style.clone()
73                        },
74                        ..typography.body.clone()
75                    };
76                    let placeholder = placeholder.clone();
77                    let placeholder_typography = typography.clone();
78                    // The field must fill the pill so it has a non-zero width
79                    // (and stays hit-testable) even when empty — otherwise an
80                    // empty search field measures to 0px and rejects every tap,
81                    // so focus + the soft keyboard never fire.
82                    Box(
83                        Modifier::empty().weight(1.0),
84                        BoxSpec::default(),
85                        move || {
86                            if is_empty {
87                                let placeholder_style = TextStyle {
88                                    span_style: SpanStyle {
89                                        color: Some(colors.tertiary_label),
90                                        ..placeholder_typography.body.span_style.clone()
91                                    },
92                                    ..placeholder_typography.body.clone()
93                                };
94                                Text(placeholder.clone(), Modifier::empty(), placeholder_style);
95                            }
96                            BasicTextField(
97                                state,
98                                Modifier::empty().fill_max_width(),
99                                field_style.clone(),
100                            );
101                        },
102                    );
103                },
104            );
105        },
106    );
107}