use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use super::circle::Circle;
use super::color::Color;
use super::font::{Font, FontDatabase, FontStretch, FontStyle};
use super::point::Point;
use super::rect::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TextAlign {
pub horizontal: HorizontalAlign,
pub vertical: VerticalAlign,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HorizontalAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VerticalAlign {
Top,
#[default]
Baseline,
Middle,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FillRule {
#[default]
NonZero,
EvenOdd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Debug, Clone, Copy)]
pub struct ColorStop {
pub offset: f32,
pub color: Color,
}
#[derive(Debug, Clone)]
pub struct LinearGradient {
pub start: Point,
pub end: Point,
pub stops: Vec<ColorStop>,
}
impl LinearGradient {
pub fn new(start: Point, end: Point) -> Self {
Self {
start,
end,
stops: Vec::new(),
}
}
pub fn add_stop(&mut self, offset: f32, color: Color) {
self.stops.push(ColorStop { offset, color });
}
}
#[derive(Debug, Clone)]
pub struct RadialGradient {
pub center1: Point,
pub radius1: f32,
pub center2: Point,
pub radius2: f32,
pub stops: Vec<ColorStop>,
}
impl RadialGradient {
pub fn new(center: Point, inner_radius: f32, outer_radius: f32) -> Self {
Self {
center1: center,
radius1: inner_radius,
center2: center,
radius2: outer_radius,
stops: Vec::new(),
}
}
pub fn add_stop(&mut self, offset: f32, color: Color) {
self.stops.push(ColorStop { offset, color });
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TextMetrics {
pub ascent: f32,
pub descent: f32,
pub leading: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FontMetrics {
pub ascent: f32,
pub descent: f32,
pub height: f32,
pub leading: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CornerRadii {
pub top_left: f32,
pub top_right: f32,
pub bottom_right: f32,
pub bottom_left: f32,
}
impl CornerRadii {
pub const fn new(radius: f32) -> Self {
Self {
top_left: radius,
top_right: radius,
bottom_right: radius,
bottom_left: radius,
}
}
pub const fn with_values(
top_left: f32,
top_right: f32,
bottom_right: f32,
bottom_left: f32,
) -> Self {
Self {
top_left,
top_right,
bottom_right,
bottom_left,
}
}
}
pub struct Canvas {
pixmap: tiny_skia::Pixmap,
path_builder: Option<tiny_skia::PathBuilder>,
fill_color: Color,
stroke_color: Color,
line_width: f32,
text_align: TextAlign,
transform: tiny_skia::Transform,
save_stack: Vec<CanvasState>,
current_font: Option<Font>,
font_size: f32,
clip_rect: Option<Rect>,
}
struct CanvasState {
fill_color: Color,
stroke_color: Color,
line_width: f32,
text_align: TextAlign,
transform: tiny_skia::Transform,
font_size: f32,
clip_rect: Option<Rect>,
}
fn to_fontdb_style(style: FontStyle) -> fontdb::Style {
match style {
FontStyle::Normal => fontdb::Style::Normal,
FontStyle::Italic => fontdb::Style::Italic,
FontStyle::Oblique => fontdb::Style::Oblique,
}
}
fn to_fontdb_stretch(stretch: FontStretch) -> fontdb::Stretch {
match stretch {
FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed,
FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed,
FontStretch::Condensed => fontdb::Stretch::Condensed,
FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed,
FontStretch::Normal => fontdb::Stretch::Normal,
FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded,
FontStretch::Expanded => fontdb::Stretch::Expanded,
FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded,
FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded,
}
}
const FALLBACK_FAMILIES: &[&str] = &[
"Noto Sans CJK SC",
"Noto Sans CJK TC",
"Noto Sans CJK JP",
"Noto Sans CJK KR",
"PingFang SC",
"PingFang TC",
"PingFang HK",
"Hiragino Sans",
"Hiragino Kaku Gothic ProN",
"Apple SD Gothic Neo",
"Malgun Gothic",
"Microsoft YaHei",
"Microsoft JhengHei",
"SimSun",
"Yu Gothic",
"Meiryo",
"Noto Sans",
"Noto Sans Symbols",
"Noto Sans Symbols2",
"Arial Unicode MS",
];
fn face_has_glyph(font_db: &FontDatabase, face_id: fontdb::ID, ch: char) -> bool {
font_db
.inner()
.with_face_data(face_id, |data, index| {
ttf_parser::Face::parse(data, index)
.ok()
.and_then(|face| face.glyph_index(ch))
.is_some()
})
.unwrap_or(false)
}
fn resolve_face_for_char(font_db: &FontDatabase, primary: fontdb::ID, ch: char) -> fontdb::ID {
static CACHE: OnceLock<Mutex<HashMap<char, fontdb::ID>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
if let Some(&id) = cache.lock().unwrap().get(&ch) {
return id;
}
if face_has_glyph(font_db, primary, ch) {
return primary;
}
for family in FALLBACK_FAMILIES {
let query = fontdb::Query {
families: &[fontdb::Family::Name(family)],
weight: fontdb::Weight(400),
stretch: fontdb::Stretch::Normal,
style: fontdb::Style::Normal,
};
if let Some(id) = font_db.inner().query(&query) {
if face_has_glyph(font_db, id, ch) {
cache.lock().unwrap().insert(ch, id);
return id;
}
}
}
for face in font_db.inner().faces() {
if face_has_glyph(font_db, face.id, ch) {
cache.lock().unwrap().insert(ch, face.id);
return face.id;
}
}
cache.lock().unwrap().insert(ch, primary);
primary
}
fn text_runs(font_db: &FontDatabase, primary: fontdb::ID, text: &str) -> Vec<(fontdb::ID, String)> {
let mut runs: Vec<(fontdb::ID, String)> = Vec::new();
for ch in text.chars() {
let face_id = resolve_face_for_char(font_db, primary, ch);
match runs.last_mut() {
Some((id, s)) if *id == face_id => s.push(ch),
_ => runs.push((face_id, ch.to_string())),
}
}
runs
}
impl Canvas {
pub fn new(width: u32, height: u32) -> Option<Self> {
let pixmap = tiny_skia::Pixmap::new(width, height)?;
Some(Self {
pixmap,
path_builder: None,
fill_color: Color::new(0.0, 0.0, 0.0, 1.0),
stroke_color: Color::new(0.0, 0.0, 0.0, 1.0),
line_width: 1.0,
text_align: TextAlign::default(),
transform: tiny_skia::Transform::identity(),
save_stack: Vec::new(),
current_font: None,
font_size: 12.0,
clip_rect: None,
})
}
pub fn from_pixmap(pixmap: tiny_skia::Pixmap) -> Self {
Self {
pixmap,
path_builder: None,
fill_color: Color::new(0.0, 0.0, 0.0, 1.0),
stroke_color: Color::new(0.0, 0.0, 0.0, 1.0),
line_width: 1.0,
text_align: TextAlign::default(),
transform: tiny_skia::Transform::identity(),
save_stack: Vec::new(),
current_font: None,
font_size: 12.0,
clip_rect: None,
}
}
pub fn width(&self) -> u32 {
self.pixmap.width()
}
pub fn height(&self) -> u32 {
self.pixmap.height()
}
pub fn pixmap(&self) -> &tiny_skia::Pixmap {
&self.pixmap
}
pub fn pixmap_mut(&mut self) -> &mut tiny_skia::Pixmap {
&mut self.pixmap
}
pub fn translate(&mut self, p: Point) {
self.transform = self.transform.pre_translate(p.x, p.y);
}
pub fn rotate(&mut self, radians: f32) {
self.transform = self.transform.pre_rotate(radians.to_degrees());
}
pub fn scale(&mut self, sx: f32, sy: f32) {
self.transform = self.transform.pre_scale(sx, sy);
}
pub fn reset_transform(&mut self) {
self.transform = tiny_skia::Transform::identity();
}
pub fn begin_path(&mut self) {
self.path_builder = Some(tiny_skia::PathBuilder::new());
}
pub fn close_path(&mut self) {
if let Some(ref mut pb) = self.path_builder {
pb.close();
}
}
pub fn move_to(&mut self, p: Point) {
if let Some(ref mut pb) = self.path_builder {
pb.move_to(p.x, p.y);
}
}
pub fn line_to(&mut self, p: Point) {
if let Some(ref mut pb) = self.path_builder {
pb.line_to(p.x, p.y);
}
}
pub fn arc(&mut self, center: Point, radius: f32, start_angle: f32, end_angle: f32, ccw: bool) {
if let Some(ref mut pb) = self.path_builder {
let sweep = if ccw {
start_angle - end_angle
} else {
end_angle - start_angle
};
let start_x = center.x + radius * start_angle.cos();
let start_y = center.y + radius * start_angle.sin();
let end_x = center.x + radius * end_angle.cos();
let end_y = center.y + radius * end_angle.sin();
pb.line_to(start_x, start_y);
let mid_angle = (start_angle + end_angle) / 2.0;
let ctrl_x = center.x + radius * 1.3 * mid_angle.cos();
let ctrl_y = center.y + radius * 1.3 * mid_angle.sin();
pb.quad_to(ctrl_x, ctrl_y, end_x, end_y);
}
}
pub fn add_rect(&mut self, r: Rect) {
if let Some(ref mut pb) = self.path_builder {
pb.push_rect(
tiny_skia::Rect::from_ltrb(r.left, r.top, r.right, r.bottom)
.unwrap_or(tiny_skia::Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()),
);
}
}
pub fn add_round_rect(&mut self, r: Rect, radius: f32) {
self.add_round_rect_varying(r, CornerRadii::new(radius));
}
pub fn add_round_rect_varying(&mut self, r: Rect, radii: CornerRadii) {
if let Some(ref mut pb) = self.path_builder {
pb.move_to(r.left + radii.top_left, r.top);
pb.line_to(r.right - radii.top_right, r.top);
if radii.top_right > 0.0 {
pb.quad_to(r.right, r.top, r.right, r.top + radii.top_right);
}
pb.line_to(r.right, r.bottom - radii.bottom_right);
if radii.bottom_right > 0.0 {
pb.quad_to(r.right, r.bottom, r.right - radii.bottom_right, r.bottom);
}
pb.line_to(r.left + radii.bottom_left, r.bottom);
if radii.bottom_left > 0.0 {
pb.quad_to(r.left, r.bottom, r.left, r.bottom - radii.bottom_left);
}
pb.line_to(r.left, r.top + radii.top_left);
if radii.top_left > 0.0 {
pb.quad_to(r.left, r.top, r.left + radii.top_left, r.top);
}
pb.close();
}
}
pub fn add_circle(&mut self, c: Circle) {
if let Some(ref mut pb) = self.path_builder {
pb.push_circle(c.center.x, c.center.y, c.radius);
}
}
pub fn fill_style(&mut self, color: Color) {
self.fill_color = color;
}
pub fn stroke_style(&mut self, color: Color) {
self.stroke_color = color;
}
pub fn line_width(&mut self, width: f32) {
self.line_width = width;
}
fn color_to_paint(color: Color) -> tiny_skia::Paint<'static> {
let mut paint = tiny_skia::Paint::default();
paint.set_color(
tiny_skia::Color::from_rgba(color.red, color.green, color.blue, color.alpha)
.unwrap_or(tiny_skia::Color::BLACK),
);
paint.anti_alias = true;
paint
}
fn create_clip_mask(&self) -> Option<tiny_skia::Mask> {
self.clip_rect.and_then(|clip| {
let mut mask = tiny_skia::Mask::new(self.pixmap.width(), self.pixmap.height())?;
let clip_path = {
let mut pb = tiny_skia::PathBuilder::new();
pb.push_rect(tiny_skia::Rect::from_ltrb(
clip.left,
clip.top,
clip.right,
clip.bottom,
)?);
pb.finish()?
};
mask.fill_path(
&clip_path,
tiny_skia::FillRule::Winding,
true,
self.transform,
);
Some(mask)
})
}
pub fn fill(&mut self) {
if let Some(pb) = self.path_builder.take() {
if let Some(path) = pb.finish() {
let paint = Self::color_to_paint(self.fill_color);
let clip_mask = self.create_clip_mask();
self.pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
self.transform,
clip_mask.as_ref(),
);
}
}
}
pub fn fill_preserve(&mut self) {
if let Some(ref pb) = self.path_builder {
if let Some(path) = pb.clone().finish() {
let paint = Self::color_to_paint(self.fill_color);
let clip_mask = self.create_clip_mask();
self.pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
self.transform,
clip_mask.as_ref(),
);
}
}
}
pub fn stroke(&mut self) {
if let Some(pb) = self.path_builder.take() {
if let Some(path) = pb.finish() {
let paint = Self::color_to_paint(self.stroke_color);
let stroke = tiny_skia::Stroke {
width: self.line_width,
..Default::default()
};
let clip_mask = self.create_clip_mask();
self.pixmap
.stroke_path(&path, &paint, &stroke, self.transform, clip_mask.as_ref());
}
}
}
pub fn stroke_preserve(&mut self) {
if let Some(ref pb) = self.path_builder {
if let Some(path) = pb.clone().finish() {
let paint = Self::color_to_paint(self.stroke_color);
let stroke = tiny_skia::Stroke {
width: self.line_width,
..Default::default()
};
let clip_mask = self.create_clip_mask();
self.pixmap
.stroke_path(&path, &paint, &stroke, self.transform, clip_mask.as_ref());
}
}
}
pub fn fill_rect(&mut self, r: Rect) {
self.begin_path();
self.add_rect(r);
self.fill();
}
pub fn fill_round_rect(&mut self, r: Rect, radius: f32) {
self.begin_path();
self.add_round_rect(r, radius);
self.fill();
}
pub fn stroke_rect(&mut self, r: Rect) {
self.begin_path();
self.add_rect(r);
self.stroke();
}
pub fn stroke_round_rect(&mut self, r: Rect, radius: f32) {
self.begin_path();
self.add_round_rect(r, radius);
self.stroke();
}
pub fn save(&mut self) {
self.save_stack.push(CanvasState {
fill_color: self.fill_color,
stroke_color: self.stroke_color,
line_width: self.line_width,
text_align: self.text_align,
transform: self.transform,
font_size: self.font_size,
clip_rect: self.clip_rect,
});
}
pub fn restore(&mut self) {
if let Some(state) = self.save_stack.pop() {
self.fill_color = state.fill_color;
self.stroke_color = state.stroke_color;
self.line_width = state.line_width;
self.text_align = state.text_align;
self.transform = state.transform;
self.font_size = state.font_size;
self.clip_rect = state.clip_rect;
}
}
pub fn set_clip_rect(&mut self, rect: Option<Rect>) {
self.clip_rect = rect;
}
pub fn clip_rect(&self) -> Option<Rect> {
self.clip_rect
}
pub fn clip(&mut self, rect: Rect) {
self.clip_rect = Some(match self.clip_rect {
Some(existing) => existing
.intersection(rect)
.unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0)),
None => rect,
});
}
pub fn font(&mut self, font: Font) {
self.current_font = Some(font);
}
pub fn font_size(&mut self, size: f32) {
self.font_size = size;
}
pub fn text_align(&mut self, align: TextAlign) {
self.text_align = align;
}
pub fn measure_text(&self, text: &str) -> TextMetrics {
let width = self.text_width(text);
TextMetrics {
ascent: self.font_size * 0.8,
descent: self.font_size * 0.2,
leading: self.font_size * 0.1,
width,
height: self.font_size,
}
}
fn primary_font_id(&self, font_db: &FontDatabase) -> Option<fontdb::ID> {
let weight = self
.current_font
.as_ref()
.map(|f| fontdb::Weight(f.weight().value()))
.unwrap_or(fontdb::Weight(400));
let stretch = self
.current_font
.as_ref()
.map(|f| to_fontdb_stretch(f.stretch()))
.unwrap_or(fontdb::Stretch::Normal);
let style = self
.current_font
.as_ref()
.map(|f| to_fontdb_style(f.style()))
.unwrap_or(fontdb::Style::Normal);
let family = match self.current_font.as_ref().map(|f| f.family()) {
Some(f) if f.eq_ignore_ascii_case("serif") => fontdb::Family::Serif,
Some(f) if f.eq_ignore_ascii_case("monospace") => fontdb::Family::Monospace,
Some(f) if f.eq_ignore_ascii_case("cursive") => fontdb::Family::Cursive,
Some(f) if f.eq_ignore_ascii_case("fantasy") => fontdb::Family::Fantasy,
Some(f) if !f.eq_ignore_ascii_case("sans-serif") => fontdb::Family::Name(f),
_ => fontdb::Family::SansSerif,
};
let query = fontdb::Query {
families: &[family],
weight,
stretch,
style,
};
font_db.inner().query(&query)
}
pub fn text_width(&self, text: &str) -> f32 {
if text.is_empty() {
return 0.0;
}
static FONT_DB: OnceLock<FontDatabase> = OnceLock::new();
let font_db = FONT_DB.get_or_init(FontDatabase::with_system_fonts);
let Some(primary) = self.primary_font_id(font_db) else {
return text.chars().count() as f32 * self.font_size * 0.6;
};
let mut total_width = 0.0f32;
let mut measured_any = false;
for (face_id, run) in text_runs(font_db, primary, text) {
font_db
.inner()
.with_face_data(face_id, |font_data_ref, face_index| {
let Ok(face) = ttf_parser::Face::parse(font_data_ref, face_index) else {
return;
};
let Some(buzz_face) = rustybuzz::Face::from_slice(font_data_ref, face_index)
else {
return;
};
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(&run);
let output = rustybuzz::shape(&buzz_face, &[], buffer);
let units_per_em = face.units_per_em() as f32;
let scale = self.font_size / units_per_em;
for pos in output.glyph_positions() {
total_width += (pos.x_advance as f32) * scale;
}
measured_any = true;
});
}
if !measured_any || total_width == 0.0 {
text.chars().count() as f32 * self.font_size * 0.6
} else {
total_width
}
}
pub fn text_width_to_position(&self, text: &str, char_position: usize) -> f32 {
if char_position == 0 || text.is_empty() {
return 0.0;
}
let prefix: String = text.chars().take(char_position).collect();
self.text_width(&prefix)
}
pub fn fill_text(&mut self, text: &str, p: Point) {
static FONT_DB: OnceLock<FontDatabase> = OnceLock::new();
let font_db = FONT_DB.get_or_init(FontDatabase::with_system_fonts);
let Some(primary) = self.primary_font_id(font_db) else {
return;
};
let runs = text_runs(font_db, primary, text);
let clip_mask = self.create_clip_mask();
let mut x_pos = p.x;
let y_pos = p.y;
for (face_id, run) in runs {
font_db
.inner()
.with_face_data(face_id, |font_data_ref, face_index| {
let Ok(face) = ttf_parser::Face::parse(font_data_ref, face_index) else {
return;
};
let Some(buzz_face) = rustybuzz::Face::from_slice(font_data_ref, face_index)
else {
return;
};
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(&run);
let output = rustybuzz::shape(&buzz_face, &[], buffer);
let units_per_em = face.units_per_em() as f32;
let scale = self.font_size / units_per_em;
let glyph_infos = output.glyph_infos();
let glyph_positions = output.glyph_positions();
for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) {
let glyph_id = ttf_parser::GlyphId(info.glyph_id as u16);
let glyph_x = x_pos + (pos.x_offset as f32) * scale;
let glyph_y = y_pos + (pos.y_offset as f32) * scale;
Self::render_glyph_static(
&mut self.pixmap,
&face,
glyph_id,
glyph_x,
glyph_y,
scale,
self.fill_color,
self.transform,
clip_mask.as_ref(),
);
x_pos += (pos.x_advance as f32) * scale;
}
});
}
}
fn render_glyph(
&mut self,
face: &ttf_parser::Face,
glyph_id: ttf_parser::GlyphId,
x: f32,
y: f32,
scale: f32,
) {
struct GlyphOutlineBuilder {
path: tiny_skia::PathBuilder,
x: f32,
y: f32,
scale: f32,
}
impl ttf_parser::OutlineBuilder for GlyphOutlineBuilder {
fn move_to(&mut self, px: f32, py: f32) {
let tx = self.x + px * self.scale;
let ty = self.y - py * self.scale; self.path.move_to(tx, ty);
}
fn line_to(&mut self, px: f32, py: f32) {
let tx = self.x + px * self.scale;
let ty = self.y - py * self.scale;
self.path.line_to(tx, ty);
}
fn quad_to(&mut self, x1: f32, y1: f32, px: f32, py: f32) {
let tx1 = self.x + x1 * self.scale;
let ty1 = self.y - y1 * self.scale;
let tx = self.x + px * self.scale;
let ty = self.y - py * self.scale;
self.path.quad_to(tx1, ty1, tx, ty);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, px: f32, py: f32) {
let tx1 = self.x + x1 * self.scale;
let ty1 = self.y - y1 * self.scale;
let tx2 = self.x + x2 * self.scale;
let ty2 = self.y - y2 * self.scale;
let tx = self.x + px * self.scale;
let ty = self.y - py * self.scale;
self.path.cubic_to(tx1, ty1, tx2, ty2, tx, ty);
}
fn close(&mut self) {
self.path.close();
}
}
let mut builder = GlyphOutlineBuilder {
path: tiny_skia::PathBuilder::new(),
x,
y,
scale,
};
if face.outline_glyph(glyph_id, &mut builder).is_some() {
if let Some(path) = builder.path.finish() {
let paint = Self::color_to_paint(self.fill_color);
self.pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
self.transform,
None,
);
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_glyph_static(
pixmap: &mut tiny_skia::Pixmap,
face: &ttf_parser::Face,
glyph_id: ttf_parser::GlyphId,
x: f32,
y: f32,
scale: f32,
fill_color: Color,
transform: tiny_skia::Transform,
clip_mask: Option<&tiny_skia::Mask>,
) {
struct GlyphOutlineBuilder {
path: tiny_skia::PathBuilder,
x: f32,
y: f32,
scale: f32,
}
impl ttf_parser::OutlineBuilder for GlyphOutlineBuilder {
fn move_to(&mut self, px: f32, py: f32) {
let tx = self.x + px * self.scale;
let ty = self.y - py * self.scale; self.path.move_to(tx, ty);
}
fn line_to(&mut self, px: f32, py: f32) {
let tx = self.x + px * self.scale;
let ty = self.y - py * self.scale;
self.path.line_to(tx, ty);
}
fn quad_to(&mut self, x1: f32, y1: f32, px: f32, py: f32) {
let tx1 = self.x + x1 * self.scale;
let ty1 = self.y - y1 * self.scale;
let tx = self.x + px * self.scale;
let ty = self.y - py * self.scale;
self.path.quad_to(tx1, ty1, tx, ty);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, px: f32, py: f32) {
let tx1 = self.x + x1 * self.scale;
let ty1 = self.y - y1 * self.scale;
let tx2 = self.x + x2 * self.scale;
let ty2 = self.y - y2 * self.scale;
let tx = self.x + px * self.scale;
let ty = self.y - py * self.scale;
self.path.cubic_to(tx1, ty1, tx2, ty2, tx, ty);
}
fn close(&mut self) {
self.path.close();
}
}
let mut builder = GlyphOutlineBuilder {
path: tiny_skia::PathBuilder::new(),
x,
y,
scale,
};
if face.outline_glyph(glyph_id, &mut builder).is_some() {
if let Some(path) = builder.path.finish() {
let paint = Self::color_to_paint(fill_color);
pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
transform,
clip_mask,
);
}
}
}
pub fn clear(&mut self, color: Color) {
self.pixmap.fill(
tiny_skia::Color::from_rgba(color.red, color.green, color.blue, color.alpha)
.unwrap_or(tiny_skia::Color::WHITE),
);
}
}
pub struct CanvasStateGuard<'a> {
canvas: &'a mut Canvas,
}
impl<'a> CanvasStateGuard<'a> {
pub fn new(canvas: &'a mut Canvas) -> Self {
canvas.save();
Self { canvas }
}
}
impl<'a> Drop for CanvasStateGuard<'a> {
fn drop(&mut self) {
self.canvas.restore();
}
}
impl<'a> std::ops::Deref for CanvasStateGuard<'a> {
type Target = Canvas;
fn deref(&self) -> &Self::Target {
self.canvas
}
}
impl<'a> std::ops::DerefMut for CanvasStateGuard<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.canvas
}
}
#[cfg(test)]
mod text_tests {
use super::*;
fn test_font_db() -> &'static FontDatabase {
static FONT_DB: OnceLock<FontDatabase> = OnceLock::new();
FONT_DB.get_or_init(FontDatabase::with_system_fonts)
}
fn default_sans_serif(font_db: &FontDatabase) -> fontdb::ID {
let query = fontdb::Query {
families: &[fontdb::Family::SansSerif],
weight: fontdb::Weight(400),
stretch: fontdb::Stretch::Normal,
style: fontdb::Style::Normal,
};
font_db
.inner()
.query(&query)
.expect("system should have a sans-serif font")
}
#[test]
fn resolve_face_for_char_finds_real_korean_coverage() {
let font_db = test_font_db();
let primary = default_sans_serif(font_db);
let resolved = resolve_face_for_char(font_db, primary, '안');
assert!(
face_has_glyph(font_db, resolved, '안'),
"resolved face has no real glyph for '안' - would render as tofu"
);
}
#[test]
fn resolve_face_for_char_finds_real_japanese_coverage() {
let font_db = test_font_db();
let primary = default_sans_serif(font_db);
let resolved = resolve_face_for_char(font_db, primary, 'こ');
assert!(
face_has_glyph(font_db, resolved, 'こ'),
"resolved face has no real glyph for 'こ' - would render as tofu"
);
}
#[test]
fn resolve_face_for_char_finds_real_chinese_coverage() {
let font_db = test_font_db();
let primary = default_sans_serif(font_db);
let resolved = resolve_face_for_char(font_db, primary, '你');
assert!(
face_has_glyph(font_db, resolved, '你'),
"resolved face has no real glyph for '你' - would render as tofu"
);
}
#[test]
fn text_width_is_positive_for_non_latin_scripts() {
let mut canvas = Canvas::new(400, 100).unwrap();
canvas.font_size(24.0);
for text in [
"Hello",
"안녕하세요",
"こんにちは",
"你好",
"Hello 안녕 こんにちは",
] {
let width = canvas.text_width(text);
assert!(
width > 0.0,
"expected positive width for {text:?}, got {width}"
);
}
}
#[test]
fn mixed_script_text_measures_wider_than_its_latin_prefix() {
let mut canvas = Canvas::new(500, 100).unwrap();
canvas.font_size(24.0);
let latin_only_width = canvas.text_width("Hello ");
let mixed_width = canvas.text_width("Hello 안녕");
assert!(
mixed_width > latin_only_width,
"mixed-script text should measure wider than its Latin prefix alone"
);
}
#[test]
fn respects_explicitly_selected_font_family() {
let mut canvas = Canvas::new(400, 100).unwrap();
canvas.font(Font::monospace());
canvas.font_size(24.0);
let font_db = test_font_db();
let primary = canvas
.primary_font_id(font_db)
.expect("system should have a monospace font");
let monospace_query_id = font_db
.inner()
.query(&fontdb::Query {
families: &[fontdb::Family::Monospace],
weight: fontdb::Weight(400),
stretch: fontdb::Stretch::Normal,
style: fontdb::Style::Normal,
})
.expect("system should have a monospace font");
assert_eq!(
primary, monospace_query_id,
"selecting a monospace font should actually query for one"
);
}
}