use super::*;
#[test]
fn test_lcd_mask_rounds_fractional_dst_to_pixel_grid() {
use crate::DrawCtx;
let mask: Vec<u8> = vec![
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
let draw = |dst_x: f64, dst_y: f64| -> Framebuffer {
let mut fb = Framebuffer::new(8, 8);
for p in fb.pixels_mut().chunks_exact_mut(4) {
p[0] = 255;
p[1] = 255;
p[2] = 255;
p[3] = 255;
}
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.draw_lcd_mask(&mask, 3, 3, Color::black(), dst_x, dst_y);
}
fb
};
let integer = draw(2.0, 2.0);
let fractional = draw(2.4, 2.4); let fractional2 = draw(1.6, 1.6); assert_eq!(
integer.pixels(),
fractional.pixels(),
"LCD mask at fractional dst (2.4, 2.4) must round to integer grid"
);
assert_eq!(
integer.pixels(),
fractional2.pixels(),
"LCD mask at fractional dst (1.6, 1.6) must round to integer grid"
);
let shifted = draw(3.0, 2.0);
assert_ne!(
integer.pixels(),
shifted.pixels(),
"integer-pixel shift should change output — otherwise the rounding test is vacuous"
);
}
#[test]
fn test_paint_subtree_backbuffered_lcd_coverage_routes_through_lcd_pipeline() {
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::framebuffer::Framebuffer;
use crate::geometry::{Rect, Size};
use crate::gfx_ctx::GfxCtx;
use crate::text::Font;
use crate::widget::{paint_subtree, BackbufferCache, BackbufferMode, Widget};
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../../demo/assets/CascadiaCode.ttf");
struct LcdTestWidget {
bounds: Rect,
cache: BackbufferCache,
font: Arc<Font>,
children: Vec<Box<dyn Widget>>,
}
impl Widget for LcdTestWidget {
fn type_name(&self) -> &'static str {
"LcdTestWidget"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, available: Size) -> Size {
available
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
ctx.set_fill_color(Color::white());
ctx.begin_path();
ctx.rect(0.0, 0.0, self.bounds.width, self.bounds.height);
ctx.fill();
ctx.set_fill_color(Color::black());
ctx.set_font(Arc::clone(&self.font));
ctx.set_font_size(18.0);
ctx.fill_text("abc", 4.0, 16.0);
}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
Some(&mut self.cache)
}
fn backbuffer_mode(&self) -> BackbufferMode {
BackbufferMode::LcdCoverage
}
}
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let mut widget = LcdTestWidget {
bounds: Rect::new(0.0, 0.0, 60.0, 24.0),
cache: BackbufferCache::default(),
font,
children: Vec::new(),
};
widget.cache.invalidate();
let mut fb = Framebuffer::new(60, 24);
{
let mut ctx = GfxCtx::new(&mut fb);
paint_subtree(&mut widget, &mut ctx);
}
let cache = widget.backbuffer_cache_mut().unwrap();
let color = cache
.pixels
.as_ref()
.expect("colour plane must be populated");
let alpha = cache
.lcd_alpha
.as_ref()
.expect("LcdCoverage mode must populate lcd_alpha");
assert_eq!(cache.width, 60);
assert_eq!(cache.height, 24);
assert_eq!(color.len(), 60 * 24 * 3, "colour plane is 3 bytes/pixel");
assert_eq!(alpha.len(), 60 * 24 * 3, "alpha plane is 3 bytes/pixel");
let mut saw_chroma = false;
for px in alpha.chunks_exact(3) {
let (r, g, b) = (px[0] as i32, px[1] as i32, px[2] as i32);
let mx = r.max(g).max(b);
let mn = r.min(g).min(b);
if mx > 30 && (mx - mn) > 10 {
saw_chroma = true;
break;
}
}
assert!(saw_chroma,
"cached alpha plane must show per-channel variation — proves LcdGfxCtx, not GfxCtx, painted");
let fully_covered = alpha
.chunks_exact(3)
.filter(|px| px[0] == 255 && px[1] == 255 && px[2] == 255)
.count();
assert!(
fully_covered > 60 * 24 / 2,
"more than half of cached pixels should have full per-channel alpha \
(opaque-bg widget); got {fully_covered} of {}",
60 * 24
);
}
#[test]
fn test_paint_subtree_backbuffered_rgba_mode_unchanged() {
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::framebuffer::Framebuffer;
use crate::geometry::{Rect, Size};
use crate::gfx_ctx::GfxCtx;
use crate::text::Font;
use crate::widget::{paint_subtree, BackbufferCache, BackbufferMode, Widget};
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../../demo/assets/CascadiaCode.ttf");
struct RgbaTestWidget {
bounds: Rect,
cache: BackbufferCache,
font: Arc<Font>,
children: Vec<Box<dyn Widget>>,
}
impl Widget for RgbaTestWidget {
fn type_name(&self) -> &'static str {
"RgbaTestWidget"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, available: Size) -> Size {
available
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
ctx.set_fill_color(Color::black());
ctx.set_font(Arc::clone(&self.font));
ctx.set_font_size(18.0);
ctx.fill_text("abc", 4.0, 16.0);
}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
Some(&mut self.cache)
}
fn backbuffer_mode(&self) -> BackbufferMode {
BackbufferMode::Rgba
}
}
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let mut widget = RgbaTestWidget {
bounds: Rect::new(0.0, 0.0, 60.0, 24.0),
cache: BackbufferCache::default(),
font,
children: Vec::new(),
};
widget.cache.invalidate();
let mut fb = Framebuffer::new(60, 24);
{
let mut ctx = GfxCtx::new(&mut fb);
paint_subtree(&mut widget, &mut ctx);
}
let cache = widget.backbuffer_cache_mut().unwrap();
let bmp = cache
.pixels
.as_ref()
.expect("backbuffer cache must be populated");
for (i, px) in bmp.chunks_exact(4).enumerate() {
let (r, g, b) = (px[0], px[1], px[2]);
assert!(
r == g && g == b,
"Rgba mode must produce grayscale pixels (R==G==B); pixel {i} = ({r}, {g}, {b})"
);
}
}
#[test]
fn test_gfx_ctx_draw_lcd_backbuffer_arc_preserves_per_channel_chroma() {
use crate::draw_ctx::DrawCtx;
use std::sync::Arc;
let color = Arc::new(vec![0u8, 0, 0]);
let alpha = Arc::new(vec![50u8, 100, 200]);
let mut fb = Framebuffer::new(1, 1);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.set_fill_color(Color::rgba(1.0, 1.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 1.0, 1.0);
ctx.fill();
ctx.draw_lcd_backbuffer_arc(&color, &alpha, 1, 1, 0.0, 0.0, 1.0, 1.0);
}
let r = fb.pixels()[0];
let g = fb.pixels()[1];
let b = fb.pixels()[2];
assert!(
(r as i32 - 205).abs() <= 1,
"R should be ~205 (255-50), got {r}"
);
assert!(
(g as i32 - 155).abs() <= 1,
"G should be ~155 (255-100), got {g}"
);
assert!(
(b as i32 - 55).abs() <= 1,
"B should be ~55 (255-200), got {b}"
);
let mx = r.max(g).max(b);
let mn = r.min(g).min(b);
assert!(
(mx - mn) > 100,
"per-channel blit must preserve chroma spread; got R={r} G={g} B={b}"
);
}
#[test]
fn test_paint_subtree_backbuffered_lcd_cache_preserves_chroma_at_destination() {
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::{Rect, Size};
use crate::text::Font;
use crate::widget::{paint_subtree, BackbufferCache, BackbufferMode, Widget};
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../../demo/assets/CascadiaCode.ttf");
struct LcdW {
bounds: Rect,
cache: BackbufferCache,
font: Arc<Font>,
children: Vec<Box<dyn Widget>>,
}
impl Widget for LcdW {
fn type_name(&self) -> &'static str {
"LcdW"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, available: Size) -> Size {
available
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
ctx.set_fill_color(Color::white());
ctx.begin_path();
ctx.rect(0.0, 0.0, self.bounds.width, self.bounds.height);
ctx.fill();
ctx.set_fill_color(Color::black());
ctx.set_font(Arc::clone(&self.font));
ctx.set_font_size(22.0);
ctx.fill_text("Wing", 4.0, 20.0);
}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
Some(&mut self.cache)
}
fn backbuffer_mode(&self) -> BackbufferMode {
BackbufferMode::LcdCoverage
}
}
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let mut widget = LcdW {
bounds: Rect::new(0.0, 0.0, 100.0, 30.0),
cache: BackbufferCache::default(),
font,
children: Vec::new(),
};
widget.cache.invalidate();
let mut fb = Framebuffer::new(100, 30);
{
let mut ctx = GfxCtx::new(&mut fb);
paint_subtree(&mut widget, &mut ctx);
}
let w = 100usize;
let h = 30usize;
let mut saw_chroma = false;
for y in 0..h {
for x in 0..w {
let i = (y * w + x) * 4;
let r = fb.pixels()[i] as i32;
let g = fb.pixels()[i + 1] as i32;
let b = fb.pixels()[i + 2] as i32;
let mx = r.max(g).max(b);
let mn = r.min(g).min(b);
if mx > 30 && mn < 230 && (mx - mn) > 15 {
saw_chroma = true;
break;
}
}
if saw_chroma {
break;
}
}
assert!(
saw_chroma,
"LcdCoverage cache + draw_lcd_backbuffer_arc blit must land per-channel \
chroma in the destination framebuffer — proves chroma survived the cache"
);
}