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