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