1use fret_core::{Color, Corners, Edges, Px};
2use fret_ui::Theme;
3use fret_ui::element::{ContainerProps, LayoutStyle, Overflow, RingPlacement, RingStyle};
4
5use crate::style::PaddingRefinement;
6use crate::style::{ColorFallback, MetricFallback};
7use crate::{ChromeRefinement, Size};
8
9#[derive(Debug, Clone, Copy)]
10pub struct InputTokenKeys {
11 pub padding_x: Option<&'static str>,
12 pub padding_y: Option<&'static str>,
13 pub min_height: Option<&'static str>,
14 pub radius: Option<&'static str>,
15 pub border_width: Option<&'static str>,
16 pub bg: Option<&'static str>,
17 pub border: Option<&'static str>,
18 pub border_focus: Option<&'static str>,
19 pub fg: Option<&'static str>,
20 pub text_px: Option<&'static str>,
21 pub selection: Option<&'static str>,
22}
23
24impl InputTokenKeys {
25 pub const fn none() -> Self {
26 Self {
27 padding_x: None,
28 padding_y: None,
29 min_height: None,
30 radius: None,
31 border_width: None,
32 bg: None,
33 border: None,
34 border_focus: None,
35 fg: None,
36 text_px: None,
37 selection: None,
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy)]
43pub struct ResolvedInputChrome {
44 pub padding: Edges,
45 pub min_height: Px,
46 pub radius: Px,
47 pub border_width: Px,
48 pub background: Color,
49 pub border_color: Color,
50 pub border_color_focused: Color,
51 pub text_color: Color,
52 pub text_px: Px,
53 pub selection_color: Color,
54}
55
56pub fn input_chrome_container_props(
57 mut layout: LayoutStyle,
58 chrome: ResolvedInputChrome,
59 border_color: Color,
60) -> ContainerProps {
61 layout.overflow = Overflow::Clip;
62 ContainerProps {
63 layout,
64 padding: chrome.padding.into(),
65 background: Some(chrome.background),
66 background_paint: None,
67 shadow: None,
68 border: Edges::all(chrome.border_width),
69 border_color: Some(border_color),
70 border_paint: None,
71 border_dash: None,
72 focus_ring: None,
73 focus_ring_always_paint: false,
74 focus_border_color: None,
75 focus_within: false,
76 corner_radii: Corners::all(chrome.radius),
77 snap_to_device_pixels: false,
78 }
79}
80
81pub fn resolve_input_chrome(
82 theme: &Theme,
83 size: Size,
84 style: &ChromeRefinement,
85 keys: InputTokenKeys,
86) -> ResolvedInputChrome {
87 let padding_x = keys
100 .padding_x
101 .and_then(|k| theme.metric_by_key(k))
102 .or_else(|| theme.metric_by_key("component.input.padding_x"))
103 .unwrap_or_else(|| size.input_px(theme));
104 let padding_y = keys
105 .padding_y
106 .and_then(|k| theme.metric_by_key(k))
107 .or_else(|| theme.metric_by_key("component.input.padding_y"))
108 .unwrap_or_else(|| size.input_py(theme));
109 let min_height = style
110 .min_height
111 .as_ref()
112 .map(|m| m.resolve(theme))
113 .or_else(|| keys.min_height.and_then(|k| theme.metric_by_key(k)))
114 .or_else(|| theme.metric_by_key("component.input.min_height"))
115 .unwrap_or_else(|| size.input_h(theme));
116 let radius = style
117 .radius
118 .as_ref()
119 .map(|m| m.resolve(theme))
120 .or_else(|| keys.radius.and_then(|k| theme.metric_by_key(k)))
121 .or_else(|| theme.metric_by_key("component.input.radius"))
122 .unwrap_or_else(|| size.control_radius(theme));
123 let border_width = style
124 .border_width
125 .as_ref()
126 .map(|m| m.resolve(theme))
127 .or_else(|| keys.border_width.and_then(|k| theme.metric_by_key(k)))
128 .or_else(|| theme.metric_by_key("component.input.border_width"))
129 .unwrap_or(Px(1.0));
130
131 let background = style
132 .background
133 .as_ref()
134 .map(|c| c.resolve(theme))
135 .or_else(|| keys.bg.and_then(|k| theme.color_by_key(k)))
136 .or_else(|| theme.color_by_key("component.input.bg"))
137 .unwrap_or_else(|| theme.color_token("background"));
138 let border_color = style
139 .border_color
140 .as_ref()
141 .map(|c| c.resolve(theme))
142 .or_else(|| keys.border.and_then(|k| theme.color_by_key(k)))
143 .or_else(|| theme.color_by_key("component.input.border"))
144 .unwrap_or_else(|| theme.color_token("input"));
145 let border_color_focused = keys
146 .border_focus
147 .and_then(|k| theme.color_by_key(k))
148 .or_else(|| theme.color_by_key("component.input.border_focus"))
149 .unwrap_or_else(|| theme.color_token("ring"));
150 let text_color = style
151 .text_color
152 .as_ref()
153 .map(|c| c.resolve(theme))
154 .or_else(|| keys.fg.and_then(|k| theme.color_by_key(k)))
155 .or_else(|| theme.color_by_key("component.input.fg"))
156 .unwrap_or_else(|| theme.color_token("foreground"));
157 let text_px = keys
158 .text_px
159 .and_then(|k| theme.metric_by_key(k))
160 .or_else(|| theme.metric_by_key("component.input.text_px"))
161 .unwrap_or_else(|| size.control_text_px(theme));
162 let selection_color = keys
163 .selection
164 .and_then(|k| theme.color_by_key(k))
165 .or_else(|| theme.color_by_key("component.input.selection"))
166 .unwrap_or_else(|| theme.color_token("selection.background"));
167
168 let padding_top = style
169 .padding
170 .as_ref()
171 .and_then(|p| p.top.as_ref())
172 .map(|m| m.resolve(theme))
173 .unwrap_or(padding_y);
174 let padding_bottom = style
175 .padding
176 .as_ref()
177 .and_then(|p| p.bottom.as_ref())
178 .map(|m| m.resolve(theme))
179 .unwrap_or(padding_y);
180 let padding_left = style
181 .padding
182 .as_ref()
183 .and_then(|p| p.left.as_ref())
184 .map(|m| m.resolve(theme))
185 .unwrap_or(padding_x);
186 let padding_right = style
187 .padding
188 .as_ref()
189 .and_then(|p| p.right.as_ref())
190 .map(|m| m.resolve(theme))
191 .unwrap_or(padding_x);
192
193 ResolvedInputChrome {
194 padding: Edges {
195 top: Px(padding_top.0.max(0.0)),
196 right: Px(padding_right.0.max(0.0)),
197 bottom: Px(padding_bottom.0.max(0.0)),
198 left: Px(padding_left.0.max(0.0)),
199 },
200 min_height: Px(min_height.0.max(0.0)),
201 radius: Px(radius.0.max(0.0)),
202 border_width: Px(border_width.0.max(0.0)),
203 background,
204 border_color,
205 border_color_focused,
206 text_color,
207 text_px,
208 selection_color,
209 }
210}
211
212pub fn default_text_input_style(theme: &Theme) -> fret_ui::TextInputStyle {
213 let ring_width = theme
214 .metric_by_key("component.ring.width")
215 .unwrap_or(Px(2.0));
216 let ring_offset = theme
217 .metric_by_key("component.ring.offset")
218 .unwrap_or(Px(2.0));
219 let ring_color = theme
221 .color_by_key("ring/50")
222 .or_else(|| theme.color_by_key("ring"))
223 .unwrap_or_else(|| theme.color_token("ring"));
224 let ring_offset_color = theme
225 .color_by_key("ring-offset-background")
226 .unwrap_or_else(|| theme.color_token("ring-offset-background"));
227
228 let background = theme
229 .color_by_key("component.input.bg")
230 .unwrap_or_else(|| theme.color_token("background"));
231 let border_color = theme
232 .color_by_key("component.input.border")
233 .unwrap_or_else(|| theme.color_token("input"));
234 let border_color_focused = theme
236 .color_by_key("ring")
237 .unwrap_or_else(|| theme.color_token("ring"));
238 let radius = theme
239 .metric_by_key("component.input.radius")
240 .unwrap_or_else(|| theme.metric_token("metric.radius.sm"));
241 let selection = theme
242 .color_by_key("component.input.selection")
243 .unwrap_or_else(|| theme.color_token("selection.background"));
244 let preedit_bg_color = {
245 let mut bg = selection;
246 bg.a = (bg.a * 0.35).clamp(0.0, 1.0);
247 bg
248 };
249
250 fret_ui::TextInputStyle {
251 padding: Edges::all(Px(0.0)),
252 background,
253 border: Edges::all(Px(1.0)),
254 border_color,
255 border_color_focused,
256 focus_ring: Some(RingStyle {
257 placement: RingPlacement::Outset,
258 width: ring_width,
259 offset: ring_offset,
260 color: ring_color,
261 offset_color: (ring_offset.0 > 0.0).then_some(ring_offset_color),
262 corner_radii: Corners::all(radius),
263 }),
264 corner_radii: Corners::all(radius),
265 text_color: theme.color_token("foreground"),
266 placeholder_color: theme.color_token("muted-foreground"),
267 selection_color: Color {
268 a: 1.0,
269 ..selection
270 },
271 caret_color: theme.color_token("foreground"),
272 preedit_bg_color,
273 preedit_color: theme.color_token("primary"),
274 preedit_underline_color: theme.color_token("primary"),
275 }
276}
277
278pub fn input_base_refinement() -> ChromeRefinement {
279 ChromeRefinement {
280 padding: Some(PaddingRefinement {
281 top: Some(crate::MetricRef::Token {
282 key: "component.input.padding_y",
283 fallback: MetricFallback::ThemePaddingSm,
284 }),
285 right: Some(crate::MetricRef::Token {
286 key: "component.input.padding_x",
287 fallback: MetricFallback::ThemePaddingSm,
288 }),
289 bottom: Some(crate::MetricRef::Token {
290 key: "component.input.padding_y",
291 fallback: MetricFallback::ThemePaddingSm,
292 }),
293 left: Some(crate::MetricRef::Token {
294 key: "component.input.padding_x",
295 fallback: MetricFallback::ThemePaddingSm,
296 }),
297 }),
298 border_width: Some(crate::MetricRef::Token {
299 key: "component.input.border_width",
300 fallback: MetricFallback::Px(Px(1.0)),
301 }),
302 radius: Some(crate::MetricRef::Token {
303 key: "component.input.radius",
304 fallback: MetricFallback::ThemeRadiusSm,
305 }),
306 background: Some(crate::ColorRef::Token {
307 key: "component.input.bg",
308 fallback: ColorFallback::ThemePanelBackground,
309 }),
310 border_color: Some(crate::ColorRef::Token {
311 key: "component.input.border",
312 fallback: ColorFallback::ThemePanelBorder,
313 }),
314 text_color: Some(crate::ColorRef::Token {
315 key: "component.input.fg",
316 fallback: ColorFallback::ThemeTextPrimary,
317 }),
318 ..ChromeRefinement::default()
319 }
320}