use crate::compat::Arc;
use crate::compat::Rc;
use crate::compat::{lock, vec, Cow, MiniToString, Mutex, OnceLock, Vec};
use crate::core::{Color, Font, HorizontalAlignment, Point, Rect, Size};
use crate::render::core::command::{LayerEffect, MatteMode};
use crate::render::pixel_bytes_len;
use crate::render::{BlendMode, PaintBackend, RenderCommand, ShapedText, TextMetrics};
#[derive(Debug, Clone)]
pub(crate) struct PathClip {
pub(crate) rect: (i32, i32, u32, u32),
pub(crate) coverage: Vec<u8>,
pub(crate) backdrop: Vec<u8>,
}
#[derive(Debug, Clone)]
pub(crate) struct LayerCapture {
pub(crate) rect: (i32, i32, u32, u32),
pub(crate) effect: Option<LayerEffect>,
pub(crate) backdrop: Vec<u8>,
pub(crate) isolated: bool,
pub(crate) opacity: f32,
pub(crate) cache_key: Option<u64>,
pub(crate) cached_foreground: Option<Arc<Vec<u8>>>,
}
#[derive(Debug, Clone)]
pub(crate) struct CachedLayer {
pub(crate) rect: (i32, i32, u32, u32),
pub(crate) opacity: f32,
pub(crate) foreground: Arc<Vec<u8>>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct BoxBlurScratch {
pub(crate) row: Vec<u8>,
pub(crate) col: Vec<u8>,
pub(crate) copy: Vec<u8>,
}
#[derive(Debug, Clone)]
pub(crate) struct MatteCapture {
pub(crate) rect: (i32, i32, u32, u32),
pub(crate) backdrop: Vec<u8>,
pub(crate) mode: crate::render::MatteMode,
pub(crate) mask: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct BackBuffer {
size: Size,
dpi_scale: f32,
front: Vec<u8>,
pub(crate) back: Vec<u8>,
pub(crate) layers: Vec<LayerCapture>,
pub(crate) mattes: Vec<MatteCapture>,
damage: Vec<(i32, i32, u32, u32)>,
pub(crate) layer_cache: Vec<(u64, CachedLayer)>,
pub(crate) layer_cache_cursor: usize,
pub(crate) layer_foreground_scratch: Vec<u8>,
pub(crate) layer_backdrop_pool: Vec<Vec<u8>>,
pub(crate) blur_scratch: Option<BoxBlurScratch>,
pub(crate) suppress_depth: usize,
}
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],
layers: Vec::new(),
mattes: Vec::new(),
damage: Vec::new(),
layer_cache: Vec::new(),
layer_cache_cursor: 0,
layer_foreground_scratch: Vec::new(),
layer_backdrop_pool: Vec::new(),
blur_scratch: None,
suppress_depth: 0,
}
}
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);
self.layers.clear();
self.mattes.clear();
self.damage.clear();
self.layer_cache.clear();
self.layer_cache_cursor = 0;
self.suppress_depth = 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(crate) fn take_front(&mut self) -> Vec<u8> {
core::mem::take(&mut self.front)
}
pub fn present(&mut self) {
core::mem::swap(&mut self.front, &mut self.back);
self.layers.clear();
self.mattes.clear();
self.damage.clear();
self.suppress_depth = 0;
}
pub fn mark_damage(&mut self, rect: crate::core::Rect) {
let Some(mut region) = clipped_damage(rect, self.size) else { return };
if self.damage.iter().any(|existing| contains_region(*existing, region)) {
return;
}
let mut index = 0;
while index < self.damage.len() {
let existing = self.damage[index];
if contains_region(region, existing) {
self.damage.swap_remove(index);
continue;
}
if regions_intersect(region, existing) {
region = union_region(region, existing);
self.damage.swap_remove(index);
continue;
}
index += 1;
}
self.damage.push(region);
if self.damage.len() > MAX_DAMAGE_REGIONS {
let mut covering = self.damage[0];
for region in &self.damage[1..] {
covering = union_region(covering, *region);
}
self.damage.clear();
self.damage.push(covering);
}
}
pub fn damage_regions(&self) -> &[(i32, i32, u32, u32)] {
&self.damage
}
pub fn clear_damage(&mut self) {
self.damage.clear();
}
pub fn present_damage(&mut self) -> bool {
if self.damage.is_empty() {
return false;
}
if self.damage.contains(&(0, 0, self.size.width, self.size.height)) {
self.present();
return true;
}
let width = self.size.width as usize;
for (x, y, region_width, region_height) in self.damage.iter().copied() {
let row_bytes = region_width as usize * 4;
for row in 0..region_height as usize {
let start = (y as usize + row) * width * 4 + x as usize * 4;
let end = start + row_bytes;
if end > self.back.len() || end > self.front.len() {
continue;
}
self.front[start..end].copy_from_slice(&self.back[start..end]);
}
}
self.damage.clear();
true
}
}
fn clipped_damage(rect: Rect, size: Size) -> Option<(i32, i32, u32, u32)> {
let left = (rect.x as i64).max(0);
let top = (rect.y as i64).max(0);
let right = (rect.x as i64 + rect.width as i64).min(size.width as i64);
let bottom = (rect.y as i64 + rect.height as i64).min(size.height as i64);
if right <= left || bottom <= top {
return None;
}
Some((left as i32, top as i32, (right - left) as u32, (bottom - top) as u32))
}
fn contains_region(outer: (i32, i32, u32, u32), inner: (i32, i32, u32, u32)) -> bool {
let (ox, oy, ow, oh) = outer;
let (ix, iy, iw, ih) = inner;
ix >= ox
&& iy >= oy
&& (ix as i64 + iw as i64) <= (ox as i64 + ow as i64)
&& (iy as i64 + ih as i64) <= (oy as i64 + oh as i64)
}
const MAX_DAMAGE_REGIONS: usize = 32;
fn regions_intersect(a: (i32, i32, u32, u32), b: (i32, i32, u32, u32)) -> bool {
let (ax, ay, aw, ah) = a;
let (bx, by, bw, bh) = b;
(ax as i64) < (bx as i64 + bw as i64)
&& (bx as i64) < (ax as i64 + aw as i64)
&& (ay as i64) < (by as i64 + bh as i64)
&& (by as i64) < (ay as i64 + ah as i64)
}
fn union_region(a: (i32, i32, u32, u32), b: (i32, i32, u32, u32)) -> (i32, i32, u32, u32) {
let left = (a.0 as i64).min(b.0 as i64);
let top = (a.1 as i64).min(b.1 as i64);
let right = (a.0 as i64 + a.2 as i64).max(b.0 as i64 + b.2 as i64);
let bottom = (a.1 as i64 + a.3 as i64).max(b.1 as i64 + b.3 as i64);
(left as i32, top as i32, (right - left) as u32, (bottom - top) as u32)
}
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) nonzero_intersections: Vec<(f32, i32)>,
pub(crate) text_coverage_scratch: Vec<u8>,
pub(crate) path_clips: Vec<Option<PathClip>>,
pub(crate) blend_mode: BlendMode,
pub(crate) glyph_run_text: bool,
#[cfg(feature = "3d")]
pub(crate) depth_buffer: Vec<f32>,
#[cfg(feature = "3d")]
pub(crate) depth_dirty: bool,
}
const LAYER_BACKDROP_POOL_LIMIT: usize = 4;
impl SoftwareSurface {
pub(crate) fn take_blur_scratch(&mut self) -> BoxBlurScratch {
self.buffer.blur_scratch.take().unwrap_or_default()
}
pub(crate) fn return_blur_scratch(&mut self, scratch: BoxBlurScratch) {
self.buffer.blur_scratch = Some(scratch);
}
pub(crate) fn take_layer_foreground_scratch(&mut self) -> Vec<u8> {
core::mem::take(&mut self.buffer.layer_foreground_scratch)
}
pub(crate) fn return_layer_foreground_scratch(&mut self, buffer: Vec<u8>) {
self.buffer.layer_foreground_scratch = buffer;
}
pub(crate) fn take_layer_backdrop(&mut self) -> Vec<u8> {
self.buffer.layer_backdrop_pool.pop().unwrap_or_default()
}
pub(crate) fn return_layer_backdrop(&mut self, buffer: Vec<u8>) {
if self.buffer.layer_backdrop_pool.len() < LAYER_BACKDROP_POOL_LIMIT {
self.buffer.layer_backdrop_pool.push(buffer);
}
}
}
#[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),
}
}
pub use crate::core::VerticalAlignment;
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<'a>(
text: &'a str,
max_width: f32,
font: &Font,
backend: &dyn PaintBackend,
) -> Cow<'a, str> {
if text.is_empty() || max_width <= 0.0 {
return Cow::Borrowed("");
}
if backend.measure_text(text, font).width as f32 <= max_width {
return Cow::Borrowed(text);
}
const ELLIPSIS: char = '\u{2026}';
const ELLIPSIS_TEXT: &str = "\u{2026}";
let ellipsis_width = backend.measure_text(ELLIPSIS_TEXT, font).width as f32;
if ellipsis_width > max_width {
return Cow::Owned(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 rest = text;
let mut matches = true;
for cluster in clusters {
match rest.strip_prefix(&*cluster.text) {
Some(tail) => rest = tail,
None => {
matches = false;
break;
}
}
}
matches && rest.is_empty()
};
if !shaped_matches_text {
return Cow::Owned(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_bytes_at = |length: usize| -> usize {
clusters[..length].iter().map(|cluster| cluster.text.len()).sum()
};
let prefix_fits = |bytes: usize| -> bool {
if bytes == 0 {
return true;
}
backend.measure_text(&text[..bytes], font).width as f32 <= budget
};
const CONFIRM_SLACK: usize = 2;
let search_from = estimated.saturating_sub(CONFIRM_SLACK);
let mut prefix_bytes = prefix_bytes_at(search_from.min(clusters.len()));
let mut confirmed = None;
let mut length = search_from;
while length <= clusters.len() {
if !prefix_fits(prefix_bytes) {
confirmed = Some(length - 1);
break;
}
if let Some(cluster) = clusters.get(length) {
prefix_bytes += cluster.text.len();
}
length += 1;
}
let confirmed = match confirmed {
Some(value) => value,
None if length > clusters.len() => clusters.len(),
None => {
return Cow::Owned(fit_text_to_width_by_prefix(text, budget, font, backend, ELLIPSIS))
}
};
let accepted_bytes = prefix_bytes_at(confirmed);
let mut kept = crate::compat::String::with_capacity(accepted_bytes + ELLIPSIS_TEXT.len());
kept.push_str(&text[..accepted_bytes]);
kept.push(ELLIPSIS);
Cow::Owned(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();
let mut candidate = crate::compat::String::new();
for ch in text.chars() {
candidate.clear();
candidate.push_str(&kept);
candidate.push(ch);
if backend.measure_text(&candidate, font).width as f32 > budget {
break;
}
core::mem::swap(&mut kept, &mut 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)>,
buckets: Option<RegionBuckets>,
last_region_passes: usize,
}
struct RegionGroup {
bounds: Option<Rect>,
commands: Vec<RenderCommand>,
}
struct RegionBuckets {
regions: Vec<Rect>,
groups: Vec<RegionGroup>,
open_groups: Vec<usize>,
free_groups: Vec<RegionGroup>,
active: bool,
}
impl RegionBuckets {
fn new(regions: Vec<Rect>) -> Self {
Self {
regions,
groups: Vec::new(),
open_groups: Vec::new(),
free_groups: Vec::new(),
active: true,
}
}
fn reset(&mut self, regions: Vec<Rect>) {
self.regions = regions;
self.recycle_groups();
self.active = true;
}
fn recycle_groups(&mut self) {
for mut group in self.groups.drain(..) {
group.bounds = None;
group.commands.clear();
self.free_groups.push(group);
}
self.open_groups.clear();
}
fn take_group(&mut self, bounds: Option<Rect>) -> RegionGroup {
let mut group =
self.free_groups.pop().unwrap_or(RegionGroup { bounds: None, commands: Vec::new() });
group.bounds = bounds;
group.commands.clear();
group
}
fn begin_control(&mut self, bounds: Rect) {
let index = self.groups.len();
let group = self.take_group(Some(bounds));
self.groups.push(group);
self.open_groups.push(index);
}
fn end_control(&mut self) {
let Some(child) = self.open_groups.pop() else {
return;
};
let Some(&parent) = self.open_groups.last() else {
return;
};
if parent == child {
return;
}
if child + 1 == self.groups.len() {
let mut child_group =
self.groups.pop().unwrap_or(RegionGroup { bounds: None, commands: Vec::new() });
self.groups[parent].commands.append(&mut child_group.commands);
child_group.bounds = None;
child_group.commands.clear();
self.free_groups.push(child_group);
return;
}
let mut commands = core::mem::take(&mut self.groups[child].commands);
self.groups[parent].commands.append(&mut commands);
}
fn record(&mut self, command: RenderCommand) {
if let Some(&index) = self.open_groups.last() {
self.groups[index].commands.push(command);
return;
}
let bounds = command.bounds();
let mut group = self.take_group(bounds);
group.commands.push(command);
self.groups.push(group);
}
fn flush(&mut self, backend: &mut dyn PaintBackend) -> usize {
for region in &self.regions {
backend.execute_command(&RenderCommand::PushClip {
x: region.x,
y: region.y,
width: region.width,
height: region.height,
});
for group in &self.groups {
let touches = group.bounds.is_none_or(|bounds| bounds.intersects(region));
if !touches {
continue;
}
for command in &group.commands {
backend.execute_command(command);
}
}
backend.execute_command(&RenderCommand::PopClip);
}
let passes = self.regions.len();
self.recycle_groups();
passes
}
}
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(),
buckets: None,
last_region_passes: 0,
}
}
fn bucket_recording(&self) -> bool {
self.buckets.as_ref().is_some_and(|buckets| buckets.active)
}
fn dispatch(&mut self, command: &RenderCommand) {
match self.buckets.as_mut() {
Some(buckets) if buckets.active => buckets.record(command.clone()),
_ => self.backend.execute_command(command),
}
}
fn dispatch_owned(&mut self, command: RenderCommand) {
match self.buckets.as_mut() {
Some(buckets) if buckets.active => buckets.record(command),
_ => self.backend.execute_command(&command),
}
}
pub fn begin_region_buckets(&mut self, regions: &[Rect]) {
let translated = regions
.iter()
.map(|region| {
Rect::new(
region.x + self.offset_x,
region.y + self.offset_y,
region.width,
region.height,
)
})
.collect();
let buckets = self.buckets.get_or_insert_with(|| RegionBuckets::new(Vec::new()));
buckets.reset(translated);
self.last_region_passes = 0;
}
pub fn end_region_buckets(&mut self) {
let Some(buckets) = self.buckets.as_mut() else {
return;
};
if !buckets.active {
return;
}
buckets.active = false;
self.last_region_passes = buckets.flush(self.backend);
}
pub fn begin_control(&mut self, bounds: Rect) {
let bounds = self.offset_rect(bounds);
if let Some(buckets) = self.buckets.as_mut() {
if buckets.active {
buckets.begin_control(bounds);
}
}
}
pub fn end_control(&mut self) {
if let Some(buckets) = self.buckets.as_mut() {
if buckets.active {
buckets.end_control();
}
}
}
pub fn region_bucket_passes(&self) -> usize {
self.last_region_passes
}
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.dispatch_owned(RenderCommand::FillRect { rect, color });
}
pub fn draw_box_shadow(
&mut self,
rect: Rect,
color: Color,
offset_x: i32,
offset_y: i32,
blur_radius: u32,
spread: i32,
) {
let rect = self.offset_rect(rect);
self.dispatch_owned(RenderCommand::BoxShadow {
rect,
color,
offset_x,
offset_y,
blur_radius,
spread,
});
}
pub fn fill_gradient(&mut self, rect: Rect, gradient: &crate::style::Gradient) {
let rect = self.offset_rect(rect);
if self.bucket_recording() {
self.dispatch_owned(RenderCommand::DrawGradient {
rect,
gradient: crate::compat::Arc::new(gradient.clone()),
});
} else {
self.backend.fill_gradient(rect, gradient);
}
}
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.dispatch_owned(RenderCommand::DrawRect { rect, color });
}
pub fn draw_rect_stroke(&mut self, rect: Rect, color: Color, width: u32) {
let rect = self.offset_rect(rect);
self.dispatch_owned(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.dispatch_owned(RenderCommand::FillRoundedRect { rect, radii: [radius; 4], color });
}
pub fn fill_rounded_rect_corners(&mut self, rect: Rect, radii: [u32; 4], color: Color) {
let rect = self.offset_rect(rect);
self.dispatch_owned(RenderCommand::FillRoundedRect { rect, radii, 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_corners(
&mut self,
rect: Rect,
fill: Color,
surface: crate::render::SurfaceStyle,
radii: [u32; 4],
shadow_tint: Color,
) {
self.face_with_gradient_corners(rect, fill, None, surface, radii, 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,
) {
self.face_with_gradient_corners(rect, fill, gradient, surface, [radius; 4], shadow_tint);
}
pub fn face_with_gradient_corners(
&mut self,
rect: Rect,
fill: Color,
gradient: Option<&crate::style::Gradient>,
surface: crate::render::SurfaceStyle,
radii: [u32; 4],
shadow_tint: Color,
) {
if let Some(shadow) = surface.shadow(shadow_tint) {
self.draw_box_shadow(rect, shadow.color, shadow.x, shadow.y, shadow.blur, 0);
}
let materialised = surface.apply_fill(fill);
match gradient {
Some(gradient) => self.fill_gradient(rect, gradient),
None => {
if radii.iter().any(|r| *r > 0) {
self.fill_rounded_rect_corners(rect, radii, 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.dispatch_owned(RenderCommand::FillRoundedRectAA { rect, radii: [radius; 4], color });
}
pub fn fill_rounded_rect_corners_aa(&mut self, rect: Rect, radii: [u32; 4], color: Color) {
let rect = self.offset_rect(rect);
self.dispatch_owned(RenderCommand::FillRoundedRectAA { rect, radii, color });
}
pub fn draw_rounded_rect_stroke(&mut self, rect: Rect, radius: u32, color: Color, width: u32) {
let rect = self.offset_rect(rect);
self.dispatch_owned(RenderCommand::DrawRoundedRectStroke {
rect,
radii: [radius; 4],
color,
width,
});
}
pub fn draw_rounded_rect_stroke_corners(
&mut self,
rect: Rect,
radii: [u32; 4],
color: Color,
width: u32,
) {
let rect = self.offset_rect(rect);
self.dispatch_owned(RenderCommand::DrawRoundedRectStroke { rect, radii, 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.dispatch_owned(RenderCommand::DrawRoundedRectStrokeAA {
rect,
radii: [radius; 4],
color,
width,
});
}
pub fn draw_rounded_rect_stroke_corners_aa(
&mut self,
rect: Rect,
radii: [u32; 4],
color: Color,
width: u32,
) {
let rect = self.offset_rect(rect);
self.dispatch_owned(RenderCommand::DrawRoundedRectStrokeAA { rect, radii, color, width });
}
pub fn draw_rounded_rect_border(
&mut self,
rect: Rect,
radii: [u32; 4],
color: Color,
width: u32,
style: crate::style::BorderStyle,
) {
use crate::style::BorderStyle;
if width == 0 {
return;
}
let rect = self.offset_rect(rect);
match style {
BorderStyle::None => {}
BorderStyle::Solid => {
self.dispatch_owned(RenderCommand::DrawRoundedRectStroke {
rect,
radii,
color,
width,
});
}
BorderStyle::Dashed => self.draw_edge_pattern(rect, radii, color, width, true),
BorderStyle::Dotted => self.draw_edge_pattern(rect, radii, color, width, false),
}
}
fn draw_edge_pattern(
&mut self,
rect: Rect,
radii: [u32; 4],
color: Color,
width: u32,
dashes: bool,
) {
use crate::render::pipeline::rounded_rect_effective_radii;
let [tl, tr, br, bl] = rounded_rect_effective_radii(rect, radii);
let left = rect.x;
let top = rect.y;
let right = rect.x + rect.width as i32;
let bottom = rect.y + rect.height as i32;
let (mark, gap) = if dashes {
((width * 3).max(1) as i64, (width * 2).max(1) as i64)
} else {
(0, ((width / 2).max(1) * 3) as i64)
};
let edges = [
((left + tl as i32, top), (right - tr as i32, top)),
((right, top + tr as i32), (right, bottom - br as i32)),
((left + bl as i32, bottom), (right - br as i32, bottom)),
((left, top + tl as i32), (left, bottom - bl as i32)),
];
for (from, to) in edges {
let (dx, dy) = ((to.0 - from.0) as i64, (to.1 - from.1) as i64);
let length = dx.abs().max(dy.abs());
if length <= 0 {
continue;
}
let point_at = |t: i64| {
Point::new(from.0 + (dx * t / length) as i32, from.1 + (dy * t / length) as i32)
};
let mut position = 0i64;
while position < length {
if dashes {
let end = (position + mark).min(length);
self.dispatch_owned(RenderCommand::DrawLineStroke {
from: point_at(position),
to: point_at(end),
color,
width,
});
position = end + gap;
} else {
self.dispatch_owned(RenderCommand::FillCircle {
center: point_at(position),
radius: (width / 2).max(1),
color,
});
position += gap;
}
}
}
}
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.dispatch_owned(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.dispatch_owned(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.dispatch_owned(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.dispatch_owned(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.dispatch_owned(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.dispatch_owned(RenderCommand::FillCircleAA { center, radius, color });
}
pub fn draw_circle(&mut self, center: Point, radius: u32, color: Color) {
let center = self.offset_point(center);
self.dispatch_owned(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.dispatch_owned(RenderCommand::DrawCircleStroke { center, radius, color, width });
}
pub fn draw_path(
&mut self,
points: &[Point],
closed: bool,
color: Color,
filled: bool,
width: u32,
) {
let points: Arc<[Point]> = points.iter().map(|point| self.offset_point(*point)).collect();
self.dispatch_owned(RenderCommand::DrawPath { points, closed, color, filled, width });
}
pub fn fill_path_non_zero(
&mut self,
points: &[Point],
contours: &[(usize, usize)],
color: Color,
) -> Result<(), crate::render::CompoundPathError> {
if self.bucket_recording() {
let translated = self.offset_points(points);
let path = crate::render::CompoundPath::new(translated, contours.to_vec())?;
self.dispatch_owned(RenderCommand::FillPathNonZero { path, color });
Ok(())
} else {
self.backend.fill_path_non_zero(points, contours, color, self.offset_x, self.offset_y)
}
}
pub fn draw_text(
&mut self,
origin: Point,
text: &str,
font: &Font,
color: Color,
alignment: HorizontalAlignment,
) {
let origin = self.offset_point(origin);
if self.bucket_recording() {
self.dispatch_owned(RenderCommand::DrawText {
origin,
text: crate::compat::Arc::from(text),
font: font.clone(),
color,
alignment,
});
} else {
self.backend.draw_text(origin, text, font, 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 {
self.draw_text_fitted_cow(bounds, text, font, color, alignment).into_owned()
}
pub(crate) fn draw_text_fitted_cow<'t>(
&mut self,
bounds: Rect,
text: &'t str,
font: &Font,
color: Color,
alignment: HorizontalAlignment,
) -> Cow<'t, str> {
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 {
self.draw_text_line_cow(bounds, text, font, color, alignment).into_owned()
}
pub(crate) fn draw_text_line_cow<'t>(
&mut self,
bounds: Rect,
text: &'t str,
font: &Font,
color: Color,
alignment: HorizontalAlignment,
) -> Cow<'t, str> {
let line = self.text_line(bounds, font);
self.draw_text_fitted_cow(line, text, font, color, alignment)
}
pub fn shape_text(&self, text: &str, font: &Font) -> Rc<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.dispatch_owned(RenderCommand::PushClip { x, y, width, height });
}
pub fn pop_clip(&mut self) {
self.dispatch_owned(RenderCommand::PopClip);
}
pub(crate) fn push_layer(&mut self, bounds: Rect, effect: Option<LayerEffect>) {
let bounds = self.offset_rect(bounds);
self.dispatch_owned(RenderCommand::PushLayer { bounds, effect });
}
pub fn push_material_layer(
&mut self,
bounds: Rect,
blur_radius: u32,
tint: Option<(Color, f32)>,
) {
let effect = match tint {
Some((tint, tint_weight)) => {
Some(LayerEffect::BackdropBlurTint { radius: blur_radius, tint, tint_weight })
}
None if blur_radius == 0 => None,
None => Some(LayerEffect::BackdropBlur { radius: blur_radius }),
};
self.push_layer(bounds, effect);
}
pub fn pop_layer(&mut self) {
self.dispatch_owned(RenderCommand::PopLayer);
}
pub fn push_matte(&mut self, bounds: Rect, mode: MatteMode) {
let bounds = self.offset_rect(bounds);
self.dispatch_owned(RenderCommand::PushMatte { bounds, mode });
}
pub fn pop_matte(&mut self) {
self.dispatch_owned(RenderCommand::PopMatte);
}
pub fn end_matte(&mut self) {
self.dispatch_owned(RenderCommand::EndMatte);
}
pub fn push_layer_group(
&mut self,
bounds: Rect,
effect: Option<LayerEffect>,
opacity: f32,
cache_key: Option<u64>,
) {
let bounds = self.offset_rect(bounds);
self.dispatch_owned(RenderCommand::PushLayerGroup { bounds, effect, opacity, cache_key });
}
pub fn clear_layer_cache(&mut self) {
self.backend.clear_layer_cache();
}
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;
if self.bucket_recording() {
self.dispatch_owned(RenderCommand::DrawImage {
x,
y,
width,
height,
data: crate::compat::Arc::from(data),
});
} else {
self.backend.draw_image(x, y, width, height, data);
}
}
pub fn execute_command(&mut self, command: RenderCommand) {
self.dispatch_owned(command);
}
pub fn execute_command_ref(&mut self, command: &RenderCommand) {
self.dispatch(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());
}
fn put(pixels: &mut [u8], width: u32, x: u32, y: u32, color: [u8; 4]) {
let offset = ((y * width + x) * 4) as usize;
pixels[offset..offset + 4].copy_from_slice(&color);
}
fn got(pixels: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
let offset = ((y * width + x) * 4) as usize;
[pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]]
}
#[test]
fn present_damage_copies_only_the_reported_region() {
let mut buffer = BackBuffer::new(Size::new(4, 4), 1.0);
crate::render::fill_pixels(buffer.back_mut(), Color::WHITE);
buffer.present();
for y in 0..2 {
for x in 0..2 {
put(buffer.back_mut(), 4, x, y, [255, 0, 0, 255]);
}
}
buffer.mark_damage(Rect::new(0, 0, 2, 2));
assert!(buffer.present_damage(), "a reported region is published");
assert_eq!(got(buffer.front(), 4, 0, 0), [255, 0, 0, 255], "the damaged pixel updates");
assert_eq!(got(buffer.front(), 4, 3, 3), [255, 255, 255, 255], "the rest is the old frame");
assert!(buffer.damage_regions().is_empty(), "the list resets after publishing");
}
#[test]
fn present_damage_with_no_regions_is_a_no_op() {
let mut buffer = BackBuffer::new(Size::new(2, 2), 1.0);
crate::render::fill_pixels(buffer.back_mut(), Color::WHITE);
buffer.present();
crate::render::fill_pixels(buffer.back_mut(), Color::RED);
assert!(!buffer.present_damage(), "an empty damage list publishes nothing");
assert_eq!(got(buffer.front(), 2, 0, 0), [255, 255, 255, 255], "the front is untouched");
}
#[test]
fn mark_damage_clips_and_deduplicates() {
let mut buffer = BackBuffer::new(Size::new(4, 4), 1.0);
buffer.mark_damage(Rect::new(-2, -2, 4, 4));
buffer.mark_damage(Rect::new(0, 0, 2, 2));
buffer.mark_damage(Rect::new(8, 8, 2, 2));
assert_eq!(
buffer.damage_regions(),
&[(0, 0, 2, 2)],
"the clipped region survives, its contained duplicate and the off-screen region do not"
);
}
#[test]
fn mark_damage_merges_overlapping_regions() {
let mut buffer = BackBuffer::new(Size::new(8, 8), 1.0);
buffer.mark_damage(Rect::new(0, 0, 2, 2));
buffer.mark_damage(Rect::new(1, 1, 2, 2));
assert_eq!(
buffer.damage_regions(),
&[(0, 0, 3, 3)],
"overlapping reports become their union"
);
buffer.clear_damage();
buffer.mark_damage(Rect::new(0, 0, 2, 2));
buffer.mark_damage(Rect::new(2, 0, 2, 2));
assert_eq!(
buffer.damage_regions(),
&[(0, 0, 2, 2), (2, 0, 2, 2)],
"rectangles that only touch do not overlap"
);
}
#[test]
fn mark_damage_is_bounded() {
let mut buffer = BackBuffer::new(Size::new(64, 64), 1.0);
for index in 0..(MAX_DAMAGE_REGIONS + 1) {
let x = (index % 8) as i32 * 8;
let y = (index / 8) as i32 * 8;
buffer.mark_damage(Rect::new(x, y, 1, 1));
}
assert_eq!(buffer.damage_regions().len(), 1, "the list collapses at the bound");
let (x, y, width, height) = buffer.damage_regions()[0];
assert_eq!((x, y), (0, 0));
assert!(width >= 8 && height >= 8, "the covering region keeps every report");
}
#[test]
fn whole_surface_damage_presents_by_swapping() {
let mut buffer = BackBuffer::new(Size::new(2, 2), 1.0);
crate::render::fill_pixels(buffer.back_mut(), Color::RED);
buffer.mark_damage(Rect::new(0, 0, 2, 2));
assert!(buffer.present_damage());
assert_eq!(got(buffer.front(), 2, 1, 1), [255, 0, 0, 255]);
}
#[test]
fn a_surface_presents_a_damaged_region() {
let mut surface = SoftwareSurface::new(Size::new(4, 4), 1.0);
surface.begin_frame(Color::WHITE);
surface.end_frame();
let previous = surface.frame_rgba().to_vec();
let mut backend = SoftwarePaintBackend::new(Size::new(4, 4), 1.0);
assert!(backend.seed_from(&previous));
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 2, 2),
color: Color::RED,
});
backend.mark_damage(Rect::new(0, 0, 2, 2));
assert!(backend.present_damage());
assert_eq!(backend.damage_regions(), &[] as &[(i32, i32, u32, u32)]);
}
#[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); }
#[cfg(feature = "3d")]
#[test]
fn a_quad_image_fills_the_parallelogram_and_nothing_around_it() {
let mut surface = SoftwareSurface::new(Size::new(40, 40), 1.0);
surface.begin_frame(Color::WHITE);
let corners =
[Point::new(20, 0), Point::new(40, 20), Point::new(20, 40), Point::new(0, 20)];
let pixels = [255u8, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255];
surface.draw_image_quad(corners, 2, 2, &pixels);
surface.end_frame();
let rgba = surface.frame_rgba();
let at = |x: u32, y: u32| {
let index = ((y * 40 + x) * 4) as usize;
[rgba[index], rgba[index + 1], rgba[index + 2]]
};
assert_eq!(at(20, 20), [255, 0, 0], "the centre of the quad is inside the image");
assert_eq!(at(2, 2), [255, 255, 255], "a corner outside the diamond is untouched");
assert_eq!(at(37, 37), [255, 255, 255], "and so is the opposite corner");
}
#[cfg(feature = "3d")]
#[test]
fn a_scaled_quad_image_resamples_bilinearly() {
let mut surface = SoftwareSurface::new(Size::new(8, 8), 1.0);
surface.begin_frame(Color::BLACK);
let pixels = [
0u8, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, ];
surface.draw_image_quad(
[Point::new(0, 0), Point::new(4, 0), Point::new(4, 4), Point::new(0, 4)],
2,
2,
&pixels,
);
surface.end_frame();
let rgba = surface.frame_rgba();
let red = |x: u32, y: u32| rgba[((y * 8 + x) * 4) as usize];
assert_eq!(red(0, 0), 0, "the first column is the source's black texel");
assert_eq!(red(3, 0), 255, "the last column is the source's white texel");
let blended = red(1, 0);
assert!(blended > 0 && blended < 255, "a stretched quad must interpolate, got {blended}");
}
#[test]
fn a_partly_offscreen_image_is_cropped_to_the_visible_rectangle() {
let mut surface = SoftwareSurface::new(Size::new(6, 6), 1.0);
surface.begin_frame(Color::WHITE);
let red = [255u8, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255];
surface.draw_image(-1, -1, 2, 2, &red);
surface.end_frame();
let rgba = surface.frame_rgba();
let at = |x: u32, y: u32| {
let index = ((y * 6 + x) * 4) as usize;
[rgba[index], rgba[index + 1], rgba[index + 2]]
};
assert_eq!(at(0, 0), [255, 0, 0], "the one visible pixel is the source's last one");
assert_eq!(at(1, 0), [255, 255, 255], "nothing is shifted to the right");
assert_eq!(at(0, 1), [255, 255, 255], "nothing is shifted down");
assert_eq!(at(5, 5), [255, 255, 255], "the rest of the surface is untouched");
}
#[test]
fn an_image_inside_a_clip_reads_the_source_columns_it_covers() {
let mut surface = SoftwareSurface::new(Size::new(4, 1), 1.0);
surface.begin_frame(Color::WHITE);
let pixels = [255u8, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 255];
surface.push_clip(1, 0, 2, 1);
surface.draw_image(0, 0, 4, 1, &pixels);
surface.pop_clip();
surface.end_frame();
let rgba = surface.frame_rgba();
let at = |x: u32| {
let index = (x * 4) as usize;
[rgba[index], rgba[index + 1], rgba[index + 2]]
};
assert_eq!(at(0), [255, 255, 255], "outside the clip");
assert_eq!(at(1), [0, 255, 0], "the clip starts at the source's second column");
assert_eq!(at(2), [0, 0, 255], "and ends after the third");
assert_eq!(at(3), [255, 255, 255], "outside the clip");
}
#[cfg(feature = "3d")]
#[test]
fn an_axis_aligned_quad_matches_a_plain_image_draw() {
let pixels = [0u8, 128, 0, 255, 0, 128, 0, 255, 0, 128, 0, 255, 0, 128, 0, 255];
let mut plain = SoftwareSurface::new(Size::new(8, 8), 1.0);
plain.begin_frame(Color::WHITE);
plain.draw_image(2, 2, 2, 2, &pixels);
plain.end_frame();
let mut quad = SoftwareSurface::new(Size::new(8, 8), 1.0);
quad.begin_frame(Color::WHITE);
quad.draw_image_quad(
[Point::new(2, 2), Point::new(4, 2), Point::new(4, 4), Point::new(2, 4)],
2,
2,
&pixels,
);
quad.end_frame();
assert_eq!(
plain.frame_rgba(),
quad.frame_rgba(),
"an identity transform must not change the pixels"
);
}
#[test]
fn fill_pixels_covers_every_length() {
use crate::render::fill_pixels;
let color = Color::rgb(10, 20, 30);
for length in [0usize, 1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 33, 64, 65] {
let mut pixels = vec![0u8; length];
fill_pixels(&mut pixels, color);
for (index, byte) in pixels.iter().enumerate() {
let expected = [color.r, color.g, color.b, color.a][index % 4];
assert_eq!(
*byte, expected,
"byte {index} of a {length}-byte fill must carry the colour pattern"
);
}
}
}
#[test]
fn fill_pixels_fills_a_whole_surface() {
use crate::render::fill_pixels;
let color = Color::rgba(1, 2, 3, 4);
let mut pixels = vec![255u8; 1280 * 960 * 4];
fill_pixels(&mut pixels, color);
assert!(
pixels.as_chunks::<4>().0.iter().all(|pixel| *pixel == [1, 2, 3, 4]),
"every pixel of the clear must be the requested colour"
);
}
#[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 borrowed_non_zero_fill_applies_offset_and_preserves_holes() {
let points = [
Point::new(1, 1),
Point::new(11, 1),
Point::new(11, 11),
Point::new(1, 11),
Point::new(4, 4),
Point::new(4, 8),
Point::new(8, 8),
Point::new(8, 4),
];
let contours = [(0, 4), (4, 8)];
let mut backend = SoftwarePaintBackend::new(Size::new(16, 16), 1.0);
backend.begin_frame(Color::TRANSPARENT);
{
let mut context = RenderContext::new(&mut backend);
context.push_offset(2, 2);
context
.fill_path_non_zero(&points, &contours, Color::rgb(255, 0, 0))
.expect("valid borrowed contours should fill");
}
backend.end_frame();
let rgba = backend.frame_rgba();
let pixel = |x: usize, y: usize| {
let index = (y * 16 + x) * 4;
&rgba[index..index + 4]
};
assert_eq!(pixel(3, 3), &[255, 0, 0, 255]);
assert_eq!(pixel(7, 7), &[0, 0, 0, 0]);
}
#[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) -> Rc<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: Arc::from(data),
});
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();
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex"))]
#[test]
fn glyph_run_text_paints_the_shapers_ligature() {
use crate::render::text::font_assets::active_faces;
let Some(face) = active_faces().first().copied() else {
eprintln!("skipping glyph-run test: no vector face in this build");
return;
};
let Some(shaper) = crate::render::RustybuzzShaper::new(face) else {
eprintln!("skipping glyph-run test: the face did not parse");
return;
};
let plain = shaper.shape_face("fi", 16.0, 1.0).expect("the face shapes its text");
let ligated =
shaper.shape_face_with_ligatures("fi", 16.0, 1.0).expect("the face shapes its text");
if ligated.glyph_ids.len() >= plain.glyph_ids.len() {
eprintln!("skipping glyph-run test: {} has no `fi` ligature", face.name);
return;
}
let font = Font::simple(face.name, 16.0);
let mut surface = SoftwareSurface::new(Size::new(80, 32), 1.0);
surface.begin_frame(Color::WHITE);
assert!(!surface.glyph_run_text(), "the glyph path is opt-in");
surface.set_glyph_run_text(true);
assert!(surface.glyph_run_text());
surface.draw_text(Point::new(2, 2), "fi", &font, Color::BLACK, HorizontalAlignment::Left);
surface.end_frame();
let frame = surface.frame_rgba();
assert!(
frame.as_chunks::<4>().0.iter().any(|pixel| pixel[0] < 200),
"the shaped glyph run must leave ink on the surface"
);
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex"))]
#[test]
fn glyph_run_text_falls_back_for_an_unknown_family() {
let font = Font::simple("no-such-family", 16.0);
let mut plain = SoftwareSurface::new(Size::new(64, 24), 1.0);
plain.begin_frame(Color::WHITE);
plain.draw_text(Point::new(2, 2), "ffi", &font, Color::BLACK, HorizontalAlignment::Left);
plain.end_frame();
let mut glyphs = SoftwareSurface::new(Size::new(64, 24), 1.0);
glyphs.begin_frame(Color::WHITE);
glyphs.set_glyph_run_text(true);
glyphs.draw_text(Point::new(2, 2), "ffi", &font, Color::BLACK, HorizontalAlignment::Left);
glyphs.end_frame();
assert_eq!(
plain.frame_rgba(),
glyphs.frame_rgba(),
"an unavailable face must fall back to the cluster path byte for byte"
);
}
#[cfg(feature = "3d")]
#[test]
fn a_projectively_divided_quad_antialiases_its_projected_edge() {
let mut surface = SoftwareSurface::new(Size::new(12, 12), 1.0);
surface.begin_frame(Color::WHITE);
surface.fill_quad_aa(
[Point::new(0, 0), Point::new(10, 0), Point::new(9, 10), Point::new(0, 10)],
Color::RED,
);
surface.end_frame();
let frame = surface.frame_rgba();
let at = |x: u32, y: u32| {
let index = ((y * 12 + x) * 4) as usize;
[frame[index], frame[index + 1], frame[index + 2], frame[index + 3]]
};
assert_eq!(at(2, 5), [255, 0, 0, 255], "the interior is fully covered");
let edge = at(9, 5);
assert_eq!(edge[0], 255, "the red channel is the fill's");
assert!(
edge[1] > 0 && edge[1] < 255,
"a half-covered edge pixel must blend with the backdrop, got {edge:?}"
);
assert_eq!(at(11, 5), [255, 255, 255, 255], "outside the quad stays untouched");
}
#[test]
fn a_projective_quad_image_follows_its_narrow_edge() {
let mut surface = SoftwareSurface::new(Size::new(20, 20), 1.0);
surface.begin_frame(Color::WHITE);
let pixels = [
255u8, 0, 0, 255, 0, 0, 255, 255, 255, 0, 0, 255, 0, 0, 255, 255,
];
surface.draw_image_quad(
[Point::new(6, 2), Point::new(14, 2), Point::new(18, 18), Point::new(2, 18)],
2,
2,
&pixels,
);
surface.end_frame();
let frame = surface.frame_rgba();
let at = |x: u32, y: u32| {
let index = ((y * 20 + x) * 4) as usize;
[frame[index], frame[index + 1], frame[index + 2], frame[index + 3]]
};
assert_eq!(at(4, 3), [255, 255, 255, 255], "outside the narrow top edge");
let wide = at(3, 17);
assert!(
wide[0] > 0 || wide[2] > 0,
"the wide bottom edge must reach this column, got {wide:?}"
);
let middle = at(10, 10);
assert!(
middle[0] > 0 && middle[2] > 0,
"the middle samples both source columns, got {middle:?}"
);
}
}