cranpose_ui/text/
draw_scope_text.rs1use std::rc::Rc;
13
14use cranpose_ui_graphics::{
15 estimate_text_measurement, DrawTextMeasurer, FontStyle as DrawFontStyle, Size, TextMeasurement,
16 TextStyle as DrawTextStyle,
17};
18
19use super::font::{FontFamily, FontStyle, FontWeight};
20use super::style::{SpanStyle, TextStyle};
21use super::unit::TextUnit;
22
23pub fn text_style_for_draw_style(style: &DrawTextStyle) -> TextStyle {
30 let mut span_style = SpanStyle {
31 font_size: TextUnit::Sp(style.resolved_font_size()),
32 font_weight: Some(FontWeight::new(style.font_weight.value())),
33 font_style: Some(match style.font_style {
34 DrawFontStyle::Normal => FontStyle::Normal,
35 DrawFontStyle::Italic | DrawFontStyle::Oblique => FontStyle::Italic,
38 }),
39 ..SpanStyle::default()
40 };
41 if let Some(family) = &style.font_family {
42 span_style.font_family = Some(FontFamily::from_name(family));
43 }
44 let letter_spacing = style.resolved_letter_spacing();
45 if letter_spacing != 0.0 {
46 span_style.letter_spacing = TextUnit::Sp(letter_spacing);
47 }
48
49 let mut text_style = TextStyle::from_span_style(span_style);
50 if let Some(line_height) = style.line_height {
51 if line_height.is_finite() && line_height > 0.0 {
52 text_style.paragraph_style.line_height = TextUnit::Sp(line_height);
53 }
54 }
55 text_style
56}
57
58#[derive(Clone, Copy, Debug, Default)]
65pub struct AppContextTextMeasurer;
66
67impl AppContextTextMeasurer {
68 pub fn shared() -> Rc<dyn DrawTextMeasurer> {
71 thread_local! {
72 static SHARED: Rc<dyn DrawTextMeasurer> = Rc::new(AppContextTextMeasurer);
73 }
74 SHARED.with(Rc::clone)
75 }
76}
77
78impl DrawTextMeasurer for AppContextTextMeasurer {
79 fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement {
80 if crate::render_state::current_app_context().is_none() {
84 return estimate_text_measurement(text, style);
85 }
86
87 let text_style = text_style_for_draw_style(style);
88 let annotated = super::shared_plain_annotated_string(text);
89 let metrics = super::measure_text(&annotated, &text_style);
90 let line_height = if metrics.line_height.is_finite() && metrics.line_height > 0.0 {
91 metrics.line_height
92 } else {
93 estimate_text_measurement(text, style).line_height
94 };
95 let first_baseline = super::measure::first_baseline(&text_style)
96 .unwrap_or_else(|| estimate_text_measurement(text, style).first_baseline);
97
98 if text.is_empty() {
99 return TextMeasurement::empty(line_height, first_baseline);
100 }
101
102 let line_count = metrics.line_count.max(1);
103 TextMeasurement {
104 size: Size::new(metrics.width.max(0.0), line_count as f32 * line_height),
108 line_height,
109 first_baseline,
110 line_count,
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use cranpose_ui_graphics::{FontWeight as DrawFontWeight, TextAlign, TextVerticalAlign};
119
120 #[test]
121 fn draw_style_maps_onto_span_attributes() {
122 let style = DrawTextStyle::new(23.0)
123 .with_font_family("Fira Sans")
124 .with_weight(DrawFontWeight::BOLD)
125 .with_style(DrawFontStyle::Italic)
126 .with_letter_spacing(1.5)
127 .with_line_height(30.0);
128 let mapped = text_style_for_draw_style(&style);
129
130 assert_eq!(mapped.span_style.font_size, TextUnit::Sp(23.0));
131 assert_eq!(mapped.span_style.font_weight, Some(FontWeight::BOLD));
132 assert_eq!(mapped.span_style.font_style, Some(FontStyle::Italic));
133 assert_eq!(
134 mapped.span_style.font_family,
135 Some(FontFamily::Named("Fira Sans".to_string()))
136 );
137 assert_eq!(mapped.span_style.letter_spacing, TextUnit::Sp(1.5));
138 assert_eq!(mapped.paragraph_style.line_height, TextUnit::Sp(30.0));
139 }
140
141 #[test]
142 fn draw_style_leaves_unset_attributes_unspecified() {
143 let mapped = text_style_for_draw_style(&DrawTextStyle::new(14.0));
144 assert_eq!(mapped.span_style.font_family, None);
145 assert!(mapped.span_style.letter_spacing.is_unspecified());
146 assert!(mapped.paragraph_style.line_height.is_unspecified());
147 assert_eq!(mapped.span_style.color, None);
148 }
149
150 #[test]
151 fn alignment_never_reaches_the_paragraph_style() {
152 let style = DrawTextStyle::new(14.0)
155 .with_align(TextAlign::Center)
156 .with_vertical_align(TextVerticalAlign::Bottom);
157 let mapped = text_style_for_draw_style(&style);
158 assert_eq!(
159 mapped.paragraph_style.text_align,
160 super::super::paragraph::TextAlign::Unspecified
161 );
162 }
163
164 #[test]
165 fn oblique_and_italic_request_the_same_face() {
166 let italic =
167 text_style_for_draw_style(&DrawTextStyle::new(14.0).with_style(DrawFontStyle::Italic));
168 let oblique =
169 text_style_for_draw_style(&DrawTextStyle::new(14.0).with_style(DrawFontStyle::Oblique));
170 assert_eq!(italic.span_style.font_style, oblique.span_style.font_style);
171 }
172
173 #[test]
174 fn degenerate_font_sizes_are_resolved_before_they_reach_the_measurer() {
175 for size in [0.0, -3.0, f32::NAN] {
176 let mapped = text_style_for_draw_style(&DrawTextStyle::new(size));
177 assert_eq!(
178 mapped.span_style.font_size,
179 TextUnit::Sp(DrawTextStyle::DEFAULT_FONT_SIZE)
180 );
181 }
182 }
183
184 #[test]
185 fn measuring_without_an_app_context_falls_back_to_the_estimate() {
186 let style = DrawTextStyle::new(16.0);
187 assert_eq!(
188 AppContextTextMeasurer.measure_text("HELLO", &style),
189 estimate_text_measurement("HELLO", &style)
190 );
191 }
192}