237 lines
8.1 KiB
Rust
237 lines
8.1 KiB
Rust
use crate::{
|
|
display_list::{Color, DisplayList, DrawCommand},
|
|
geometry::{Point, Rect},
|
|
text::{self, TextStyle},
|
|
};
|
|
|
|
pub struct SoftwareRenderer;
|
|
|
|
impl SoftwareRenderer {
|
|
pub fn render(list: &DisplayList, target: &mut [u32], width: u32, height: u32) {
|
|
let mut canvas = Canvas {
|
|
pixels: target,
|
|
width,
|
|
height,
|
|
clip: Rect::new(0.0, 0.0, width as f32, height as f32),
|
|
};
|
|
let mut clips = Vec::new();
|
|
|
|
for command in list.commands() {
|
|
match command {
|
|
DrawCommand::PushClip(rect) => {
|
|
clips.push(canvas.clip);
|
|
let x = canvas.clip.x.max(rect.x);
|
|
let y = canvas.clip.y.max(rect.y);
|
|
canvas.clip = Rect::new(
|
|
x,
|
|
y,
|
|
(canvas.clip.x + canvas.clip.width)
|
|
.min(rect.x + rect.width)
|
|
.max(x)
|
|
- x,
|
|
(canvas.clip.y + canvas.clip.height)
|
|
.min(rect.y + rect.height)
|
|
.max(y)
|
|
- y,
|
|
);
|
|
}
|
|
DrawCommand::PopClip => {
|
|
if let Some(clip) = clips.pop() {
|
|
canvas.clip = clip;
|
|
}
|
|
}
|
|
DrawCommand::Clear(color) => canvas.clear(*color),
|
|
DrawCommand::FillRoundedRect {
|
|
rect,
|
|
radius,
|
|
color,
|
|
} => canvas.fill_rounded_rect(*rect, *radius, *color),
|
|
DrawCommand::StrokeRoundedRect {
|
|
rect,
|
|
radius,
|
|
width,
|
|
color,
|
|
} => canvas.stroke_rounded_rect(*rect, *radius, *width, *color),
|
|
DrawCommand::Text {
|
|
origin,
|
|
value,
|
|
style,
|
|
color,
|
|
} => canvas.draw_text(*origin, value, *style, *color),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Canvas<'a> {
|
|
pixels: &'a mut [u32],
|
|
width: u32,
|
|
height: u32,
|
|
clip: Rect,
|
|
}
|
|
|
|
impl Canvas<'_> {
|
|
fn clear(&mut self, color: Color) {
|
|
self.pixels.fill(pack(color));
|
|
}
|
|
|
|
fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
|
|
let bounds = self.clamped_bounds(rect);
|
|
let radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0);
|
|
for y in bounds.1..bounds.3 {
|
|
for x in bounds.0..bounds.2 {
|
|
if rounded_rect_contains(rect, radius, x as f32 + 0.5, y as f32 + 0.5) {
|
|
self.blend_pixel(x, y, color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, width: f32, color: Color) {
|
|
let bounds = self.clamped_bounds(rect);
|
|
let width = width.max(1.0).min(rect.width / 2.0).min(rect.height / 2.0);
|
|
let outer_radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0);
|
|
let inner = Rect::new(
|
|
rect.x + width,
|
|
rect.y + width,
|
|
(rect.width - width * 2.0).max(0.0),
|
|
(rect.height - width * 2.0).max(0.0),
|
|
);
|
|
let inner_radius = (outer_radius - width).max(0.0);
|
|
|
|
for y in bounds.1..bounds.3 {
|
|
for x in bounds.0..bounds.2 {
|
|
let px = x as f32 + 0.5;
|
|
let py = y as f32 + 0.5;
|
|
if rounded_rect_contains(rect, outer_radius, px, py)
|
|
&& !rounded_rect_contains(inner, inner_radius, px, py)
|
|
{
|
|
self.blend_pixel(x, y, color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_text(&mut self, origin: Point, value: &str, style: TextStyle, color: Color) {
|
|
let x = origin.x.round() as i32;
|
|
let y = origin.y.round() as i32;
|
|
text::draw(value, style, color, |dx, dy, pixel| {
|
|
self.blend_pixel(x + dx, y + dy, pixel)
|
|
});
|
|
}
|
|
|
|
fn clamped_bounds(&self, rect: Rect) -> (i32, i32, i32, i32) {
|
|
(
|
|
rect.x.max(self.clip.x).max(0.0).floor() as i32,
|
|
rect.y.max(self.clip.y).max(0.0).floor() as i32,
|
|
(rect.x + rect.width)
|
|
.min(self.clip.x + self.clip.width)
|
|
.min(self.width as f32)
|
|
.ceil() as i32,
|
|
(rect.y + rect.height)
|
|
.min(self.clip.y + self.clip.height)
|
|
.min(self.height as f32)
|
|
.ceil() as i32,
|
|
)
|
|
}
|
|
|
|
fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
|
|
if x < 0
|
|
|| y < 0
|
|
|| x >= self.width as i32
|
|
|| y >= self.height as i32
|
|
|| source.alpha == 0
|
|
|| (x as f32) < self.clip.x
|
|
|| (y as f32) < self.clip.y
|
|
|| (x as f32) >= self.clip.x + self.clip.width
|
|
|| (y as f32) >= self.clip.y + self.clip.height
|
|
{
|
|
return;
|
|
}
|
|
|
|
let index = y as usize * self.width as usize + x as usize;
|
|
if source.alpha == 255 {
|
|
self.pixels[index] = pack(source);
|
|
return;
|
|
}
|
|
|
|
let destination = self.pixels[index];
|
|
let dst_red = (destination >> 16) & 0xff;
|
|
let dst_green = (destination >> 8) & 0xff;
|
|
let dst_blue = destination & 0xff;
|
|
let alpha = source.alpha as u32;
|
|
let inverse = 255 - alpha;
|
|
let red = (source.red as u32 * alpha + dst_red * inverse) / 255;
|
|
let green = (source.green as u32 * alpha + dst_green * inverse) / 255;
|
|
let blue = (source.blue as u32 * alpha + dst_blue * inverse) / 255;
|
|
self.pixels[index] = (red << 16) | (green << 8) | blue;
|
|
}
|
|
}
|
|
|
|
fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
|
|
if rect.width <= 0.0 || rect.height <= 0.0 {
|
|
return false;
|
|
}
|
|
if radius <= 0.0 {
|
|
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
}
|
|
|
|
let closest_x = x.clamp(rect.x + radius, rect.x + rect.width - radius);
|
|
let closest_y = y.clamp(rect.y + radius, rect.y + rect.height - radius);
|
|
let dx = x - closest_x;
|
|
let dy = y - closest_y;
|
|
dx * dx + dy * dy <= radius * radius
|
|
}
|
|
|
|
fn pack(color: Color) -> u32 {
|
|
(color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn renderer_changes_pixels_inside_a_rectangle() {
|
|
let mut list = DisplayList::default();
|
|
list.push(DrawCommand::Clear(Color::rgb(0, 0, 0)));
|
|
list.push(DrawCommand::FillRoundedRect {
|
|
rect: Rect::new(1.0, 1.0, 2.0, 2.0),
|
|
radius: 0.0,
|
|
color: Color::rgb(255, 0, 0),
|
|
});
|
|
let mut pixels = vec![0; 16];
|
|
SoftwareRenderer::render(&list, &mut pixels, 4, 4);
|
|
assert_eq!(pixels[5], 0x00ff0000);
|
|
assert_eq!(pixels[0], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn text_edges_are_antialiased_and_respect_opacity() {
|
|
let mut list = DisplayList::default();
|
|
list.push(DrawCommand::Clear(Color::rgb(0, 0, 0)));
|
|
list.push(DrawCommand::Text {
|
|
origin: Point::new(2.0, 2.0),
|
|
value: "Hello 中文".into(),
|
|
style: TextStyle::ui(1).scaled(1.25),
|
|
color: Color::rgba(255, 255, 255, 128),
|
|
});
|
|
let mut pixels = vec![0; 256 * 48];
|
|
SoftwareRenderer::render(&list, &mut pixels, 256, 48);
|
|
assert!(pixels.iter().any(|pixel| *pixel > 0));
|
|
assert!(pixels.iter().all(|pixel| (pixel & 255) <= 128));
|
|
assert!(pixels.iter().any(|pixel| (1..128).contains(&(pixel & 255))));
|
|
|
|
let mut invisible = DisplayList::default();
|
|
invisible.push(DrawCommand::Text {
|
|
origin: Point::new(2.0, 2.0),
|
|
value: "Hello 中文".into(),
|
|
style: TextStyle::ui(1),
|
|
color: Color::rgba(255, 255, 255, 0),
|
|
});
|
|
let mut pixels = vec![0; 256 * 48];
|
|
SoftwareRenderer::render(&invisible, &mut pixels, 256, 48);
|
|
assert!(pixels.iter().all(|pixel| *pixel == 0));
|
|
}
|
|
}
|