use super::batch::BatchState;
use crate::core::{Color, Font, Size};
use crate::render::pipeline::set_pixel;
use crate::render::{
BlendMode, RenderCommand, ShapedText, SoftwareRenderConfig, SoftwareSurface, TextMetrics,
};
pub trait PaintBackend {
fn begin_frame(&mut self, clear: Color);
fn end_frame(&mut self);
fn execute_command(&mut self, command: &RenderCommand);
fn size(&self) -> Size;
fn set_size(&mut self, size: Size);
fn dpi_scale(&self) -> f32;
fn set_dpi_scale(&mut self, dpi_scale: f32);
fn measure_text(&self, text: &str, font: &Font) -> TextMetrics;
fn shape_text(&self, text: &str, font: &Font) -> ShapedText;
fn frame_rgba(&self) -> &[u8];
fn apply_render_config(&mut self, _config: SoftwareRenderConfig) {}
fn render_config(&self) -> SoftwareRenderConfig {
SoftwareRenderConfig::default()
}
}
pub struct SoftwarePaintBackend {
pub(crate) surface: SoftwareSurface,
pub(crate) batch_state: BatchState,
pub(crate) current_blend_mode: BlendMode,
}
impl SoftwarePaintBackend {
pub fn new(size: Size, dpi_scale: f32) -> Self {
Self {
surface: SoftwareSurface::new(size, dpi_scale),
batch_state: BatchState::new(),
current_blend_mode: BlendMode::Normal,
}
}
pub fn surface(&self) -> &SoftwareSurface {
&self.surface
}
pub fn surface_mut(&mut self) -> &mut SoftwareSurface {
&mut self.surface
}
pub fn apply_render_config(&mut self, config: SoftwareRenderConfig) {
self.surface.apply_render_config(config);
}
pub fn render_config(&self) -> SoftwareRenderConfig {
self.surface.render_config()
}
}
impl PaintBackend for SoftwarePaintBackend {
fn begin_frame(&mut self, clear: Color) {
self.surface.begin_frame(clear);
}
fn end_frame(&mut self) {
self.surface.end_frame();
}
fn execute_command(&mut self, command: &RenderCommand) {
match command {
RenderCommand::FillRect { rect, color } => self.surface.fill_rect(*rect, *color),
RenderCommand::DrawRect { rect, color } => self.surface.draw_rect(*rect, *color),
RenderCommand::DrawRectStroke { rect, color, width } => {
self.surface.draw_rect_with_width(*rect, *color, *width)
}
RenderCommand::FillRoundedRect { rect, radius, color } => {
self.surface.fill_rounded_rect(*rect, *radius, *color)
}
RenderCommand::FillRoundedRectAA { rect, radius, color } => {
self.surface.fill_rounded_rect_aa(*rect, *radius, *color)
}
RenderCommand::DrawRoundedRectStroke { rect, radius, color, width } => {
self.surface.draw_rounded_rect_with_width(*rect, *radius, *color, *width)
}
RenderCommand::DrawRoundedRectStrokeAA { rect, radius, color, width } => {
self.surface.draw_rounded_rect_aa_with_width(*rect, *radius, *color, *width)
}
RenderCommand::DrawLine { from, to, color } => {
self.surface.draw_line(*from, *to, *color)
}
RenderCommand::DrawLineAA { from, to, color } => {
self.surface.draw_line_aa(*from, *to, *color)
}
RenderCommand::DrawLineStrokeAA { from, to, color, width } => {
self.surface.draw_line_aa_with_width(*from, *to, *color, *width)
}
RenderCommand::DrawLineStroke { from, to, color, width } => {
self.surface.draw_line_with_width(*from, *to, *color, *width)
}
RenderCommand::FillCircle { center, radius, color } => {
self.surface.fill_circle(*center, *radius, *color)
}
RenderCommand::FillCircleAA { center, radius, color } => {
self.surface.fill_circle_aa(*center, *radius, *color)
}
RenderCommand::DrawCircle { center, radius, color } => {
self.surface.draw_circle(*center, *radius, *color)
}
RenderCommand::DrawCircleStroke { center, radius, color, width } => {
self.surface.draw_circle_with_width(*center, *radius, *color, *width)
}
RenderCommand::DrawText { origin, text, font, color, alignment } => {
self.surface.draw_text(*origin, text, font, *color, *alignment)
}
RenderCommand::DrawImage { x, y, width, height, data } => {
self.surface.draw_image(*x, *y, *width, *height, data)
}
RenderCommand::PushClip { x, y, width, height } => {
self.surface.push_clip(*x, *y, *width, *height)
}
RenderCommand::PopClip => self.surface.pop_clip(),
RenderCommand::DrawGradient { rect, gradient } => {
self.surface.fill_rect_gradient(*rect, gradient);
}
RenderCommand::DrawArc { center, radius, start_angle, end_angle, color, filled } => {
self.surface.draw_arc(*center, *radius, *start_angle, *end_angle, *color, *filled);
}
RenderCommand::DrawPath { points, closed, color, filled, width } => {
self.surface.draw_path(points, *closed, *color, *filled, *width);
}
RenderCommand::BoxShadow { rect, color, offset_x, offset_y, blur_radius, spread } => {
let spread_rect = crate::core::Rect::new(
rect.x + offset_x - *spread,
rect.y + offset_y - *spread,
(rect.width as i32 + *spread * 2).max(0) as u32,
(rect.height as i32 + *spread * 2).max(0) as u32,
);
let shadow_color =
Color::rgba(color.r, color.g, color.b, (color.a as f32 * 0.5) as u8);
self.surface.fill_rect(spread_rect, shadow_color);
if *blur_radius > 0 {
let size = self.surface.size();
let w = size.width as usize;
let h = size.height as usize;
if w > 0 && h > 0 {
let back = self.surface.buffer.back_mut();
let radius = (*blur_radius).min(100) as usize;
let blur_x0 = spread_rect.x.max(0) as usize;
let blur_y0 = spread_rect.y.max(0) as usize;
let blur_w = ((spread_rect.x as usize + spread_rect.width as usize).min(w))
.saturating_sub(blur_x0);
let blur_h = ((spread_rect.y as usize + spread_rect.height as usize)
.min(h))
.saturating_sub(blur_y0);
box_blur_region(back, w, h, blur_x0, blur_y0, blur_w, blur_h, radius);
}
}
}
RenderCommand::Blur { radius } => {
let r = (*radius).min(100) as usize;
if r == 0 {
return;
}
let size = self.surface.size();
let w = size.width as usize;
let h = size.height as usize;
if w == 0 || h == 0 {
return;
}
let back = self.surface.buffer.back_mut();
box_blur_region(back, w, h, 0, 0, w, h, r);
}
RenderCommand::ClipPath { points } => {
if points.is_empty() {
return;
}
let min_x = points.iter().map(|p| p.x).min().unwrap();
let max_x = points.iter().map(|p| p.x).max().unwrap();
let min_y = points.iter().map(|p| p.y).min().unwrap();
let max_y = points.iter().map(|p| p.y).max().unwrap();
if min_x < max_x && min_y < max_y {
let cw = (max_x - min_x) as u32;
let ch = (max_y - min_y) as u32;
if cw > 0 && ch > 0 {
self.surface.push_clip(min_x, min_y, cw, ch);
}
}
}
RenderCommand::SetBlendMode { mode } => {
self.current_blend_mode = *mode;
}
RenderCommand::DrawConicGradient { center, start_angle, stops } => {
if stops.is_empty() {
return;
}
let size = self.surface.size();
let w = size.width as usize;
let h = size.height as usize;
if w == 0 || h == 0 {
return;
}
let back = self.surface.buffer.back_mut();
let cx = center.x as f32;
let cy = center.y as f32;
let angle_offset = *start_angle;
for py in 0..h {
for px in 0..w {
let dx = px as f32 - cx;
let dy = py as f32 - cy;
let mut t = dy.atan2(dx) + std::f32::consts::PI;
t = (t + angle_offset) % (2.0 * std::f32::consts::PI);
let pos = t / (2.0 * std::f32::consts::PI);
let color = if pos <= stops[0].0 {
stops[0].1
} else if pos >= stops.last().unwrap().0 {
stops.last().unwrap().1
} else {
let mut lo = 0usize;
let mut hi = stops.len() - 1;
while hi - lo > 1 {
let mid = (lo + hi) / 2;
if stops[mid].0 <= pos {
lo = mid;
} else {
hi = mid;
}
}
let t_local =
(pos - stops[lo].0) / (stops[hi].0 - stops[lo].0).max(0.0001);
let ca = stops[lo].1;
let cb = stops[hi].1;
Color::rgba(
(ca.r as f32 + (cb.r as f32 - ca.r as f32) * t_local) as u8,
(ca.g as f32 + (cb.g as f32 - ca.g as f32) * t_local) as u8,
(ca.b as f32 + (cb.b as f32 - ca.b as f32) * t_local) as u8,
(ca.a as f32 + (cb.a as f32 - ca.a as f32) * t_local) as u8,
)
};
set_pixel(back, w as u32, px as u32, py as u32, color);
}
}
}
}
}
fn size(&self) -> Size {
self.surface.size()
}
fn set_size(&mut self, size: Size) {
self.surface.resize(size);
}
fn dpi_scale(&self) -> f32 {
self.surface.dpi_scale()
}
fn set_dpi_scale(&mut self, dpi_scale: f32) {
self.surface.set_dpi_scale(dpi_scale);
}
fn measure_text(&self, text: &str, font: &Font) -> TextMetrics {
self.surface.measure_text(text, font)
}
fn shape_text(&self, text: &str, font: &Font) -> ShapedText {
self.surface.shape_text(text, font)
}
fn frame_rgba(&self) -> &[u8] {
self.surface.frame_rgba()
}
fn apply_render_config(&mut self, config: SoftwareRenderConfig) {
self.surface.apply_render_config(config);
}
fn render_config(&self) -> SoftwareRenderConfig {
self.surface.render_config()
}
}
fn box_blur_region(
back: &mut [u8],
w: usize,
h: usize,
region_x: usize,
region_y: usize,
region_w: usize,
region_h: usize,
radius: usize,
) {
if w == 0 || h == 0 || region_w == 0 || region_h == 0 || radius == 0 {
return;
}
let ex0 = region_x.saturating_sub(radius);
let ey0 = region_y.saturating_sub(radius);
let ex1 = (region_x + region_w + radius).min(w);
let ey1 = (region_y + region_h + radius).min(h);
let ew = ex1 - ex0;
let eh = ey1 - ey0;
if ew == 0 || eh == 0 {
return;
}
let mut temp = vec![0u8; ew * eh * 4];
for y in 0..eh {
let src_start = ((ey0 + y) * w + ex0) * 4;
let dst_start = y * ew * 4;
temp[dst_start..dst_start + ew * 4].copy_from_slice(&back[src_start..src_start + ew * 4]);
}
for y in 0..eh {
for x in 0..ew {
let sx = ex0 + x;
let mut r_sum = 0u32;
let mut g_sum = 0u32;
let mut b_sum = 0u32;
let mut a_sum = 0u32;
let mut count = 0u32;
let x_min = sx.saturating_sub(radius);
let x_max = (sx + radius).min(w - 1);
for kx in x_min..=x_max {
let kx_local = kx.saturating_sub(ex0);
let ti = (y * ew + kx_local) * 4;
r_sum += temp[ti] as u32;
g_sum += temp[ti + 1] as u32;
b_sum += temp[ti + 2] as u32;
a_sum += temp[ti + 3] as u32;
count += 1;
}
let di = (y * ew + x) * 4;
temp[di] = (r_sum / count) as u8;
temp[di + 1] = (g_sum / count) as u8;
temp[di + 2] = (b_sum / count) as u8;
temp[di + 3] = (a_sum / count) as u8;
}
}
for x in 0..ew {
for y in 0..eh {
let sy = ey0 + y;
let mut r_sum = 0u32;
let mut g_sum = 0u32;
let mut b_sum = 0u32;
let mut a_sum = 0u32;
let mut count = 0u32;
let y_min = sy.saturating_sub(radius);
let y_max = (sy + radius).min(h - 1);
for ky in y_min..=y_max {
let ky_local = ky.saturating_sub(ey0);
let ti = (ky_local * ew + x) * 4;
r_sum += temp[ti] as u32;
g_sum += temp[ti + 1] as u32;
b_sum += temp[ti + 2] as u32;
a_sum += temp[ti + 3] as u32;
count += 1;
}
let di = (sy * w + ex0 + x) * 4;
back[di] = (r_sum / count) as u8;
back[di + 1] = (g_sum / count) as u8;
back[di + 2] = (b_sum / count) as u8;
back[di + 3] = (a_sum / count) as u8;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Color, HorizontalAlignment, Point, Rect, Size};
#[test]
fn software_paint_backend_new_creates_surface() {
let size = Size::new(100, 100);
let backend = SoftwarePaintBackend::new(size, 1.0);
assert_eq!(backend.size(), size);
assert!((backend.dpi_scale() - 1.0).abs() < 1e-6);
}
#[test]
fn software_paint_backend_zero_size() {
let backend = SoftwarePaintBackend::new(Size::new(0, 0), 1.0);
assert_eq!(backend.size(), Size::new(0, 0));
let rgba = backend.frame_rgba();
assert!(rgba.is_empty());
}
#[test]
fn software_paint_backend_high_dpi() {
let backend = SoftwarePaintBackend::new(Size::new(50, 50), 2.0);
assert!((backend.dpi_scale() - 2.0).abs() < 1e-6);
}
#[test]
fn software_paint_backend_minimum_dpi_scale() {
let backend = SoftwarePaintBackend::new(Size::new(10, 10), 0.0);
assert!((backend.dpi_scale() - 0.1).abs() < 1e-6);
}
#[test]
fn software_paint_backend_surface_accessor() {
let backend = SoftwarePaintBackend::new(Size::new(30, 30), 1.0);
let surface = backend.surface();
assert_eq!(surface.size(), Size::new(30, 30));
}
#[test]
fn software_paint_backend_surface_mut_accessor() {
let mut backend = SoftwarePaintBackend::new(Size::new(40, 40), 1.0);
{
let surface = backend.surface_mut();
assert_eq!(surface.size(), Size::new(40, 40));
}
}
#[test]
fn paint_backend_begin_end_frame_clears() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::RED);
backend.end_frame();
let rgba = backend.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 paint_backend_execute_fill_rect() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(2, 2, 10, 10),
color: Color::BLUE,
});
backend.end_frame();
let rgba = backend.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 paint_backend_execute_draw_rect_stroke() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawRectStroke {
rect: Rect::new(0, 0, 20, 20),
color: Color::GREEN,
width: 1,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 0); assert_eq!(rgba[1], 255); assert_eq!(rgba[2], 0); }
#[test]
fn paint_backend_execute_draw_line() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawLine {
from: Point::new(0, 0),
to: Point::new(9, 9),
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255); assert_eq!(rgba[3], 255); }
#[test]
fn paint_backend_execute_push_pop_clip() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::PushClip { x: 0, y: 0, width: 5, height: 5 });
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 10, 10),
color: Color::RED,
});
backend.execute_command(&RenderCommand::PopClip);
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 10 * 4;
let idx = 2 * stride + 2 * 4;
assert_eq!(rgba[idx], 255); assert_eq!(rgba[idx + 3], 255); }
#[test]
fn paint_backend_execute_fill_circle() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillCircleAA {
center: Point::new(10, 10),
radius: 5,
color: Color::BLUE,
});
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 20 * 4;
let idx = 10 * stride + 10 * 4;
assert_eq!(rgba[idx], 0); assert_eq!(rgba[idx + 2], 255); }
#[test]
fn paint_backend_size_set_size() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
assert_eq!(backend.size(), Size::new(10, 10));
let new_size = Size::new(50, 50);
backend.set_size(new_size);
assert_eq!(backend.size(), new_size);
}
#[test]
fn paint_backend_dpi_scale_set_dpi_scale() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.set_dpi_scale(1.5);
assert!((backend.dpi_scale() - 1.5).abs() < 1e-6);
}
#[test]
fn paint_backend_render_config_default() {
let backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let config = backend.render_config();
assert_eq!(config.aa_samples_per_axis, 4);
}
#[test]
fn paint_backend_apply_render_config() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let config = SoftwareRenderConfig { aa_samples_per_axis: 2 };
backend.apply_render_config(config);
assert_eq!(backend.render_config().aa_samples_per_axis, 2);
}
#[test]
fn paint_backend_apply_render_config_clamps_to_normalized_range() {
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
let config = SoftwareRenderConfig { aa_samples_per_axis: 99 };
backend.apply_render_config(config);
assert_eq!(backend.render_config().aa_samples_per_axis, 8);
let config = SoftwareRenderConfig { aa_samples_per_axis: 0 };
backend.apply_render_config(config);
assert_eq!(backend.render_config().aa_samples_per_axis, 1);
}
#[test]
fn paint_backend_default_render_config() {
let config = <SoftwarePaintBackend as PaintBackend>::render_config(
&SoftwarePaintBackend::new(Size::new(1, 1), 1.0),
);
assert_eq!(config, SoftwareRenderConfig::default());
}
#[test]
fn paint_backend_execute_draw_text_does_not_panic() {
let size = Size::new(100, 100);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
let font = Font::simple("Arial", 12.0);
backend.execute_command(&RenderCommand::DrawText {
origin: Point::new(10, 20),
text: "Hello".to_string(),
font,
color: Color::BLACK,
alignment: HorizontalAlignment::Left,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn paint_backend_execute_draw_image() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
let data = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ];
backend.execute_command(&RenderCommand::DrawImage {
x: 0,
y: 0,
width: 2,
height: 2,
data,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255); assert_eq!(rgba[1], 0); assert_eq!(rgba[2], 0); }
#[test]
fn paint_backend_execute_draw_rect() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawRect {
rect: Rect::new(0, 0, 10, 10),
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert_eq!(rgba[0], 255); assert_eq!(rgba[3], 255); }
#[test]
fn paint_backend_execute_fill_rounded_rect() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRoundedRect {
rect: Rect::new(2, 2, 16, 16),
radius: 4,
color: Color::GREEN,
});
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 20 * 4;
let idx = 10 * stride + 10 * 4;
assert_eq!(rgba[idx + 1], 255); }
#[test]
fn paint_backend_execute_draw_circle_stroke() {
let size = Size::new(20, 20);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawCircleStroke {
center: Point::new(10, 10),
radius: 5,
color: Color::RED,
width: 2,
});
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn paint_backend_measure_text_returns_metrics() {
let backend = SoftwarePaintBackend::new(Size::new(100, 100), 1.0);
let font = Font::simple("Arial", 12.0);
let metrics = backend.measure_text("Hello", &font);
assert!(metrics.width > 0);
assert!(metrics.height > 0);
}
#[test]
fn paint_backend_shape_text_returns_shaped() {
let backend = SoftwarePaintBackend::new(Size::new(100, 100), 1.0);
let font = Font::simple("Arial", 12.0);
let shaped = backend.shape_text("Hi", &font);
assert!(!shaped.clusters.is_empty());
}
#[test]
fn paint_backend_frame_rgba_after_clear() {
let size = Size::new(5, 5);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::rgb(128, 64, 32));
backend.end_frame();
let rgba = backend.frame_rgba();
let expected_len = 5 * 5 * 4;
assert_eq!(rgba.len(), expected_len);
assert_eq!(rgba[0], 128);
assert_eq!(rgba[1], 64);
assert_eq!(rgba[2], 32);
assert_eq!(rgba[3], 255);
}
#[test]
fn paint_backend_execute_fill_rect_out_of_bounds() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(100, 100, 50, 50),
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
for chunk in rgba.chunks(4) {
assert_eq!(chunk[0], 255);
assert_eq!(chunk[1], 255);
assert_eq!(chunk[2], 255);
}
}
#[test]
fn paint_backend_fill_rect_zero_size() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 0, 0),
color: Color::RED,
});
backend.end_frame();
let rgba = backend.frame_rgba();
for chunk in rgba.chunks(4) {
assert_eq!(chunk[0], 255);
}
}
#[test]
fn paint_backend_multiple_commands_in_frame() {
let size = Size::new(10, 10);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(0, 0, 5, 10),
color: Color::RED,
});
backend.execute_command(&RenderCommand::FillRect {
rect: Rect::new(5, 0, 5, 10),
color: Color::BLUE,
});
backend.end_frame();
let rgba = backend.frame_rgba();
let stride = 10 * 4;
let left_idx = 2 * stride + 2 * 4;
assert_eq!(rgba[left_idx], 255); assert_eq!(rgba[left_idx + 2], 0);
let right_idx = 5 * stride + 7 * 4;
assert_eq!(rgba[right_idx], 0); assert_eq!(rgba[right_idx + 2], 255); }
}