use crate::compat::Vec;
use crate::core::{Color, Font, HorizontalAlignment, Point, Rect};
use crate::render::pipeline::pixel_ops::{
blend_painted_glyph, circle_fill_coverage_grid, circle_stroke_coverage_grid, fill_pixels,
inset_rect, line_stroke_coverage_grid, pixel_visible, rounded_rect_coverage,
rounded_rect_coverage_grid, rounded_rect_effective_radius, u32_to_i32_saturating,
write_pixel_with_mode, GlyphDrawConfig,
};
use crate::render::BlendMode;
#[cfg(feature = "fonts-emoji-color")]
use crate::render::pipeline::pixel_ops::blend_color_glyph;
#[cfg(feature = "text-shaping")]
use crate::render::pipeline::pixel_ops::blend_coverage_bitmap;
use crate::render::text::{is_combining_mark, is_variation_selector};
use crate::render::SoftwareSurface;
macro_rules! set_pixel_clipped {
($blend:expr, $clip:expr, $frame:expr, $width:expr, $x:expr, $y:expr, $color:expr) => {
if pixel_visible($clip, $x as i32, $y as i32) {
write_pixel_with_mode($blend, $frame, $width, $x as u32, $y as u32, $color, 1.0);
}
};
}
macro_rules! blend_pixel_clipped {
($blend:expr, $clip:expr, $frame:expr, $width:expr, $x:expr, $y:expr, $color:expr, $coverage:expr) => {
if pixel_visible($clip, $x as i32, $y as i32) {
write_pixel_with_mode($blend, $frame, $width, $x as u32, $y as u32, $color, $coverage);
}
};
}
fn visible_bounds(
x: i32,
y: i32,
width: u32,
height: u32,
screen: (u32, u32),
clip: Option<(i32, i32, u32, u32)>,
) -> Option<(i32, i32, i32, i32)> {
let mut left = x.max(0);
let mut top = y.max(0);
let mut right = (x as i64 + width as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
let mut bottom = (y as i64 + height as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
right = right.min(screen.0 as i32);
bottom = bottom.min(screen.1 as i32);
if let Some((clip_x, clip_y, clip_width, clip_height)) = clip {
left = left.max(clip_x);
top = top.max(clip_y);
right = right.min(clip_x.saturating_add(clip_width as i32));
bottom = bottom.min(clip_y.saturating_add(clip_height as i32));
}
if right <= left || bottom <= top {
return None;
}
Some((left, top, right, bottom))
}
fn write_solid_bounds(
blend: BlendMode,
clip: Option<(i32, i32, u32, u32)>,
frame: &mut [u8],
screen: (u32, u32),
rect: (i32, i32, u32, u32),
color: Color,
) {
let (x, y, width, height) = rect;
let Some((left, top, right, bottom)) = visible_bounds(x, y, width, height, screen, clip) else {
return;
};
write_solid_rows(blend, frame, screen.0, top, bottom, left, right, color);
}
fn write_solid_rows(
blend: BlendMode,
frame: &mut [u8],
width: u32,
top: i32,
bottom: i32,
left: i32,
right: i32,
color: Color,
) {
if right <= left || bottom <= top {
return;
}
if matches!(blend, BlendMode::Normal) && color.a == 255 {
let stride = width as usize * 4;
let row_len = (right - left) as usize * 4;
for y in top..bottom {
let start = y as usize * stride + left as usize * 4;
fill_pixels(&mut frame[start..start + row_len], color);
}
return;
}
for y in top..bottom {
for x in left..right {
write_pixel_with_mode(blend, frame, width, x as u32, y as u32, color, 1.0);
}
}
}
fn corner_row_reach(
py: i32,
corner_cy: i32,
left_cx: i32,
right_cx: i32,
radius: i32,
) -> (i32, i32) {
if radius <= 0 || radius > (1 << 20) {
return (i32::MIN, i32::MAX);
}
let dy = (((py as f32 + 0.5) - corner_cy as f32).abs() - 0.5).max(0.0);
let r = radius as f32;
let half_extent = (r * r - dy * dy).max(0.0).sqrt();
let first = (left_cx as f32 - half_extent).floor() as i32;
let last = (right_cx as f32 + half_extent).ceil() as i32;
(first, last)
}
impl SoftwareSurface {
pub fn fill_rect(&mut self, rect: Rect, color: Color) {
let size = self.buffer.size();
if rect.width == 0 || rect.height == 0 {
return;
}
let clip = self.current_clip();
let Some((left, top, right, bottom)) = visible_bounds(
rect.x,
rect.y,
rect.width,
rect.height,
(size.width, size.height),
clip,
) else {
return;
};
let blend = self.blend_mode;
let width = size.width;
let frame = self.buffer.back_mut();
if matches!(blend, BlendMode::Normal) && color.a == 255 {
let pattern = [color.r, color.g, color.b, color.a];
let row_len = (right - left) as usize * 4;
let stride = width as usize * 4;
let mut start = top as usize * stride + left as usize * 4;
for chunk in frame[start..start + row_len].as_chunks_mut::<4>().0 {
chunk.copy_from_slice(&pattern);
}
for _ in top + 1..bottom {
frame.copy_within(start..start + row_len, start + stride);
start += stride;
}
return;
}
for y in top..bottom {
for x in left..right {
write_pixel_with_mode(blend, frame, width, x as u32, y as u32, color, 1.0);
}
}
}
pub fn draw_rect(&mut self, rect: Rect, color: Color) {
self.draw_rect_with_width(rect, color, 1);
}
pub fn draw_rect_with_width(&mut self, rect: Rect, color: Color, stroke_width: u32) {
if stroke_width == 0 {
return;
}
if rect.width == 0 || rect.height == 0 {
return;
}
let x0 = rect.x;
let y0 = rect.y;
let x1 = rect.right() - 1;
let y1 = rect.bottom() - 1;
self.draw_line_with_width(
Point { x: x0, y: y0 },
Point { x: x1, y: y0 },
color,
stroke_width,
);
self.draw_line_with_width(
Point { x: x0, y: y1 },
Point { x: x1, y: y1 },
color,
stroke_width,
);
self.draw_line_with_width(
Point { x: x0, y: y0 },
Point { x: x0, y: y1 },
color,
stroke_width,
);
self.draw_line_with_width(
Point { x: x1, y: y0 },
Point { x: x1, y: y1 },
color,
stroke_width,
);
}
pub fn fill_rounded_rect(&mut self, rect: Rect, radius: u32, color: Color) {
if rect.width == 0 || rect.height == 0 {
return;
}
let size = self.buffer.size();
let width = size.width as i32;
let height = size.height as i32;
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let blend = self.blend_mode;
let x0 = rect.x.max(0);
let y0 = rect.y.max(0);
let x1 = (rect.right() - 1).min(width - 1);
let y1 = (rect.bottom() - 1).min(height - 1);
if x1 < x0 || y1 < y0 {
return;
}
let effective_radius = rounded_rect_effective_radius(rect, radius);
let r = u32_to_i32_saturating(effective_radius);
let middle_first = rect.x.saturating_add(r);
let middle_after_last = rect.right().saturating_sub(r);
let top_band_last = rect.y.saturating_add(r).saturating_sub(1);
let bottom_band_first = rect.bottom().saturating_sub(r);
let (clip_left, clip_right, clip_top, clip_bottom) = match clip {
Some((clip_x, clip_y, clip_w, clip_h)) => (
clip_x,
clip_x.saturating_add(u32_to_i32_saturating(clip_w)),
clip_y,
clip_y.saturating_add(u32_to_i32_saturating(clip_h)),
),
None => (i32::MIN, i32::MAX, i32::MIN, i32::MAX),
};
for py in y0..=y1 {
let top_corner = py <= top_band_last;
let bottom_corner = py >= bottom_band_first;
let row_visible = py >= clip_top && py < clip_bottom;
if !top_corner && !bottom_corner {
let start = x0.max(clip_left);
let end = x1.min(clip_right.saturating_sub(1));
if row_visible {
write_solid_rows(blend, frame, size.width, py, py + 1, start, end + 1, color);
}
continue;
}
let corner_cy =
if top_corner { rect.y.saturating_add(r) } else { rect.bottom().saturating_sub(r) };
let (reach_first, reach_last) =
corner_row_reach(py, corner_cy, middle_first, middle_after_last, r);
let left_band_start = x0.max(reach_first);
let left_band_end = middle_first.saturating_sub(1).min(x1);
if left_band_start <= left_band_end {
for px in left_band_start..=left_band_end {
let coverage = rounded_rect_coverage(px, py, rect, effective_radius);
if coverage > 0.0 {
blend_pixel_clipped!(
blend, clip, frame, size.width, px, py, color, coverage
);
}
}
}
let middle_start = middle_first.max(x0).max(clip_left);
let middle_end =
middle_after_last.saturating_sub(1).min(x1).min(clip_right.saturating_sub(1));
if row_visible {
write_solid_rows(
blend,
frame,
size.width,
py,
py + 1,
middle_start,
middle_end + 1,
color,
);
}
let right_band_start = middle_after_last.max(middle_first).max(x0);
let right_band_end = x1.min(reach_last);
if right_band_start <= right_band_end {
for px in right_band_start..=right_band_end {
let coverage = rounded_rect_coverage(px, py, rect, effective_radius);
if coverage > 0.0 {
blend_pixel_clipped!(
blend, clip, frame, size.width, px, py, color, coverage
);
}
}
}
}
}
pub fn fill_rounded_rect_aa(&mut self, rect: Rect, radius: u32, color: Color) {
if rect.width == 0 || rect.height == 0 {
return;
}
let sample_grid = self.aa_samples_per_axis;
let size = self.buffer.size();
let width = size.width as i32;
let height = size.height as i32;
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let x0 = rect.x.max(0);
let y0 = rect.y.max(0);
let x1 = (rect.right() - 1).min(width - 1);
let y1 = (rect.bottom() - 1).min(height - 1);
let effective_radius = rounded_rect_effective_radius(rect, radius);
for py in y0..=y1 {
for px in x0..=x1 {
let coverage =
rounded_rect_coverage_grid(px, py, rect, effective_radius, sample_grid);
if coverage > 0.0 {
blend_pixel_clipped!(
self.blend_mode,
clip,
frame,
size.width,
px,
py,
color,
coverage
);
}
}
}
}
pub fn draw_rounded_rect_with_width(
&mut self,
rect: Rect,
radius: u32,
color: Color,
stroke_width: u32,
) {
if stroke_width == 0 || rect.width == 0 || rect.height == 0 {
return;
}
let size = self.buffer.size();
let width = size.width as i32;
let height = size.height as i32;
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let x0 = rect.x.max(0);
let y0 = rect.y.max(0);
let x1 = (rect.right() - 1).min(width - 1);
let y1 = (rect.bottom() - 1).min(height - 1);
let effective_radius = rounded_rect_effective_radius(rect, radius);
let inner = inset_rect(rect, stroke_width as i32);
let has_inner = inner.width > 0 && inner.height > 0;
let inner_radius = effective_radius.saturating_sub(stroke_width);
for py in y0..=y1 {
for px in x0..=x1 {
let outer_coverage = rounded_rect_coverage(px, py, rect, effective_radius);
if outer_coverage <= 0.0 {
continue;
}
let inner_coverage = if has_inner {
rounded_rect_coverage(px, py, inner, inner_radius)
} else {
0.0
};
let stroke_coverage = (outer_coverage - inner_coverage).clamp(0.0, 1.0);
if stroke_coverage > 0.0 {
blend_pixel_clipped!(
self.blend_mode,
clip,
frame,
size.width,
px,
py,
color,
stroke_coverage
);
}
}
}
}
pub fn draw_rounded_rect_aa_with_width(
&mut self,
rect: Rect,
radius: u32,
color: Color,
stroke_width: u32,
) {
if stroke_width == 0 || rect.width == 0 || rect.height == 0 {
return;
}
let sample_grid = self.aa_samples_per_axis;
let size = self.buffer.size();
let width = size.width as i32;
let height = size.height as i32;
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let x0 = rect.x.max(0);
let y0 = rect.y.max(0);
let x1 = (rect.right() - 1).min(width - 1);
let y1 = (rect.bottom() - 1).min(height - 1);
let effective_radius = rounded_rect_effective_radius(rect, radius);
let inner = inset_rect(rect, stroke_width as i32);
let has_inner = inner.width > 0 && inner.height > 0;
let inner_radius = effective_radius.saturating_sub(stroke_width);
for py in y0..=y1 {
for px in x0..=x1 {
let outer_coverage =
rounded_rect_coverage_grid(px, py, rect, effective_radius, sample_grid);
if outer_coverage <= 0.0 {
continue;
}
let inner_coverage = if has_inner {
rounded_rect_coverage_grid(px, py, inner, inner_radius, sample_grid)
} else {
0.0
};
let stroke_coverage = (outer_coverage - inner_coverage).clamp(0.0, 1.0);
if stroke_coverage > 0.0 {
blend_pixel_clipped!(
self.blend_mode,
clip,
frame,
size.width,
px,
py,
color,
stroke_coverage
);
}
}
}
}
pub fn draw_line(&mut self, from: Point, to: Point, color: Color) {
self.draw_line_with_width(from, to, color, 1);
}
pub fn draw_line_with_width(
&mut self,
from: Point,
to: Point,
color: Color,
stroke_width: u32,
) {
if stroke_width == 0 {
return;
}
let size = self.buffer.size();
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let blend = self.blend_mode;
let brush_start = -(stroke_width as i32 / 2);
let idempotent = matches!(blend, BlendMode::Normal) && color.a == 255;
if idempotent && (from.y == to.y || from.x == to.x) {
let x_start = from.x.min(to.x);
let x_end = from.x.max(to.x);
let y_start = from.y.min(to.y);
let y_end = from.y.max(to.y);
let span_width = (x_end as i64 - x_start as i64)
.saturating_add(stroke_width as i64)
.clamp(0, u32::MAX as i64) as u32;
let span_height = (y_end as i64 - y_start as i64)
.saturating_add(stroke_width as i64)
.clamp(0, u32::MAX as i64) as u32;
write_solid_bounds(
blend,
clip,
frame,
(size.width, size.height),
(
x_start.saturating_add(brush_start),
y_start.saturating_add(brush_start),
span_width,
span_height,
),
color,
);
return;
}
let mut x0 = from.x;
let mut y0 = from.y;
let x1 = to.x;
let y1 = to.y;
let dx = (x1 - x0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let dy = -(y1 - y0).abs();
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
loop {
write_solid_bounds(
blend,
clip,
frame,
(size.width, size.height),
(
x0.saturating_add(brush_start),
y0.saturating_add(brush_start),
stroke_width,
stroke_width,
),
color,
);
if x0 == x1 && y0 == y1 {
break;
}
let e2 = err * 2;
if e2 >= dy {
err += dy;
x0 += sx;
}
if e2 <= dx {
err += dx;
y0 += sy;
}
}
}
pub fn draw_line_aa(&mut self, from: Point, to: Point, color: Color) {
self.draw_line_aa_with_width(from, to, color, 1);
}
pub fn draw_line_aa_with_width(
&mut self,
from: Point,
to: Point,
color: Color,
stroke_width: u32,
) {
if stroke_width == 0 {
return;
}
let sample_grid = self.aa_samples_per_axis;
let size = self.buffer.size();
let width = size.width as i32;
let height = size.height as i32;
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let half = stroke_width as f32 / 2.0;
let pad = half.ceil() as i32 + 1;
let min_x = from.x.min(to.x).saturating_sub(pad).max(0);
let max_x = (from.x.max(to.x) + pad).min(width - 1);
let min_y = from.y.min(to.y).saturating_sub(pad).max(0);
let max_y = (from.y.max(to.y) + pad).min(height - 1);
let ax = from.x as f32;
let ay = from.y as f32;
let bx = to.x as f32;
let by = to.y as f32;
for py in min_y..=max_y {
for px in min_x..=max_x {
let coverage = line_stroke_coverage_grid(px, py, ax, ay, bx, by, half, sample_grid);
if coverage > 0.0 {
blend_pixel_clipped!(
self.blend_mode,
clip,
frame,
size.width,
px,
py,
color,
coverage
);
}
}
}
}
pub fn fill_circle(&mut self, center: Point, radius: u32, color: Color) {
if radius == 0 {
return;
}
let size = self.buffer.size();
let width = size.width as i32;
let height = size.height as i32;
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let r = radius as i32;
let rr = r as i64 * r as i64;
for y in -r..=r {
let y2 = y as i64 * y as i64;
if y2 > rr {
continue;
}
let span = ((rr - y2) as f64).sqrt() as i32;
let py = center.y + y;
if py < 0 || py >= height {
continue;
}
for x in -span..=span {
let px = center.x + x;
if px < 0 || px >= width {
continue;
}
set_pixel_clipped!(self.blend_mode, clip, frame, size.width, px, py, color);
}
}
}
pub fn fill_circle_aa(&mut self, center: Point, radius: u32, color: Color) {
if radius == 0 {
return;
}
let sample_grid = self.aa_samples_per_axis;
let size = self.buffer.size();
let width = size.width as i32;
let height = size.height as i32;
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let r = radius as f32;
let x0 = (center.x - radius as i32 - 1).max(0);
let y0 = (center.y - radius as i32 - 1).max(0);
let x1 = (center.x + radius as i32 + 1).min(width - 1);
let y1 = (center.y + radius as i32 + 1).min(height - 1);
for py in y0..=y1 {
for px in x0..=x1 {
let coverage = circle_fill_coverage_grid(px, py, center, r, sample_grid);
if coverage > 0.0 {
blend_pixel_clipped!(
self.blend_mode,
clip,
frame,
size.width,
px,
py,
color,
coverage
);
}
}
}
}
pub fn draw_circle(&mut self, center: Point, radius: u32, color: Color) {
self.draw_circle_with_width(center, radius, color, 1);
}
pub fn draw_circle_with_width(
&mut self,
center: Point,
radius: u32,
color: Color,
stroke_width: u32,
) {
if radius == 0 {
return;
}
if stroke_width == 0 {
return;
}
let sample_grid = self.aa_samples_per_axis;
let size = self.buffer.size();
let width = size.width as i32;
let height = size.height as i32;
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let ring_radius = radius as f32;
let x0 = (center.x - radius as i32 - 1).max(0);
let y0 = (center.y - radius as i32 - 1).max(0);
let x1 = (center.x + radius as i32 + 1).min(width - 1);
let y1 = (center.y + radius as i32 + 1).min(height - 1);
for py in y0..=y1 {
for px in x0..=x1 {
let stroke_coverage = circle_stroke_coverage_grid(
px,
py,
center,
ring_radius,
stroke_width as f32,
sample_grid,
);
if stroke_coverage > 0.0 {
blend_pixel_clipped!(
self.blend_mode,
clip,
frame,
size.width,
px,
py,
color,
stroke_coverage
);
}
}
}
}
pub fn draw_arc(
&mut self,
center: Point,
radius: u32,
start_angle: f32,
end_angle: f32,
color: Color,
filled: bool,
) {
if radius == 0 {
return;
}
const TWO_PI: f32 = core::f32::consts::TAU;
let mut start = start_angle % TWO_PI;
if start < 0.0 {
start += TWO_PI;
}
let mut end = end_angle % TWO_PI;
if end < 0.0 {
end += TWO_PI;
}
let step = 0.02f32;
let mut angle = start;
let mut points: Vec<Point> = Vec::new();
if start <= end {
while angle <= end && angle <= start + TWO_PI {
let x = center.x + (radius as f32 * angle.cos()).round() as i32;
let y = center.y + (radius as f32 * angle.sin()).round() as i32;
points.push(Point::new(x, y));
angle += step;
}
} else {
while angle < TWO_PI && angle <= start + TWO_PI {
let x = center.x + (radius as f32 * angle.cos()).round() as i32;
let y = center.y + (radius as f32 * angle.sin()).round() as i32;
points.push(Point::new(x, y));
angle += step;
}
angle = 0.0;
while angle <= end && angle <= TWO_PI {
let x = center.x + (radius as f32 * angle.cos()).round() as i32;
let y = center.y + (radius as f32 * angle.sin()).round() as i32;
points.push(Point::new(x, y));
angle += step;
}
}
let last_x = center.x + (radius as f32 * end.cos()).round() as i32;
let last_y = center.y + (radius as f32 * end.sin()).round() as i32;
points.push(Point::new(last_x, last_y));
if filled {
for i in 0..points.len().saturating_sub(1) {
self.fill_triangle(center, points[i], points[i + 1], color);
}
} else {
for i in 0..points.len().saturating_sub(1) {
self.draw_line_with_width(points[i], points[i + 1], color, 1);
}
}
}
pub fn draw_path(
&mut self,
points: &[Point],
closed: bool,
color: Color,
filled: bool,
width: u32,
) {
if points.len() < 2 {
return;
}
if filled {
self.fill_polygon(points, color);
} else {
for i in 0..points.len().saturating_sub(1) {
self.draw_line_with_width(points[i], points[i + 1], color, width);
}
if closed {
self.draw_line_with_width(points[points.len() - 1], points[0], color, width);
}
}
}
pub fn fill_path_non_zero(&mut self, path: &crate::render::CompoundPath, color: Color) {
self.fill_path_non_zero_borrowed(path.points(), path.contours(), color, 0, 0)
.expect("CompoundPath owns validated contour ranges");
}
pub(crate) fn fill_path_non_zero_borrowed(
&mut self,
points: &[Point],
contours: &[(usize, usize)],
color: Color,
offset_x: i32,
offset_y: i32,
) -> Result<(), crate::render::CompoundPathError> {
crate::render::CompoundPath::validate_ranges(points.len(), contours)?;
let size = self.buffer.size();
let clip = self.current_clip();
let intersections = &mut self.nonzero_intersections;
if intersections.capacity() < points.len() {
intersections.reserve(points.len());
}
let frame = self.buffer.back_mut();
let min_y = contours
.iter()
.flat_map(|&(start, end)| &points[start..end])
.map(|point| point.y + offset_y)
.min()
.unwrap_or(0)
.max(0);
let max_y = contours
.iter()
.flat_map(|&(start, end)| &points[start..end])
.map(|point| point.y + offset_y)
.max()
.unwrap_or(0)
.min(size.height as i32 - 1);
for y in min_y..=max_y {
intersections.clear();
for &(start, end) in contours {
for index in start..end {
let first = points[index];
let second = points[if index + 1 == end { start } else { index + 1 }];
let first_y = first.y + offset_y;
let second_y = second.y + offset_y;
let winding = if first_y <= y && second_y > y {
1
} else if second_y <= y && first_y > y {
-1
} else {
continue;
};
let first_x = first.x + offset_x;
let second_x = second.x + offset_x;
let t = (y - first_y) as f32 / (second_y - first_y) as f32;
let x = first_x as f32 + (second_x - first_x) as f32 * t;
intersections.push((x, winding));
}
}
intersections.sort_by(|left, right| {
left.0.partial_cmp(&right.0).unwrap_or(core::cmp::Ordering::Equal)
});
let mut index = 0;
let mut winding = 0;
let mut previous_x: Option<f32> = None;
while index < intersections.len() {
let x = intersections[index].0;
if winding != 0 {
if let Some(previous) = previous_x {
let x_start = previous.ceil() as i32;
let x_end = x.floor() as i32;
for pixel_x in x_start.max(0)..=x_end.min(size.width as i32 - 1) {
set_pixel_clipped!(
self.blend_mode,
clip,
frame,
size.width,
pixel_x as u32,
y as u32,
color
);
}
}
}
let mut delta = 0;
while index < intersections.len() && intersections[index].0 == x {
delta += intersections[index].1;
index += 1;
}
winding += delta;
previous_x = Some(x);
}
}
Ok(())
}
fn fill_polygon(&mut self, points: &[Point], color: Color) {
let size = self.buffer.size();
let clip = self.current_clip();
let frame = self.buffer.back_mut();
let n = points.len();
if n < 3 {
return;
}
let min_y = points.iter().map(|p| p.y).min().unwrap_or(0).max(0);
let max_y = points.iter().map(|p| p.y).max().unwrap_or(0).min(size.height as i32 - 1);
let mut intersections: Vec<f32> = Vec::with_capacity(n);
for y in min_y..=max_y {
intersections.clear();
let mut j = n - 1;
for i in 0..n {
let p1 = points[i];
let p2 = points[j];
if (p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y) {
let t = (y - p1.y) as f32 / (p2.y - p1.y) as f32;
let x = p1.x as f32 + (p2.x - p1.x) as f32 * t;
intersections.push(x);
}
j = i;
}
intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let mut i = 0;
while i + 1 < intersections.len() {
let x_start = intersections[i].ceil() as i32;
let x_end = intersections[i + 1].floor() as i32;
let x_start = x_start.max(0);
let x_end = x_end.min(size.width as i32 - 1);
for x in x_start..=x_end {
set_pixel_clipped!(self.blend_mode, clip, frame, size.width, x, y, color);
}
i += 2;
}
}
}
#[cfg(feature = "3d")]
pub fn fill_quad_aa(&mut self, corners: [Point; 4], color: Color) {
self.fill_triangle_aa(corners[0], corners[1], corners[2], color);
self.fill_triangle_aa(corners[0], corners[2], corners[3], color);
}
#[cfg(feature = "3d")]
fn fill_triangle_aa(&mut self, a: Point, b: Point, c: Point, color: Color) {
let size = self.buffer.size();
if size.width == 0 || size.height == 0 {
return;
}
let ax = a.x as f32;
let ay = a.y as f32;
let mut bx = b.x as f32;
let mut by = b.y as f32;
let mut cx = c.x as f32;
let mut cy = c.y as f32;
let area = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
if area.abs() < f32::EPSILON {
return;
}
if area < 0.0 {
core::mem::swap(&mut bx, &mut cx);
core::mem::swap(&mut by, &mut cy);
}
let min_x = a.x.min(b.x).min(c.x).max(0);
let min_y = a.y.min(b.y).min(c.y).max(0);
let max_x = a.x.max(b.x).max(c.x).min(size.width as i32 - 1);
let max_y = a.y.max(b.y).max(c.y).min(size.height as i32 - 1);
if max_x < min_x || max_y < min_y {
return;
}
let clip = self.current_clip();
let blend = self.blend_mode;
let width = size.width;
let frame = self.buffer.back_mut();
let sample_step = 1.0 / QUAD_AA_SAMPLES as f32;
let sample_count = (QUAD_AA_SAMPLES * QUAD_AA_SAMPLES) as f32;
for y in min_y..=max_y {
for x in min_x..=max_x {
if !pixel_visible(clip, x, y) {
continue;
}
let mut inside = 0u32;
for sy in 0..QUAD_AA_SAMPLES {
let py = y as f32 + (sy as f32 + 0.5) * sample_step;
for sx in 0..QUAD_AA_SAMPLES {
let px = x as f32 + (sx as f32 + 0.5) * sample_step;
if inside_top_left(ax, ay, bx, by, cx, cy, px, py) {
inside += 1;
}
}
}
if inside == 0 {
continue;
}
let coverage = inside as f32 / sample_count;
write_pixel_with_mode(blend, frame, width, x as u32, y as u32, color, coverage);
}
}
}
fn fill_triangle(&mut self, v0: Point, v1: Point, v2: Point, color: Color) {
let mut vs = [v0, v1, v2];
vs.sort_by_key(|v| v.y);
let [a, b, c] = vs;
let size = self.buffer.size();
let clip = self.current_clip();
let frame = self.buffer.back_mut();
if a.y == c.y {
let min_x = a.x.min(b.x).min(c.x);
let max_x = a.x.max(b.x).max(c.x);
if max_x > min_x {
for x in min_x.max(0)..=max_x.min(size.width as i32 - 1) {
set_pixel_clipped!(self.blend_mode, clip, frame, size.width, x, a.y, color);
}
}
return;
}
let total_height = c.y - a.y;
if total_height <= 0 {
return;
}
let lerp_x =
|p1: Point, p2: Point, t: f32| -> f32 { p1.x as f32 + (p2.x - p1.x) as f32 * t };
for y in a.y..=c.y {
let (x1, x2) = if y < b.y {
let sub_h = b.y - a.y;
if sub_h == 0 {
continue;
}
(
lerp_x(a, c, (y - a.y) as f32 / total_height as f32),
lerp_x(a, b, (y - a.y) as f32 / sub_h as f32),
)
} else {
let sub_h = c.y - b.y;
if sub_h == 0 {
continue;
}
(
lerp_x(a, c, (y - a.y) as f32 / total_height as f32),
lerp_x(b, c, (y - b.y) as f32 / sub_h as f32),
)
};
if y < 0 || y as u32 >= size.height {
continue;
}
let x_start = x1.min(x2).ceil() as i32;
let x_end = x1.max(x2).floor() as i32;
if x_end >= x_start {
let x_start = x_start.max(0);
let x_end = x_end.min(size.width as i32 - 1);
for x in x_start..=x_end {
set_pixel_clipped!(self.blend_mode, clip, frame, size.width, x, y, color);
}
}
}
}
pub fn draw_text(
&mut self,
origin: Point,
text: &str,
font: &Font,
color: Color,
alignment: HorizontalAlignment,
) {
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", cjk_outline_face))]
if self.glyph_run_text && self.draw_text_glyph_run(origin, text, font, color, alignment) {
return;
}
let shaped = self.shape_text(text, font);
let scale = self.dpi_scale();
let line = crate::render::TextMetrics::for_font(font, scale);
let width = shaped.advance().round() as u32;
if width == 0 || line.height == 0 {
return;
}
let metrics = crate::render::TextMetrics { width, ..line };
let adjusted_origin_x = match alignment {
HorizontalAlignment::Left => origin.x,
HorizontalAlignment::Center => origin.x - (metrics.width / 2) as i32,
HorizontalAlignment::Right => origin.x - metrics.width as i32,
};
let mut pen_x = adjusted_origin_x as f32;
let glyph_height = metrics.height.max(1) as i32;
let size = self.buffer.size();
let clip = self.current_clip();
let tracking = font.letter_spacing() * scale;
let last_index = shaped.clusters().len().saturating_sub(1);
let mut coverage = core::mem::take(&mut self.text_coverage_scratch);
let mut max_cell = coverage.len();
let frame = self.buffer.back_mut();
#[cfg(feature = "fonts-emoji-color")]
let mut color_scratch = take_color_scratch();
for (index, cluster) in shaped.clusters().iter().enumerate() {
let glyph_width = cluster.advance.max(1.0).round() as i32;
let display_char = cluster
.text
.chars()
.find(|ch| !is_combining_mark(*ch) && !is_variation_selector(*ch));
if let Some(ch) = display_char {
let cell_len = (glyph_width as usize).saturating_mul(glyph_height as usize);
if cell_len > max_cell {
coverage.resize(cell_len, 0);
max_cell = cell_len;
}
#[cfg(feature = "fonts-emoji-color")]
{
let color_len = cell_len.saturating_mul(4);
if color_scratch.len() < color_len {
color_scratch.resize(color_len, 0);
}
}
let mut config = GlyphDrawConfig {
canvas: &mut *frame,
canvas_width: size.width,
canvas_height: size.height,
w: glyph_width as u32,
h: glyph_height as u32,
color,
clip,
weight: font.weight(),
italic: font.is_italic(),
};
#[cfg(feature = "fonts-emoji-color")]
let drawn = blend_color_glyph(
ch,
pen_x.round() as i32,
origin.y,
&mut color_scratch,
&mut config,
);
#[cfg(not(feature = "fonts-emoji-color"))]
let drawn = false;
#[cfg(feature = "text-shaping")]
let drawn = drawn
|| blend_glyph_from_atlas(
ch,
pen_x.round() as i32,
origin.y,
font.family(),
&mut config,
);
if !drawn {
blend_painted_glyph(
ch,
pen_x.round() as i32,
origin.y,
&mut coverage,
&mut config,
);
}
}
pen_x += cluster.advance;
if index < last_index {
pen_x += tracking;
}
}
self.text_coverage_scratch = coverage;
#[cfg(feature = "fonts-emoji-color")]
return_color_scratch(color_scratch);
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", cjk_outline_face))]
fn draw_text_glyph_run(
&mut self,
origin: Point,
text: &str,
font: &Font,
color: Color,
alignment: HorizontalAlignment,
) -> bool {
use crate::render::text::{outline_glyph_at_weight, shaping, Cell};
let scale = self.dpi_scale();
let weight = font.weight();
let Some(run) = shaping::glyph_run_for_family_at_weight(text, font, scale, weight) else {
return false;
};
let line = crate::render::TextMetrics::for_font(font, scale);
if line.height == 0 {
return true;
}
let tracking = font.letter_spacing() * scale;
let glyph_count = run.glyph_ids.len();
let total_width = run.width + tracking * glyph_count.saturating_sub(1) as f32;
let width = total_width.round().max(0.0) as u32;
let origin_x = match alignment {
HorizontalAlignment::Left => origin.x,
HorizontalAlignment::Center => origin.x - (width / 2) as i32,
HorizontalAlignment::Right => origin.x - width as i32,
};
let cell = Cell::new(width.max(1), line.height);
let mut scratch = take_outline_scratch();
for (index, (&glyph_id, &(x_offset, y_offset))) in
run.glyph_ids.iter().zip(run.positions.iter()).enumerate()
{
let Some(count) = outline_glyph_at_weight(
glyph_id as u16,
cell,
font.family(),
weight,
&mut scratch.points,
&mut scratch.contours,
) else {
continue;
};
let vertex_count =
scratch.contours[..count].iter().map(|&(_, end)| end).max().unwrap_or(0);
if vertex_count == 0 {
continue;
}
scratch.polygon.clear();
scratch.polygon.extend(scratch.points[..vertex_count].iter().map(|point| {
let x = origin_x as f32 + x_offset + tracking * index as f32 + point.x;
let y = origin.y as f32 + y_offset + point.y;
Point::new(x.round() as i32, y.round() as i32)
}));
let _ = self.fill_path_non_zero_borrowed(
&scratch.polygon,
&scratch.contours[..count],
color,
0,
0,
);
}
return_outline_scratch(scratch);
true
}
pub fn draw_image(&mut self, x: i32, y: i32, width: u32, height: u32, data: &[u8]) {
let size = self.buffer.size();
if width == 0 || height == 0 {
return;
}
let Some(expected_len) =
(width as usize).checked_mul(height as usize).and_then(|pixels| pixels.checked_mul(4))
else {
return;
};
if data.len() < expected_len {
return;
}
let clip = self.current_clip();
let screen_width = size.width;
let screen_height = size.height;
let Some((left, top, right, bottom)) =
visible_bounds(x, y, width, height, (screen_width, screen_height), clip)
else {
return;
};
let stride = width as usize;
let frame = self.buffer.back_mut();
for row in top..bottom {
let columns = (right - left) as usize;
let source_start = (row - y) as usize * stride + (left - x) as usize;
let source = &data[source_start * 4..(source_start + columns) * 4];
let destination_start = (row as u32 * screen_width + left as u32) as usize * 4;
let destination = &mut frame[destination_start..destination_start + columns * 4];
let source_pixels = source.as_chunks::<4>().0;
let destination_pixels = destination.as_chunks_mut::<4>().0;
let mut index = 0;
while index < source_pixels.len() {
if source_pixels[index][3] == 255 {
let mut end = index;
while end < source_pixels.len() && source_pixels[end][3] == 255 {
end += 1;
}
for (target, pixel) in
destination_pixels[index..end].iter_mut().zip(&source_pixels[index..end])
{
*target = *pixel;
}
index = end;
} else {
blend_source_over(&mut destination_pixels[index], source_pixels[index]);
index += 1;
}
}
}
}
pub fn draw_image_quad(
&mut self,
corners: [Point; 4],
source_width: u32,
source_height: u32,
data: &[u8],
) {
if source_width == 0 || source_height == 0 {
return;
}
let expected_len = (source_width as usize) * (source_height as usize) * 4;
if data.len() < expected_len {
return;
}
if !corners_are_parallelogram(&corners) {
self.draw_image_quad_projective(corners, source_width, source_height, data);
return;
}
let [p00, p10, _, p01] = corners;
let ux = (p10.x - p00.x) as f32;
let uy = (p10.y - p00.y) as f32;
let vx = (p01.x - p00.x) as f32;
let vy = (p01.y - p00.y) as f32;
let det = ux * vy - uy * vx;
if det.abs() < f32::EPSILON {
return;
}
let min_x = corners.iter().map(|p| p.x).min().unwrap_or(0);
let min_y = corners.iter().map(|p| p.y).min().unwrap_or(0);
let max_x = corners.iter().map(|p| p.x).max().unwrap_or(0);
let max_y = corners.iter().map(|p| p.y).max().unwrap_or(0);
let size = self.buffer.size();
let clip = self.current_clip();
let screen_width = size.width;
let screen_height = size.height;
let frame = self.buffer.back_mut();
for y in min_y..=max_y {
if y < 0 || y as u32 >= screen_height {
continue;
}
for x in min_x..=max_x {
if x < 0 || x as u32 >= screen_width {
continue;
}
if !pixel_visible(clip, x, y) {
continue;
}
let dx = (x - p00.x) as f32 + 0.5;
let dy = (y - p00.y) as f32 + 0.5;
let u = (dx * vy - dy * vx) / det;
let v = (dy * ux - dx * uy) / det;
if !(0.0..1.0).contains(&u) || !(0.0..1.0).contains(&v) {
continue;
}
let [r, g, b, a] = sample_bilinear(data, source_width, source_height, u, v);
blend_source_pixel(frame, screen_width, x as u32, y as u32, r, g, b, a);
}
}
}
fn draw_image_quad_projective(
&mut self,
corners: [Point; 4],
source_width: u32,
source_height: u32,
data: &[u8],
) {
let Some(inverse) = inverse_quad_homography(&corners) else {
return;
};
let size = self.buffer.size();
let clip = self.current_clip();
let screen_width = size.width;
let max_x = corners.iter().map(|p| p.x).max().unwrap_or(0);
let max_y = corners.iter().map(|p| p.y).max().unwrap_or(0);
let min_x = corners.iter().map(|p| p.x).min().unwrap_or(0);
let min_y = corners.iter().map(|p| p.y).min().unwrap_or(0);
let frame = self.buffer.back_mut();
for y in min_y..=max_y {
if y < 0 || y as u32 >= size.height {
continue;
}
for x in min_x..=max_x {
if x < 0 || x as u32 >= screen_width {
continue;
}
if !pixel_visible(clip, x, y) {
continue;
}
let px = x as f32 + 0.5;
let py = y as f32 + 0.5;
let w = inverse[6] * px + inverse[7] * py + inverse[8];
if w.abs() < f32::EPSILON {
continue;
}
let u = (inverse[0] * px + inverse[1] * py + inverse[2]) / w;
let v = (inverse[3] * px + inverse[4] * py + inverse[5]) / w;
if !(0.0..1.0).contains(&u) || !(0.0..1.0).contains(&v) {
continue;
}
let [r, g, b, a] = sample_bilinear(data, source_width, source_height, u, v);
blend_source_pixel(frame, screen_width, x as u32, y as u32, r, g, b, a);
}
}
}
}
fn corners_are_parallelogram(corners: &[Point; 4]) -> bool {
let [p00, p10, p20, p01] = corners;
let implied_x = p10.x as i64 + p01.x as i64 - p00.x as i64;
let implied_y = p10.y as i64 + p01.y as i64 - p00.y as i64;
(p20.x as i64 - implied_x).abs() <= 1 && (p20.y as i64 - implied_y).abs() <= 1
}
fn inverse_quad_homography(corners: &[Point; 4]) -> Option<[f32; 9]> {
let [p00, p10, p20, p01] = *corners;
let (x0, y0) = (p00.x as f32, p00.y as f32);
let (x1, y1) = (p10.x as f32, p10.y as f32);
let (x2, y2) = (p20.x as f32, p20.y as f32);
let (x3, y3) = (p01.x as f32, p01.y as f32);
let dx1 = x1 - x2;
let dx2 = x3 - x2;
let dx3 = x0 - x1 + x2 - x3;
let dy1 = y1 - y2;
let dy2 = y3 - y2;
let dy3 = y0 - y1 + y2 - y3;
let (a, b, c, d, e, f, g, h);
if dx3.abs() < f32::EPSILON && dy3.abs() < f32::EPSILON {
a = x1 - x0;
b = x3 - x0;
c = x0;
d = y1 - y0;
e = y3 - y0;
f = y0;
g = 0.0;
h = 0.0;
} else {
let denominator = dx1 * dy2 - dx2 * dy1;
if denominator.abs() < f32::EPSILON {
return None;
}
g = (dx3 * dy2 - dx2 * dy3) / denominator;
h = (dx1 * dy3 - dx3 * dy1) / denominator;
a = x1 - x0 + g * x1;
b = x3 - x0 + h * x3;
c = x0;
d = y1 - y0 + g * y1;
e = y3 - y0 + h * y3;
f = y0;
}
let determinant = a * (e - f * h) - b * (d - f * g) + c * (d * h - e * g);
if determinant.abs() < f32::EPSILON {
return None;
}
let scale = 1.0 / determinant;
Some([
(e - f * h) * scale,
(c * h - b) * scale,
(b * f - c * e) * scale,
(f * g - d) * scale,
(a - c * g) * scale,
(c * d - a * f) * scale,
(d * h - e * g) * scale,
(b * g - a * h) * scale,
(a * e - b * d) * scale,
])
}
fn sample_bilinear(data: &[u8], width: u32, height: u32, u: f32, v: f32) -> [u8; 4] {
let fx = u * width as f32 - 0.5;
let fy = v * height as f32 - 0.5;
let x0 = fx.floor();
let y0 = fy.floor();
let tx = fx - x0;
let ty = fy - y0;
let max_x = width.saturating_sub(1) as i32;
let max_y = height.saturating_sub(1) as i32;
let clamp_x = |value: f32| (value as i32).clamp(0, max_x) as usize;
let clamp_y = |value: f32| (value as i32).clamp(0, max_y) as usize;
let x0i = clamp_x(x0);
let x1i = clamp_x(x0 + 1.0);
let y0i = clamp_y(y0);
let y1i = clamp_y(y0 + 1.0);
let stride = width as usize;
let fetch = |x: usize, y: usize| -> [u8; 4] {
let index = (y * stride + x) * 4;
[data[index], data[index + 1], data[index + 2], data[index + 3]]
};
let top_left = fetch(x0i, y0i);
let top_right = fetch(x1i, y0i);
let bottom_left = fetch(x0i, y1i);
let bottom_right = fetch(x1i, y1i);
let mut out = [0u8; 4];
for channel in 0..4 {
let top =
top_left[channel] as f32 + (top_right[channel] as f32 - top_left[channel] as f32) * tx;
let bottom = bottom_left[channel] as f32
+ (bottom_right[channel] as f32 - bottom_left[channel] as f32) * tx;
let value = top + (bottom - top) * ty;
out[channel] = value.round().clamp(0.0, 255.0) as u8;
}
out
}
#[allow(clippy::too_many_arguments)]
fn blend_source_pixel(
frame: &mut [u8],
screen_width: u32,
x: u32,
y: u32,
r: u8,
g: u8,
b: u8,
a: u8,
) {
let index = ((y * screen_width + x) as usize) * 4;
blend_source_over(&mut frame[index..index + 4], [r, g, b, a]);
}
#[inline]
fn blend_source_over(destination: &mut [u8], source: [u8; 4]) {
if source[3] == 0 {
return;
}
if source[3] == 255 {
destination[..4].copy_from_slice(&source);
return;
}
let src_alpha = source[3] as f32 / 255.0;
let dst_alpha = destination[3] as f32 / 255.0;
let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
if out_alpha <= 0.0 {
return;
}
let src_weight = src_alpha / out_alpha;
let dst_weight = dst_alpha * (1.0 - src_alpha) / out_alpha;
for channel in 0..3 {
destination[channel] =
(source[channel] as f32 * src_weight + destination[channel] as f32 * dst_weight) as u8;
}
destination[3] = (out_alpha * 255.0) as u8;
}
#[cfg(feature = "3d")]
const QUAD_AA_SAMPLES: u32 = 4;
#[cfg(feature = "3d")]
#[inline]
fn quad_edge(ax: f32, ay: f32, bx: f32, by: f32, px: f32, py: f32) -> f32 {
(bx - ax) * (py - ay) - (by - ay) * (px - ax)
}
#[cfg(feature = "3d")]
#[inline]
fn quad_edge_is_top_left(ax: f32, ay: f32, bx: f32, by: f32) -> bool {
by > ay || (by == ay && bx < ax)
}
#[cfg(feature = "3d")]
#[inline]
fn inside_top_left(ax: f32, ay: f32, bx: f32, by: f32, cx: f32, cy: f32, px: f32, py: f32) -> bool {
let edges = [(ax, ay, bx, by), (bx, by, cx, cy), (cx, cy, ax, ay)];
for (ex0, ey0, ex1, ey1) in edges {
let value = quad_edge(ex0, ey0, ex1, ey1, px, py);
if value < 0.0 {
return false;
}
if value == 0.0 && !quad_edge_is_top_left(ex0, ey0, ex1, ey1) {
return false;
}
}
true
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", cjk_outline_face))]
struct OutlineScratch {
points: Vec<crate::render::text::OutlinePoint>,
contours: Vec<(usize, usize)>,
polygon: Vec<Point>,
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", cjk_outline_face))]
impl OutlineScratch {
fn new() -> Self {
Self {
points: crate::compat::vec![
crate::render::text::OutlinePoint { x: 0.0, y: 0.0 };
crate::render::text::OUTLINE_MAX_POINTS
],
contours: crate::compat::vec![
(0usize, 0usize); crate::render::text::OUTLINE_MAX_CONTOURS
],
polygon: Vec::new(),
}
}
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", cjk_outline_face))]
thread_local! {
static OUTLINE_SCRATCH: core::cell::RefCell<Option<OutlineScratch>> =
core::cell::RefCell::new(Some(OutlineScratch::new()));
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", cjk_outline_face))]
fn take_outline_scratch() -> OutlineScratch {
OUTLINE_SCRATCH
.with(|slot| match slot.try_borrow_mut() {
Ok(mut slot) => slot.take(),
Err(_) => None,
})
.unwrap_or_else(OutlineScratch::new)
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", cjk_outline_face))]
fn return_outline_scratch(scratch: OutlineScratch) {
OUTLINE_SCRATCH.with(|slot| {
if let Ok(mut slot) = slot.try_borrow_mut() {
if slot.is_none() {
*slot = Some(scratch);
}
}
});
}
#[cfg(feature = "fonts-emoji-color")]
thread_local! {
static TEXT_COLOR_SCRATCH: core::cell::RefCell<Vec<u8>> =
const { core::cell::RefCell::new(Vec::new()) };
}
#[cfg(feature = "fonts-emoji-color")]
fn take_color_scratch() -> Vec<u8> {
TEXT_COLOR_SCRATCH.with(|slot| match slot.try_borrow_mut() {
Ok(mut slot) => core::mem::take(&mut *slot),
Err(_) => Vec::new(),
})
}
#[cfg(feature = "fonts-emoji-color")]
fn return_color_scratch(scratch: Vec<u8>) {
TEXT_COLOR_SCRATCH.with(|slot| {
if let Ok(mut slot) = slot.try_borrow_mut() {
if scratch.capacity() > slot.capacity() {
*slot = scratch;
}
}
});
}
#[cfg(feature = "text-shaping")]
const ATLAS_STACK_RESOLVED_FACE: &str = "\u{1}";
#[cfg(feature = "text-shaping")]
const ATLAS_LOOKUPS_PER_EPOCH: u32 = 4096;
#[cfg(feature = "text-shaping")]
thread_local! {
static TEXT_GLYPH_ATLAS: core::cell::RefCell<crate::render::text::GlyphAtlas> =
core::cell::RefCell::new(crate::render::text::GlyphAtlas::new());
#[allow(clippy::missing_const_for_thread_local)]
static TEXT_GLYPH_ATLAS_CALLS: core::cell::Cell<u32> = core::cell::Cell::new(0);
}
#[cfg(feature = "text-shaping")]
fn blend_glyph_from_atlas(
ch: char,
x: i32,
y: i32,
family: &str,
config: &mut GlyphDrawConfig,
) -> bool {
if config.w == 0 || config.h == 0 || config.w > u8::MAX as u32 || config.h > u8::MAX as u32 {
return false;
}
if config.weight != crate::core::Font::default_weight() {
return false;
}
let family_face = crate::render::text::shaping::face_for_family(family);
if let Some(face) = family_face {
let Some(stack_face) = crate::render::text::face_for_char(ch) else {
return false;
};
if !core::ptr::eq(face.bytes.as_ptr(), stack_face.bytes.as_ptr()) {
return false;
}
}
let key = crate::render::text::GlyphKey::new(
family_face.map_or(ATLAS_STACK_RESOLVED_FACE, |face| face.name),
ch,
config.h as u8,
config.weight,
)
.with_cell(config.w, config.h)
.with_italic(config.italic);
let advance_epoch = TEXT_GLYPH_ATLAS_CALLS.with(|calls| {
let next = calls.get().wrapping_add(1);
calls.set(next);
next % ATLAS_LOOKUPS_PER_EPOCH == 0
});
let slot = TEXT_GLYPH_ATLAS.with(|atlas| match atlas.try_borrow_mut() {
Ok(mut atlas) => {
if advance_epoch {
atlas.begin_frame();
}
Some(atlas.get_or_raster(key))
}
Err(_) => None,
});
let Some(slot) = slot else {
return false;
};
if slot.width() != config.w || slot.height() != config.h {
return false;
}
if !matches!(
slot.ink(),
crate::render::text::InkKind::Bitmap | crate::render::text::InkKind::Coverage
) {
return false;
}
blend_coverage_bitmap(slot.width(), slot.height(), x, y, slot.bitmap(), config, false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Size;
#[test]
fn draw_image_with_overflowing_dimensions_is_ignored() {
let mut surface = SoftwareSurface::new(Size::new(4, 4), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_image(0, 0, 1 << 31, 1 << 31, &[0, 0, 0, 0]);
surface.draw_image(0, 0, u32::MAX, u32::MAX, &[]);
surface.end_frame();
let rgba = surface.frame_rgba();
assert!(
rgba.as_chunks::<4>().0.iter().all(|pixel| pixel.iter().all(|&byte| byte == 255)),
"an impossible size leaves the surface untouched"
);
}
#[test]
fn draw_image_paints_a_buffer_that_matches_its_dimensions() {
let mut surface = SoftwareSurface::new(Size::new(2, 2), 1.0);
surface.begin_frame(Color::WHITE);
let data = [255u8, 0, 0, 255].repeat(4);
surface.draw_image(0, 0, 2, 2, &data);
surface.end_frame();
let rgba = surface.frame_rgba();
assert_eq!(&rgba[..4], &[255, 0, 0, 255]);
}
#[test]
fn the_banded_rounded_fill_matches_the_per_pixel_sampler() {
let cases = [(40u32, 16u32), (17, 9), (100, 40), (10, 10), (7, 30)];
for (width, height) in cases {
for radius in 0..=12u32 {
let rect = Rect::new(0, 0, width, height);
let mut banded = SoftwareSurface::new(Size::new(width + 4, height + 4), 1.0);
banded.begin_frame(Color::BLACK);
banded.fill_rounded_rect(rect, radius, Color::WHITE);
banded.end_frame();
let produced = banded.frame_rgba();
let effective = rounded_rect_effective_radius(rect, radius);
for py in 0..height as i32 {
for px in 0..width as i32 {
let expected =
(rounded_rect_coverage(px, py, rect, effective) * 255.0).round() as u8;
let index = (py as usize * (width + 4) as usize + px as usize) * 4;
let actual = produced[index];
assert!(
actual.abs_diff(expected) <= 1,
"rect {width}x{height} r={radius} at ({px},{py}): banded={actual} expected={expected}"
);
}
}
}
}
}
}