use uzor::render::{RenderContext, TextAlign, TextBaseline};
use crate::theme::FigureTheme;
const PLATE_PAD: f64 = 3.0;
const PLATE_ALPHA: f64 = 0.88;
const HALO_OFFSET: f64 = 1.1;
pub fn plate_under_text(ctx: &mut dyn RenderContext, theme: &FigureTheme, text: &str, x: f64, y: f64, align: TextAlign, baseline: TextBaseline) {
if text.is_empty() {
return;
}
let w = ctx.measure_text(text);
let h = ctx.text_bounds("Ag", &theme.label_font).h.max(10.0);
let left = match align {
TextAlign::Left => x,
TextAlign::Center => x - w / 2.0,
TextAlign::Right => x - w,
};
let top = match baseline {
TextBaseline::Top => y,
TextBaseline::Middle => y - h / 2.0,
TextBaseline::Bottom => y - h,
TextBaseline::Alphabetic => y - h * 0.8,
};
ctx.set_fill_color(&theme.background);
ctx.set_global_alpha(PLATE_ALPHA);
ctx.fill_rect(left - PLATE_PAD, top - PLATE_PAD, w + PLATE_PAD * 2.0, h + PLATE_PAD * 2.0);
ctx.set_global_alpha(1.0);
}
pub fn fill_text_with_halo(ctx: &mut dyn RenderContext, text: &str, x: f64, y: f64, fill: &str, halo: &str) {
if text.is_empty() {
return;
}
ctx.set_fill_color(halo);
for &(dx, dy) in &[
(-HALO_OFFSET, -HALO_OFFSET),
(0.0, -HALO_OFFSET),
(HALO_OFFSET, -HALO_OFFSET),
(-HALO_OFFSET, 0.0),
(HALO_OFFSET, 0.0),
(-HALO_OFFSET, HALO_OFFSET),
(0.0, HALO_OFFSET),
(HALO_OFFSET, HALO_OFFSET),
] {
ctx.fill_text(text, x + dx, y + dy);
}
ctx.set_fill_color(fill);
ctx.fill_text(text, x, y);
}
#[cfg(test)]
mod tests {
use super::*;
use uzor::types::Rect;
use uzor_export::{render_to_png, ExportSpec};
fn spec() -> ExportSpec {
ExportSpec { width_px: 200, height_px: 120, dpr: 1.0, background: None }
}
#[test]
fn plate_under_text_renders_without_panicking_for_every_align_baseline_combo() {
let theme = FigureTheme::dark();
let result = render_to_png(&spec(), |ctx| {
ctx.set_font(&theme.label_font);
for align in [TextAlign::Left, TextAlign::Center, TextAlign::Right] {
for baseline in [TextBaseline::Top, TextBaseline::Middle, TextBaseline::Bottom, TextBaseline::Alphabetic] {
plate_under_text(ctx, &theme, "sample label", 100.0, 60.0, align, baseline);
}
}
});
assert!(result.is_ok());
}
#[test]
fn plate_under_text_empty_text_is_a_no_op() {
let theme = FigureTheme::dark();
let result = render_to_png(&spec(), |ctx| {
ctx.set_font(&theme.label_font);
plate_under_text(ctx, &theme, "", 100.0, 60.0, TextAlign::Left, TextBaseline::Top);
});
assert!(result.is_ok());
}
#[test]
fn plate_under_text_left_align_extends_rightward_from_x() {
let theme = FigureTheme::dark();
let bounds = Rect::new(0.0, 0.0, 200.0, 120.0);
let result = render_to_png(&spec(), |ctx| {
ctx.set_fill_color(&theme.background);
ctx.fill_rect(bounds.x, bounds.y, bounds.width, bounds.height);
ctx.set_font(&theme.label_font);
plate_under_text(ctx, &theme, "a considerably longer sample label", 10.0, 40.0, TextAlign::Left, TextBaseline::Top);
plate_under_text(ctx, &theme, "short", 10.0, 70.0, TextAlign::Left, TextBaseline::Top);
});
assert!(result.is_ok());
}
#[test]
fn fill_text_with_halo_renders_without_panicking() {
let theme = FigureTheme::dark();
let result = render_to_png(&spec(), |ctx| {
ctx.set_font(&theme.label_font);
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Top);
fill_text_with_halo(ctx, "node-label", 20.0, 20.0, "#e6e6ea", "#0d0f14");
});
assert!(result.is_ok());
}
#[test]
fn fill_text_with_halo_empty_text_is_a_no_op() {
let theme = FigureTheme::dark();
let result = render_to_png(&spec(), |ctx| {
ctx.set_font(&theme.label_font);
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Top);
fill_text_with_halo(ctx, "", 20.0, 20.0, "#e6e6ea", "#0d0f14");
});
assert!(result.is_ok());
}
#[test]
fn fill_text_with_halo_leaves_the_final_fill_color_as_the_caller_supplied_fill() {
let theme = FigureTheme::dark();
let result = render_to_png(&spec(), |ctx| {
ctx.set_font(&theme.label_font);
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Top);
fill_text_with_halo(ctx, "node-label", 20.0, 20.0, "#e6e6ea", "#0d0f14");
ctx.fill_rect(0.0, 90.0, 20.0, 20.0);
});
assert!(result.is_ok());
}
}