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}