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