use crate::compat::atomic::{AtomicU64, Ordering};
use crate::compat::{HashMap, Rc, RefCell, Vec, VecDeque};
use crate::core::{Color, Font, Point, Rect, Size};
use crate::render::backend::surface::PathClip;
use crate::render::default_software_render_config;
use crate::render::pipeline::pixel_ops::{
fill_pixels, pixel_visible, set_pixel, write_pixel_with_mode,
};
use crate::render::{
BackBuffer, BlendMode, ShapedText, SoftwareRenderConfig, SoftwareSurface, TextMetrics,
};
use crate::style::Gradient;
use crate::style::GradientType;
static TEXT_METRICS_EPOCH: AtomicU64 = AtomicU64::new(1);
#[cfg(feature = "runtime-fonts")]
pub(crate) fn invalidate_text_measurements() {
TEXT_METRICS_EPOCH.fetch_add(1, Ordering::Relaxed);
}
const TEXT_METRICS_CACHE_LIMIT: usize = 2048;
const TEXT_SHAPES_CACHE_LIMIT: usize = 512;
type TextMetricsCache = HashMap<(u64, u64), (u64, TextMetrics)>;
type ShapeCache = HashMap<(u64, u64), (u64, Rc<ShapedText>)>;
thread_local! {
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static TEXT_METRICS: RefCell<TextMetricsCache> = RefCell::new(HashMap::new());
static TEXT_METRICS_ORDER: RefCell<VecDeque<(u64, u64)>> =
const { RefCell::new(VecDeque::new()) };
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static TEXT_SHAPES: RefCell<ShapeCache> = RefCell::new(HashMap::new());
static TEXT_SHAPES_ORDER: RefCell<VecDeque<(u64, u64)>> =
const { RefCell::new(VecDeque::new()) };
}
fn text_measurement_fingerprint(text: &str, font: &Font, scale: f32) -> (u64, u64) {
use core::hash::{Hash, Hasher};
let size = font.size().to_bits();
let letter = font.letter_spacing().to_bits();
let word = font.word_spacing().to_bits();
let line = font.line_height().to_bits();
let scale = scale.to_bits();
let mut first = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut first);
font.family().hash(&mut first);
size.hash(&mut first);
font.weight().hash(&mut first);
font.is_bold().hash(&mut first);
font.is_italic().hash(&mut first);
letter.hash(&mut first);
word.hash(&mut first);
line.hash(&mut first);
scale.hash(&mut first);
let bytes = text.as_bytes();
let head = &bytes[..bytes.len().min(8)];
let tail = &bytes[bytes.len().saturating_sub(8)..];
let mut second = std::collections::hash_map::DefaultHasher::new();
0xA5u8.hash(&mut second);
bytes.len().hash(&mut second);
head.hash(&mut second);
tail.hash(&mut second);
scale.hash(&mut second);
line.hash(&mut second);
word.hash(&mut second);
letter.hash(&mut second);
font.is_italic().hash(&mut second);
font.is_bold().hash(&mut second);
font.weight().hash(&mut second);
size.hash(&mut second);
font.family().hash(&mut second);
(first.finish(), second.finish())
}
const GRADIENT_LUT_SIZE: usize = 256;
const GRADIENT_LUT_CACHE_LIMIT: usize = 16;
thread_local! {
#[cfg_attr(alloc_frugal, allow(clippy::missing_const_for_thread_local))]
static GRADIENT_LUTS: RefCell<GradientLutCache> = RefCell::new((VecDeque::new(), HashMap::new()));
}
type GradientLutCache = (VecDeque<u64>, HashMap<u64, Rc<[Color; GRADIENT_LUT_SIZE]>>);
fn gradient_ramp_fingerprint(gradient: &Gradient) -> u64 {
use core::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
gradient.stops.len().hash(&mut hasher);
for stop in &gradient.stops {
stop.position.to_bits().hash(&mut hasher);
stop.color.r.hash(&mut hasher);
stop.color.g.hash(&mut hasher);
stop.color.b.hash(&mut hasher);
stop.color.a.hash(&mut hasher);
}
hasher.finish()
}
fn gradient_stops_fingerprint(stops: &[(f32, Color)]) -> u64 {
use core::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
stops.len().hash(&mut hasher);
for (position, color) in stops {
position.to_bits().hash(&mut hasher);
color.r.hash(&mut hasher);
color.g.hash(&mut hasher);
color.b.hash(&mut hasher);
color.a.hash(&mut hasher);
}
hasher.finish()
}
fn cached_ramp_lut(
key: u64,
build: impl FnOnce() -> [Color; GRADIENT_LUT_SIZE],
) -> Rc<[Color; GRADIENT_LUT_SIZE]> {
GRADIENT_LUTS.with(|cache| {
let existing = {
let cache = cache.borrow();
cache.1.get(&key).cloned()
};
if let Some(lut) = existing {
return lut;
}
let lut = Rc::new(build());
let mut cache = cache.borrow_mut();
if cache.0.len() >= GRADIENT_LUT_CACHE_LIMIT {
if let Some(oldest) = cache.0.pop_front() {
cache.1.remove(&oldest);
}
}
cache.0.push_back(key);
cache.1.insert(key, Rc::clone(&lut));
lut
})
}
fn ramp_stop_color(stops: &[(f32, Color)], position: f32) -> Color {
if stops.is_empty() {
return Color::TRANSPARENT;
}
if stops.len() == 1 {
return stops[0].1;
}
let last = stops.len() - 1;
if position <= stops[0].0 {
return stops[0].1;
}
if position >= stops[last].0 {
return stops[last].1;
}
let index = stops.partition_point(|stop| stop.0 <= position);
let (current_position, current_color) = stops[index - 1];
let (next_position, next_color) = stops[index];
let span = next_position - current_position;
if span <= 0.0 {
return current_color;
}
let t = ((position - current_position) / span).clamp(0.0, 1.0);
let channel = |from: u8, to: u8| -> u8 { ((1.0 - t) * from as f32 + t * to as f32) as u8 };
Color::rgba(
channel(current_color.r, next_color.r),
channel(current_color.g, next_color.g),
channel(current_color.b, next_color.b),
channel(current_color.a, next_color.a),
)
}
fn build_ramp(stops: &[(f32, Color)]) -> [Color; GRADIENT_LUT_SIZE] {
let mut lut = [Color::BLACK; GRADIENT_LUT_SIZE];
for (index, slot) in lut.iter_mut().enumerate() {
*slot = ramp_stop_color(stops, index as f32 / (GRADIENT_LUT_SIZE - 1) as f32);
}
lut
}
fn gradient_ramp_lut(gradient: &Gradient) -> Rc<[Color; GRADIENT_LUT_SIZE]> {
cached_ramp_lut(gradient_ramp_fingerprint(gradient), || {
let stops: Vec<(f32, Color)> =
gradient.stops.iter().map(|stop| (stop.position, stop.color)).collect();
build_ramp(&stops)
})
}
pub(crate) fn ramp_lut_for_stops(stops: &[(f32, Color)]) -> Rc<[Color; GRADIENT_LUT_SIZE]> {
cached_ramp_lut(gradient_stops_fingerprint(stops), || build_ramp(stops))
}
pub(crate) enum ConicWrite {
Replace,
Blend(BlendMode),
}
pub(crate) fn fill_conic_ramp(
frame: &mut [u8],
surface_width: u32,
bounds: (u32, u32, u32, u32),
center: Point,
start_angle: f32,
lut: &[Color; GRADIENT_LUT_SIZE],
write: ConicWrite,
) {
const TAU: f32 = core::f32::consts::TAU;
let (x0, y0, x1, y1) = bounds;
let cx = center.x as f32;
let cy = center.y as f32;
for y in y0..y1 {
let dy = y as f32 - cy;
for x in x0..x1 {
let dx = x as f32 - cx;
let angle = dy.atan2(dx) + core::f32::consts::PI;
let angle = (angle + start_angle) % TAU;
let angle = if angle < 0.0 { angle + TAU } else { angle };
let pos = angle / TAU;
let index = ((pos.clamp(0.0, 1.0) * (GRADIENT_LUT_SIZE - 1) as f32) as usize)
.min(GRADIENT_LUT_SIZE - 1);
match write {
ConicWrite::Replace => set_pixel(frame, surface_width, x, y, lut[index]),
ConicWrite::Blend(mode) => {
write_pixel_with_mode(mode, frame, surface_width, x, y, lut[index], 1.0)
}
}
}
}
}
impl SoftwareSurface {
pub fn new(size: Size, dpi_scale: f32) -> Self {
let config = default_software_render_config();
Self {
buffer: BackBuffer::new(size, dpi_scale),
aa_samples_per_axis: config.aa_samples_per_axis,
clip_stack: Vec::new(),
path_clips: Vec::new(),
nonzero_intersections: Vec::new(),
text_coverage_scratch: Vec::new(),
blend_mode: BlendMode::Normal,
glyph_run_text: false,
#[cfg(feature = "3d")]
depth_buffer: Vec::new(),
#[cfg(feature = "3d")]
depth_dirty: true,
}
}
pub fn set_glyph_run_text(&mut self, enabled: bool) {
if self.glyph_run_text == enabled {
return;
}
self.glyph_run_text = enabled;
}
pub fn glyph_run_text(&self) -> bool {
self.glyph_run_text
}
pub fn set_blend_mode(&mut self, mode: BlendMode) {
self.blend_mode = mode;
}
pub fn blend_mode(&self) -> BlendMode {
self.blend_mode
}
pub fn render_config(&self) -> SoftwareRenderConfig {
SoftwareRenderConfig { aa_samples_per_axis: self.aa_samples_per_axis }
}
pub fn apply_render_config(&mut self, config: SoftwareRenderConfig) {
let normalized = config.normalized();
self.aa_samples_per_axis = normalized.aa_samples_per_axis;
}
pub fn set_aa_samples_per_axis(&mut self, samples: u8) {
self.apply_render_config(SoftwareRenderConfig { aa_samples_per_axis: samples });
}
pub fn aa_samples_per_axis(&self) -> u8 {
self.aa_samples_per_axis
}
pub fn begin_frame(&mut self, clear: Color) {
fill_pixels(self.buffer.back_mut(), clear);
#[cfg(feature = "3d")]
{
self.depth_dirty = true;
}
}
pub fn end_frame(&mut self) {
self.buffer.present();
}
pub fn mark_damage(&mut self, rect: Rect) {
self.buffer.mark_damage(rect);
}
pub fn damage_regions(&self) -> &[(i32, i32, u32, u32)] {
self.buffer.damage_regions()
}
pub fn clear_damage(&mut self) {
self.buffer.clear_damage();
}
pub fn present_damage(&mut self) -> bool {
self.buffer.present_damage()
}
pub fn size(&self) -> Size {
self.buffer.size()
}
pub fn resize(&mut self, size: Size) {
self.buffer.resize(size);
#[cfg(feature = "3d")]
{
self.depth_dirty = true;
}
}
pub fn set_dpi_scale(&mut self, dpi_scale: f32) {
self.buffer.set_dpi_scale(dpi_scale);
}
pub fn dpi_scale(&self) -> f32 {
self.buffer.dpi_scale()
}
pub fn frame_rgba(&self) -> &[u8] {
self.buffer.front()
}
pub fn into_frame_rgba(mut self) -> Vec<u8> {
self.buffer.take_front()
}
pub fn measure_text(&self, text: &str, font: &Font) -> TextMetrics {
let scale = self.buffer.dpi_scale();
let epoch = TEXT_METRICS_EPOCH.load(Ordering::Relaxed);
let key = text_measurement_fingerprint(text, font, scale);
if let Some(hit) = TEXT_METRICS.with(|cache| {
let cache = cache.borrow();
cache.get(&key).and_then(|(built, metrics)| (*built == epoch).then_some(*metrics))
}) {
return hit;
}
let shaped = self.shape_text(text, font);
let width = shaped.advance().round() as u32;
let metrics = TextMetrics { width, ..TextMetrics::for_font(font, scale) };
note_measurement_built();
TEXT_METRICS.with(|cache| {
let mut cache = cache.borrow_mut();
let is_new = !cache.contains_key(&key);
if is_new {
TEXT_METRICS_ORDER.with(|order| {
let mut order = order.borrow_mut();
if order.len() >= TEXT_METRICS_CACHE_LIMIT {
if let Some(oldest) = order.pop_front() {
cache.remove(&oldest);
}
}
order.push_back(key);
});
}
cache.insert(key, (epoch, metrics));
});
metrics
}
pub fn shape_text(&self, text: &str, font: &Font) -> Rc<ShapedText> {
let scale = self.buffer.dpi_scale();
let epoch = TEXT_METRICS_EPOCH.load(Ordering::Relaxed);
let key = text_measurement_fingerprint(text, font, scale);
if let Some(hit) = TEXT_SHAPES.with(|cache| {
let cache = cache.borrow();
cache.get(&key).and_then(|(built, shaped)| (*built == epoch).then(|| Rc::clone(shaped)))
}) {
return hit;
}
let shaped = Rc::new(crate::render::text::shape_line(text, font, scale));
note_shape_built();
TEXT_SHAPES.with(|cache| {
let mut cache = cache.borrow_mut();
let is_new = !cache.contains_key(&key);
if is_new {
TEXT_SHAPES_ORDER.with(|order| {
let mut order = order.borrow_mut();
if order.len() >= TEXT_SHAPES_CACHE_LIMIT {
if let Some(oldest) = order.pop_front() {
cache.remove(&oldest);
}
}
order.push_back(key);
});
}
cache.insert(key, (epoch, Rc::clone(&shaped)));
});
shaped
}
pub fn fill_rect_gradient(&mut self, rect: Rect, gradient: &Gradient) {
let size = self.buffer.size();
let clip = self.current_clip();
let x0 = rect.x.max(0) as u32;
let y0 = rect.y.max(0) as u32;
let x1 = rect.x.saturating_add(rect.width as i32).max(0) as u32;
let y1 = rect.y.saturating_add(rect.height as i32).max(0) as u32;
let x1 = x1.min(size.width);
let y1 = y1.min(size.height);
if gradient.gradient_type == GradientType::Conic {
let (visible_x0, visible_x1) = match clip {
Some((clip_x, _, clip_w, _)) => {
intersect_axis(x0, x1, clip_x, clip_x.saturating_add(clip_w as i32))
}
None => (x0, x1),
};
let (visible_y0, visible_y1) = match clip {
Some((_, clip_y, _, clip_h)) => {
intersect_axis(y0, y1, clip_y, clip_y.saturating_add(clip_h as i32))
}
None => (y0, y1),
};
if visible_x1 <= visible_x0 || visible_y1 <= visible_y0 {
return;
}
let lut = gradient_ramp_lut(gradient);
let blend_mode = self.blend_mode;
fill_conic_ramp(
self.buffer.back_mut(),
size.width,
(visible_x0, visible_y0, visible_x1, visible_y1),
gradient.center,
gradient.angle.to_radians(),
&lut,
ConicWrite::Blend(blend_mode),
);
return;
}
let frame = self.buffer.back_mut();
let lut = gradient_ramp_lut(gradient);
let lut_index = |pos: f32| -> usize {
((pos.clamp(0.0, 1.0) * (GRADIENT_LUT_SIZE - 1) as f32) as usize)
.min(GRADIENT_LUT_SIZE - 1)
};
let linear_dx = gradient.end_point.x as f32 - gradient.start_point.x as f32;
let linear_dy = gradient.end_point.y as f32 - gradient.start_point.y as f32;
let linear_dot = linear_dx * linear_dx + linear_dy * linear_dy;
let linear_step = if linear_dot == 0.0 { 0.0 } else { linear_dx / linear_dot };
let radial_cx = gradient.center.x as f32;
for y in y0..y1 {
let row_start = if linear_dot == 0.0 {
0.0
} else {
((x0 as f32 - gradient.start_point.x as f32) * linear_dx
+ (y as f32 - gradient.start_point.y as f32) * linear_dy)
/ linear_dot
};
let radial_dy2 = if matches!(gradient.gradient_type, GradientType::Radial) {
let dy = y as f32 - gradient.center.y as f32;
dy * dy
} else {
0.0
};
for x in x0..x1 {
let pos = match gradient.gradient_type {
GradientType::Linear => {
if linear_dot == 0.0 {
0.0
} else {
(row_start + (x - x0) as f32 * linear_step).clamp(0.0, 1.0)
}
}
GradientType::Radial => {
let dx = x as f32 - radial_cx;
let dist = (dx * dx + radial_dy2).sqrt();
if gradient.radius <= 0.0 {
0.0
} else {
(dist / gradient.radius).clamp(0.0, 1.0)
}
}
GradientType::Conic => return,
};
let color = lut[lut_index(pos)];
if pixel_visible(clip, x as i32, y as i32) {
write_pixel_with_mode(self.blend_mode, frame, size.width, x, y, color, 1.0);
}
}
}
}
pub fn push_clip(&mut self, x: i32, y: i32, width: u32, height: u32) {
if let Some(&(cx, cy, cw, ch)) = self.clip_stack.last() {
let nx = x.max(cx);
let ny = y.max(cy);
let nx2 = (x.saturating_add(width as i32)).min(cx.saturating_add(cw as i32));
let ny2 = (y.saturating_add(height as i32)).min(cy.saturating_add(ch as i32));
let nw = (nx2 - nx).max(0) as u32;
let nh = (ny2 - ny).max(0) as u32;
self.clip_stack.push((nx, ny, nw, nh));
} else {
self.clip_stack.push((x, y, width, height));
}
self.path_clips.push(None);
}
pub fn pop_clip(&mut self) {
self.clip_stack.pop();
let Some(Some(clip)) = self.path_clips.pop() else { return };
let size = self.buffer.size();
apply_path_clip(&mut self.buffer.back, size, &clip);
}
pub(crate) fn push_path_clip(&mut self, points: &[Point]) {
let bounds = path_bounds(points);
match bounds {
Some((x, y, width, height)) => self.push_clip(x, y, width, height),
None => self.push_clip(0, 0, 0, 0),
}
let size = self.buffer.size();
let Some(rect) = visible_path_region(self.current_clip(), size) else {
return;
};
let coverage = path_coverage(points, rect);
let backdrop = crate::render::backend::paint::capture_region(
self.buffer.back.as_slice(),
size.width as usize,
rect,
);
if let Some(slot) = self.path_clips.last_mut() {
*slot = Some(PathClip { rect, coverage, backdrop });
}
}
pub fn current_clip(&self) -> Option<(i32, i32, u32, u32)> {
self.clip_stack.last().copied()
}
}
fn intersect_axis(start: u32, end: u32, clip_start: i32, clip_end: i32) -> (u32, u32) {
let clip_start = clip_start.max(0) as u32;
let clip_end = clip_end.max(0) as u32;
(start.max(clip_start), end.min(clip_end))
}
fn path_bounds(points: &[Point]) -> Option<(i32, i32, u32, u32)> {
if points.len() < 3 {
return None;
}
let mut min_x = i64::MAX;
let mut min_y = i64::MAX;
let mut max_x = i64::MIN;
let mut max_y = i64::MIN;
for point in points {
min_x = min_x.min(point.x as i64);
min_y = min_y.min(point.y as i64);
max_x = max_x.max(point.x as i64);
max_y = max_y.max(point.y as i64);
}
let width = max_x - min_x;
let height = max_y - min_y;
if width <= 0 || height <= 0 {
return None;
}
let x = min_x.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
let y = min_y.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
let width = width.min(u32::MAX as i64) as u32;
let height = height.min(u32::MAX as i64) as u32;
Some((x, y, width, height))
}
fn visible_path_region(
clip: Option<(i32, i32, u32, u32)>,
size: Size,
) -> Option<(i32, i32, u32, u32)> {
let (x, y, width, height) = clip?;
let x0 = (x as i64).max(0);
let y0 = (y as i64).max(0);
let x1 = (x as i64 + width as i64).min(size.width as i64);
let y1 = (y as i64 + height as i64).min(size.height as i64);
if x1 <= x0 || y1 <= y0 {
return None;
}
Some((x0 as i32, y0 as i32, (x1 - x0) as u32, (y1 - y0) as u32))
}
const PATH_CLIP_SUBSAMPLES: usize = 4;
fn path_coverage(points: &[Point], rect: (i32, i32, u32, u32)) -> Vec<u8> {
let (_, y0, width, height) = rect;
let pixel_count = width as usize * height as usize;
let mut coverage = vec![0u8; pixel_count];
if points.len() < 3 || width == 0 || height == 0 {
return coverage;
}
let mut accumulated = vec![0f32; pixel_count];
let mut crossings: Vec<(f32, i32)> = Vec::new();
for row in 0..height as usize {
for sub in 0..PATH_CLIP_SUBSAMPLES {
let sample_y =
y0 as f32 + row as f32 + (sub as f32 + 0.5) / PATH_CLIP_SUBSAMPLES as f32;
crossings.clear();
for index in 0..points.len() {
let first = points[index];
let second = points[(index + 1) % points.len()];
let first_y = first.y as f32;
let second_y = second.y as f32;
let winding = if first_y <= sample_y && second_y > sample_y {
1
} else if second_y <= sample_y && first_y > sample_y {
-1
} else {
continue;
};
let t = (sample_y - first_y) / (second_y - first_y);
let x = first.x as f32 + (second.x - first.x) as f32 * t;
crossings.push((x, winding));
}
crossings.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 span_start: Option<f32> = None;
while index < crossings.len() {
let x = crossings[index].0;
if winding != 0 {
if let Some(start) = span_start {
add_span(&mut accumulated, width, row, start, x);
}
}
let mut delta = 0;
while index < crossings.len() && crossings[index].0 == x {
delta += crossings[index].1;
index += 1;
}
winding += delta;
span_start = Some(x);
}
}
}
let scale = 255.0 / PATH_CLIP_SUBSAMPLES as f32;
for (slot, value) in coverage.iter_mut().zip(accumulated.iter()) {
*slot = (value * scale).round().clamp(0.0, 255.0) as u8;
}
coverage
}
fn add_span(accumulated: &mut [f32], width: u32, row: usize, start: f32, end: f32) {
if !(start.is_finite() && end.is_finite()) || end <= start {
return;
}
let width_f = width as f32;
let start = start.max(0.0);
let end = end.min(width_f);
if end <= start {
return;
}
let first = start.floor() as usize;
let last = (end.ceil() as usize).min(width as usize);
let row_start = row * width as usize;
for column in first..last {
let left = (column as f32).max(start);
let right = (column as f32 + 1.0).min(end);
if right > left {
accumulated[row_start + column] += right - left;
}
}
}
fn apply_path_clip(frame: &mut [u8], size: Size, clip: &PathClip) {
let (x, y, width, height) = clip.rect;
if width == 0 || height == 0 {
return;
}
let pixel_count = width as usize * height as usize;
if clip.coverage.len() != pixel_count || clip.backdrop.len() != pixel_count * 4 {
return;
}
let surface_width = size.width as usize;
for row in 0..height as usize {
for column in 0..width as usize {
let mask_index = row * width as usize + column;
let coverage = clip.coverage[mask_index];
if coverage == 255 {
continue;
}
let frame_index = ((y as usize + row) * surface_width + x as usize + column) * 4;
let backdrop_index = mask_index * 4;
let Some(pixel) = frame.get_mut(frame_index..frame_index + 4) else { continue };
let backdrop = &clip.backdrop[backdrop_index..backdrop_index + 4];
if coverage == 0 {
pixel.copy_from_slice(backdrop);
continue;
}
let foreground = [pixel[0], pixel[1], pixel[2], pixel[3]];
pixel.copy_from_slice(backdrop);
crate::render::pipeline::pixel_ops::blend_pixel(
frame,
surface_width as u32,
(x as usize + column) as u32,
(y as usize + row) as u32,
Color::rgba(foreground[0], foreground[1], foreground[2], foreground[3]),
coverage as f32 / 255.0,
);
}
}
}
#[cfg(test)]
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static TEXT_SHAPES_BUILT: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
#[allow(clippy::missing_const_for_thread_local)]
static TEXT_METRICS_BUILT: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[cfg(test)]
fn note_shape_built() {
TEXT_SHAPES_BUILT.with(|count| count.set(count.get() + 1));
}
#[cfg(not(test))]
#[inline(always)]
fn note_shape_built() {}
#[cfg(test)]
fn note_measurement_built() {
TEXT_METRICS_BUILT.with(|count| count.set(count.get() + 1));
}
#[cfg(not(test))]
#[inline(always)]
fn note_measurement_built() {}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Font, Size};
use crate::render::SoftwareSurface;
fn take_builds() -> (usize, usize) {
let shapes = TEXT_SHAPES_BUILT.with(|count| count.replace(0));
let metrics = TEXT_METRICS_BUILT.with(|count| count.replace(0));
(shapes, metrics)
}
#[test]
fn repeated_text_measurements_are_cached() {
#[cfg(all(feature = "runtime-fonts", feature = "text-shaping"))]
let _guard = crate::render::text::runtime_fonts::runtime_fonts_test_guard();
let surface = SoftwareSurface::new(Size::new(20, 20), 1.0);
let font = Font::default();
let warm = surface.measure_text("hello", &font);
let (shapes, metrics) = take_builds();
assert_eq!((shapes, metrics), (1, 1), "the first measurement has to build its answer");
assert_eq!(surface.measure_text("hello", &font), warm);
assert_eq!(take_builds(), (0, 0), "a repeated measurement must be a cache hit");
let _ = surface.shape_text("hello", &font);
assert_eq!(take_builds(), (0, 0), "a repeated shape must be a cache hit too");
let _ = surface.measure_text("world", &font);
let (shapes, metrics) = take_builds();
assert_eq!((shapes, metrics), (1, 1), "a different string is a different key");
}
#[test]
fn a_shaped_run_hit_is_the_same_allocation_not_a_copy() {
#[cfg(all(feature = "runtime-fonts", feature = "text-shaping"))]
let _guard = crate::render::text::runtime_fonts::runtime_fonts_test_guard();
let surface = SoftwareSurface::new(Size::new(20, 20), 1.0);
let font = Font::default();
let first = surface.shape_text("hello", &font);
let _ = take_builds();
let second = surface.shape_text("hello", &font);
assert!(Rc::ptr_eq(&first, &second), "a cache hit must return the cached run itself");
assert_eq!(take_builds(), (0, 0), "and it must not have shaped anything");
}
#[cfg(feature = "runtime-fonts")]
#[test]
fn invalidating_text_measurements_forces_a_rebuild() {
#[cfg(all(feature = "runtime-fonts", feature = "text-shaping"))]
let _guard = crate::render::text::runtime_fonts::runtime_fonts_test_guard();
let surface = SoftwareSurface::new(Size::new(20, 20), 1.0);
let font = Font::default();
let _ = surface.measure_text("hello", &font);
let _ = take_builds();
invalidate_text_measurements();
let _ = surface.measure_text("hello", &font);
assert_eq!(take_builds(), (1, 1), "a new epoch must miss the old entry");
}
#[test]
fn fingerprints_separate_texts_that_share_a_summary() {
let font = Font::default();
let head = "head-head-";
let tail = "-tail-tail";
let a = format!("{head}alpha{tail}");
let b = format!("{head}bravo{tail}");
assert_eq!(a.len(), b.len(), "the fixture must agree on the summary's length");
assert_eq!(
fingerprint_summary(&a),
fingerprint_summary(&b),
"and on the head/tail bytes, or this test proves nothing"
);
let key_a = text_measurement_fingerprint(&a, &font, 1.0);
let key_b = text_measurement_fingerprint(&b, &font, 1.0);
assert_ne!(key_a, key_b, "different middles must still be different keys");
assert_eq!(text_measurement_fingerprint(&a, &font, 1.0), key_a);
let mut bigger = Font::default();
bigger.set_size(28.0);
assert_ne!(text_measurement_fingerprint(&a, &bigger, 1.0), key_a);
}
fn fingerprint_summary(text: &str) -> (usize, &[u8], &[u8]) {
let bytes = text.as_bytes();
(bytes.len(), &bytes[..bytes.len().min(8)], &bytes[bytes.len().saturating_sub(8)..])
}
fn pixel(frame: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
let offset = ((y * width + x) * 4) as usize;
[frame[offset], frame[offset + 1], frame[offset + 2], frame[offset + 3]]
}
#[test]
fn a_path_clip_keeps_only_the_pixels_the_path_covers() {
let mut surface = SoftwareSurface::new(Size::new(4, 4), 1.0);
surface.begin_frame(Color::WHITE);
surface.push_path_clip(&[Point::new(0, 0), Point::new(0, 4), Point::new(4, 4)]);
surface.fill_rect(Rect::new(0, 0, 4, 4), Color::RED);
surface.pop_clip();
surface.end_frame();
let frame = surface.frame_rgba();
assert_eq!(pixel(frame, 4, 0, 3), [255, 0, 0, 255], "inside the triangle");
assert_eq!(pixel(frame, 4, 3, 0), [255, 255, 255, 255], "outside it, inside the box");
}
#[test]
fn popping_a_path_clip_restores_the_backdrop_for_the_next_draw() {
let mut surface = SoftwareSurface::new(Size::new(4, 4), 1.0);
surface.begin_frame(Color::WHITE);
surface.push_path_clip(&[Point::new(0, 0), Point::new(0, 2), Point::new(2, 2)]);
surface.fill_rect(Rect::new(0, 0, 4, 4), Color::RED);
surface.pop_clip();
surface.fill_rect(Rect::new(3, 0, 1, 1), Color::GREEN);
surface.end_frame();
let frame = surface.frame_rgba();
assert_eq!(pixel(frame, 4, 3, 0), [0, 255, 0, 255], "the later fill is unclipped");
assert_eq!(pixel(frame, 4, 3, 3), [255, 255, 255, 255], "and the mask was removed");
}
#[test]
fn a_degenerate_path_clip_draws_nothing() {
let mut surface = SoftwareSurface::new(Size::new(4, 4), 1.0);
surface.begin_frame(Color::WHITE);
surface.push_path_clip(&[Point::new(1, 1), Point::new(2, 2)]);
surface.fill_rect(Rect::new(0, 0, 4, 4), Color::RED);
surface.pop_clip();
surface.end_frame();
assert!(
surface
.frame_rgba()
.as_chunks::<4>()
.0
.iter()
.all(|pixel| *pixel == [255, 255, 255, 255]),
"an unclosed path encloses no area and must clip everything away"
);
}
}