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)]
70pub struct AppContextTextMeasurer;
71
72impl AppContextTextMeasurer {
73 pub fn shared() -> Rc<dyn DrawTextMeasurer> {
76 thread_local! {
77 static SHARED: Rc<dyn DrawTextMeasurer> = Rc::new(AppContextTextMeasurer);
78 }
79 SHARED.with(Rc::clone)
80 }
81}
82
83impl DrawTextMeasurer for AppContextTextMeasurer {
84 fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement {
85 if crate::render_state::current_app_context().is_none() {
89 return estimate_text_measurement(text, style);
90 }
91
92 let text_style = text_style_for_draw_style(style);
93 let annotated = super::shared_plain_annotated_string(text);
94 let metrics = super::measure::measure_resolved_text(&annotated, &text_style);
95 let line_height = if metrics.line_height.is_finite() && metrics.line_height > 0.0 {
96 metrics.line_height
97 } else {
98 estimate_text_measurement(text, style).line_height
99 };
100 let first_baseline = super::measure::resolved_first_baseline(&text_style)
101 .unwrap_or_else(|| estimate_text_measurement(text, style).first_baseline);
102
103 if text.is_empty() {
104 return TextMeasurement::empty(line_height, first_baseline);
105 }
106
107 let line_count = metrics.line_count.max(1);
108 TextMeasurement {
109 size: Size::new(metrics.width.max(0.0), line_count as f32 * line_height),
113 line_height,
114 first_baseline,
115 line_count,
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use cranpose_ui_graphics::{FontWeight as DrawFontWeight, TextAlign, TextVerticalAlign};
124
125 #[test]
126 fn draw_style_maps_onto_span_attributes() {
127 let style = DrawTextStyle::new(23.0)
128 .with_font_family("Fira Sans")
129 .with_weight(DrawFontWeight::BOLD)
130 .with_style(DrawFontStyle::Italic)
131 .with_letter_spacing(1.5)
132 .with_line_height(30.0);
133 let mapped = text_style_for_draw_style(&style);
134
135 assert_eq!(mapped.span_style.font_size, TextUnit::Sp(23.0));
136 assert_eq!(mapped.span_style.font_weight, Some(FontWeight::BOLD));
137 assert_eq!(mapped.span_style.font_style, Some(FontStyle::Italic));
138 assert_eq!(
139 mapped.span_style.font_family,
140 Some(FontFamily::Named("Fira Sans".to_string()))
141 );
142 assert_eq!(mapped.span_style.letter_spacing, TextUnit::Sp(1.5));
143 assert_eq!(mapped.paragraph_style.line_height, TextUnit::Sp(30.0));
144 }
145
146 #[test]
147 fn draw_style_leaves_unset_attributes_unspecified() {
148 let mapped = text_style_for_draw_style(&DrawTextStyle::new(14.0));
149 assert_eq!(mapped.span_style.font_family, None);
150 assert!(mapped.span_style.letter_spacing.is_unspecified());
151 assert!(mapped.paragraph_style.line_height.is_unspecified());
152 assert_eq!(mapped.span_style.color, None);
153 }
154
155 #[test]
156 fn alignment_never_reaches_the_paragraph_style() {
157 let style = DrawTextStyle::new(14.0)
160 .with_align(TextAlign::Center)
161 .with_vertical_align(TextVerticalAlign::Bottom);
162 let mapped = text_style_for_draw_style(&style);
163 assert_eq!(
164 mapped.paragraph_style.text_align,
165 super::super::paragraph::TextAlign::Unspecified
166 );
167 }
168
169 #[test]
170 fn oblique_and_italic_request_the_same_face() {
171 let italic =
172 text_style_for_draw_style(&DrawTextStyle::new(14.0).with_style(DrawFontStyle::Italic));
173 let oblique =
174 text_style_for_draw_style(&DrawTextStyle::new(14.0).with_style(DrawFontStyle::Oblique));
175 assert_eq!(italic.span_style.font_style, oblique.span_style.font_style);
176 }
177
178 #[test]
179 fn degenerate_font_sizes_are_resolved_before_they_reach_the_measurer() {
180 for size in [0.0, -3.0, f32::NAN] {
181 let mapped = text_style_for_draw_style(&DrawTextStyle::new(size));
182 assert_eq!(
183 mapped.span_style.font_size,
184 TextUnit::Sp(DrawTextStyle::DEFAULT_FONT_SIZE)
185 );
186 }
187 }
188
189 #[test]
190 fn measuring_without_an_app_context_falls_back_to_the_estimate() {
191 let style = DrawTextStyle::new(16.0);
192 assert_eq!(
193 AppContextTextMeasurer.measure_text("HELLO", &style),
194 estimate_text_measurement("HELLO", &style)
195 );
196 }
197}