use super::*;
use crate::framebuffer::Framebuffer;
use crate::gfx_ctx::GfxCtx;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
fn font() -> Arc<Font> {
Arc::new(Font::from_slice(FONT_BYTES).expect("font"))
}
#[test]
fn test_lcd_gfx_ctx_basic_fill_text_smoke() {
let mut buf = LcdBuffer::new(80, 24);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.set_font(font());
ctx.set_font_size(16.0);
ctx.fill_text("ABC", 4.0, 14.0);
}
let any_dark = buf
.color_plane()
.chunks_exact(3)
.any(|p| p[0] < 250 || p[1] < 250 || p[2] < 250);
assert!(any_dark, "fill_text via LcdGfxCtx left buffer fully white");
}
#[test]
fn test_lcd_gfx_ctx_text_matches_legacy_lcd_mode() {
let f = font();
let w = 120u32;
let h = 28u32;
let mut fb = Framebuffer::new(w, h);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.set_lcd_mode(true);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.set_font(Arc::clone(&f));
ctx.set_font_size(18.0);
<GfxCtx as DrawCtx>::fill_text(&mut ctx, "Hello!", 4.0, 18.0);
}
let mut buf = LcdBuffer::new(w, h);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.set_font(Arc::clone(&f));
ctx.set_font_size(18.0);
ctx.fill_text("Hello!", 4.0, 18.0);
}
for y in 0..h as usize {
for x in 0..w as usize {
let ai = (y * w as usize + x) * 4;
let bi = (y * w as usize + x) * 3;
let a_rgb = (fb.pixels()[ai], fb.pixels()[ai + 1], fb.pixels()[ai + 2]);
let b_rgb = (
buf.color_plane()[bi],
buf.color_plane()[bi + 1],
buf.color_plane()[bi + 2],
);
assert_eq!(
a_rgb, b_rgb,
"pixel mismatch at ({x},{y}): legacy={a_rgb:?} LcdGfxCtx={b_rgb:?}"
);
}
}
}
#[test]
fn test_lcd_gfx_ctx_stroke_horizontal_line() {
let mut buf = LcdBuffer::new(20, 11);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_stroke_color(Color::black());
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.move_to(2.0, 5.0);
ctx.line_to(18.0, 5.0);
ctx.stroke();
}
let row_brightness = |y: usize| -> u32 {
(4..16)
.map(|x| {
let i = (y * 20 + x) * 3;
buf.color_plane()[i] as u32
+ buf.color_plane()[i + 1] as u32
+ buf.color_plane()[i + 2] as u32
})
.sum()
};
let line = row_brightness(5); let above = row_brightness(8);
let below = row_brightness(2);
assert!(
line < above,
"stroke row should be darker than row above (line={line}, above={above})"
);
assert!(
line < below,
"stroke row should be darker than row below (line={line}, below={below})"
);
}
#[test]
fn test_lcd_gfx_ctx_circle_darkens_center_not_corner() {
let mut buf = LcdBuffer::new(20, 20);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.begin_path();
ctx.circle(10.0, 10.0, 5.0);
ctx.fill();
}
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 20 + x) * 3;
(
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
)
};
let (cr, cg, cb) = pixel(10, 10);
assert!(
cr < 60 && cg < 60 && cb < 60,
"circle centre should be dark; got ({cr}, {cg}, {cb})"
);
let (xr, xg, xb) = pixel(1, 1);
assert!(
xr > 240 && xg > 240 && xb > 240,
"outside-circle corner should stay white; got ({xr}, {xg}, {xb})"
);
}
#[test]
fn test_lcd_gfx_ctx_rounded_rect_clips_corners() {
let mut buf = LcdBuffer::new(20, 20);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, 20.0, 20.0, 8.0);
ctx.fill();
}
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 20 + x) * 3;
(
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
)
};
let (cr, cg, cb) = pixel(10, 10);
assert!(
cr < 50 && cg < 50 && cb < 50,
"rounded rect centre should be dark; got ({cr}, {cg}, {cb})"
);
let (xr, xg, xb) = pixel(1, 1);
assert!(
xr > 240 && xg > 240 && xb > 240,
"rounded rect corner area should stay white; got ({xr}, {xg}, {xb})"
);
let (er, eg, eb) = pixel(10, 1);
assert!(
er < 50 && eg < 50 && eb < 50,
"rounded rect mid-edge should be dark; got ({er}, {eg}, {eb})"
);
}
#[test]
fn test_lcd_gfx_ctx_image_blit_y_flips_correctly() {
let img: Vec<u8> = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 128, 128, 128, 255,
];
let mut buf = LcdBuffer::new(8, 8);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::black());
ctx.draw_image_rgba(&img, 2, 2, 1.0, 1.0, 2.0, 2.0);
}
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 8 + x) * 3;
(
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
)
};
assert_eq!(
pixel(1, 2),
(255, 0, 0),
"top-left source must land at top-left of dst rect (Y-up high)"
);
assert_eq!(
pixel(2, 2),
(0, 255, 0),
"top-right source must land at top-right of dst rect"
);
assert_eq!(
pixel(1, 1),
(0, 0, 255),
"bottom-left source must land at bottom-left of dst rect (Y-up low)"
);
assert_eq!(
pixel(2, 1),
(128, 128, 128),
"bottom-right source must land at bottom-right of dst rect"
);
assert_eq!(
pixel(0, 0),
(0, 0, 0),
"pixel outside blit rect should be untouched"
);
}
#[test]
fn test_lcd_gfx_ctx_image_blit_alpha_blends_with_destination() {
let img: Vec<u8> = vec![255, 0, 0, 128];
let mut buf = LcdBuffer::new(4, 4);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.draw_image_rgba(&img, 1, 1, 1.0, 1.0, 1.0, 1.0);
}
let i = (1 * 4 + 1) * 3;
let (r, g, b) = (
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
);
assert!(r > 250, "R should be near 255 (bg + src red); got {r}");
assert!(
g > 120 && g < 140,
"G should be near 127 (white minus alpha-attenuated red); got {g}"
);
assert!(b > 120 && b < 140, "B should be near 127; got {b}");
}
#[test]
fn test_lcd_gfx_ctx_clip_rect_constrains_fill() {
let mut buf = LcdBuffer::new(20, 10);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.clip_rect(0.0, 0.0, 10.0, 10.0); ctx.begin_path();
ctx.rect(2.0, 2.0, 16.0, 6.0); ctx.fill();
}
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 20 + x) * 3;
(
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
)
};
let (lr, lg, lb) = pixel(5, 5);
assert!(
lr < 50 && lg < 50 && lb < 50,
"pixel inside clip + rect should be dark; got ({lr}, {lg}, {lb})"
);
let (rr, rg, rb) = pixel(15, 5);
assert!(
rr > 240 && rg > 240 && rb > 240,
"pixel outside clip should stay white; got ({rr}, {rg}, {rb})"
);
}
#[test]
fn test_lcd_gfx_ctx_clip_rect_constrains_fill_text() {
let mut buf = LcdBuffer::new(120, 24);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.set_font(font());
ctx.set_font_size(18.0);
ctx.clip_rect(0.0, 0.0, 40.0, 24.0); ctx.fill_text("MMMMMMMMMMMM", 2.0, 18.0);
}
let mut saw_dark_inside = false;
for x in 0..40 {
for y in 0..24 {
let i = (y * 120 + x) * 3;
if buf.color_plane()[i] < 100 {
saw_dark_inside = true;
break;
}
}
if saw_dark_inside {
break;
}
}
assert!(
saw_dark_inside,
"expected some dark text pixel inside the clip"
);
for x in 42..120 {
for y in 0..24 {
let i = (y * 120 + x) * 3;
let (r, g, b) = (
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
);
assert!(
r > 240 && g > 240 && b > 240,
"pixel at ({x},{y}) outside clip should stay white; got ({r}, {g}, {b})"
);
}
}
}
#[test]
fn test_lcd_gfx_ctx_clip_rect_constrains_image_blit() {
let img: Vec<u8> = (0..10 * 10).flat_map(|_| [255u8, 0, 0, 255]).collect();
let mut buf = LcdBuffer::new(20, 10);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.clip_rect(0.0, 0.0, 5.0, 10.0); ctx.draw_image_rgba(&img, 10, 10, 0.0, 0.0, 10.0, 10.0);
}
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 20 + x) * 3;
(
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
)
};
assert_eq!(
pixel(2, 5),
(255, 0, 0),
"inside clip should show source red"
);
assert_eq!(
pixel(7, 5),
(255, 255, 255),
"outside clip should stay white"
);
}
#[test]
fn test_lcd_gfx_ctx_reset_clip_restores_full_buffer() {
let mut buf = LcdBuffer::new(20, 10);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.clip_rect(0.0, 0.0, 5.0, 10.0);
ctx.reset_clip();
ctx.begin_path();
ctx.rect(2.0, 2.0, 16.0, 6.0); ctx.fill();
}
let i = (5 * 20 + 15) * 3;
let (r, g, b) = (
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
);
assert!(
r < 50 && g < 50 && b < 50,
"after reset_clip, fill at x=15 should be dark; got ({r}, {g}, {b})"
);
}
#[test]
fn test_lcd_gfx_ctx_clip_rect_nests_via_intersection() {
let mut buf = LcdBuffer::new(20, 20);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.clip_rect(0.0, 0.0, 10.0, 20.0);
ctx.clip_rect(0.0, 10.0, 20.0, 10.0);
ctx.begin_path();
ctx.rect(0.0, 0.0, 20.0, 20.0); ctx.fill();
}
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 20 + x) * 3;
(
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
)
};
let (tlr, tlg, tlb) = pixel(2, 17);
assert!(
tlr < 50 && tlg < 50 && tlb < 50,
"top-left should be dark; got ({tlr}, {tlg}, {tlb})"
);
let (trr, trg, trb) = pixel(17, 17);
assert!(
trr > 240 && trg > 240 && trb > 240,
"top-right should stay white; got ({trr}, {trg}, {trb})"
);
let (blr, blg, blb) = pixel(2, 2);
assert!(
blr > 240 && blg > 240 && blb > 240,
"bottom-left should stay white; got ({blr}, {blg}, {blb})"
);
}
#[test]
fn test_lcd_gfx_ctx_push_pop_layer_flushes_into_parent() {
let mut buf = LcdBuffer::new(20, 20);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.translate(5.0, 5.0);
ctx.push_layer(8.0, 8.0);
ctx.set_fill_color(Color::black());
ctx.begin_path();
ctx.rect(0.0, 0.0, 8.0, 8.0); ctx.fill();
ctx.pop_layer();
}
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 20 + x) * 3;
(
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
)
};
assert_eq!(
pixel(8, 8),
(0, 0, 0),
"interior of flushed layer should be dark"
);
assert_eq!(
pixel(2, 2),
(255, 255, 255),
"outside layer region should stay white"
);
assert_eq!(
pixel(15, 15),
(255, 255, 255),
"outside layer region should stay white"
);
}
#[test]
fn test_lcd_gfx_ctx_push_pop_layer_restores_state() {
let mut buf = LcdBuffer::new(20, 20);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.set_fill_color(Color::white()); ctx.translate(3.0, 4.0);
assert_eq!((ctx.transform().tx, ctx.transform().ty), (3.0, 4.0));
ctx.push_layer(10.0, 10.0);
assert_eq!(
(ctx.transform().tx, ctx.transform().ty),
(0.0, 0.0),
"push_layer must reset transform inside the layer"
);
ctx.set_fill_color(Color::rgba(0.1, 0.2, 0.3, 1.0));
ctx.translate(1.0, 1.0);
ctx.pop_layer();
assert_eq!(
(ctx.transform().tx, ctx.transform().ty),
(3.0, 4.0),
"pop_layer must restore transform to its push-time value"
);
ctx.begin_path();
ctx.rect(0.0, 0.0, 4.0, 4.0);
ctx.fill();
}
let i = (5 * 20 + 5) * 3;
let (r, g, b) = (
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
);
assert_eq!(
(r, g, b),
(255, 255, 255),
"post-pop fill must use restored white colour"
);
}
#[test]
fn test_lcd_gfx_ctx_push_layer_isolates_paint_until_pop() {
let mut buf = LcdBuffer::new(20, 20);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.push_layer(10.0, 10.0);
ctx.set_fill_color(Color::black());
ctx.begin_path();
ctx.rect(0.0, 0.0, 10.0, 10.0);
ctx.fill();
let base = ctx.buffer();
assert!(
base.color_plane()
.chunks_exact(3)
.all(|p| p[0] == 255 && p[1] == 255 && p[2] == 255),
"base buffer must not see layer paint until pop_layer"
);
ctx.pop_layer();
}
let i = (5 * 20 + 5) * 3;
let (r, g, b) = (
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
);
assert_eq!(
(r, g, b),
(0, 0, 0),
"after pop_layer, painted pixels should appear in base"
);
}
#[test]
fn test_lcd_gfx_ctx_push_layer_nests() {
let mut buf = LcdBuffer::new(30, 30);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.translate(2.0, 2.0);
ctx.push_layer(20.0, 20.0); ctx.set_fill_color(Color::black());
ctx.translate(4.0, 4.0);
ctx.push_layer(8.0, 8.0); ctx.begin_path();
ctx.rect(0.0, 0.0, 8.0, 8.0);
ctx.fill();
ctx.pop_layer();
ctx.pop_layer(); }
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 30 + x) * 3;
(
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
)
};
assert_eq!(
pixel(10, 10),
(0, 0, 0),
"centre of nested layer region should be dark"
);
assert_eq!(
pixel(2, 2),
(255, 255, 255),
"well outside nested region should stay white"
);
assert_eq!(
pixel(20, 20),
(255, 255, 255),
"well outside nested region should stay white"
);
}
#[test]
fn test_lcd_gfx_ctx_unmatched_pop_layer_is_noop() {
let mut buf = LcdBuffer::new(8, 8);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
ctx.pop_layer(); ctx.set_fill_color(Color::black());
ctx.begin_path();
ctx.rect(0.0, 0.0, 8.0, 8.0);
ctx.fill();
}
let i = (4 * 8 + 4) * 3;
let (r, g, b) = (
buf.color_plane()[i],
buf.color_plane()[i + 1],
buf.color_plane()[i + 2],
);
assert_eq!(
(r, g, b),
(0, 0, 0),
"subsequent paint after unmatched pop should still work"
);
}
#[test]
fn test_lcd_gfx_ctx_fill_text_honours_translation() {
let f = font();
let w = 100u32;
let h = 24u32;
let mut buf_a = LcdBuffer::new(w, h);
{
let mut ctx = LcdGfxCtx::new(&mut buf_a);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.set_font(Arc::clone(&f));
ctx.set_font_size(16.0);
ctx.translate(10.0, 4.0);
ctx.fill_text("Hi", 0.0, 12.0);
}
let mut buf_b = LcdBuffer::new(w, h);
{
let mut ctx = LcdGfxCtx::new(&mut buf_b);
ctx.clear(Color::white());
ctx.set_fill_color(Color::black());
ctx.set_font(f);
ctx.set_font_size(16.0);
ctx.fill_text("Hi", 10.0, 16.0);
}
assert_eq!(
buf_a.color_plane(),
buf_b.color_plane(),
"translate(10,4) + fill_text(0,12) must equal fill_text(10,16)"
);
}