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::line_box::LineBox;
21use super::style::{SpanStyle, TextStyle};
22use super::unit::TextUnit;
23
24pub fn text_style_for_draw_style(style: &DrawTextStyle) -> TextStyle {
36 let mut span_style = SpanStyle {
37 font_size: TextUnit::Sp(style.resolved_font_size()),
38 font_weight: Some(FontWeight::new(style.font_weight.value())),
39 font_style: Some(match style.font_style {
40 DrawFontStyle::Normal => FontStyle::Normal,
41 DrawFontStyle::Italic | DrawFontStyle::Oblique => FontStyle::Italic,
44 }),
45 ..SpanStyle::default()
46 };
47 if let Some(family) = &style.font_family {
48 span_style.font_family = Some(FontFamily::from_name(family));
49 }
50 let letter_spacing = style.resolved_letter_spacing();
51 if letter_spacing != 0.0 {
52 span_style.letter_spacing = TextUnit::Sp(letter_spacing);
53 }
54
55 let mut text_style = TextStyle::from_span_style(span_style);
56 if let Some(line_height) = style.line_height {
57 if line_height.is_finite() && line_height > 0.0 {
58 text_style.paragraph_style.line_height = TextUnit::Sp(line_height);
59 }
60 }
61 text_style.paragraph_style.line_height_style = style.line_height_style;
62 text_style
63}
64
65pub fn draw_style_line_box(style: &DrawTextStyle) -> Option<LineBox> {
77 super::measure::resolved_line_box(&text_style_for_draw_style(style))
78}
79
80#[derive(Clone, Copy, Debug, Default)]
92pub struct AppContextTextMeasurer;
93
94impl AppContextTextMeasurer {
95 pub fn shared() -> Rc<dyn DrawTextMeasurer> {
98 thread_local! {
99 static SHARED: Rc<dyn DrawTextMeasurer> = Rc::new(AppContextTextMeasurer);
100 }
101 SHARED.with(Rc::clone)
102 }
103}
104
105impl DrawTextMeasurer for AppContextTextMeasurer {
106 fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement {
107 if crate::render_state::current_app_context().is_none() {
111 return estimate_text_measurement(text, style);
112 }
113
114 let text_style = text_style_for_draw_style(style);
115 let annotated = super::shared_plain_annotated_string(text);
116 let metrics = super::measure::measure_resolved_text(&annotated, &text_style);
117 let line_height = if metrics.line_height.is_finite() && metrics.line_height > 0.0 {
118 metrics.line_height
119 } else {
120 estimate_text_measurement(text, style).line_height
121 };
122 let first_baseline = super::measure::resolved_first_baseline(&text_style)
123 .unwrap_or_else(|| estimate_text_measurement(text, style).first_baseline);
124
125 if text.is_empty() {
126 return TextMeasurement::empty(line_height, first_baseline);
127 }
128
129 let line_count = metrics.line_count.max(1);
130 TextMeasurement {
131 size: Size::new(metrics.width.max(0.0), line_count as f32 * line_height),
135 line_height,
136 first_baseline,
137 line_count,
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use cranpose_ui_graphics::{FontWeight as DrawFontWeight, TextAlign, TextVerticalAlign};
146
147 #[test]
148 fn draw_style_maps_onto_span_attributes() {
149 let style = DrawTextStyle::new(23.0)
150 .with_font_family("Fira Sans")
151 .with_weight(DrawFontWeight::BOLD)
152 .with_style(DrawFontStyle::Italic)
153 .with_letter_spacing(1.5)
154 .with_line_height(30.0);
155 let mapped = text_style_for_draw_style(&style);
156
157 assert_eq!(mapped.span_style.font_size, TextUnit::Sp(23.0));
158 assert_eq!(mapped.span_style.font_weight, Some(FontWeight::BOLD));
159 assert_eq!(mapped.span_style.font_style, Some(FontStyle::Italic));
160 assert_eq!(
161 mapped.span_style.font_family,
162 Some(FontFamily::Named("Fira Sans".to_string()))
163 );
164 assert_eq!(mapped.span_style.letter_spacing, TextUnit::Sp(1.5));
165 assert_eq!(mapped.paragraph_style.line_height, TextUnit::Sp(30.0));
166 }
167
168 #[test]
169 fn draw_style_leaves_unset_attributes_unspecified() {
170 let mapped = text_style_for_draw_style(&DrawTextStyle::new(14.0));
171 assert_eq!(mapped.span_style.font_family, None);
172 assert!(mapped.span_style.letter_spacing.is_unspecified());
173 assert!(mapped.paragraph_style.line_height.is_unspecified());
174 assert_eq!(mapped.paragraph_style.line_height_style, None);
175 assert_eq!(mapped.span_style.color, None);
176 }
177
178 #[test]
179 fn a_drawn_run_and_a_composed_text_resolve_the_same_line_box() {
180 use crate::text::line_box::{line_box, FontExtent};
185 use crate::widgets::wear::wear_line_height_style;
186
187 let extent = FontExtent::new(32.0 * 1900.0 / 2048.0, 32.0 * 500.0 / 2048.0, 0.0);
188 let drawn = DrawTextStyle::new(32.0)
189 .with_line_height(36.0)
190 .with_line_height_style(wear_line_height_style());
191 let resolved = line_box(&text_style_for_draw_style(&drawn), extent, 36.0, 1.0);
192 let composed = line_box(
193 &crate::widgets::wear::WearTextStyle::TITLE_MEDIUM
194 .resolve(cranpose_ui_graphics::Color::WHITE),
195 extent,
196 36.0,
197 1.0,
198 );
199 assert_eq!(resolved, composed);
200 assert_eq!(resolved.height, 38.0);
203 assert_eq!(resolved.baseline, 30.0);
204
205 let unstyled = line_box(
208 &text_style_for_draw_style(&DrawTextStyle::new(32.0).with_line_height(36.0)),
209 extent,
210 36.0,
211 1.0,
212 );
213 assert_ne!(unstyled, resolved);
214 }
215
216 #[test]
217 fn alignment_never_reaches_the_paragraph_style() {
218 let style = DrawTextStyle::new(14.0)
221 .with_align(TextAlign::Center)
222 .with_vertical_align(TextVerticalAlign::Bottom);
223 let mapped = text_style_for_draw_style(&style);
224 assert_eq!(
225 mapped.paragraph_style.text_align,
226 super::super::paragraph::TextAlign::Unspecified
227 );
228 }
229
230 #[test]
231 fn oblique_and_italic_request_the_same_face() {
232 let italic =
233 text_style_for_draw_style(&DrawTextStyle::new(14.0).with_style(DrawFontStyle::Italic));
234 let oblique =
235 text_style_for_draw_style(&DrawTextStyle::new(14.0).with_style(DrawFontStyle::Oblique));
236 assert_eq!(italic.span_style.font_style, oblique.span_style.font_style);
237 }
238
239 #[test]
240 fn degenerate_font_sizes_are_resolved_before_they_reach_the_measurer() {
241 for size in [0.0, -3.0, f32::NAN] {
242 let mapped = text_style_for_draw_style(&DrawTextStyle::new(size));
243 assert_eq!(
244 mapped.span_style.font_size,
245 TextUnit::Sp(DrawTextStyle::DEFAULT_FONT_SIZE)
246 );
247 }
248 }
249
250 #[test]
251 fn measuring_without_an_app_context_falls_back_to_the_estimate() {
252 let style = DrawTextStyle::new(16.0);
253 assert_eq!(
254 AppContextTextMeasurer.measure_text("HELLO", &style),
255 estimate_text_measurement("HELLO", &style)
256 );
257 }
258}