Skip to main content

teksilo_widgets/styles/
recipe_search_field_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Default `SearchFieldStyle` impl + design tokens.
5//!
6//! Design tokens for `SearchField` live as `pub const`s on this module.
7//! `SearchField` and `SuggestionPanel` read them directly when
8//! building the magnifier glyph, the suggestion-list row chrome, and
9//! the popup padding. The suggestion popup *surface* is routed through
10//! `PopoverStyle::Menu`; only the row chrome + panel padding remain
11//! SearchField-specific.
12
13use teksilo_core::build_context::BuildContext;
14use teksilo_core::styles::density::{dp, spacing};
15use teksilo_core::styles::{SearchFieldStyle, SearchFieldStyleConfig, SharedSearchFieldStyle};
16use teksilo_core::widget_id::WidgetId;
17use teksilo_tokens::{InputTokens, TargetRole};
18
19// ─── IntUI design tokens for SearchField ───────────────────────────
20
21/// Visual size of the magnifier glyph drawn inside the leading slot.
22pub const GLYPH_SIZE: f32 = 14.0;
23/// Reserved width of the leading slot — wider than the glyph so it
24/// doesn't sit flush against the field's leading edge.
25pub const GLYPH_SLOT_WIDTH: f32 = 22.0;
26/// Vertical gap between the input field and the suggestions popup
27/// rendered below it.
28pub const INPUT_PANEL_GAP: f32 = 2.0;
29/// Padding between the popup surface border and the row column.
30pub const PANEL_PADDING: f32 = 4.0;
31/// Suggestion popup outer corner radius.
32pub const PANEL_CORNER_RADIUS: f32 = 6.0;
33/// Per-row hover-highlight corner radius.
34pub const ROW_CORNER_RADIUS: f32 = 2.0;
35pub const ROW_PADDING_HORIZONTAL: f32 = 10.0;
36pub const ROW_PADDING_VERTICAL: f32 = 4.0;
37pub const ROW_HEIGHT: f32 = 26.0;
38
39/// Dimension tokens for `SearchField` bundled into a copyable struct.
40///
41/// `RecipeSearchFieldStyle::default()` fills every field from the
42/// corresponding `pub const` above.  Apps that want a custom size can
43/// construct `SearchFieldRecipe { row_height: 32.0, ..Default::default() }`
44/// and pass it to `RecipeSearchFieldStyle::new(recipe)`.
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct SearchFieldRecipe {
47    pub glyph_size: f32,
48    pub glyph_slot_width: f32,
49    pub input_panel_gap: f32,
50    pub panel_padding: f32,
51    pub panel_corner_radius: f32,
52    pub row_corner_radius: f32,
53    pub row_padding_horizontal: f32,
54    pub row_padding_vertical: f32,
55    pub row_height: f32,
56}
57
58impl SearchFieldRecipe {
59    /// This recipe's dimensions resolved against a density's [`InputTokens`].
60    ///
61    /// [`Default`] is `for_tokens(&InputTokens::default())` — the Compact
62    /// ladder — so the shipped values below are the Compact column by
63    /// construction and cannot drift from it.
64    pub fn for_tokens(tokens: &InputTokens) -> Self {
65        Self {
66            glyph_size: GLYPH_SIZE,
67            glyph_slot_width: GLYPH_SLOT_WIDTH,
68            input_panel_gap: spacing(INPUT_PANEL_GAP, tokens),
69            panel_padding: spacing(PANEL_PADDING, tokens),
70            panel_corner_radius: PANEL_CORNER_RADIUS,
71            row_corner_radius: ROW_CORNER_RADIUS,
72            row_padding_horizontal: spacing(ROW_PADDING_HORIZONTAL, tokens),
73            row_padding_vertical: spacing(ROW_PADDING_VERTICAL, tokens),
74            row_height: dp(ROW_HEIGHT, TargetRole::Target, tokens),
75        }
76    }
77}
78
79impl Default for SearchFieldRecipe {
80    fn default() -> Self {
81        Self::for_tokens(&InputTokens::default())
82    }
83}
84
85/// Default `SearchFieldStyle` shipped with Teksilo. Passthrough —
86/// IntUI's search-field chrome lives inside `TextInput`'s leading
87/// slot + clear button, which are already themed.
88#[derive(Debug, Default, Clone, Copy)]
89pub struct RecipeSearchFieldStyle {
90    pub recipe: SearchFieldRecipe,
91}
92
93impl RecipeSearchFieldStyle {
94    pub fn new(recipe: SearchFieldRecipe) -> Self {
95        Self { recipe }
96    }
97
98    /// This style with every dimension resolved against a density's
99    /// [`InputTokens`], as `RecipeSearchFieldStyle::for_tokens(&ctx.theme().input)` at
100    /// the widget's own build site.
101    ///
102    /// [`Default`] is the `TargetDensity::Compact` projection, so a Compact
103    /// tree gets exactly the values this module documents.
104    pub fn for_tokens(tokens: &InputTokens) -> Self {
105        Self {
106            recipe: SearchFieldRecipe::for_tokens(tokens),
107        }
108    }
109}
110
111impl SearchFieldStyle for RecipeSearchFieldStyle {
112    fn make_body(&self, cfg: &SearchFieldStyleConfig, _ctx: &mut BuildContext) -> WidgetId {
113        cfg.body
114    }
115
116    /// This style's own row gutter, so a recipe built by a preset — macOS's
117    /// 8 dp `NSSearchField` results row, say — is the one a suggestion row
118    /// pads by. `tokens` is unused: the recipe was already resolved against a
119    /// density by [`SearchFieldRecipe::for_tokens`] at the widget's build site.
120    fn row_padding_horizontal(&self, _tokens: &InputTokens) -> f32 {
121        self.recipe.row_padding_horizontal
122    }
123
124    /// This style's own vertical row gutter. See
125    /// [`row_padding_horizontal`](Self::row_padding_horizontal).
126    fn row_padding_vertical(&self, _tokens: &InputTokens) -> f32 {
127        self.recipe.row_padding_vertical
128    }
129}
130
131pub fn resolve_search_field_style(
132    override_: &Option<SharedSearchFieldStyle>,
133    ctx: &BuildContext,
134) -> SharedSearchFieldStyle {
135    if let Some(s) = override_.clone() {
136        return s;
137    }
138    ctx.theme_signal()
139        .get()
140        .style_slots
141        .search_field
142        .clone()
143        .unwrap_or_else(|| {
144            std::rc::Rc::new(RecipeSearchFieldStyle::for_tokens(&ctx.theme().input))
145                as SharedSearchFieldStyle
146        })
147}