1use std::sync::Arc;
2
3use fret_core::{
4 FontId, FontWeight, Px, TextAlign, TextOverflow, TextStyle, TextStyleRefinement, TextWrap,
5};
6use fret_ui::element::{AnyElement, LayoutStyle, TextInkOverflow, TextProps};
7use fret_ui::{ElementContext, Theme, UiHost};
8
9use crate::typography as ui_typography;
10use crate::typography::UiTextSize;
11
12pub(crate) fn text_xs_style(theme: &Theme) -> TextStyle {
13 ui_typography::control_text_style(theme, UiTextSize::Xs)
14}
15
16pub(crate) fn text_sm_style(theme: &Theme) -> TextStyle {
17 ui_typography::control_text_style(theme, UiTextSize::Sm)
18}
19
20pub(crate) fn text_base_style(theme: &Theme) -> TextStyle {
21 ui_typography::control_text_style(theme, UiTextSize::Base)
22}
23
24pub(crate) fn text_prose_style(theme: &Theme) -> TextStyle {
25 ui_typography::control_text_style(theme, UiTextSize::Prose)
26}
27
28pub(crate) fn text_xs_refinement(theme: &Theme) -> TextStyleRefinement {
29 ui_typography::composable_refinement_from_style(&text_xs_style(theme))
30}
31
32pub(crate) fn text_sm_refinement(theme: &Theme) -> TextStyleRefinement {
33 ui_typography::composable_refinement_from_style(&text_sm_style(theme))
34}
35
36pub(crate) fn text_base_refinement(theme: &Theme) -> TextStyleRefinement {
37 ui_typography::composable_refinement_from_style(&text_base_style(theme))
38}
39
40pub(crate) fn text_prose_refinement(theme: &Theme) -> TextStyleRefinement {
41 ui_typography::composable_refinement_from_style(&text_prose_style(theme))
42}
43
44fn scoped_text<H: UiHost>(
45 cx: &mut ElementContext<'_, H>,
46 text: impl Into<Arc<str>>,
47 refinement: TextStyleRefinement,
48 wrap: TextWrap,
49 overflow: TextOverflow,
50) -> AnyElement {
51 ui_typography::scope_text_style(
52 cx.text_props(TextProps {
53 layout: LayoutStyle::default(),
54 text: text.into(),
55 style: None,
56 color: None,
57 wrap,
58 overflow,
59 align: TextAlign::Start,
60 ink_overflow: TextInkOverflow::None,
61 }),
62 refinement,
63 )
64}
65
66pub fn text_truncate<H: UiHost>(
72 cx: &mut ElementContext<'_, H>,
73 text: impl Into<Arc<str>>,
74) -> AnyElement {
75 cx.text_props(TextProps {
76 layout: LayoutStyle::default(),
77 text: text.into(),
78 style: None,
79 color: None,
80 wrap: TextWrap::None,
81 overflow: TextOverflow::Ellipsis,
82 align: TextAlign::Start,
83 ink_overflow: TextInkOverflow::None,
84 })
85}
86
87pub fn text_nowrap<H: UiHost>(
89 cx: &mut ElementContext<'_, H>,
90 text: impl Into<Arc<str>>,
91) -> AnyElement {
92 cx.text_props(TextProps {
93 layout: LayoutStyle::default(),
94 text: text.into(),
95 style: None,
96 color: None,
97 wrap: TextWrap::None,
98 overflow: TextOverflow::Clip,
99 align: TextAlign::Start,
100 ink_overflow: TextInkOverflow::None,
101 })
102}
103
104#[track_caller]
111pub fn text_sm<H: UiHost>(cx: &mut ElementContext<'_, H>, text: impl Into<Arc<str>>) -> AnyElement {
112 let refinement = {
113 let theme = Theme::global(&*cx.app);
114 text_sm_refinement(theme)
115 };
116 scoped_text(cx, text, refinement, TextWrap::Word, TextOverflow::Clip)
117}
118
119#[track_caller]
125pub fn text_xs<H: UiHost>(cx: &mut ElementContext<'_, H>, text: impl Into<Arc<str>>) -> AnyElement {
126 let refinement = {
127 let theme = Theme::global(&*cx.app);
128 text_xs_refinement(theme)
129 };
130 scoped_text(cx, text, refinement, TextWrap::Word, TextOverflow::Clip)
131}
132
133pub fn text_base<H: UiHost>(
139 cx: &mut ElementContext<'_, H>,
140 text: impl Into<Arc<str>>,
141) -> AnyElement {
142 let refinement = {
143 let theme = Theme::global(&*cx.app);
144 text_base_refinement(theme)
145 };
146 scoped_text(cx, text, refinement, TextWrap::Word, TextOverflow::Clip)
147}
148
149pub fn text_prose<H: UiHost>(
163 cx: &mut ElementContext<'_, H>,
164 text: impl Into<Arc<str>>,
165) -> AnyElement {
166 let refinement = {
167 let theme = Theme::global(&*cx.app);
168 text_prose_refinement(theme)
169 };
170 scoped_text(cx, text, refinement, TextWrap::Word, TextOverflow::Clip)
171}
172
173pub fn text_prose_break_words<H: UiHost>(
176 cx: &mut ElementContext<'_, H>,
177 text: impl Into<Arc<str>>,
178) -> AnyElement {
179 let refinement = {
180 let theme = Theme::global(&*cx.app);
181 text_prose_refinement(theme)
182 };
183 scoped_text(
184 cx,
185 text,
186 refinement,
187 TextWrap::WordBreak,
188 TextOverflow::Clip,
189 )
190}
191
192pub fn text_prose_bold<H: UiHost>(
194 cx: &mut ElementContext<'_, H>,
195 text: impl Into<Arc<str>>,
196) -> AnyElement {
197 let mut refinement = {
198 let theme = Theme::global(&*cx.app);
199 text_prose_refinement(theme)
200 };
201 refinement.weight = Some(FontWeight::BOLD);
202
203 scoped_text(cx, text, refinement, TextWrap::Word, TextOverflow::Clip)
204}
205
206pub(crate) fn label_style(theme: &Theme) -> (TextStyle, Px) {
208 let px = theme
209 .metric_by_key("component.label.text_px")
210 .or_else(|| theme.metric_by_key("font.size"))
211 .unwrap_or_else(|| theme.metric_token("font.size"));
212 let line_height = theme
213 .metric_by_key("component.label.line_height")
214 .or_else(|| theme.metric_by_key("font.line_height"))
215 .unwrap_or_else(|| theme.metric_token("font.line_height"));
216
217 let mut style = ui_typography::fixed_line_box_style(FontId::ui(), px, line_height);
218 style.weight = FontWeight::MEDIUM;
219 (style, line_height)
220}
221
222pub(crate) fn label_text_refinement(theme: &Theme) -> (TextStyleRefinement, Px) {
223 let (style, line_height) = label_style(theme);
224 let mut refinement = ui_typography::composable_refinement_from_style(&style);
225 refinement.font = Some(FontId::ui());
226 (refinement, line_height)
227}
228
229pub fn text_code_wrap<H: UiHost>(
235 cx: &mut ElementContext<'_, H>,
236 text: impl Into<Arc<str>>,
237) -> AnyElement {
238 let refinement = {
239 let theme = Theme::global(&*cx.app);
240 ui_typography::composable_refinement_from_style(&ui_typography::fixed_line_box_style(
241 FontId::monospace(),
242 theme.metric_token("metric.font.mono_size"),
243 theme.metric_token("metric.font.mono_line_height"),
244 ))
245 };
246
247 scoped_text(cx, text, refinement, TextWrap::Grapheme, TextOverflow::Clip)
248}
249
250pub fn text_prose_nowrap<H: UiHost>(
252 cx: &mut ElementContext<'_, H>,
253 text: impl Into<Arc<str>>,
254) -> AnyElement {
255 let refinement = {
256 let theme = Theme::global(&*cx.app);
257 text_prose_refinement(theme)
258 };
259 scoped_text(cx, text, refinement, TextWrap::None, TextOverflow::Clip)
260}
261
262pub fn text_prose_bold_nowrap<H: UiHost>(
264 cx: &mut ElementContext<'_, H>,
265 text: impl Into<Arc<str>>,
266) -> AnyElement {
267 let mut refinement = {
268 let theme = Theme::global(&*cx.app);
269 text_prose_refinement(theme)
270 };
271 refinement.weight = Some(FontWeight::BOLD);
272
273 scoped_text(cx, text, refinement, TextWrap::None, TextOverflow::Clip)
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 use fret_app::App;
281 use fret_core::{AppWindowId, Point, Rect, Size};
282 use fret_ui::element::ElementKind;
283 use fret_ui::elements;
284 use fret_ui::{Theme, ThemeConfig};
285
286 fn test_app() -> App {
287 let mut app = App::new();
288 Theme::with_global_mut(&mut app, |theme| {
289 theme.apply_config(&ThemeConfig {
290 name: "Text Helpers Test".to_string(),
291 metrics: std::collections::HashMap::from([
292 ("font.size".to_string(), 13.0),
293 ("font.line_height".to_string(), 20.0),
294 (
295 crate::theme_tokens::metric::COMPONENT_TEXT_XS_PX.to_string(),
296 12.0,
297 ),
298 (
299 crate::theme_tokens::metric::COMPONENT_TEXT_XS_LINE_HEIGHT.to_string(),
300 16.0,
301 ),
302 (
303 crate::theme_tokens::metric::COMPONENT_TEXT_SM_PX.to_string(),
304 13.0,
305 ),
306 (
307 crate::theme_tokens::metric::COMPONENT_TEXT_SM_LINE_HEIGHT.to_string(),
308 18.0,
309 ),
310 (
311 crate::theme_tokens::metric::COMPONENT_TEXT_BASE_PX.to_string(),
312 14.0,
313 ),
314 (
315 crate::theme_tokens::metric::COMPONENT_TEXT_BASE_LINE_HEIGHT.to_string(),
316 20.0,
317 ),
318 (
319 crate::theme_tokens::metric::COMPONENT_TEXT_PROSE_PX.to_string(),
320 16.0,
321 ),
322 (
323 crate::theme_tokens::metric::COMPONENT_TEXT_PROSE_LINE_HEIGHT.to_string(),
324 24.0,
325 ),
326 ("metric.font.mono_size".to_string(), 13.0),
327 ("metric.font.mono_line_height".to_string(), 18.0),
328 ]),
329 ..ThemeConfig::default()
330 });
331 });
332 app
333 }
334
335 fn test_bounds() -> Rect {
336 Rect::new(
337 Point::new(Px(0.0), Px(0.0)),
338 Size::new(Px(320.0), Px(160.0)),
339 )
340 }
341
342 #[test]
343 fn text_sm_scopes_inherited_refinement_without_leaf_style() {
344 let window = AppWindowId::default();
345 let mut app = test_app();
346 let bounds = test_bounds();
347
348 let el =
349 elements::with_element_cx(&mut app, window, bounds, "test", |cx| text_sm(cx, "Hello"));
350 let theme = Theme::global(&app);
351
352 let ElementKind::Text(props) = &el.kind else {
353 panic!("expected text_sm(...) to build a Text element");
354 };
355
356 assert!(props.style.is_none());
357 assert!(props.color.is_none());
358 assert_eq!(props.wrap, TextWrap::Word);
359 assert_eq!(props.overflow, TextOverflow::Clip);
360 assert_eq!(el.inherited_text_style, Some(text_sm_refinement(&theme)));
361 }
362
363 #[test]
364 fn prose_variants_and_code_wrap_install_semantic_inherited_overrides() {
365 let window = AppWindowId::default();
366 let mut app = test_app();
367 let bounds = test_bounds();
368 let mut expected_prose = {
369 let theme = Theme::global(&app);
370 text_prose_refinement(theme)
371 };
372 expected_prose.weight = Some(FontWeight::BOLD);
373
374 let prose_bold = elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
375 text_prose_bold(cx, "Heading")
376 });
377 let ElementKind::Text(props) = &prose_bold.kind else {
378 panic!("expected text_prose_bold(...) to build a Text element");
379 };
380 assert!(props.style.is_none());
381 assert!(props.color.is_none());
382 assert_eq!(prose_bold.inherited_text_style, Some(expected_prose));
383
384 let code = elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
385 text_code_wrap(cx, "let answer = 42;")
386 });
387 let ElementKind::Text(props) = &code.kind else {
388 panic!("expected text_code_wrap(...) to build a Text element");
389 };
390 assert!(props.style.is_none());
391 assert!(props.color.is_none());
392 assert_eq!(props.wrap, TextWrap::Grapheme);
393 assert_eq!(props.overflow, TextOverflow::Clip);
394 assert_eq!(
395 code.inherited_text_style
396 .as_ref()
397 .and_then(|style| style.font.clone()),
398 Some(FontId::monospace())
399 );
400 }
401}