use std::rc::Rc;
use cranpose_ui_graphics::{
estimate_text_measurement, DrawTextMeasurer, FontStyle as DrawFontStyle, Size, TextMeasurement,
TextStyle as DrawTextStyle,
};
use super::font::{FontFamily, FontStyle, FontWeight};
use super::style::{SpanStyle, TextStyle};
use super::unit::TextUnit;
pub fn text_style_for_draw_style(style: &DrawTextStyle) -> TextStyle {
let mut span_style = SpanStyle {
font_size: TextUnit::Sp(style.resolved_font_size()),
font_weight: Some(FontWeight::new(style.font_weight.value())),
font_style: Some(match style.font_style {
DrawFontStyle::Normal => FontStyle::Normal,
DrawFontStyle::Italic | DrawFontStyle::Oblique => FontStyle::Italic,
}),
..SpanStyle::default()
};
if let Some(family) = &style.font_family {
span_style.font_family = Some(FontFamily::from_name(family));
}
let letter_spacing = style.resolved_letter_spacing();
if letter_spacing != 0.0 {
span_style.letter_spacing = TextUnit::Sp(letter_spacing);
}
let mut text_style = TextStyle::from_span_style(span_style);
if let Some(line_height) = style.line_height {
if line_height.is_finite() && line_height > 0.0 {
text_style.paragraph_style.line_height = TextUnit::Sp(line_height);
}
}
text_style
}
#[derive(Clone, Copy, Debug, Default)]
pub struct AppContextTextMeasurer;
impl AppContextTextMeasurer {
pub fn shared() -> Rc<dyn DrawTextMeasurer> {
thread_local! {
static SHARED: Rc<dyn DrawTextMeasurer> = Rc::new(AppContextTextMeasurer);
}
SHARED.with(Rc::clone)
}
}
impl DrawTextMeasurer for AppContextTextMeasurer {
fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement {
if crate::render_state::current_app_context().is_none() {
return estimate_text_measurement(text, style);
}
let text_style = text_style_for_draw_style(style);
let annotated = super::shared_plain_annotated_string(text);
let metrics = super::measure_text(&annotated, &text_style);
let line_height = if metrics.line_height.is_finite() && metrics.line_height > 0.0 {
metrics.line_height
} else {
estimate_text_measurement(text, style).line_height
};
let first_baseline = super::measure::first_baseline(&text_style)
.unwrap_or_else(|| estimate_text_measurement(text, style).first_baseline);
if text.is_empty() {
return TextMeasurement::empty(line_height, first_baseline);
}
let line_count = metrics.line_count.max(1);
TextMeasurement {
size: Size::new(metrics.width.max(0.0), line_count as f32 * line_height),
line_height,
first_baseline,
line_count,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cranpose_ui_graphics::{FontWeight as DrawFontWeight, TextAlign, TextVerticalAlign};
#[test]
fn draw_style_maps_onto_span_attributes() {
let style = DrawTextStyle::new(23.0)
.with_font_family("Fira Sans")
.with_weight(DrawFontWeight::BOLD)
.with_style(DrawFontStyle::Italic)
.with_letter_spacing(1.5)
.with_line_height(30.0);
let mapped = text_style_for_draw_style(&style);
assert_eq!(mapped.span_style.font_size, TextUnit::Sp(23.0));
assert_eq!(mapped.span_style.font_weight, Some(FontWeight::BOLD));
assert_eq!(mapped.span_style.font_style, Some(FontStyle::Italic));
assert_eq!(
mapped.span_style.font_family,
Some(FontFamily::Named("Fira Sans".to_string()))
);
assert_eq!(mapped.span_style.letter_spacing, TextUnit::Sp(1.5));
assert_eq!(mapped.paragraph_style.line_height, TextUnit::Sp(30.0));
}
#[test]
fn draw_style_leaves_unset_attributes_unspecified() {
let mapped = text_style_for_draw_style(&DrawTextStyle::new(14.0));
assert_eq!(mapped.span_style.font_family, None);
assert!(mapped.span_style.letter_spacing.is_unspecified());
assert!(mapped.paragraph_style.line_height.is_unspecified());
assert_eq!(mapped.span_style.color, None);
}
#[test]
fn alignment_never_reaches_the_paragraph_style() {
let style = DrawTextStyle::new(14.0)
.with_align(TextAlign::Center)
.with_vertical_align(TextVerticalAlign::Bottom);
let mapped = text_style_for_draw_style(&style);
assert_eq!(
mapped.paragraph_style.text_align,
super::super::paragraph::TextAlign::Unspecified
);
}
#[test]
fn oblique_and_italic_request_the_same_face() {
let italic =
text_style_for_draw_style(&DrawTextStyle::new(14.0).with_style(DrawFontStyle::Italic));
let oblique =
text_style_for_draw_style(&DrawTextStyle::new(14.0).with_style(DrawFontStyle::Oblique));
assert_eq!(italic.span_style.font_style, oblique.span_style.font_style);
}
#[test]
fn degenerate_font_sizes_are_resolved_before_they_reach_the_measurer() {
for size in [0.0, -3.0, f32::NAN] {
let mapped = text_style_for_draw_style(&DrawTextStyle::new(size));
assert_eq!(
mapped.span_style.font_size,
TextUnit::Sp(DrawTextStyle::DEFAULT_FONT_SIZE)
);
}
}
#[test]
fn measuring_without_an_app_context_falls_back_to_the_estimate() {
let style = DrawTextStyle::new(16.0);
assert_eq!(
AppContextTextMeasurer.measure_text("HELLO", &style),
estimate_text_measurement("HELLO", &style)
);
}
}