1use fret_core::{
2 Color, FontId, FontWeight, Px, TextLineHeightPolicy, TextSlant, TextStrutStyle, TextStyle,
3 TextStyleRefinement, TextVerticalPlacement,
4};
5use fret_ui::element::AnyElement;
6
7use crate::style::ThemeTokenRead;
8use crate::theme_tokens;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum UiTextSize {
12 Xs,
13 Sm,
14 Base,
15 Prose,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum UiTextFamily {
20 Ui,
21 Monospace,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TextIntent {
26 Control,
27 Content,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct TypographyPreset {
32 pub intent: TextIntent,
33 pub family: UiTextFamily,
34 pub size: UiTextSize,
35}
36
37impl TypographyPreset {
38 pub const fn new(intent: TextIntent, family: UiTextFamily, size: UiTextSize) -> Self {
39 Self {
40 intent,
41 family,
42 size,
43 }
44 }
45
46 pub const fn control_ui(size: UiTextSize) -> Self {
47 Self::new(TextIntent::Control, UiTextFamily::Ui, size)
48 }
49
50 pub const fn content_ui(size: UiTextSize) -> Self {
51 Self::new(TextIntent::Content, UiTextFamily::Ui, size)
52 }
53
54 pub const fn control_monospace(size: UiTextSize) -> Self {
55 Self::new(TextIntent::Control, UiTextFamily::Monospace, size)
56 }
57
58 pub const fn content_monospace(size: UiTextSize) -> Self {
59 Self::new(TextIntent::Content, UiTextFamily::Monospace, size)
60 }
61
62 pub fn resolve(self, theme: &impl ThemeTokenRead) -> TextStyle {
63 match self.intent {
64 TextIntent::Control => control_text_style_with_family(theme, self.size, self.family),
65 TextIntent::Content => content_text_style_with_family(theme, self.size, self.family),
66 }
67 }
68}
69
70fn font_size(theme: &impl ThemeTokenRead) -> Px {
71 theme
72 .metric_by_key("font.size")
73 .unwrap_or_else(|| theme.metric_token("font.size"))
74}
75
76fn font_line_height(theme: &impl ThemeTokenRead) -> Px {
77 theme
78 .metric_by_key("font.line_height")
79 .unwrap_or_else(|| theme.metric_token("font.line_height"))
80}
81
82fn base_line_height_ratio(theme: &impl ThemeTokenRead) -> f32 {
83 let base_size_px = font_size(theme).0;
84 let base_line_height_px = font_line_height(theme).0;
85 if base_size_px.is_finite()
86 && base_line_height_px.is_finite()
87 && base_size_px > 0.0
88 && base_line_height_px > 0.0
89 {
90 base_line_height_px / base_size_px
91 } else {
92 1.25
93 }
94}
95
96pub fn fixed_line_box_style(font: FontId, size: Px, line_height: Px) -> TextStyle {
101 TextStyle {
102 font,
103 size,
104 line_height: Some(line_height),
105 line_height_policy: TextLineHeightPolicy::FixedFromStyle,
106 vertical_placement: TextVerticalPlacement::BoundsAsLineBox,
107 ..Default::default()
108 }
109}
110
111pub fn with_intent(mut style: TextStyle, intent: TextIntent) -> TextStyle {
117 match intent {
118 TextIntent::Control => {
119 style.line_height_policy = TextLineHeightPolicy::FixedFromStyle;
120 style.vertical_placement = TextVerticalPlacement::BoundsAsLineBox;
121 }
122 TextIntent::Content => {
123 style.line_height_policy = TextLineHeightPolicy::ExpandToFit;
124 style.vertical_placement = TextVerticalPlacement::CenterMetricsBox;
125 }
126 }
127 style
128}
129
130pub fn as_control_text(style: TextStyle) -> TextStyle {
131 with_intent(style, TextIntent::Control)
132}
133
134pub fn as_content_text(style: TextStyle) -> TextStyle {
135 with_intent(style, TextIntent::Content)
136}
137
138fn force_strut_from_style(style: &TextStyle) -> Option<TextStrutStyle> {
139 if style.line_height.is_none() && style.line_height_em.is_none() {
140 return None;
141 }
142
143 Some(TextStrutStyle {
144 line_height: style.line_height,
145 line_height_em: style.line_height_em,
146 force: true,
147 ..Default::default()
148 })
149}
150
151pub fn text_area_content_text_style(theme: &impl ThemeTokenRead) -> TextStyle {
155 TextStyle {
156 font: FontId::ui(),
157 size: font_size(theme),
158 line_height: Some(font_line_height(theme)),
159 ..Default::default()
160 }
161}
162
163pub fn text_area_content_text_style_scaled(
166 theme: &impl ThemeTokenRead,
167 font: FontId,
168 size: Px,
169) -> TextStyle {
170 let ratio = base_line_height_ratio(theme);
171 let line_height = Px((size.0 * ratio).max(size.0));
172
173 let mut style = TextStyle {
174 font,
175 size,
176 line_height: Some(line_height),
177 ..Default::default()
178 };
179 style.line_height_policy = TextLineHeightPolicy::ExpandToFit;
180 style.vertical_placement = TextVerticalPlacement::CenterMetricsBox;
181 style
182}
183
184pub fn text_area_control_text_style(theme: &impl ThemeTokenRead) -> TextStyle {
191 let mut style = text_area_content_text_style(theme);
192 style.line_height_policy = TextLineHeightPolicy::FixedFromStyle;
193 style.strut_style = force_strut_from_style(&style);
194 style
195}
196
197pub fn text_area_control_text_style_scaled(
200 theme: &impl ThemeTokenRead,
201 font: FontId,
202 size: Px,
203) -> TextStyle {
204 let ratio = base_line_height_ratio(theme);
205 let line_height = Px((size.0 * ratio).max(size.0));
206
207 let mut style = TextStyle {
208 font,
209 size,
210 line_height: Some(line_height),
211 ..Default::default()
212 };
213 style.line_height_policy = TextLineHeightPolicy::FixedFromStyle;
214 style.vertical_placement = TextVerticalPlacement::BoundsAsLineBox;
215 style.strut_style = force_strut_from_style(&style);
216 style
217}
218
219pub fn control_text_style(theme: &impl ThemeTokenRead, size: UiTextSize) -> TextStyle {
224 control_text_style_with_family(theme, size, UiTextFamily::Ui)
225}
226
227pub fn preset_text_style_with_overrides(
228 theme: &impl ThemeTokenRead,
229 preset: TypographyPreset,
230 weight: Option<FontWeight>,
231 slant: Option<TextSlant>,
232) -> TextStyle {
233 let mut style = preset.resolve(theme);
234 if let Some(weight) = weight {
235 style.weight = weight;
236 }
237 if let Some(slant) = slant {
238 style.slant = slant;
239 }
240 style
241}
242
243pub fn control_text_style_with_family(
245 theme: &impl ThemeTokenRead,
246 size: UiTextSize,
247 family: UiTextFamily,
248) -> TextStyle {
249 let font = match family {
250 UiTextFamily::Ui => FontId::ui(),
251 UiTextFamily::Monospace => FontId::monospace(),
252 };
253
254 match size {
255 UiTextSize::Xs => {
256 let px = theme
257 .metric_by_key(theme_tokens::metric::COMPONENT_TEXT_XS_PX)
258 .unwrap_or(Px(12.0));
259 let line_height = theme
260 .metric_by_key(theme_tokens::metric::COMPONENT_TEXT_XS_LINE_HEIGHT)
261 .unwrap_or(Px(16.0));
262 fixed_line_box_style(font, px, line_height)
263 }
264 UiTextSize::Sm => {
265 let px = theme
266 .metric_by_key(theme_tokens::metric::COMPONENT_TEXT_SM_PX)
267 .unwrap_or_else(|| font_size(theme));
268 let line_height = theme
269 .metric_by_key(theme_tokens::metric::COMPONENT_TEXT_SM_LINE_HEIGHT)
270 .unwrap_or_else(|| font_line_height(theme));
271 fixed_line_box_style(font, px, line_height)
272 }
273 UiTextSize::Base => {
274 let px = theme
275 .metric_by_key(theme_tokens::metric::COMPONENT_TEXT_BASE_PX)
276 .unwrap_or_else(|| Px(font_size(theme).0 + 1.0));
277
278 let line_height = theme
279 .metric_by_key(theme_tokens::metric::COMPONENT_TEXT_BASE_LINE_HEIGHT)
280 .unwrap_or_else(|| Px((px.0 * base_line_height_ratio(theme)).max(px.0)));
281
282 fixed_line_box_style(font, px, line_height)
283 }
284 UiTextSize::Prose => {
285 let px = theme
286 .metric_by_key(theme_tokens::metric::COMPONENT_TEXT_PROSE_PX)
287 .unwrap_or_else(|| Px(font_size(theme).0 + 2.0));
288 let line_height = theme
289 .metric_by_key(theme_tokens::metric::COMPONENT_TEXT_PROSE_LINE_HEIGHT)
290 .unwrap_or_else(|| Px(font_line_height(theme).0 + 4.0));
291 fixed_line_box_style(font, px, line_height)
292 }
293 }
294}
295
296pub fn content_text_style(theme: &impl ThemeTokenRead, size: UiTextSize) -> TextStyle {
298 let mut style = control_text_style(theme, size);
299 style.line_height_policy = TextLineHeightPolicy::ExpandToFit;
300 style.vertical_placement = TextVerticalPlacement::CenterMetricsBox;
301 style
302}
303
304pub fn content_text_style_with_family(
306 theme: &impl ThemeTokenRead,
307 size: UiTextSize,
308 family: UiTextFamily,
309) -> TextStyle {
310 let mut style = control_text_style_with_family(theme, size, family);
311 style.line_height_policy = TextLineHeightPolicy::ExpandToFit;
312 style.vertical_placement = TextVerticalPlacement::CenterMetricsBox;
313 style
314}
315
316pub fn control_text_style_scaled(theme: &impl ThemeTokenRead, font: FontId, size: Px) -> TextStyle {
322 let ratio = base_line_height_ratio(theme);
323 let line_height = Px((size.0 * ratio).max(size.0));
324 fixed_line_box_style(font, size, line_height)
325}
326
327pub fn control_text_style_for_font_size(
332 theme: &impl ThemeTokenRead,
333 font: FontId,
334 size: Px,
335) -> TextStyle {
336 fixed_line_box_style(font, size, font_line_height(theme))
337}
338
339fn color_by_aliases(theme: &impl ThemeTokenRead, aliases: &[&str], fallback_token: &str) -> Color {
340 aliases
341 .iter()
342 .find_map(|key| theme.color_by_key(key))
343 .unwrap_or_else(|| theme.color_token(fallback_token))
344}
345
346pub fn muted_foreground_color(theme: &impl ThemeTokenRead) -> Color {
347 color_by_aliases(
348 theme,
349 &["muted.foreground", "muted-foreground", "muted_foreground"],
350 "muted-foreground",
351 )
352}
353
354pub fn scope_text_style(element: AnyElement, refinement: TextStyleRefinement) -> AnyElement {
355 element.inherit_text_style(refinement)
356}
357
358pub fn scope_text_style_with_color(
359 element: AnyElement,
360 refinement: TextStyleRefinement,
361 foreground: Color,
362) -> AnyElement {
363 element
364 .inherit_foreground(foreground)
365 .inherit_text_style(refinement)
366}
367
368pub fn title_text_refinement(
369 theme: &impl ThemeTokenRead,
370 metric_prefix: &str,
371) -> TextStyleRefinement {
372 title_text_refinement_with_fallbacks(theme, metric_prefix, None, None)
373}
374
375pub fn title_text_refinement_with_fallbacks(
376 theme: &impl ThemeTokenRead,
377 metric_prefix: &str,
378 fallback_size_key: Option<&str>,
379 fallback_line_height_key: Option<&str>,
380) -> TextStyleRefinement {
381 let size_key = format!("{metric_prefix}_px");
382 let line_height_key = format!("{metric_prefix}_line_height");
383
384 let size = theme
385 .metric_by_key(&size_key)
386 .or_else(|| fallback_size_key.and_then(|key| theme.metric_by_key(key)))
387 .or_else(|| theme.metric_by_key("font.size"))
388 .unwrap_or_else(|| theme.metric_token("font.size"));
389 let line_height = theme
390 .metric_by_key(&line_height_key)
391 .or_else(|| fallback_line_height_key.and_then(|key| theme.metric_by_key(key)))
392 .unwrap_or(size);
393
394 TextStyleRefinement {
395 font: Some(FontId::ui()),
396 size: Some(size),
397 weight: Some(FontWeight::SEMIBOLD),
398 line_height: Some(line_height),
399 line_height_policy: Some(TextLineHeightPolicy::FixedFromStyle),
400 ..Default::default()
401 }
402}
403
404pub fn description_text_refinement(
405 theme: &impl ThemeTokenRead,
406 metric_prefix: &str,
407) -> TextStyleRefinement {
408 description_text_refinement_with_fallbacks(theme, metric_prefix, None, None)
409}
410
411pub fn description_text_refinement_with_fallbacks(
412 theme: &impl ThemeTokenRead,
413 metric_prefix: &str,
414 fallback_size_key: Option<&str>,
415 fallback_line_height_key: Option<&str>,
416) -> TextStyleRefinement {
417 let size_key = format!("{metric_prefix}_px");
418 let line_height_key = format!("{metric_prefix}_line_height");
419
420 let size = theme
421 .metric_by_key(&size_key)
422 .or_else(|| fallback_size_key.and_then(|key| theme.metric_by_key(key)))
423 .or_else(|| theme.metric_by_key("font.size"))
424 .unwrap_or_else(|| theme.metric_token("font.size"));
425 let line_height = theme
426 .metric_by_key(&line_height_key)
427 .or_else(|| fallback_line_height_key.and_then(|key| theme.metric_by_key(key)))
428 .or_else(|| theme.metric_by_key("font.line_height"))
429 .unwrap_or_else(|| theme.metric_token("font.line_height"));
430
431 TextStyleRefinement {
432 size: Some(size),
433 line_height: Some(line_height),
434 line_height_policy: Some(TextLineHeightPolicy::FixedFromStyle),
435 ..Default::default()
436 }
437}
438
439pub fn scope_description_text(
440 element: AnyElement,
441 theme: &impl ThemeTokenRead,
442 metric_prefix: &str,
443) -> AnyElement {
444 scope_description_text_with_fallbacks(element, theme, metric_prefix, None, None)
445}
446
447pub fn scope_description_text_with_fallbacks(
448 element: AnyElement,
449 theme: &impl ThemeTokenRead,
450 metric_prefix: &str,
451 fallback_size_key: Option<&str>,
452 fallback_line_height_key: Option<&str>,
453) -> AnyElement {
454 scope_text_style_with_color(
455 element,
456 description_text_refinement_with_fallbacks(
457 theme,
458 metric_prefix,
459 fallback_size_key,
460 fallback_line_height_key,
461 ),
462 muted_foreground_color(theme),
463 )
464}
465
466pub fn refinement_from_style(style: &TextStyle) -> TextStyleRefinement {
467 TextStyleRefinement {
468 font: Some(style.font.clone()),
469 size: Some(style.size),
470 weight: Some(style.weight),
471 slant: Some(style.slant),
472 line_height: style.line_height,
473 line_height_em: style.line_height_em,
474 line_height_policy: Some(style.line_height_policy),
475 letter_spacing_em: style.letter_spacing_em,
476 vertical_placement: Some(style.vertical_placement),
477 leading_distribution: Some(style.leading_distribution),
478 }
479}
480
481pub fn composable_refinement_from_style(style: &TextStyle) -> TextStyleRefinement {
486 let default = TextStyle::default();
487
488 TextStyleRefinement {
489 font: (style.font != default.font).then(|| style.font.clone()),
490 size: Some(style.size),
491 weight: (style.weight != default.weight).then_some(style.weight),
492 slant: (style.slant != default.slant).then_some(style.slant),
493 line_height: style.line_height,
494 line_height_em: style.line_height_em,
495 line_height_policy: (style.line_height_policy != default.line_height_policy)
496 .then_some(style.line_height_policy),
497 letter_spacing_em: style.letter_spacing_em,
498 vertical_placement: (style.vertical_placement != default.vertical_placement)
499 .then_some(style.vertical_placement),
500 leading_distribution: (style.leading_distribution != default.leading_distribution)
501 .then_some(style.leading_distribution),
502 }
503}
504
505pub fn preset_text_refinement(
506 theme: &impl ThemeTokenRead,
507 preset: TypographyPreset,
508) -> TextStyleRefinement {
509 refinement_from_style(&preset.resolve(theme))
510}
511
512pub fn composable_preset_text_refinement(
513 theme: &impl ThemeTokenRead,
514 preset: TypographyPreset,
515) -> TextStyleRefinement {
516 composable_refinement_from_style(&preset.resolve(theme))
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use fret_ui::element::{ContainerProps, ElementKind};
523 use fret_ui::elements::GlobalElementId;
524 use fret_ui::{Theme, ThemeConfig};
525
526 #[test]
527 fn title_text_refinement_uses_ui_semibold_and_tight_line_height_fallback() {
528 let mut app = fret_app::App::default();
529 Theme::with_global_mut(&mut app, |theme| {
530 theme.apply_config(&ThemeConfig {
531 name: "Test".to_string(),
532 metrics: std::collections::HashMap::from([(
533 "component.card.title_px".to_string(),
534 18.0,
535 )]),
536 ..ThemeConfig::default()
537 });
538 });
539 let theme = Theme::global(&app).snapshot();
540
541 let refinement = title_text_refinement(&theme, "component.card.title");
542 assert_eq!(refinement.font, Some(FontId::ui()));
543 assert_eq!(refinement.size, Some(Px(18.0)));
544 assert_eq!(refinement.weight, Some(FontWeight::SEMIBOLD));
545 assert_eq!(refinement.line_height, Some(Px(18.0)));
546 assert_eq!(
547 refinement.line_height_policy,
548 Some(TextLineHeightPolicy::FixedFromStyle)
549 );
550 assert_eq!(refinement.vertical_placement, None);
551 }
552
553 #[test]
554 fn description_text_refinement_uses_component_metrics_and_fixed_policy() {
555 let mut app = fret_app::App::default();
556 Theme::with_global_mut(&mut app, |theme| {
557 theme.apply_config(&ThemeConfig {
558 name: "Test".to_string(),
559 metrics: std::collections::HashMap::from([
560 ("font.size".to_string(), 14.0),
561 ("font.line_height".to_string(), 20.0),
562 ("component.dialog.description_px".to_string(), 13.0),
563 ("component.dialog.description_line_height".to_string(), 18.0),
564 ]),
565 ..ThemeConfig::default()
566 });
567 });
568 let theme = Theme::global(&app).snapshot();
569
570 let refinement = description_text_refinement(&theme, "component.dialog.description");
571 assert_eq!(refinement.size, Some(Px(13.0)));
572 assert_eq!(refinement.line_height, Some(Px(18.0)));
573 assert_eq!(
574 refinement.line_height_policy,
575 Some(TextLineHeightPolicy::FixedFromStyle)
576 );
577 }
578
579 #[test]
580 fn scope_description_text_attaches_color_and_inherited_refinement() {
581 let mut app = fret_app::App::default();
582 Theme::with_global_mut(&mut app, |theme| {
583 theme.apply_config(&ThemeConfig {
584 name: "Test".to_string(),
585 metrics: std::collections::HashMap::from([
586 ("font.size".to_string(), 14.0),
587 ("font.line_height".to_string(), 20.0),
588 ("component.card.description_px".to_string(), 12.0),
589 ("component.card.description_line_height".to_string(), 17.0),
590 ]),
591 colors: std::collections::HashMap::from([(
592 "muted-foreground".to_string(),
593 "#778899".to_string(),
594 )]),
595 ..ThemeConfig::default()
596 });
597 });
598 let theme = Theme::global(&app).snapshot();
599 let element = scope_description_text(
600 AnyElement::new(
601 GlobalElementId(1),
602 ElementKind::Container(ContainerProps::default()),
603 Vec::new(),
604 ),
605 &theme,
606 "component.card.description",
607 );
608
609 assert_eq!(
610 element.inherited_foreground,
611 Some(muted_foreground_color(&theme))
612 );
613 assert_eq!(
614 element.inherited_text_style,
615 Some(description_text_refinement(
616 &theme,
617 "component.card.description"
618 ))
619 );
620 }
621
622 #[test]
623 fn preset_text_refinement_matches_resolved_preset() {
624 let mut app = fret_app::App::default();
625 Theme::with_global_mut(&mut app, |theme| {
626 theme.apply_config(&ThemeConfig {
627 name: "Test".to_string(),
628 metrics: std::collections::HashMap::from([
629 ("font.size".to_string(), 14.0),
630 ("font.line_height".to_string(), 20.0),
631 (
632 crate::theme_tokens::metric::COMPONENT_TEXT_SM_PX.to_string(),
633 13.0,
634 ),
635 (
636 crate::theme_tokens::metric::COMPONENT_TEXT_SM_LINE_HEIGHT.to_string(),
637 18.0,
638 ),
639 ]),
640 ..ThemeConfig::default()
641 });
642 });
643 let theme = Theme::global(&app).snapshot();
644 let preset = TypographyPreset::control_ui(UiTextSize::Sm);
645 let style = preset.resolve(&theme);
646
647 assert_eq!(
648 preset_text_refinement(&theme, preset),
649 refinement_from_style(&style)
650 );
651 }
652
653 #[test]
654 fn composable_refinement_keeps_size_but_omits_default_emphasis() {
655 let style = TextStyle {
656 font: FontId::ui(),
657 size: Px(13.0),
658 line_height: Some(Px(18.0)),
659 line_height_policy: TextLineHeightPolicy::FixedFromStyle,
660 vertical_placement: TextVerticalPlacement::BoundsAsLineBox,
661 ..Default::default()
662 };
663
664 let refinement = composable_refinement_from_style(&style);
665 assert_eq!(refinement.font, None);
666 assert_eq!(refinement.size, Some(Px(13.0)));
667 assert_eq!(refinement.weight, None);
668 assert_eq!(refinement.slant, None);
669 assert_eq!(refinement.line_height, Some(Px(18.0)));
670 assert_eq!(
671 refinement.line_height_policy,
672 Some(TextLineHeightPolicy::FixedFromStyle)
673 );
674 assert_eq!(
675 refinement.vertical_placement,
676 Some(TextVerticalPlacement::BoundsAsLineBox)
677 );
678 }
679
680 #[test]
681 fn preset_text_style_with_overrides_updates_weight_and_slant() {
682 let app = fret_app::App::default();
683 let theme = fret_ui::Theme::global(&app).clone();
684 let preset = TypographyPreset::control_ui(UiTextSize::Sm);
685 let mut expected = preset.resolve(&theme);
686 expected.weight = FontWeight::MEDIUM;
687 expected.slant = TextSlant::Italic;
688
689 assert_eq!(
690 preset_text_style_with_overrides(
691 &theme,
692 preset,
693 Some(FontWeight::MEDIUM),
694 Some(TextSlant::Italic),
695 ),
696 expected
697 );
698 }
699
700 #[test]
701 fn composable_preset_refinement_matches_composable_resolved_preset() {
702 let mut app = fret_app::App::default();
703 Theme::with_global_mut(&mut app, |theme| {
704 theme.apply_config(&ThemeConfig {
705 name: "Test".to_string(),
706 metrics: std::collections::HashMap::from([
707 ("font.size".to_string(), 14.0),
708 ("font.line_height".to_string(), 20.0),
709 (
710 crate::theme_tokens::metric::COMPONENT_TEXT_XS_PX.to_string(),
711 12.0,
712 ),
713 (
714 crate::theme_tokens::metric::COMPONENT_TEXT_XS_LINE_HEIGHT.to_string(),
715 16.0,
716 ),
717 ]),
718 ..ThemeConfig::default()
719 });
720 });
721 let theme = Theme::global(&app).snapshot();
722 let preset = TypographyPreset::control_ui(UiTextSize::Xs);
723 let style = preset.resolve(&theme);
724
725 assert_eq!(
726 composable_preset_text_refinement(&theme, preset),
727 composable_refinement_from_style(&style)
728 );
729 }
730
731 #[test]
732 fn with_intent_updates_line_height_policy() {
733 let base = TextStyle {
734 font: FontId::ui(),
735 size: Px(12.0),
736 line_height: Some(Px(16.0)),
737 ..Default::default()
738 };
739
740 let control = with_intent(base.clone(), TextIntent::Control);
741 assert_eq!(
742 control.line_height_policy,
743 TextLineHeightPolicy::FixedFromStyle
744 );
745 assert_eq!(
746 control.vertical_placement,
747 TextVerticalPlacement::BoundsAsLineBox
748 );
749
750 let content = with_intent(base, TextIntent::Content);
751 assert_eq!(
752 content.line_height_policy,
753 TextLineHeightPolicy::ExpandToFit
754 );
755 assert_eq!(
756 content.vertical_placement,
757 TextVerticalPlacement::CenterMetricsBox
758 );
759 }
760
761 #[test]
762 fn typography_preset_resolves_to_intended_policy() {
763 let mut app = fret_app::App::default();
764 Theme::with_global_mut(&mut app, |theme| {
765 theme.apply_config(&ThemeConfig {
766 name: "Test".to_string(),
767 metrics: std::collections::HashMap::from([
768 ("font.size".to_string(), 10.0),
769 ("font.line_height".to_string(), 15.0),
770 ]),
771 ..ThemeConfig::default()
772 });
773 });
774 let theme = Theme::global(&app).clone();
775
776 let control = TypographyPreset::control_ui(UiTextSize::Sm).resolve(&theme);
777 assert_eq!(
778 control.line_height_policy,
779 TextLineHeightPolicy::FixedFromStyle
780 );
781 assert_eq!(
782 control.vertical_placement,
783 TextVerticalPlacement::BoundsAsLineBox
784 );
785
786 let content = TypographyPreset::content_ui(UiTextSize::Sm).resolve(&theme);
787 assert_eq!(
788 content.line_height_policy,
789 TextLineHeightPolicy::ExpandToFit
790 );
791 assert_eq!(
792 content.vertical_placement,
793 TextVerticalPlacement::CenterMetricsBox
794 );
795 }
796
797 #[test]
798 fn control_text_styles_use_fixed_line_boxes() {
799 let mut app = fret_app::App::default();
800 Theme::with_global_mut(&mut app, |theme| {
801 theme.apply_config(&ThemeConfig {
802 name: "Test".to_string(),
803 metrics: std::collections::HashMap::from([
804 ("font.size".to_string(), 10.0),
805 ("font.line_height".to_string(), 15.0),
806 ]),
807 ..ThemeConfig::default()
808 });
809 });
810 let theme = Theme::global(&app).clone();
811
812 for size in [
813 UiTextSize::Xs,
814 UiTextSize::Sm,
815 UiTextSize::Base,
816 UiTextSize::Prose,
817 ] {
818 let style = control_text_style(&theme, size);
819 assert_eq!(
820 style.line_height_policy,
821 TextLineHeightPolicy::FixedFromStyle,
822 "expected control text styles to use fixed line boxes: size={size:?}, style={style:?}"
823 );
824 assert!(
825 style.line_height.is_some(),
826 "expected control text styles to set an explicit line height: size={size:?}, style={style:?}"
827 );
828 }
829 }
830
831 #[test]
832 fn content_text_styles_expand_to_fit() {
833 let mut app = fret_app::App::default();
834 Theme::with_global_mut(&mut app, |theme| {
835 theme.apply_config(&ThemeConfig {
836 name: "Test".to_string(),
837 metrics: std::collections::HashMap::from([
838 ("font.size".to_string(), 10.0),
839 ("font.line_height".to_string(), 15.0),
840 ]),
841 ..ThemeConfig::default()
842 });
843 });
844 let theme = Theme::global(&app).clone();
845
846 for size in [
847 UiTextSize::Xs,
848 UiTextSize::Sm,
849 UiTextSize::Base,
850 UiTextSize::Prose,
851 ] {
852 let style = content_text_style(&theme, size);
853 assert_eq!(
854 style.line_height_policy,
855 TextLineHeightPolicy::ExpandToFit,
856 "expected content text styles to expand to fit: size={size:?}, style={style:?}"
857 );
858 assert!(
859 style.line_height.is_some(),
860 "expected content text styles to keep an explicit line height: size={size:?}, style={style:?}"
861 );
862 }
863 }
864
865 #[test]
866 fn control_text_style_scaled_uses_theme_ratio_and_fixed_line_box() {
867 let mut app = fret_app::App::default();
868 Theme::with_global_mut(&mut app, |theme| {
869 theme.apply_config(&ThemeConfig {
870 name: "Test".to_string(),
871 metrics: std::collections::HashMap::from([
872 ("font.size".to_string(), 10.0),
873 ("font.line_height".to_string(), 15.0),
874 ]),
875 ..ThemeConfig::default()
876 });
877 });
878 let theme = Theme::global(&app).clone();
879
880 let style = control_text_style_scaled(&theme, FontId::ui(), Px(20.0));
881 assert_eq!(style.size, Px(20.0));
882 assert_eq!(style.line_height, Some(Px(30.0)));
883 assert_eq!(
884 style.line_height_policy,
885 TextLineHeightPolicy::FixedFromStyle
886 );
887 }
888
889 #[test]
890 fn text_area_control_text_style_scaled_uses_bounds_line_box_and_strut() {
891 let mut app = fret_app::App::default();
892 Theme::with_global_mut(&mut app, |theme| {
893 theme.apply_config(&ThemeConfig {
894 name: "Test".to_string(),
895 metrics: std::collections::HashMap::from([
896 ("font.size".to_string(), 12.0),
897 ("font.line_height".to_string(), 18.0),
898 ]),
899 ..ThemeConfig::default()
900 });
901 });
902 let theme = Theme::global(&app).clone();
903
904 let style = text_area_control_text_style_scaled(&theme, FontId::ui(), Px(18.0));
905 assert_eq!(
906 style.line_height_policy,
907 TextLineHeightPolicy::FixedFromStyle
908 );
909 assert_eq!(
910 style.vertical_placement,
911 TextVerticalPlacement::BoundsAsLineBox
912 );
913 assert!(style.strut_style.is_some());
914 }
915}