use crate::core::{Color, Point, Rect, Size};
pub(crate) struct GlyphDrawConfig<'a> {
pub w: u32,
pub h: u32,
pub color: Color,
pub canvas_width: u32,
pub canvas_height: u32,
pub canvas: &'a mut [u8],
pub clip: Option<(i32, i32, u32, u32)>,
}
pub(crate) fn glyph_rects(
ch: char,
x: i32,
y: i32,
w: u32,
h: u32,
) -> impl Iterator<Item = (i32, i32, i32, i32)> {
let width = w as i32;
let height = h as i32;
let mut rects = crate::compat::Vec::new();
if !ch.is_whitespace() && w != 0 && h != 0 {
let (glyph, _) = crate::render::text::resolve(ch);
let gw = glyph.width as i32;
let gh = glyph.height as i32;
if gw > 0 && gh > 0 {
for gy in 0..gh {
let y0 = y + (gy * height) / gh;
let mut y1 = y + ((gy + 1) * height) / gh;
if y1 <= y0 {
y1 = y0 + 1;
}
for gx in 0..gw {
if !glyph.bit(gx as u32, gy as u32) {
continue;
}
let x0 = x + (gx * width) / gw;
let mut x1 = x + ((gx + 1) * width) / gw;
if x1 <= x0 {
x1 = x0 + 1;
}
rects.push((x0, y0, x1, y1));
}
}
}
}
rects.into_iter()
}
pub(crate) fn blend_painted_glyph(
ch: char,
x: i32,
y: i32,
coverage: &mut [u8],
config: &mut GlyphDrawConfig,
) -> bool {
if ch.is_whitespace() {
return false;
}
let cell = crate::render::text::Cell::new(config.w, config.h);
if cell.is_empty() || coverage.len() < cell.area() {
return false;
}
let Some(painted) = crate::render::text::paint_active(ch, cell, coverage) else {
return false;
};
if painted.is_color() {
return false;
}
let (width, height) = (cell.width as i32, cell.height as i32);
let mut any = false;
for py in 0..height {
let cy = y + py;
if cy < 0 || cy >= config.canvas_height as i32 {
continue;
}
for px in 0..width {
let value = coverage[(py * width + px) as usize];
if value == 0 {
continue;
}
let cx = x + px;
if cx < 0 || cx >= config.canvas_width as i32 {
continue;
}
if pixel_visible(config.clip, cx, cy) {
blend_pixel(
config.canvas,
config.canvas_width,
cx as u32,
cy as u32,
config.color,
value as f32 / 255.0,
);
any = true;
}
}
}
any
}
#[cfg(feature = "fonts-emoji-color")]
pub(crate) fn blend_color_glyph(
ch: char,
x: i32,
y: i32,
pixels: &mut [u8],
config: &mut GlyphDrawConfig,
) -> bool {
if ch.is_whitespace() {
return false;
}
let cell = crate::render::text::Cell::new(config.w, config.h);
if cell.is_empty() || pixels.len() < cell.area() * 4 {
return false;
}
let Some(painted) = crate::render::text::paint_active(ch, cell, pixels) else {
return false;
};
if !painted.is_color() {
return false;
}
blend_rgba_over_canvas(pixels, cell, x, y, config)
}
#[cfg(feature = "fonts-emoji-color")]
fn blend_rgba_over_canvas(
pixels: &[u8],
cell: crate::render::text::Cell,
x: i32,
y: i32,
config: &mut GlyphDrawConfig,
) -> bool {
let (width, height) = (cell.width as i32, cell.height as i32);
let mut any = false;
for py in 0..height {
let cy = y + py;
if cy < 0 || cy >= config.canvas_height as i32 {
continue;
}
for px in 0..width {
let si = ((py * width + px) * 4) as usize;
let Some(src) = pixels.get(si..si + 4) else {
continue;
};
if src[3] == 0 {
continue;
}
let cx = x + px;
if cx < 0 || cx >= config.canvas_width as i32 {
continue;
}
if !pixel_visible(config.clip, cx, cy) {
continue;
}
blend_pixel(
config.canvas,
config.canvas_width,
cx as u32,
cy as u32,
Color::rgba(src[0], src[1], src[2], src[3]),
1.0,
);
any = true;
}
}
any
}
pub(crate) fn pixel_bytes_len(size: Size) -> usize {
size.width.saturating_mul(size.height).saturating_mul(4) as usize
}
pub fn fill_pixels(pixels: &mut [u8], color: Color) {
let chunk_size = 4;
let color_arr = [color.r, color.g, color.b, color.a];
for chunk in pixels.chunks_mut(chunk_size) {
if chunk.len() == chunk_size {
chunk.copy_from_slice(&color_arr);
} else {
chunk.copy_from_slice(&color_arr[..chunk.len()]);
}
}
}
pub(crate) fn set_pixel(frame: &mut [u8], width: u32, x: u32, y: u32, color: Color) {
let idx = ((y * width + x) * 4) as usize;
if idx + 3 >= frame.len() {
return;
}
frame[idx] = color.r;
frame[idx + 1] = color.g;
frame[idx + 2] = color.b;
frame[idx + 3] = color.a;
}
pub(crate) fn pixel_visible(clip: Option<(i32, i32, u32, u32)>, x: i32, y: i32) -> bool {
let Some((clip_x, clip_y, clip_width, clip_height)) = clip else {
return true;
};
x >= clip_x
&& y >= clip_y
&& x < clip_x.saturating_add(clip_width as i32)
&& y < clip_y.saturating_add(clip_height as i32)
}
pub fn blend_pixel(frame: &mut [u8], width: u32, x: u32, y: u32, color: Color, coverage: f32) {
if coverage <= 0.0 {
return;
}
let idx = ((y * width + x) * 4) as usize;
if idx + 3 >= frame.len() {
return;
}
let src_a = (color.a as f32 / 255.0) * coverage.clamp(0.0, 1.0);
if src_a <= 0.0 {
frame[idx] = 0;
frame[idx + 1] = 0;
frame[idx + 2] = 0;
frame[idx + 3] = 0;
return;
}
let dst = &mut frame[idx..idx + 4];
let src = [color.r, color.g, color.b, color.a];
let src_f: [f32; 4] = [
src[0] as f32 / 255.0,
src[1] as f32 / 255.0,
src[2] as f32 / 255.0,
src[3] as f32 / 255.0,
];
let dst_f: [f32; 4] = [
dst[0] as f32 / 255.0,
dst[1] as f32 / 255.0,
dst[2] as f32 / 255.0,
dst[3] as f32 / 255.0,
];
let out_a = src_a + dst_f[3] * (1.0 - src_a);
if out_a <= f32::EPSILON {
dst.copy_from_slice(&[0, 0, 0, 0]);
return;
}
let out_r = (src_f[0] * src_a + dst_f[0] * dst_f[3] * (1.0 - src_a)) / out_a;
let out_g = (src_f[1] * src_a + dst_f[1] * dst_f[3] * (1.0 - src_a)) / out_a;
let out_b = (src_f[2] * src_a + dst_f[2] * dst_f[3] * (1.0 - src_a)) / out_a;
dst[0] = (out_r * 255.0).round().clamp(0.0, 255.0) as u8;
dst[1] = (out_g * 255.0).round().clamp(0.0, 255.0) as u8;
dst[2] = (out_b * 255.0).round().clamp(0.0, 255.0) as u8;
dst[3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
}
pub(crate) fn circle_fill_coverage(distance: f32, radius: f32) -> f32 {
if radius <= 0.0 {
return 0.0;
}
(radius + 1.0 - distance).clamp(0.0, 1.0)
}
pub(crate) fn circle_fill_coverage_grid(
px: i32,
py: i32,
center: Point,
radius: f32,
grid: u8,
) -> f32 {
let sample_count = grid.clamp(1, 8) as u32;
let total = sample_count * sample_count;
let mut coverage_sum = 0.0f32;
for sy in 0..sample_count {
for sx in 0..sample_count {
let sample_x = px as f32 + (sx as f32 + 0.5) / sample_count as f32;
let sample_y = py as f32 + (sy as f32 + 0.5) / sample_count as f32;
let dx = sample_x - center.x as f32;
let dy = sample_y - center.y as f32;
let distance = (dx * dx + dy * dy).sqrt();
coverage_sum += circle_fill_coverage(distance, radius);
}
}
(coverage_sum / total as f32).clamp(0.0, 1.0)
}
pub(crate) fn circle_stroke_coverage_grid(
px: i32,
py: i32,
center: Point,
radius: f32,
stroke_width: f32,
grid: u8,
) -> f32 {
let sample_count = grid.clamp(1, 8) as u32;
let total = sample_count * sample_count;
let mut coverage_sum = 0.0f32;
let outer_radius = radius;
let inner_radius = (radius - stroke_width).max(0.0);
for sy in 0..sample_count {
for sx in 0..sample_count {
let sample_x = px as f32 + (sx as f32 + 0.5) / sample_count as f32;
let sample_y = py as f32 + (sy as f32 + 0.5) / sample_count as f32;
let dx = sample_x - center.x as f32;
let dy = sample_y - center.y as f32;
let distance = (dx * dx + dy * dy).sqrt();
let inner_coverage = circle_fill_coverage(distance, inner_radius);
let outer_coverage = circle_fill_coverage(distance, outer_radius);
coverage_sum += (outer_coverage - inner_coverage).max(0.0);
}
}
(coverage_sum / total as f32).clamp(0.0, 1.0)
}
pub(crate) fn point_to_segment_distance(
px: f32,
py: f32,
ax: f32,
ay: f32,
bx: f32,
by: f32,
) -> f32 {
let abx = bx - ax;
let aby = by - ay;
let apx = px - ax;
let apy = py - ay;
let ab_len2 = abx * abx + aby * aby;
if ab_len2 <= f32::EPSILON {
let dx = px - ax;
let dy = py - ay;
return (dx * dx + dy * dy).sqrt();
}
let t = ((apx * abx + apy * aby) / ab_len2).clamp(0.0, 1.0);
let cx = ax + t * abx;
let cy = ay + t * aby;
let dx = px - cx;
let dy = py - cy;
(dx * dx + dy * dy).sqrt()
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn line_stroke_coverage_grid(
px: i32,
py: i32,
ax: f32,
ay: f32,
bx: f32,
by: f32,
half_width: f32,
grid: u8,
) -> f32 {
let sample_count = grid.clamp(1, 8) as u32;
let total = sample_count * sample_count;
let mut coverage_sum = 0.0f32;
for sy in 0..sample_count {
for sx in 0..sample_count {
let sample_x = px as f32 + (sx as f32 + 0.5) / sample_count as f32;
let sample_y = py as f32 + (sy as f32 + 0.5) / sample_count as f32;
let distance = point_to_segment_distance(sample_x, sample_y, ax, ay, bx, by);
coverage_sum += (half_width + 0.5 - distance).clamp(0.0, 1.0);
}
}
(coverage_sum / total as f32).clamp(0.0, 1.0)
}
pub(crate) fn rounded_rect_effective_radius(rect: Rect, radius: u32) -> u32 {
radius.min(rect.width / 2).min(rect.height / 2)
}
pub(crate) fn inset_rect(rect: Rect, inset: i32) -> Rect {
let x = rect.x + inset;
let y = rect.y + inset;
let width = (rect.width as i32 - inset * 2).max(0) as u32;
let height = (rect.height as i32 - inset * 2).max(0) as u32;
Rect { x, y, width, height }
}
pub(crate) fn point_in_rounded_rect_f32(px: f32, py: f32, rect: Rect, radius: u32) -> bool {
if rect.width == 0 || rect.height == 0 {
return false;
}
let left = rect.x as f32;
let top = rect.y as f32;
let right = rect.x as f32 + rect.width as f32;
let bottom = rect.y as f32 + rect.height as f32;
if px < left || px >= right || py < top || py >= bottom {
return false;
}
let r = rounded_rect_effective_radius(rect, radius) as f32;
if r <= 0.0 {
return true;
}
if (px >= left + r && px < right - r) || (py >= top + r && py < bottom - r) {
return true;
}
let cx = if px < left + r {
left + r
} else if px >= right - r {
right - r
} else {
px
};
let cy = if py < top + r {
top + r
} else if py >= bottom - r {
bottom - r
} else {
py
};
let dx = px - cx;
let dy = py - cy;
dx * dx + dy * dy <= r * r
}
pub(crate) fn rounded_rect_coverage(px: i32, py: i32, rect: Rect, radius: u32) -> f32 {
rounded_rect_coverage_grid(px, py, rect, radius, 2)
}
pub(crate) fn rounded_rect_coverage_grid(
px: i32,
py: i32,
rect: Rect,
radius: u32,
grid: u8,
) -> f32 {
let sample_count = grid.clamp(1, 8) as u32;
let mut covered = 0u32;
let total = sample_count * sample_count;
for sy in 0..sample_count {
for sx in 0..sample_count {
let sample_x = (sx as f32 + 0.5) / sample_count as f32;
let sample_y = (sy as f32 + 0.5) / sample_count as f32;
if point_in_rounded_rect_f32(px as f32 + sample_x, py as f32 + sample_y, rect, radius) {
covered += 1;
}
}
}
covered as f32 / total as f32
}
#[cfg(test)]
mod text_coverage_tests {
use crate::render::text;
#[test]
fn ascii_resolves_to_a_real_glyph() {
assert_eq!(text::source_for('A'), Some("font8x8"), "'A' is inside the base face");
assert_eq!(text::source_for('5'), Some("font8x8"));
}
#[cfg(not(feature = "fonts-cjk-bitmap"))]
#[test]
fn non_latin_resolves_to_the_fallback_glyph_by_default() {
for ch in ['\u{4e2d}', '\u{0416}', '\u{0627}', '\u{1f600}'] {
assert_eq!(
text::source_for(ch),
None,
"U+{:04X} is outside the base face and must take the fallback glyph",
ch as u32
);
}
}
#[cfg(feature = "fonts-cjk-bitmap")]
#[test]
fn enabling_the_cjk_data_moves_the_boundary_by_exactly_one_face() {
assert_eq!(text::source_for('\u{4e2d}'), Some("cjk-bitmap"));
assert_eq!(text::source_for('\u{0416}'), None, "Cyrillic is not in the CJK subset");
assert_eq!(text::source_for('\u{0627}'), None, "Arabic is not in the CJK subset");
assert_eq!(text::source_for('\u{1f600}'), None, "emoji is not in the CJK subset");
}
}