Skip to main content

rosace_widgets/tree/
search_bar.rs

1//! `SearchBar` — there is no separate "search bar" widget. A search field is
2//! just a [`super::TextInput`] with a **leading** search icon (and an optional
3//! **trailing** clear ×), using `TextInput`'s adornment API. This type is a
4//! thin, convenient preset over exactly that — the icon lives *inside* the
5//! field (one pill), not beside it. The same adornments give you password
6//! fields (`.trailing(eye).on_trailing(toggle)`), prefixes (`$`), etc.
7
8use std::sync::{Arc, OnceLock};
9
10use rosace_core::types::Size;
11
12use super::{BoxedWidget, Children, LayoutCtx, PaintCtx, Widget};
13
14pub struct SearchBar {
15    value: String,
16    placeholder: String,
17    width: Option<f32>,
18    height: f32,
19    on_change: Option<Arc<dyn Fn(String) + Send + Sync>>,
20    on_clear: Option<Arc<dyn Fn() + Send + Sync>>,
21    inner: OnceLock<BoxedWidget>,
22}
23
24impl SearchBar {
25    pub fn new() -> Self {
26        Self {
27            value: String::new(),
28            placeholder: "Search\u{2026}".to_string(),
29            width: None,
30            height: 36.0,
31            on_change: None,
32            on_clear: None,
33            inner: OnceLock::new(),
34        }
35    }
36    pub fn value(mut self, v: impl Into<String>) -> Self { self.value = v.into(); self }
37    pub fn placeholder(mut self, p: impl Into<String>) -> Self { self.placeholder = p.into(); self }
38    pub fn width(mut self, w: f32) -> Self { self.width = Some(w); self }
39    pub fn height(mut self, h: f32) -> Self { self.height = h; self }
40    pub fn on_change(mut self, f: impl Fn(String) + Send + Sync + 'static) -> Self {
41        self.on_change = Some(Arc::new(f)); self
42    }
43    /// Shows a trailing clear (×) whenever the value is non-empty.
44    pub fn on_clear(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
45        self.on_clear = Some(Arc::new(f)); self
46    }
47
48    fn inner(&self) -> &BoxedWidget {
49        self.inner.get_or_init(|| {
50            let mut input = super::TextInput::new()
51                .value(self.value.clone())
52                .placeholder(self.placeholder.clone())
53                .height(self.height)
54                .leading(super::Icon::new(super::IconKind::Search).size(18.0));
55            if let Some(w) = self.width { input = input.width(w); }
56            if let Some(f) = &self.on_change {
57                let f = Arc::clone(f);
58                input = input.on_change(move |v| f(v));
59            }
60            if let Some(clear) = &self.on_clear {
61                if !self.value.is_empty() {
62                    let clear = Arc::clone(clear);
63                    input = input
64                        .trailing(super::Text::new("\u{00d7}").size(16.0))
65                        .on_trailing(move || clear());
66                }
67            }
68            Box::new(input)
69        })
70    }
71}
72
73impl Default for SearchBar {
74    fn default() -> Self { Self::new() }
75}
76
77impl Widget for SearchBar {
78    fn children(&self) -> Children<'_> { Children::One(self.inner().as_ref()) }
79    fn layout(&self, ctx: &LayoutCtx) -> Size { self.inner().layout(ctx) }
80    fn paint(&self, ctx: &mut PaintCtx) { self.inner().paint(ctx); }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use rosace_layout::Constraints;
87
88    #[test]
89    fn search_bar_is_a_text_input_with_a_leading_icon() {
90        let font = rosace_render::FontCache::embedded();
91        let theme = rosace_theme::built_in::dark_theme();
92        let ctx = LayoutCtx::new(Constraints::loose(500.0, 60.0), &font, &theme);
93        let sb = SearchBar::new().width(200.0);
94        // Delegates to a TextInput of the requested width (adornments are
95        // inside the field, so the outer size is the field's size).
96        assert_eq!(sb.layout(&ctx).width, 200.0);
97    }
98}