use alloc::vec::Vec;
use rlvgl_core::draw::draw_widget_bg;
use rlvgl_core::event::Event;
use rlvgl_core::image::{BlitOpts, ImageDescriptor};
use rlvgl_core::renderer::Renderer;
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Color, Rect, Widget};
pub struct PixelBuffer {
pixels: Vec<Color>,
width: u32,
height: u32,
}
impl PixelBuffer {
pub fn new(width: u32, height: u32) -> Self {
let count = (width as usize).saturating_mul(height as usize);
Self {
pixels: alloc::vec![Color(0, 0, 0, 255); count],
width,
height,
}
}
pub fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
if x < 0 || y < 0 {
return;
}
let (x, y) = (x as u32, y as u32);
if x >= self.width || y >= self.height {
return;
}
let idx = (y * self.width + x) as usize;
if let Some(slot) = self.pixels.get_mut(idx) {
*slot = color;
}
}
pub fn pixels(&self) -> Vec<Color> {
self.pixels.clone()
}
pub fn get_pixel(&self, x: u32, y: u32) -> Option<Color> {
if x >= self.width || y >= self.height {
return None;
}
self.pixels.get((y * self.width + x) as usize).copied()
}
}
pub struct CanvasWidget {
bounds: Rect,
pub style: Style,
inner: PixelBuffer,
pub dirty: bool,
}
impl CanvasWidget {
pub fn new(bounds: Rect, width: u32, height: u32) -> Self {
Self {
bounds,
style: Style::default(),
inner: PixelBuffer::new(width, height),
dirty: false,
}
}
pub fn inner(&self) -> &PixelBuffer {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut PixelBuffer {
&mut self.inner
}
pub fn canvas_size(&self) -> (u32, u32) {
self.inner.size()
}
pub fn fill(&mut self, color: Color) {
self.inner.pixels.fill(color);
self.dirty = true;
}
pub fn fill_rect(&mut self, rect: Rect, color: Color) {
let (w, h) = self.inner.size();
let x0 = rect.x.max(0) as u32;
let y0 = rect.y.max(0) as u32;
let x1 = (rect.x + rect.width).max(0).min(w as i32) as u32;
let y1 = (rect.y + rect.height).max(0).min(h as i32) as u32;
for y in y0..y1 {
for x in x0..x1 {
let idx = (y * w + x) as usize;
if let Some(slot) = self.inner.pixels.get_mut(idx) {
*slot = color;
}
}
}
self.dirty = true;
}
pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
self.inner.draw_pixel(x, y, color);
self.dirty = true;
}
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
let mut x = x0;
let mut y = y0;
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let sx: i32 = if x0 < x1 { 1 } else { -1 };
let sy: i32 = if y0 < y1 { 1 } else { -1 };
let mut err = dx - dy;
loop {
self.inner.draw_pixel(x, y, color);
if x == x1 && y == y1 {
break;
}
let e2 = 2 * err;
if e2 > -dy {
err -= dy;
x += sx;
}
if e2 < dx {
err += dx;
y += sy;
}
}
self.dirty = true;
}
pub fn as_image_descriptor(&self) -> ImageDescriptor<'_> {
let (w, h) = self.inner.size();
ImageDescriptor::from_color_slice(
&self.inner.pixels,
w.min(u16::MAX as u32) as u16,
h.min(u16::MAX as u32) as u16,
)
}
}
impl Widget for CanvasWidget {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
if self.inner.width == 0 || self.inner.height == 0 {
return;
}
let descriptor = self.as_image_descriptor();
renderer.blit_image(self.bounds, &descriptor, &BlitOpts::default());
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;
fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
struct BlitRecord {
dest: Rect,
width: u16,
height: u16,
}
struct RecordingRenderer {
blits: Vec<BlitRecord>,
fills: Vec<Rect>,
}
impl RecordingRenderer {
fn new() -> Self {
Self {
blits: Vec::new(),
fills: Vec::new(),
}
}
}
impl Renderer for RecordingRenderer {
fn fill_rect(&mut self, rect: Rect, _color: Color) {
self.fills.push(rect);
}
fn draw_text(&mut self, _position: (i32, i32), _text: &str, _color: Color) {}
fn blit_image(&mut self, dest: Rect, descriptor: &ImageDescriptor<'_>, _opts: &BlitOpts) {
self.blits.push(BlitRecord {
dest,
width: descriptor.width,
height: descriptor.height,
});
}
}
#[test]
fn pixel_set_and_readback_via_inner() {
let mut cw = CanvasWidget::new(rect(0, 0, 100, 100), 4, 4);
cw.draw_pixel(2, 3, Color(255, 0, 0, 255));
assert_eq!(
cw.inner().get_pixel(2, 3),
Some(Color(255, 0, 0, 255)),
"pixel should read back after draw_pixel"
);
}
#[test]
fn fill_sets_all_pixels() {
let mut cw = CanvasWidget::new(rect(0, 0, 100, 100), 2, 2);
cw.fill(Color(0, 128, 255, 255));
let pixels = cw.inner().pixels();
assert_eq!(pixels.len(), 4, "2×2 buffer has 4 pixels");
for px in pixels {
assert_eq!(px, Color(0, 128, 255, 255));
}
}
#[test]
fn fill_rect_sets_subregion() {
let mut cw = CanvasWidget::new(rect(0, 0, 100, 100), 4, 4);
cw.fill(Color(0, 0, 0, 255));
cw.fill_rect(rect(1, 1, 2, 2), Color(255, 255, 255, 255));
for &(x, y) in &[(1u32, 1u32), (2, 1), (1, 2), (2, 2)] {
assert_eq!(
cw.inner().get_pixel(x, y),
Some(Color(255, 255, 255, 255)),
"pixel ({x},{y}) should be white"
);
}
assert_eq!(cw.inner().get_pixel(0, 0), Some(Color(0, 0, 0, 255)));
}
#[test]
fn draw_blits_buffer_to_renderer() {
let cw = CanvasWidget::new(rect(10, 20, 80, 60), 8, 8);
let mut r = RecordingRenderer::new();
cw.draw(&mut r);
assert_eq!(r.blits.len(), 1, "exactly one blit_image call");
let b = &r.blits[0];
assert_eq!(
b.dest,
rect(10, 20, 80, 60),
"blit destination matches bounds"
);
assert_eq!(b.width, 8, "descriptor carries buffer width");
assert_eq!(b.height, 8, "descriptor carries buffer height");
}
#[test]
fn set_bounds_repositions_without_changing_buffer() {
let mut cw = CanvasWidget::new(rect(0, 0, 50, 50), 4, 4);
cw.set_bounds(rect(10, 20, 200, 100));
assert_eq!(cw.bounds(), rect(10, 20, 200, 100));
assert_eq!(
cw.canvas_size(),
(4, 4),
"buffer unchanged after set_bounds"
);
}
#[test]
fn dirty_flag_set_by_mutations() {
let mut cw = CanvasWidget::new(rect(0, 0, 10, 10), 2, 2);
assert!(!cw.dirty, "initially clean");
cw.fill(Color(0, 0, 0, 255));
assert!(cw.dirty, "fill marks dirty");
cw.dirty = false;
cw.draw_pixel(0, 0, Color(1, 2, 3, 255));
assert!(cw.dirty, "draw_pixel marks dirty");
cw.dirty = false;
cw.draw_line(0, 0, 1, 1, Color(5, 6, 7, 255));
assert!(cw.dirty, "draw_line marks dirty");
}
#[test]
fn as_image_descriptor_matches_buffer_dimensions() {
let cw = CanvasWidget::new(rect(0, 0, 50, 50), 16, 9);
let desc = cw.as_image_descriptor();
assert_eq!(desc.width, 16);
assert_eq!(desc.height, 9);
}
#[test]
fn draw_line_visits_endpoints() {
let mut cw = CanvasWidget::new(rect(0, 0, 10, 10), 10, 10);
cw.fill(Color(0, 0, 0, 255));
cw.draw_line(0, 0, 3, 3, Color(255, 255, 255, 255));
assert_eq!(cw.inner().get_pixel(0, 0), Some(Color(255, 255, 255, 255)));
assert_eq!(cw.inner().get_pixel(3, 3), Some(Color(255, 255, 255, 255)));
}
#[test]
fn out_of_bounds_draw_pixel_is_ignored() {
let mut cw = CanvasWidget::new(rect(0, 0, 10, 10), 2, 2);
cw.draw_pixel(100, 100, Color(255, 0, 0, 255));
cw.draw_pixel(-1, -1, Color(255, 0, 0, 255));
}
}