use crate::geometry::Rect;
use crate::layout::Cell;
use crate::scales::chrome::AxisSide;
use crate::scene::SceneBuilder;
pub(crate) fn text_cell_for_element(
s: &str,
el: &crate::plot::theme::TextElement,
parent_pt: f64,
dpi: f64,
theme: &crate::plot::theme::Theme,
) -> Cell {
use crate::plot::theme::text_concrete_defaults;
let style = text_style_from(el, parent_pt);
let run = measure_for_element(s, el, &style, dpi, theme);
let margin = el
.margin
.or(text_concrete_defaults().margin)
.expect("text_concrete_defaults sets margin");
let (mt, mr, mb, ml) = margin.resolve(parent_pt);
let pt_to_px = dpi / 72.0;
let margins_px = (mt * pt_to_px, mr * pt_to_px, mb * pt_to_px, ml * pt_to_px);
if margins_px.0 == 0.0 && margins_px.1 == 0.0 && margins_px.2 == 0.0 && margins_px.3 == 0.0 {
Cell::measured_boxed(run)
} else {
Cell::measured(crate::text::WithMargin::new(run, margins_px))
}
}
pub(crate) fn measure_for_element(
s: &str,
el: &crate::plot::theme::TextElement,
style: &crate::text::TextStyle,
dpi: f64,
theme: &crate::plot::theme::Theme,
) -> Box<dyn crate::layout::Measure> {
use crate::plot::theme::text_concrete_defaults;
if matches!(el.markdown, Some(true)) {
let color = el
.color
.clone()
.or_else(|| text_concrete_defaults().color.clone())
.expect("color default");
return Box::new(crate::text::rich::RichTextRun::new(
s,
style,
color.resolve(&theme.palette),
&theme.rich_text,
&theme.palette,
dpi,
));
}
Box::new(crate::text::TextRun::new(s, style, dpi))
}
pub(crate) fn text_style_from(
el: &crate::plot::theme::TextElement,
parent_pt: f64,
) -> crate::text::TextStyle {
use crate::plot::theme::{text_concrete_defaults, FontFamily, FontStyle, FontWidth, Length};
use crate::text::{
FontFamilyEntry, FontFeatureSetting, FontStyleKind, FontVariationSetting,
GenericFamilyKind, LineHeight,
};
let defaults = text_concrete_defaults();
let size_len = el.size_pt.or(defaults.size_pt).expect("size_pt default");
let size = size_len.resolve(parent_pt) as f32;
let mut style = crate::text::TextStyle::new(size);
let lineheight = el
.lineheight
.or(defaults.lineheight)
.expect("lineheight default");
style = style.line_height(match lineheight {
Length::Rel(mult) => LineHeight::Relative(mult as f32),
Length::Abs(pt) => LineHeight::Absolute(pt as f32),
});
let letter_spacing = el
.letter_spacing
.or(defaults.letter_spacing)
.expect("letter_spacing default");
let letter_spacing_pt = match letter_spacing {
Length::Abs(pt) => pt,
Length::Rel(mult) => mult * size as f64,
};
style = style.letter_spacing_pt(letter_spacing_pt as f32);
let underline = el
.underline
.or(defaults.underline)
.expect("underline default");
style = style.underline(underline);
let strikethrough = el
.strikethrough
.or(defaults.strikethrough)
.expect("strikethrough default");
style = style.strikethrough(strikethrough);
if let Some(weight) = el.font.weight {
style = style.weight(weight.0);
}
if let Some(width) = el.font.width {
style = style.width(match width {
FontWidth::UltraCondensed => 0.5,
FontWidth::ExtraCondensed => 0.625,
FontWidth::Condensed => 0.75,
FontWidth::SemiCondensed => 0.875,
FontWidth::Normal => 1.0,
FontWidth::SemiExpanded => 1.125,
FontWidth::Expanded => 1.25,
FontWidth::ExtraExpanded => 1.5,
FontWidth::UltraExpanded => 2.0,
});
}
style = style.style(match el.font.style {
Some(FontStyle::Italic) => FontStyleKind::Italic,
Some(FontStyle::Oblique(angle)) => FontStyleKind::Oblique(angle),
Some(FontStyle::Normal) | None => FontStyleKind::Normal,
});
if let Some(family) = &el.font.family {
let entries: Vec<FontFamilyEntry> = match family {
FontFamily::Named(names) => names
.iter()
.map(|n| FontFamilyEntry::Named(n.clone()))
.collect(),
FontFamily::Serif => vec![FontFamilyEntry::Generic(GenericFamilyKind::Serif)],
FontFamily::SansSerif => vec![FontFamilyEntry::Generic(GenericFamilyKind::SansSerif)],
FontFamily::Mono => vec![FontFamilyEntry::Generic(GenericFamilyKind::Mono)],
FontFamily::Cursive => vec![FontFamilyEntry::Generic(GenericFamilyKind::Cursive)],
FontFamily::Fantasy => vec![FontFamilyEntry::Generic(GenericFamilyKind::Fantasy)],
FontFamily::SystemUi => vec![FontFamilyEntry::Generic(GenericFamilyKind::SystemUi)],
};
style = style.families(entries);
}
if !el.font.features.is_empty() {
let features: Vec<FontFeatureSetting> = el
.font
.features
.iter()
.map(|f| FontFeatureSetting {
tag: f.tag,
value: f.value.min(u16::MAX as u32) as u16,
})
.collect();
style = style.features(features);
}
if !el.font.variations.is_empty() {
let variations: Vec<FontVariationSetting> = el
.font
.variations
.iter()
.map(|v| FontVariationSetting {
tag: v.tag,
value: v.value,
})
.collect();
style = style.variations(variations);
}
style
}
#[derive(Debug, Clone)]
pub(crate) struct TextOutline {
pub brush: crate::brush::Brush,
pub stroke: crate::stroke::Stroke,
}
pub(crate) fn text_outline_from(
el: &crate::plot::theme::TextElement,
palette: &crate::plot::theme::Palette,
dpi: f64,
) -> Option<TextOutline> {
let color = el.text_stroke.as_ref()?.resolve(palette);
let width_pt = el
.text_linewidth_pt
.or_else(|| crate::plot::theme::text_concrete_defaults().text_linewidth_pt)
.expect("text_concrete_defaults sets text_linewidth_pt")
.resolve(crate::plot::theme::DEFAULT_LINEWIDTH_PT);
let width_px = width_pt * dpi / 72.0;
if !width_px.is_finite() || width_px <= 0.0 {
return None;
}
Some(TextOutline {
brush: crate::brush::Brush::Solid(color),
stroke: crate::stroke::Stroke::new(width_px),
})
}
pub(crate) fn draw_text_outline_pass(
scene: &mut dyn SceneBuilder,
outline: Option<&TextOutline>,
run: &crate::text::TextRun,
x: f64,
y: f64,
transform: crate::geometry::Affine,
) {
if let Some(o) = outline {
crate::text::draw_text_outline(
scene,
run,
x,
y,
&o.brush,
&o.stroke,
transform,
crate::pick::PickId::Skip,
);
}
}
pub(crate) fn effective_text(
slot: &crate::plot::theme::Element<crate::plot::theme::TextElement>,
root: &crate::plot::theme::TextElement,
) -> Option<crate::plot::theme::TextElement> {
match slot {
crate::plot::theme::Element::Blank => None,
crate::plot::theme::Element::Inherit => Some(root.clone()),
crate::plot::theme::Element::Set(el) => Some(el.cascade(root)),
}
}
pub(crate) fn axis_title_cell(
title: &str,
side: AxisSide,
theme: &crate::plot::theme::Theme,
dpi: f64,
) -> Cell {
let (ch, side_idx) = crate::plot::chrome::axis::axis_side_to_channel_side(side);
let resolved = theme.resolved_axis(ch, side_idx);
let root_pt = crate::plot::chrome::root_text_pt(theme);
let Some(el) = resolved.title else {
return Cell::empty();
};
let style = text_style_from(&el, root_pt);
let run = measure_for_element(title, &el, &style, dpi, theme);
if side.is_vertical() {
Cell::measured(RotatedAxisTitleMeasure {
rotated_w: run.height_at(f64::INFINITY, dpi),
})
} else {
Cell::measured_boxed(run)
}
}
struct RotatedAxisTitleMeasure {
rotated_w: f64,
}
impl crate::layout::Measure for RotatedAxisTitleMeasure {
fn width_hint(&self, _dpi: f64) -> crate::layout::WidthHint {
crate::layout::WidthHint::Min(self.rotated_w)
}
fn height_at(&self, _width: f64, _dpi: f64) -> f64 {
0.0
}
fn width_at(&self, _height: f64, _dpi: f64) -> f64 {
self.rotated_w
}
}
pub(crate) fn rotated_wrap_width(w: f64, h: f64, angle_rad: f64) -> f64 {
w * angle_rad.cos().abs() + h * angle_rad.sin().abs()
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_text_element_in_rect(
scene: &mut dyn SceneBuilder,
text: &str,
el: &crate::plot::theme::TextElement,
rect: Rect,
palette: &crate::plot::theme::Palette,
parent_pt: f64,
dpi: f64,
pick_id: crate::pick::PickId,
sheet: Option<&std::sync::Arc<crate::text::rich::RichTextStyleSheet>>,
) {
use crate::brush::Brush;
use crate::geometry::{Affine, Vec2};
use crate::plot::theme::{text_concrete_defaults, HAlign, Rotation, VAlign};
use crate::text::rich::{draw_rich_text, HAnchor, RichAnchor, RichTextRun, VAnchor};
use crate::text::{draw_text, TextRun};
let defaults = text_concrete_defaults();
let margin = el.margin.or(defaults.margin).expect("margin default");
let (mt, mr, mb, ml) = margin.resolve(parent_pt);
let pt_to_px = dpi / 72.0;
let inset = Rect::new(
rect.x0 + ml * pt_to_px,
rect.y0 + mt * pt_to_px,
(rect.x1 - mr * pt_to_px).max(rect.x0 + ml * pt_to_px),
(rect.y1 - mb * pt_to_px).max(rect.y0 + mt * pt_to_px),
);
let style = text_style_from(el, parent_pt);
let color = el
.color
.clone()
.or_else(|| defaults.color.clone())
.expect("color default");
let brush = Brush::Solid(color.resolve(palette));
let outline = text_outline_from(el, palette, dpi);
let use_markdown = matches!(el.markdown, Some(true)) && sheet.is_some();
if use_markdown {
let sheet = sheet.expect("sheet checked above");
let align_h = el.align.or(defaults.align).expect("align default");
let align_v = el.valign.or(defaults.valign).expect("valign default");
let angle = el.angle.or(defaults.angle).expect("angle default");
let angle_rad = match angle {
Rotation::Degrees(d) => (d as f64).to_radians(),
Rotation::Along | Rotation::Across => 0.0,
};
let inner_w = inset.x1 - inset.x0;
let inner_h = inset.y1 - inset.y0;
let along_px = rotated_wrap_width(inner_w, inner_h, angle_rad);
let cross_px = rotated_wrap_width(inner_h, inner_w, angle_rad);
let base_brush_col = color.resolve(palette);
let outlined_sheet: Option<std::sync::Arc<_>> = match (&el.text_stroke, outline.as_ref()) {
(Some(stroke_color), Some(o)) => {
let mut s = (**sheet).clone();
let base = s.get("base").cloned().unwrap_or_default();
s.set(
"base",
crate::text::rich::StyleDelta {
text_stroke: Some(stroke_color.clone()),
text_stroke_width: Some(crate::text::rich::pt(o.stroke.width * 72.0 / dpi)),
..base
},
);
Some(std::sync::Arc::new(s))
}
_ => None,
};
let sheet = outlined_sheet.as_ref().unwrap_or(sheet);
let rich = RichTextRun::new(text, &style, base_brush_col, sheet, palette, dpi);
rich.set_max_width(along_px as f32, align_h);
let block_w = rich.content_width();
let block_h = rich.current_height();
let hf = match align_h {
HAlign::Start => 0.0,
HAlign::Center | HAlign::Justify => 0.5,
HAlign::End => 1.0,
};
let vf = match align_v {
VAlign::Top | VAlign::Baseline => 0.0,
VAlign::Middle => 0.5,
VAlign::Bottom => 1.0,
};
if angle_rad.abs() < 1e-9 {
let tx = inset.x0 + (along_px - block_w) * hf;
let ty = inset.y0 + (cross_px - block_h) * vf;
draw_rich_text(
scene,
&rich,
tx,
ty,
RichAnchor {
h: HAnchor::Left,
v: VAnchor::Top,
},
Affine::IDENTITY,
pick_id,
);
} else {
let centre = Vec2::new((inset.x0 + inset.x1) * 0.5, (inset.y0 + inset.y1) * 0.5);
let transform = Affine::translate(centre)
* Affine::rotate(angle_rad)
* Affine::translate(Vec2::new(
-along_px * 0.5 + (along_px - block_w) * hf,
-cross_px * 0.5 + (cross_px - block_h) * vf,
));
draw_rich_text(
scene,
&rich,
0.0,
0.0,
RichAnchor {
h: HAnchor::Left,
v: VAnchor::Top,
},
transform,
pick_id,
);
}
return;
}
let run = TextRun::new(text, &style, dpi);
let alignment = el.align.or(defaults.align).expect("align default");
let angle = el.angle.or(defaults.angle).expect("angle default");
let angle_rad = match angle {
Rotation::Degrees(d) => (d as f64).to_radians(),
Rotation::Along | Rotation::Across => 0.0,
};
let inner_w = inset.x1 - inset.x0;
let inner_h = inset.y1 - inset.y0;
let along_px = rotated_wrap_width(inner_w, inner_h, angle_rad);
let cross_px = rotated_wrap_width(inner_h, inner_w, angle_rad);
let _ = run.set_max_width(along_px as f32, alignment);
let block_h = run.inked_height();
let ascender_offset = run.first_line_ascender_offset();
let valign = el.valign.or(defaults.valign).expect("valign default");
let cross_offset = match valign {
VAlign::Top | VAlign::Baseline => 0.0,
VAlign::Middle => ((cross_px - block_h) * 0.5).max(0.0),
VAlign::Bottom => (cross_px - block_h).max(0.0),
};
if angle_rad.abs() < 1e-9 {
let (tx, ty) = (inset.x0, inset.y0 + cross_offset - ascender_offset);
draw_text_outline_pass(scene, outline.as_ref(), &run, tx, ty, Affine::IDENTITY);
draw_text(scene, &run, tx, ty, &brush, Affine::IDENTITY, pick_id);
} else {
let centre = Vec2::new((inset.x0 + inset.x1) * 0.5, (inset.y0 + inset.y1) * 0.5);
let transform = Affine::translate(centre)
* Affine::rotate(angle_rad)
* Affine::translate(Vec2::new(
-along_px * 0.5,
cross_offset - cross_px * 0.5 - ascender_offset,
));
draw_text_outline_pass(scene, outline.as_ref(), &run, 0.0, 0.0, transform);
draw_text(scene, &run, 0.0, 0.0, &brush, transform, pick_id);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_axis_title(
scene: &mut dyn SceneBuilder,
run: &crate::text::TextRun,
rect: Rect,
side: AxisSide,
brush: &crate::brush::Brush,
outline: Option<&TextOutline>,
angle: crate::plot::theme::Rotation,
) {
use crate::geometry::{Affine, Vec2};
use crate::plot::theme::HAlign;
use crate::text::draw_text;
let cx = (rect.x0 + rect.x1) * 0.5;
let cy = (rect.y0 + rect.y1) * 0.5;
let pid = crate::pick::PickId::Skip;
let baseline_deg: f32 = match side {
AxisSide::Top | AxisSide::Bottom => 0.0,
AxisSide::Left => -90.0,
AxisSide::Right => 90.0,
};
let resolved_deg = angle.resolve(baseline_deg);
let theta = (resolved_deg as f64).to_radians();
if theta.abs() < 1e-9 {
let w = (rect.x1 - rect.x0) as f32;
run.set_max_width(w, HAlign::Center);
draw_text_outline_pass(scene, outline, run, rect.x0, rect.y0, Affine::IDENTITY);
draw_text(scene, run, rect.x0, rect.y0, brush, Affine::IDENTITY, pid);
} else {
let h = run.set_max_width(f32::INFINITY, HAlign::Start) as f64;
let w = run.content_width();
let transform = Affine::translate(Vec2::new(cx, cy))
* Affine::rotate(theta)
* Affine::translate(Vec2::new(-w * 0.5, -h * 0.5));
draw_text_outline_pass(scene, outline, run, 0.0, 0.0, transform);
draw_text(scene, run, 0.0, 0.0, brush, transform, pid);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_axis_title_markdown(
scene: &mut dyn SceneBuilder,
text: &str,
style: &crate::text::TextStyle,
fill: crate::color::Color,
palette: &crate::plot::theme::Palette,
sheet: &std::sync::Arc<crate::text::rich::RichTextStyleSheet>,
dpi: f64,
rect: Rect,
side: AxisSide,
angle: crate::plot::theme::Rotation,
) {
use crate::geometry::{Affine, Vec2};
use crate::plot::theme::HAlign;
use crate::text::rich::{draw_rich_text, HAnchor, RichAnchor, RichTextRun, VAnchor};
let cx = (rect.x0 + rect.x1) * 0.5;
let cy = (rect.y0 + rect.y1) * 0.5;
let pid = crate::pick::PickId::Skip;
let baseline_deg: f32 = match side {
AxisSide::Top | AxisSide::Bottom => 0.0,
AxisSide::Left => -90.0,
AxisSide::Right => 90.0,
};
let resolved_deg = angle.resolve(baseline_deg);
let theta = (resolved_deg as f64).to_radians();
let run = RichTextRun::new(text, style, fill, sheet, palette, dpi);
if theta.abs() < 1e-9 {
let w = (rect.x1 - rect.x0) as f32;
run.set_max_width(w, HAlign::Center);
draw_rich_text(
scene,
&run,
rect.x0,
rect.y0,
RichAnchor {
h: HAnchor::Left,
v: VAnchor::Top,
},
Affine::IDENTITY,
pid,
);
} else {
let w = run.natural_width();
let h = run.natural_height();
let transform = Affine::translate(Vec2::new(cx, cy))
* Affine::rotate(theta)
* Affine::translate(Vec2::new(-w * 0.5, -h * 0.5));
draw_rich_text(
scene,
&run,
0.0,
0.0,
RichAnchor {
h: HAnchor::Left,
v: VAnchor::Top,
},
transform,
pid,
);
}
}
pub(crate) struct BoxMeasure(Box<dyn crate::layout::Measure>);
impl BoxMeasure {
pub(crate) fn new(inner: Box<dyn crate::layout::Measure>) -> Self {
Self(inner)
}
}
impl crate::layout::Measure for BoxMeasure {
fn width_hint(&self, dpi: f64) -> crate::layout::WidthHint {
self.0.width_hint(dpi)
}
fn height_at(&self, width: f64, dpi: f64) -> f64 {
self.0.height_at(width, dpi)
}
fn width_at(&self, height: f64, dpi: f64) -> f64 {
self.0.width_at(height, dpi)
}
}
pub(crate) fn rotated_bbox(text_w: f64, text_h: f64, angle_deg: f32) -> (f64, f64) {
let theta = (angle_deg as f64).to_radians();
let (cos_t, sin_t) = (theta.cos().abs(), theta.sin().abs());
(
text_w * cos_t + text_h * sin_t,
text_w * sin_t + text_h * cos_t,
)
}