use crate::compat::{lock, vec, MiniToString, Mutex, OnceLock, Vec};
use crate::core::{Color, Font, HorizontalAlignment, Point, Rect, Size};
use crate::render::pixel_bytes_len;
use crate::render::{BlendMode, PaintBackend, RenderCommand, ShapedText, TextMetrics};
#[derive(Debug, Clone)]
pub struct BackBuffer {
size: Size,
dpi_scale: f32,
front: Vec<u8>,
pub(crate) back: Vec<u8>,
}
impl BackBuffer {
pub fn new(size: Size, dpi_scale: f32) -> Self {
let bytes = pixel_bytes_len(size);
Self { size, dpi_scale: dpi_scale.max(0.1), front: vec![0; bytes], back: vec![0; bytes] }
}
pub fn resize(&mut self, size: Size) {
self.size = size;
let bytes = pixel_bytes_len(size);
self.front.resize(bytes, 0);
self.back.resize(bytes, 0);
}
pub fn size(&self) -> Size {
self.size
}
pub fn dpi_scale(&self) -> f32 {
self.dpi_scale
}
pub fn set_dpi_scale(&mut self, dpi_scale: f32) {
self.dpi_scale = dpi_scale.max(0.1);
}
pub fn back_mut(&mut self) -> &mut [u8] {
&mut self.back
}
pub fn front(&self) -> &[u8] {
&self.front
}
pub fn present(&mut self) {
core::mem::swap(&mut self.front, &mut self.back);
}
}
pub struct SoftwareSurface {
pub(crate) buffer: BackBuffer,
pub(crate) aa_samples_per_axis: u8,
pub(crate) clip_stack: Vec<(i32, i32, u32, u32)>,
pub(crate) blend_mode: BlendMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SoftwareRenderConfig {
pub aa_samples_per_axis: u8,
}
impl Default for SoftwareRenderConfig {
fn default() -> Self {
Self { aa_samples_per_axis: 4 }
}
}
impl SoftwareRenderConfig {
pub fn normalized(self) -> Self {
Self { aa_samples_per_axis: self.aa_samples_per_axis.clamp(1, 8) }
}
}
fn global_software_render_config() -> &'static Mutex<SoftwareRenderConfig> {
static CONFIG: OnceLock<Mutex<SoftwareRenderConfig>> = OnceLock::new();
CONFIG.get_or_init(|| Mutex::new(SoftwareRenderConfig::default()))
}
pub const TEXT_FIT_MARGIN: u32 = 3;
pub(crate) fn fitted_origin(bounds: Rect, advance: i32, alignment: HorizontalAlignment) -> Point {
let inset = TEXT_FIT_MARGIN as i32;
let left = bounds.x.saturating_add(inset);
let right = (bounds.x + bounds.width as i32 - inset).max(left);
let free = (right - left) - advance;
match alignment {
HorizontalAlignment::Left => Point::new(left, bounds.y),
HorizontalAlignment::Center => Point::new(left + (free + 1) / 2, bounds.y),
HorizontalAlignment::Right => Point::new(right - advance, bounds.y),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VerticalAlignment {
Top,
#[default]
Center,
Bottom,
}
pub fn text_line(
band: Rect,
font: &Font,
alignment: VerticalAlignment,
context: &RenderContext,
) -> Rect {
let height = context.measure_text("M", font).height.max(1) as i32;
let offset = match alignment {
VerticalAlignment::Top => 0,
VerticalAlignment::Center => ((band.height as i32 - height) / 2).max(0),
VerticalAlignment::Bottom => (band.height as i32 - height).max(0),
};
Rect::new(band.x, band.y.saturating_add(offset), band.width, height as u32)
}
fn fit_text_to_width(
text: &str,
max_width: f32,
font: &Font,
backend: &dyn PaintBackend,
) -> crate::compat::String {
if text.is_empty() || max_width <= 0.0 {
return crate::compat::String::new();
}
if backend.measure_text(text, font).width as f32 <= max_width {
return text.to_string();
}
const ELLIPSIS: char = '\u{2026}';
let ellipsis_width = backend.measure_text(&ELLIPSIS.to_string(), font).width as f32;
if ellipsis_width > max_width {
return ELLIPSIS.to_string();
}
let budget = max_width - ellipsis_width;
let shaped = backend.shape_text(text, font);
let clusters = shaped.clusters();
let shaped_matches_text = {
let mut rebuilt = crate::compat::String::new();
for cluster in clusters {
rebuilt.push_str(&cluster.text);
}
rebuilt == text
};
if !shaped_matches_text {
return fit_text_to_width_by_prefix(text, budget, font, backend, ELLIPSIS);
}
let tracking = font.letter_spacing() * backend.dpi_scale();
let mut estimated = 0usize;
{
let mut advance = 0.0f32;
for cluster in clusters {
let with_tracking = if estimated == 0 { 0.0 } else { tracking };
let candidate_advance = advance + with_tracking + cluster.advance;
if candidate_advance.round() > budget {
break;
}
advance = candidate_advance;
estimated += 1;
}
}
let prefix_fits = |length: usize| -> bool {
if length == 0 {
return true;
}
let prefix: crate::compat::String =
clusters[..length].iter().flat_map(|cluster| cluster.text.chars()).collect();
backend.measure_text(&prefix, font).width as f32 <= budget
};
const CONFIRM_SLACK: usize = 2;
let search_from = estimated.saturating_sub(CONFIRM_SLACK);
let mut confirmed = None;
let mut length = search_from;
while length <= clusters.len() {
if !prefix_fits(length) {
confirmed = Some(length - 1);
break;
}
length += 1;
}
let confirmed = match confirmed {
Some(value) => value,
None if length > clusters.len() => clusters.len(),
None => return fit_text_to_width_by_prefix(text, budget, font, backend, ELLIPSIS),
};
let mut kept = crate::compat::String::new();
for cluster in &clusters[..confirmed] {
kept.push_str(&cluster.text);
}
kept.push(ELLIPSIS);
kept
}
fn fit_text_to_width_by_prefix(
text: &str,
budget: f32,
font: &Font,
backend: &dyn PaintBackend,
ellipsis: char,
) -> crate::compat::String {
let mut kept = crate::compat::String::new();
for ch in text.chars() {
let candidate = {
let mut next = kept.clone();
next.push(ch);
next
};
if backend.measure_text(&candidate, font).width as f32 > budget {
break;
}
kept = candidate;
}
kept.push(ellipsis);
kept
}
#[cfg(test)]
pub(crate) fn software_render_config_test_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
pub fn set_default_software_render_config(config: SoftwareRenderConfig) {
*lock(global_software_render_config()) = config.normalized();
}
pub fn default_software_render_config() -> SoftwareRenderConfig {
*lock(global_software_render_config())
}
pub struct RenderContext<'a> {
backend: &'a mut dyn PaintBackend,
offset_x: i32,
offset_y: i32,
offset_stack: Vec<(i32, i32)>,
}
impl<'a> RenderContext<'a> {
pub fn new(backend: &'a mut dyn PaintBackend) -> Self {
Self { backend, offset_x: 0, offset_y: 0, offset_stack: Vec::new() }
}
pub fn backend(&mut self) -> &mut dyn PaintBackend {
self.backend
}
pub fn size(&self) -> Size {
self.backend.size()
}
pub fn dpi_scale(&self) -> f32 {
self.backend.dpi_scale()
}
pub fn offset(&self) -> (i32, i32) {
(self.offset_x, self.offset_y)
}
pub fn push_offset(&mut self, dx: i32, dy: i32) {
self.offset_stack.push((self.offset_x, self.offset_y));
self.offset_x += dx;
self.offset_y += dy;
}
pub fn pop_offset(&mut self) {
if let Some(previous) = self.offset_stack.pop() {
self.offset_x = previous.0;
self.offset_y = previous.1;
}
}
fn offset_rect(&self, rect: Rect) -> Rect {
let mut translated = rect;
translated.x += self.offset_x;
translated.y += self.offset_y;
translated
}
fn offset_point(&self, point: Point) -> Point {
Point::new(point.x + self.offset_x, point.y + self.offset_y)
}
fn offset_points(&self, points: &[Point]) -> Vec<Point> {
points.iter().map(|point| self.offset_point(*point)).collect()
}
pub fn fill_rect(&mut self, rect: Rect, color: Color) {
let rect = self.offset_rect(rect);
self.backend.execute_command(&RenderCommand::FillRect { rect, color });
}
pub fn fill_gradient(&mut self, rect: Rect, gradient: &crate::style::Gradient) {
let rect = self.offset_rect(rect);
self.backend
.execute_command(&RenderCommand::DrawGradient { rect, gradient: gradient.clone() });
}
pub fn fill_background(
&mut self,
rect: Rect,
color: Color,
gradient: Option<&crate::style::Gradient>,
) {
match gradient {
Some(gradient) => self.fill_gradient(rect, gradient),
None => self.fill_rect(rect, color),
}
}
pub fn draw_rect(&mut self, rect: Rect, color: Color) {
let rect = self.offset_rect(rect);
self.backend.execute_command(&RenderCommand::DrawRect { rect, color });
}
pub fn draw_rect_stroke(&mut self, rect: Rect, color: Color, width: u32) {
let rect = self.offset_rect(rect);
self.backend.execute_command(&RenderCommand::DrawRectStroke { rect, color, width });
}
pub fn fill_rounded_rect(&mut self, rect: Rect, radius: u32, color: Color) {
let rect = self.offset_rect(rect);
self.backend.execute_command(&RenderCommand::FillRoundedRect { rect, radius, color });
}
pub fn face(
&mut self,
rect: Rect,
fill: Color,
surface: crate::render::SurfaceStyle,
radius: u32,
shadow_tint: Color,
) {
self.face_with_gradient(rect, fill, None, surface, radius, shadow_tint);
}
pub fn face_with_gradient(
&mut self,
rect: Rect,
fill: Color,
gradient: Option<&crate::style::Gradient>,
surface: crate::render::SurfaceStyle,
radius: u32,
shadow_tint: Color,
) {
if let Some(shadow) = surface.shadow(shadow_tint) {
self.fill_rect(
Rect::new(rect.x + shadow.x, rect.y + shadow.y, rect.width, rect.height),
shadow.color,
);
}
let materialised = surface.apply_fill(fill);
match gradient {
Some(gradient) => self.fill_gradient(rect, gradient),
None => {
if radius > 0 {
self.fill_rounded_rect(rect, radius, materialised);
} else {
self.fill_rect(rect, materialised);
}
}
}
}
pub fn fill_rounded_rect_aa(&mut self, rect: Rect, radius: u32, color: Color) {
let rect = self.offset_rect(rect);
self.backend.execute_command(&RenderCommand::FillRoundedRectAA { rect, radius, color });
}
pub fn draw_rounded_rect_stroke(&mut self, rect: Rect, radius: u32, color: Color, width: u32) {
let rect = self.offset_rect(rect);
self.backend.execute_command(&RenderCommand::DrawRoundedRectStroke {
rect,
radius,
color,
width,
});
}
pub fn draw_rounded_rect_stroke_aa(
&mut self,
rect: Rect,
radius: u32,
color: Color,
width: u32,
) {
let rect = self.offset_rect(rect);
self.backend.execute_command(&RenderCommand::DrawRoundedRectStrokeAA {
rect,
radius,
color,
width,
});
}
pub fn draw_line(&mut self, from: Point, to: Point, color: Color) {
let from = self.offset_point(from);
let to = self.offset_point(to);
self.backend.execute_command(&RenderCommand::DrawLine { from, to, color });
}
pub fn draw_line_aa(&mut self, from: Point, to: Point, color: Color) {
let from = self.offset_point(from);
let to = self.offset_point(to);
self.backend.execute_command(&RenderCommand::DrawLineAA { from, to, color });
}
pub fn draw_line_stroke(&mut self, from: Point, to: Point, color: Color, width: u32) {
let from = self.offset_point(from);
let to = self.offset_point(to);
self.backend.execute_command(&RenderCommand::DrawLineStroke { from, to, color, width });
}
pub fn draw_line_stroke_aa(&mut self, from: Point, to: Point, color: Color, width: u32) {
let from = self.offset_point(from);
let to = self.offset_point(to);
self.backend.execute_command(&RenderCommand::DrawLineStrokeAA { from, to, color, width });
}
pub fn fill_circle(&mut self, center: Point, radius: u32, color: Color) {
let center = self.offset_point(center);
self.backend.execute_command(&RenderCommand::FillCircle { center, radius, color });
}
pub fn fill_circle_aa(&mut self, center: Point, radius: u32, color: Color) {
let center = self.offset_point(center);
self.backend.execute_command(&RenderCommand::FillCircleAA { center, radius, color });
}
pub fn draw_circle(&mut self, center: Point, radius: u32, color: Color) {
let center = self.offset_point(center);
self.backend.execute_command(&RenderCommand::DrawCircle { center, radius, color });
}
pub fn draw_circle_stroke(&mut self, center: Point, radius: u32, color: Color, width: u32) {
let center = self.offset_point(center);
self.backend.execute_command(&RenderCommand::DrawCircleStroke {
center,
radius,
color,
width,
});
}
pub fn draw_path(
&mut self,
points: &[Point],
closed: bool,
color: Color,
filled: bool,
width: u32,
) {
let points = self.offset_points(points);
self.backend.execute_command(&RenderCommand::DrawPath {
points,
closed,
color,
filled,
width,
});
}
pub fn draw_text(
&mut self,
origin: Point,
text: &str,
font: &Font,
color: Color,
alignment: HorizontalAlignment,
) {
let origin = self.offset_point(origin);
self.backend.execute_command(&RenderCommand::DrawText {
origin,
text: text.to_string(),
font: font.clone(),
color,
alignment,
});
}
pub fn measure_text(&self, text: &str, font: &Font) -> TextMetrics {
self.backend.measure_text(text, font)
}
pub fn text_line(&self, band: Rect, font: &Font) -> Rect {
text_line(band, font, VerticalAlignment::Center, self)
}
pub fn text_line_aligned(&self, band: Rect, font: &Font, alignment: VerticalAlignment) -> Rect {
text_line(band, font, alignment, self)
}
pub fn draw_text_fitted(
&mut self,
bounds: Rect,
text: &str,
font: &Font,
color: Color,
alignment: HorizontalAlignment,
) -> crate::compat::String {
let inset = TEXT_FIT_MARGIN as i32;
let available = (bounds.width as i32 - 2 * inset).max(0) as f32;
let fitted = fit_text_to_width(text, available, font, self.backend);
let advance = self.measure_text(&fitted, font).width as i32;
let origin = fitted_origin(bounds, advance, alignment);
self.draw_text(origin, &fitted, font, color, HorizontalAlignment::Left);
fitted
}
pub fn draw_text_line(
&mut self,
bounds: Rect,
text: &str,
font: &Font,
color: Color,
alignment: HorizontalAlignment,
) -> crate::compat::String {
let line = self.text_line(bounds, font);
self.draw_text_fitted(line, text, font, color, alignment)
}
pub fn shape_text(&self, text: &str, font: &Font) -> ShapedText {
self.backend.shape_text(text, font)
}
pub fn push_clip(&mut self, x: i32, y: i32, width: u32, height: u32) {
let x = x + self.offset_x;
let y = y + self.offset_y;
self.backend.execute_command(&RenderCommand::PushClip { x, y, width, height });
}
pub fn pop_clip(&mut self) {
self.backend.execute_command(&RenderCommand::PopClip);
}
pub fn draw_image(&mut self, x: i32, y: i32, width: u32, height: u32, data: &[u8]) {
let x = x + self.offset_x;
let y = y + self.offset_y;
self.backend.draw_image(x, y, width, height, data);
}
pub fn execute_command(&mut self, command: RenderCommand) {
self.backend.execute_command(&command);
}
}
#[cfg(test)]
mod tests {
use super::super::paint::take_image_bytes_drawn_without_copy;
use super::*;
use crate::compat::{lock, MutexGuard};
use crate::compat::{MiniToString, String};
use crate::core::{Color, Font, Point, Rect, Size};
use crate::render::SoftwarePaintBackend;
use crate::render::{PaintBackend, RenderCommand};
struct SoftwareRenderConfigTestGuard {
_lock: MutexGuard<'static, ()>,
original: SoftwareRenderConfig,
}
impl SoftwareRenderConfigTestGuard {
fn new() -> Self {
let lock = lock(software_render_config_test_lock());
let original = default_software_render_config();
Self { _lock: lock, original }
}
}
impl Drop for SoftwareRenderConfigTestGuard {
fn drop(&mut self) {
set_default_software_render_config(self.original);
}
}
#[test]
fn back_buffer_new_creates_buffers() {
let bb = BackBuffer::new(Size::new(10, 10), 1.0);
assert_eq!(bb.size(), Size::new(10, 10));
assert!((bb.dpi_scale() - 1.0).abs() < 1e-6);
assert_eq!(bb.front().len(), 10 * 10 * 4);
assert_eq!(bb.back.len(), 10 * 10 * 4);
}
#[test]
fn back_buffer_zero_size() {
let bb = BackBuffer::new(Size::new(0, 0), 1.0);
assert_eq!(bb.size(), Size::new(0, 0));
assert!(bb.front().is_empty());
assert!(bb.back.is_empty());
}
#[test]
fn back_buffer_resize() {
let mut bb = BackBuffer::new(Size::new(10, 10), 1.0);
bb.resize(Size::new(20, 20));
assert_eq!(bb.size(), Size::new(20, 20));
assert_eq!(bb.front().len(), 20 * 20 * 4);
assert_eq!(bb.back.len(), 20 * 20 * 4);
}
#[test]
fn back_buffer_resize_to_zero() {
let mut bb = BackBuffer::new(Size::new(10, 10), 1.0);
bb.resize(Size::new(0, 0));
assert_eq!(bb.size(), Size::new(0, 0));
assert!(bb.front().is_empty());
assert!(bb.back.is_empty());
}
#[test]
fn back_buffer_set_dpi_scale() {
let mut bb = BackBuffer::new(Size::new(10, 10), 1.0);
bb.set_dpi_scale(2.5);
assert!((bb.dpi_scale() - 2.5).abs() < 1e-6);
}
#[test]
fn back_buffer_set_dpi_scale_clamps_minimum() {
let mut bb = BackBuffer::new(Size::new(10, 10), 1.0);
bb.set_dpi_scale(0.0);
assert!((bb.dpi_scale() - 0.1).abs() < 1e-6);
}
#[test]
fn back_buffer_back_mut_returns_writable_slice() {
let mut bb = BackBuffer::new(Size::new(5, 5), 1.0);
let back = bb.back_mut();
assert_eq!(back.len(), 5 * 5 * 4);
back[0] = 255;
assert_eq!(bb.back[0], 255);
}
#[test]
fn back_buffer_present_swaps_front_and_back() {
let mut bb = BackBuffer::new(Size::new(5, 5), 1.0);
bb.back_mut()[0] = 42;
assert_ne!(bb.front()[0], 42);
bb.present();
assert_eq!(bb.front()[0], 42);
assert_eq!(bb.back[0], 0);
}
#[test]
fn back_buffer_present_swaps_again() {
let mut bb = BackBuffer::new(Size::new(2, 2), 1.0);
bb.back_mut()[0] = 10;
bb.present();
bb.back_mut()[0] = 20;
bb.present();
assert_eq!(bb.front()[0], 20);
}
#[test]
fn back_buffer_dpi_scale_initial_clamp() {
let bb = BackBuffer::new(Size::new(1, 1), 0.0);
assert!((bb.dpi_scale() - 0.1).abs() < 1e-6);
}
#[test]
fn software_surface_new_creates_surface() {
let surface = SoftwareSurface::new(Size::new(100, 100), 1.0);
assert_eq!(surface.size(), Size::new(100, 100));
assert!((surface.dpi_scale() - 1.0).abs() < 1e-6);
}
#[test]
fn software_surface_zero_size() {
let surface = SoftwareSurface::new(Size::new(0, 0), 1.0);
assert_eq!(surface.size(), Size::new(0, 0));
}
#[test]
fn software_surface_begin_frame_clears() {
let mut surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
surface.begin_frame(Color::RED);
surface.end_frame();
let rgba = surface.frame_rgba();
for chunk in rgba.chunks(4) {
assert_eq!(chunk[0], 255);
assert_eq!(chunk[1], 0);
assert_eq!(chunk[2], 0);
assert_eq!(chunk[3], 255);
}
}
#[test]
fn software_surface_fill_rect() {
let mut surface = SoftwareSurface::new(Size::new(20, 20), 1.0);
surface.begin_frame(Color::WHITE);
surface.fill_rect(Rect::new(2, 2, 10, 10), Color::BLUE);
surface.end_frame();
let rgba = surface.frame_rgba();
let stride = 20 * 4;
let idx = 5 * stride + 5 * 4;
assert_eq!(rgba[idx], 0); assert_eq!(rgba[idx + 1], 0); assert_eq!(rgba[idx + 2], 255); }
#[test]
fn set_blend_mode_composites_on_the_software_backend() {
use crate::render::RenderCommand;
let mut surface = SoftwareSurface::new(Size::new(8, 8), 1.0);
surface.begin_frame(Color::WHITE);
surface.set_blend_mode(BlendMode::Multiply);
surface.fill_rect(Rect::new(0, 0, 4, 8), Color::rgb(128, 128, 128));
surface.end_frame();
let rgba = surface.frame_rgba();
let blended = &rgba[..4];
assert_eq!(
(blended[0], blended[1], blended[2]),
(128, 128, 128),
"multiply against a white backdrop keeps the source colour"
);
let mut dark = SoftwareSurface::new(Size::new(8, 8), 1.0);
dark.begin_frame(Color::rgb(128, 128, 128));
dark.set_blend_mode(BlendMode::Multiply);
dark.fill_rect(Rect::new(0, 0, 8, 8), Color::rgb(128, 128, 128));
dark.end_frame();
assert_eq!(dark.frame_rgba()[0], 64, "two mid greys multiply to a quarter of full");
let mut restored = SoftwareSurface::new(Size::new(8, 8), 1.0);
restored.begin_frame(Color::rgb(128, 128, 128));
restored.set_blend_mode(BlendMode::Multiply);
restored.set_blend_mode(BlendMode::Normal);
restored.fill_rect(Rect::new(0, 0, 8, 8), Color::rgb(128, 128, 128));
restored.end_frame();
assert_eq!(restored.frame_rgba()[0], 128, "Normal replaces the backdrop");
use crate::render::PaintBackend;
let mut via_command = crate::render::SoftwarePaintBackend::new(Size::new(8, 8), 1.0);
via_command.begin_frame(Color::rgb(128, 128, 128));
via_command.execute_command(&RenderCommand::SetBlendMode { mode: BlendMode::Multiply });
via_command.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 8, 8),
color: Color::rgb(128, 128, 128),
});
via_command.end_frame();
assert_eq!(
via_command.surface().frame_rgba()[0],
64,
"the command path must set the mode too"
);
}
#[test]
fn software_surface_fill_rect_zero_size() {
let mut surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
surface.begin_frame(Color::WHITE);
surface.fill_rect(Rect::new(0, 0, 0, 0), Color::RED);
surface.end_frame();
let rgba = surface.frame_rgba();
for chunk in rgba.chunks(4) {
assert_eq!(chunk[0], 255); }
}
#[test]
fn software_surface_draw_rect() {
let mut surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_rect(Rect::new(0, 0, 10, 10), Color::GREEN);
surface.end_frame();
let rgba = surface.frame_rgba();
assert_eq!(rgba[0], 0); assert_eq!(rgba[1], 255); assert_eq!(rgba[2], 0); }
#[test]
fn software_surface_resize() {
let mut surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
surface.resize(Size::new(50, 50));
assert_eq!(surface.size(), Size::new(50, 50));
}
#[test]
fn software_surface_set_dpi_scale() {
let mut surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
surface.set_dpi_scale(1.5);
assert!((surface.dpi_scale() - 1.5).abs() < 1e-6);
}
#[test]
fn software_surface_aa_samples_per_axis_default() {
let surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
assert_eq!(surface.aa_samples_per_axis(), 4);
}
#[test]
fn software_surface_set_aa_samples() {
let mut surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
surface.set_aa_samples_per_axis(2);
assert_eq!(surface.aa_samples_per_axis(), 2);
}
#[test]
fn software_surface_apply_render_config_clamps() {
let mut surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
surface.apply_render_config(SoftwareRenderConfig { aa_samples_per_axis: 99 });
assert_eq!(surface.aa_samples_per_axis(), 8);
}
#[test]
fn software_surface_render_config_roundtrip() {
let surface = SoftwareSurface::new(Size::new(10, 10), 1.0);
let config = surface.render_config();
assert_eq!(config.aa_samples_per_axis, 4);
}
fn measurement_context<'a>(backend: &'a mut SoftwarePaintBackend) -> RenderContext<'a> {
RenderContext::new(backend)
}
#[test]
fn text_line_centres_the_line_box_in_the_band() {
let mut backend = SoftwarePaintBackend::new(Size::new(240, 120), 1.0);
let ctx = measurement_context(&mut backend);
let font = Font::default_ui();
let line_height = ctx.measure_text("M", &font).height as i32;
assert!(line_height > 0, "the measuring font must report a positive line height");
let band = Rect::new(0, 0, 240, 120);
let line = ctx.text_line(band, &font);
assert_eq!(line.y, (120 - line_height) / 2, "the line box is centred, not top-dropped");
assert_eq!(line.height, line_height as u32);
assert_eq!((line.x, line.width), (band.x, band.width), "horizontally unchanged");
assert_ne!(line.y, band.y + band.height as i32 / 2);
}
#[test]
fn text_line_honours_each_vertical_alignment() {
let mut backend = SoftwarePaintBackend::new(Size::new(240, 120), 1.0);
let ctx = measurement_context(&mut backend);
let font = Font::default_ui();
let line_height = ctx.measure_text("M", &font).height as i32;
let band = Rect::new(0, 10, 240, 120);
let top = ctx.text_line_aligned(band, &font, VerticalAlignment::Top);
assert_eq!(top.y, 10, "top alignment pins the line box to the band's top edge");
let bottom = ctx.text_line_aligned(band, &font, VerticalAlignment::Bottom);
assert_eq!(bottom.y, 10 + 120 - line_height);
let centre = ctx.text_line(band, &font);
assert_eq!(centre.y, 10 + (120 - line_height) / 2);
assert!(top.y < centre.y && centre.y < bottom.y, "the three orders must differ");
}
#[test]
fn text_line_survives_degenerate_bands() {
let mut backend = SoftwarePaintBackend::new(Size::new(240, 120), 1.0);
let ctx = measurement_context(&mut backend);
let font = Font::default_ui();
for band in [
Rect::new(0, 0, 0, 0),
Rect::new(5, 5, 0, 0),
Rect::new(0, 0, 100, 1),
Rect::new(0, 120, 240, 0),
Rect::new(-30, -30, 10, 4),
] {
let line = ctx.text_line(band, &font);
assert!(line.y >= band.y, "a clamped band never starts above itself: {band:?}");
assert_eq!(line.x, band.x);
assert_eq!(line.width, band.width);
assert!(line.height >= 1, "a zero-height line box would be un-drawable");
}
}
#[test]
fn draw_text_line_places_ink_at_the_top_of_a_taller_band() {
let mut backend = SoftwarePaintBackend::new(Size::new(240, 120), 1.0);
backend.begin_frame(Color::WHITE);
let font = Font::default_ui();
let band = Rect::new(0, 0, 240, 120);
let line_y;
{
let mut ctx = RenderContext::new(&mut backend);
line_y = ctx.text_line(band, &font).y;
ctx.draw_text_line(band, "MMMM", &font, Color::BLACK, HorizontalAlignment::Left);
}
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 240 * 4;
let row_has_ink = |y: usize| {
(0..240).any(|x| {
let idx = y * stride + x * 4;
rgba[idx] != 255 || rgba[idx + 1] != 255 || rgba[idx + 2] != 255
})
};
assert!(row_has_ink(line_y as usize + 1), "the line box row carries ink");
assert!(!row_has_ink(2), "the band's top edge must stay clear of a centred line");
}
#[test]
fn render_context_new_wraps_backend() {
let mut backend = SoftwarePaintBackend::new(Size::new(50, 50), 1.0);
let ctx = RenderContext::new(&mut backend);
assert_eq!(ctx.size(), Size::new(50, 50));
}
#[test]
fn render_context_backend_accessor() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let mut ctx = RenderContext::new(&mut backend);
let be = ctx.backend();
assert_eq!(be.size(), Size::new(10, 10));
}
#[test]
fn render_context_push_offset_translates_primitives() {
let mut backend = SoftwarePaintBackend::new(Size::new(16, 16), 1.0);
backend.begin_frame(Color::WHITE);
let offset_after;
{
let mut ctx = RenderContext::new(&mut backend);
ctx.fill_rect(Rect::new(2, 2, 4, 4), Color::RED);
ctx.push_offset(6, 8);
ctx.fill_rect(Rect::new(2, 2, 4, 4), Color::BLUE);
ctx.pop_offset();
offset_after = ctx.offset();
}
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 16 * 4;
let pixel = |x: usize, y: usize| {
let idx = y * stride + x * 4;
(rgba[idx], rgba[idx + 1], rgba[idx + 2])
};
assert_eq!(pixel(3, 3), (255, 0, 0));
assert_eq!(pixel(9, 11), (0, 0, 255));
assert_eq!(offset_after, (0, 0));
}
#[test]
fn render_context_offset_nesting_restores_previous() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let mut ctx = RenderContext::new(&mut backend);
ctx.push_offset(1, 2);
ctx.push_offset(3, 4);
assert_eq!(ctx.offset(), (4, 6));
ctx.pop_offset();
assert_eq!(ctx.offset(), (1, 2));
ctx.pop_offset();
assert_eq!(ctx.offset(), (0, 0));
ctx.pop_offset();
assert_eq!(ctx.offset(), (0, 0));
}
#[test]
fn render_context_push_offset_translates_points_lines_and_text() {
let mut backend = SoftwarePaintBackend::new(Size::new(16, 16), 1.0);
backend.begin_frame(Color::WHITE);
{
let mut ctx = RenderContext::new(&mut backend);
ctx.push_offset(3, 4);
ctx.draw_line(Point::new(0, 0), Point::new(8, 8), Color::RED);
ctx.pop_offset();
}
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 16 * 4;
let idx = 6 * stride + 5 * 4;
assert_eq!(rgba[idx], 255); }
#[test]
fn render_context_fill_rect() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let mut ctx = RenderContext::new(&mut backend);
ctx.fill_rect(Rect::new(0, 0, 10, 10), Color::RED);
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 0);
let mut backend2 = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend2.begin_frame(Color::WHITE);
let mut ctx2 = RenderContext::new(&mut backend2);
ctx2.fill_rect(Rect::new(0, 0, 5, 5), Color::BLUE);
backend2.end_frame();
let rgba = backend2.frame_rgba();
assert_eq!(rgba[0], 0); assert_eq!(rgba[2], 255); }
#[test]
fn render_context_draw_rect() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_rect(Rect::new(0, 0, 10, 10), Color::GREEN);
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 0); assert_eq!(rgba[1], 255); }
#[test]
fn render_context_draw_rect_stroke() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_rect_stroke(Rect::new(0, 0, 10, 10), Color::RED, 2);
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255); }
#[test]
fn render_context_fill_rounded_rect() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.fill_rounded_rect(Rect::new(1, 1, 8, 8), 2, Color::BLUE);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn render_context_fill_rounded_rect_aa() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.fill_rounded_rect_aa(Rect::new(1, 1, 8, 8), 2, Color::RED);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn face_with_gradient_paints_a_ramp_not_a_solid() {
use crate::core::Point;
use crate::render::SurfaceStyle;
use crate::style::Gradient;
let rect = Rect::new(0, 0, 16, 4);
let ramp = Gradient::linear(Point::new(0, 0), Point::new(16, 0))
.add_stop(0.0, Color::BLACK)
.add_stop(1.0, Color::WHITE);
let mut backend = SoftwarePaintBackend::new(Size::new(16, 4), 1.0);
backend.begin_frame(Color::rgb(1, 2, 3));
{
let mut ctx = RenderContext::new(&mut backend);
ctx.face_with_gradient(
rect,
Color::rgb(200, 0, 0),
Some(&ramp),
SurfaceStyle::solid(),
0,
Color::BLACK,
);
}
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 16 * 4;
let left = rgba[2 * 4];
let right = rgba[2 * stride + 14 * 4];
assert!(
(right as i32 - left as i32).abs() > 100,
"a left-to-right ramp must differ across the face, got left={left} right={right}"
);
}
#[test]
fn face_is_the_identity_of_face_with_gradient_without_a_gradient() {
use crate::render::SurfaceStyle;
let rect = Rect::new(1, 1, 8, 8);
let fill = Color::rgb(40, 90, 160);
let mut plain = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
plain.begin_frame(Color::WHITE);
{
let mut ctx = RenderContext::new(&mut plain);
ctx.face(rect, fill, SurfaceStyle::solid(), 0, Color::BLACK);
}
plain.end_frame();
let mut via_gradient = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
via_gradient.begin_frame(Color::WHITE);
{
let mut ctx = RenderContext::new(&mut via_gradient);
ctx.face_with_gradient(rect, fill, None, SurfaceStyle::solid(), 0, Color::BLACK);
}
via_gradient.end_frame();
assert_eq!(
plain.frame_rgba(),
via_gradient.frame_rgba(),
"face must be byte-for-byte identical to face_with_gradient(None)"
);
}
#[test]
fn face_radius_clips_the_corner_of_the_fill() {
use crate::render::SurfaceStyle;
let rect = Rect::new(0, 0, 12, 12);
let fill = Color::rgb(10, 120, 220);
let mut square = SoftwarePaintBackend::new(Size::new(12, 12), 1.0);
square.begin_frame(Color::WHITE);
{
let mut ctx = RenderContext::new(&mut square);
ctx.face(rect, fill, SurfaceStyle::solid(), 0, Color::BLACK);
}
square.end_frame();
let mut rounded = SoftwarePaintBackend::new(Size::new(12, 12), 1.0);
rounded.begin_frame(Color::WHITE);
{
let mut ctx = RenderContext::new(&mut rounded);
ctx.face(rect, fill, SurfaceStyle::solid(), 5, Color::BLACK);
}
rounded.end_frame();
let square_corner = &square.frame_rgba()[..4];
let rounded_corner = &rounded.frame_rgba()[..4];
assert_eq!(&square_corner[..3], &[10, 120, 220], "a square face fills its corner");
assert_eq!(&rounded_corner[..3], &[255, 255, 255], "a radius must clip the corner");
}
#[test]
fn render_context_draw_rounded_rect_stroke() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_rounded_rect_stroke(Rect::new(1, 1, 8, 8), 2, Color::GREEN, 1);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn render_context_draw_rounded_rect_stroke_aa() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_rounded_rect_stroke_aa(Rect::new(1, 1, 8, 8), 2, Color::BLUE, 1);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn render_context_draw_line() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_line(Point::new(0, 0), Point::new(9, 9), Color::RED);
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255);
}
#[test]
fn render_context_draw_line_aa() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_line_aa(Point::new(0, 0), Point::new(9, 9), Color::BLUE);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn render_context_draw_line_stroke() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_line_stroke(Point::new(0, 0), Point::new(9, 9), Color::GREEN, 2);
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[1], 255);
}
#[test]
fn render_context_draw_line_stroke_aa() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_line_stroke_aa(Point::new(0, 0), Point::new(9, 9), Color::RED, 2);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn render_context_fill_circle() {
let mut backend = SoftwarePaintBackend::new(Size::new(20, 20), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.fill_circle(Point::new(10, 10), 5, Color::BLUE);
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 20 * 4;
let idx = 10 * stride + 10 * 4;
assert_eq!(rgba[idx + 2], 255); }
#[test]
fn render_context_fill_circle_aa() {
let mut backend = SoftwarePaintBackend::new(Size::new(20, 20), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.fill_circle_aa(Point::new(10, 10), 5, Color::RED);
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255);
}
#[test]
fn render_context_draw_circle() {
let mut backend = SoftwarePaintBackend::new(Size::new(20, 20), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_circle(Point::new(10, 10), 5, Color::GREEN);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn render_context_draw_circle_stroke() {
let mut backend = SoftwarePaintBackend::new(Size::new(20, 20), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_circle_stroke(Point::new(10, 10), 5, Color::RED, 2);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn render_context_draw_text() {
let mut backend = SoftwarePaintBackend::new(Size::new(100, 100), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
let font = Font::simple("Arial", 12.0);
ctx.draw_text(Point::new(10, 20), "Hello", &font, Color::BLACK, HorizontalAlignment::Left);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn render_context_measure_text() {
let mut backend = SoftwarePaintBackend::new(Size::new(100, 100), 1.0);
let ctx = RenderContext::new(&mut backend);
let font = Font::simple("Arial", 12.0);
let metrics = ctx.measure_text("Hello", &font);
assert!(metrics.width > 0);
assert!(metrics.height > 0);
}
#[test]
fn render_context_shape_text() {
let mut backend = SoftwarePaintBackend::new(Size::new(100, 100), 1.0);
let ctx = RenderContext::new(&mut backend);
let font = Font::simple("Arial", 12.0);
let shaped = ctx.shape_text("Hi", &font);
assert!(!shaped.clusters.is_empty());
}
#[test]
fn render_context_push_clip() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.push_clip(0, 0, 5, 5);
ctx.fill_rect(Rect::new(0, 0, 10, 10), Color::RED);
ctx.pop_clip();
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 10 * 4;
let idx = 2 * stride + 2 * 4;
assert_eq!(rgba[idx], 255); }
#[test]
fn render_context_clip_applies_to_non_rect_primitives() {
let mut backend = SoftwarePaintBackend::new(Size::new(20, 20), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.push_clip(0, 0, 8, 8);
ctx.fill_circle(Point::new(6, 6), 6, Color::BLUE);
ctx.draw_line(Point::new(0, 0), Point::new(19, 19), Color::RED);
let image = (0..16).flat_map(|_| [0, 255, 0, 255]).collect::<Vec<_>>();
ctx.draw_image(5, 5, 4, 4, &image);
ctx.pop_clip();
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 20 * 4;
let pixel = |x: usize, y: usize| {
let index = y * stride + x * 4;
(rgba[index], rgba[index + 1], rgba[index + 2])
};
assert_eq!(pixel(6, 6), (0, 255, 0));
assert_eq!(pixel(7, 7), (0, 255, 0));
assert_eq!(pixel(15, 15), (255, 255, 255));
assert_eq!(pixel(10, 10), (255, 255, 255));
}
#[test]
fn render_context_pop_clip() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
ctx.push_clip(0, 0, 5, 5);
ctx.pop_clip();
ctx.fill_rect(Rect::new(0, 0, 10, 10), Color::RED);
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 10 * 4;
let idx = 7 * stride + 7 * 4; assert_eq!(rgba[idx], 255); }
#[test]
fn render_context_draw_image() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut ctx = RenderContext::new(&mut backend);
let data = vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255];
ctx.draw_image(0, 0, 2, 2, &data);
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255); }
#[test]
fn render_context_size_and_dpi_scale() {
let mut backend = SoftwarePaintBackend::new(Size::new(80, 60), 1.5);
let ctx = RenderContext::new(&mut backend);
assert_eq!(ctx.size(), Size::new(80, 60));
assert!((ctx.dpi_scale() - 1.5).abs() < 1e-6);
}
#[test]
fn software_render_config_default_values() {
let config = SoftwareRenderConfig::default();
assert_eq!(config.aa_samples_per_axis, 4);
}
#[test]
fn software_render_config_normalized_clamps_low() {
let config = SoftwareRenderConfig { aa_samples_per_axis: 0 };
let norm = config.normalized();
assert_eq!(norm.aa_samples_per_axis, 1);
}
#[test]
fn software_render_config_normalized_clamps_high() {
let config = SoftwareRenderConfig { aa_samples_per_axis: 100 };
let norm = config.normalized();
assert_eq!(norm.aa_samples_per_axis, 8);
}
#[test]
fn software_render_config_normalized_preserves_valid() {
let config = SoftwareRenderConfig { aa_samples_per_axis: 2 };
let norm = config.normalized();
assert_eq!(norm.aa_samples_per_axis, 2);
}
#[test]
fn software_render_config_normalized_edge_cases() {
let config_low = SoftwareRenderConfig { aa_samples_per_axis: 1 };
assert_eq!(config_low.normalized().aa_samples_per_axis, 1);
let config_high = SoftwareRenderConfig { aa_samples_per_axis: 8 };
assert_eq!(config_high.normalized().aa_samples_per_axis, 8);
}
#[test]
fn software_render_config_equality() {
let a = SoftwareRenderConfig { aa_samples_per_axis: 4 };
let b = SoftwareRenderConfig { aa_samples_per_axis: 4 };
let c = SoftwareRenderConfig { aa_samples_per_axis: 2 };
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn default_software_render_config_returns_default() {
let _guard = SoftwareRenderConfigTestGuard::new();
set_default_software_render_config(SoftwareRenderConfig::default());
let config = default_software_render_config();
assert_eq!(config.aa_samples_per_axis, 4);
}
#[test]
fn set_default_software_render_config_updates_global() {
let _guard = SoftwareRenderConfigTestGuard::new();
let custom = SoftwareRenderConfig { aa_samples_per_axis: 2 };
set_default_software_render_config(custom);
let retrieved = default_software_render_config();
assert_eq!(retrieved.aa_samples_per_axis, 2);
}
#[test]
fn set_default_software_render_config_clamps() {
let _guard = SoftwareRenderConfigTestGuard::new();
let custom = SoftwareRenderConfig { aa_samples_per_axis: 99 };
set_default_software_render_config(custom);
let retrieved = default_software_render_config();
assert_eq!(retrieved.aa_samples_per_axis, 8);
}
#[test]
fn render_context_dpi_scale_propagates() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 2.0);
let ctx = RenderContext::new(&mut backend);
assert!((ctx.dpi_scale() - 2.0).abs() < 1e-6);
}
#[test]
fn render_context_backend_mutability() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
{
let mut ctx = RenderContext::new(&mut backend);
ctx.fill_rect(Rect::new(0, 0, 5, 5), Color::RED);
}
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 10, 10),
color: Color::BLUE,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
struct CountingBackend<'a> {
inner: &'a SoftwarePaintBackend,
shapes: core::cell::Cell<usize>,
measures: core::cell::Cell<usize>,
}
impl CountingBackend<'_> {
fn shapes(&self) -> usize {
self.shapes.get()
}
}
impl PaintBackend for CountingBackend<'_> {
fn begin_frame(&mut self, _clear: Color) {}
fn end_frame(&mut self) {}
fn execute_command(&mut self, _command: &RenderCommand) {}
fn size(&self) -> Size {
self.inner.size()
}
fn set_size(&mut self, _size: Size) {}
fn dpi_scale(&self) -> f32 {
self.inner.dpi_scale()
}
fn set_dpi_scale(&mut self, _dpi_scale: f32) {}
fn measure_text(&self, text: &str, font: &Font) -> TextMetrics {
self.measures.set(self.measures.get() + 1);
self.inner.measure_text(text, font)
}
fn shape_text(&self, text: &str, font: &Font) -> ShapedText {
self.shapes.set(self.shapes.get() + 1);
self.inner.shape_text(text, font)
}
fn frame_rgba(&self) -> &[u8] {
self.inner.frame_rgba()
}
}
fn counting_backend() -> SoftwarePaintBackend {
let mut backend = SoftwarePaintBackend::new(Size::new(400, 40), 1.0);
backend.begin_frame(Color::WHITE);
backend
}
#[test]
fn fitting_a_long_label_shapes_bounded_times() {
let backend = counting_backend();
let counter = CountingBackend {
inner: &backend,
shapes: core::cell::Cell::new(0),
measures: core::cell::Cell::new(0),
};
let font = Font::default();
let long = "The quick brown fox jumps over the lazy dog repeatedly";
let _fitted = fit_text_to_width(long, 60.0, &font, &counter);
assert!(
counter.shapes() <= 1,
"fitting must shape the source at most once, but shaped {} times",
counter.shapes()
);
}
#[test]
fn fitting_steps_do_not_grow_with_the_kept_prefix() {
let backend = counting_backend();
let font = Font::default();
let long = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let measure = |max_width: f32| {
let counter = CountingBackend {
inner: &backend,
shapes: core::cell::Cell::new(0),
measures: core::cell::Cell::new(0),
};
let fitted = fit_text_to_width(long, max_width, &font, &counter);
(counter.shapes(), fitted.chars().count())
};
let (narrow_shapes, narrow_len) = measure(40.0);
let (wide_shapes, wide_len) = measure(200.0);
assert!(wide_len > narrow_len, "the wider budget must keep more clusters");
assert!(
wide_shapes <= narrow_shapes && wide_shapes <= 1,
"shaping calls must stay bounded as the kept prefix grows: narrow={narrow_shapes}, wide={wide_shapes}"
);
}
#[test]
fn linear_fit_matches_a_prefix_walk_for_all_scripts() {
let backend = counting_backend();
let font = Font::default();
fn reference(
text: &str,
max_width: f32,
font: &Font,
backend: &dyn PaintBackend,
) -> String {
if text.is_empty() || max_width <= 0.0 {
return String::new();
}
if backend.measure_text(text, font).width as f32 <= max_width {
return text.to_string();
}
const ELLIPSIS: char = '\u{2026}';
let ellipsis_width = backend.measure_text(&ELLIPSIS.to_string(), font).width as f32;
if ellipsis_width > max_width {
return ELLIPSIS.to_string();
}
let budget = max_width - ellipsis_width;
let mut kept = String::new();
for ch in text.chars() {
let mut next = kept.clone();
next.push(ch);
if backend.measure_text(&next, font).width as f32 > budget {
break;
}
kept = next;
}
kept.push(ELLIPSIS);
kept
}
let samples = [
"The quick brown fox jumps over the lazy dog",
"中文标签测试文本内容在这个控件里显示",
"e\u{301}cole cafe\u{301} nai\u{308}ve re\u{301}sume\u{301}",
"short",
"oneverylongsinglewordwithoutanyspacesatall",
];
for sample in samples {
let mut width = 0.0f32;
while width <= 240.0 {
let expected = reference(sample, width, &font, &backend);
let actual = fit_text_to_width(sample, width, &font, &backend);
assert_eq!(
actual, expected,
"fitted text differs from the prefix walk at width {width} for {sample:?}"
);
width += 3.0;
}
}
}
#[test]
fn fit_boundaries_match_the_prefix_walk() {
let backend = counting_backend();
let font = Font::default();
let text = "hello world";
assert_eq!(fit_text_to_width(text, 1000.0, &font, &backend), text);
assert_eq!(fit_text_to_width(text, 1.0, &font, &backend), "\u{2026}");
assert_eq!(fit_text_to_width(text, 0.0, &font, &backend), "");
}
fn distinct_image() -> ([u8; 16], [(u8, u8, u8, u8); 4]) {
let pixels =
[(255u8, 0u8, 0u8, 255u8), (0, 255, 0, 255), (0, 0, 255, 255), (255, 255, 0, 255)];
let mut data = [0u8; 16];
for (index, px) in pixels.iter().enumerate() {
data[index * 4] = px.0;
data[index * 4 + 1] = px.1;
data[index * 4 + 2] = px.2;
data[index * 4 + 3] = px.3;
}
(data, pixels)
}
#[test]
fn borrowed_draw_image_matches_the_command_path_pixel_for_pixel() {
let (data, pixels) = distinct_image();
let size = Size::new(8, 8);
let mut reference = SoftwarePaintBackend::new(size, 1.0);
reference.begin_frame(Color::BLACK);
reference.execute_command(&RenderCommand::DrawImage {
x: 2,
y: 3,
width: 2,
height: 2,
data: data.to_vec(),
});
reference.end_frame();
let expected = reference.frame_rgba().to_vec();
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::BLACK);
{
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_image(2, 3, 2, 2, &data);
}
backend.end_frame();
assert_eq!(
backend.frame_rgba(),
expected.as_slice(),
"borrowed and owning draws must agree"
);
let stride = 8 * 4;
for (index, px) in pixels.iter().enumerate() {
let (x, y) = (2 + (index % 2), 3 + (index / 2));
let at = y * stride + x * 4;
let got = backend.frame_rgba();
assert_eq!(
(got[at], got[at + 1], got[at + 2], got[at + 3]),
*px,
"pixel {index} must come from the source at ({x}, {y})"
);
}
}
#[test]
fn the_immediate_path_draws_the_source_without_copying_it() {
let (data, _) = distinct_image();
let mut backend = SoftwarePaintBackend::new(Size::new(8, 8), 1.0);
backend.begin_frame(Color::BLACK);
let _ = take_image_bytes_drawn_without_copy();
{
let mut ctx = RenderContext::new(&mut backend);
ctx.draw_image(0, 0, 2, 2, &data);
}
let drawn = take_image_bytes_drawn_without_copy();
assert_eq!(
drawn,
data.len(),
"the software backend must draw the full source through the no-copy path"
);
backend.end_frame();
}
}