// Pixel-level "golden-like" coverage. We deliberately avoid storing reference // PNG bytes: their value depends on fontdue / tiny-skia / theme versions, so a // byte-exact reference would break on every dependency bump. Instead these // tests assert *structural* properties of the rendered buffer (background // colour, transparency, determinism, focus-state divergence) plus a few // canonical scenes that exercise the partial-redraw and full-redraw paths. use ltk::core::{ Canvas, RenderOptions, UiSurface }; use ltk::{ button, column, container, spacer, text, Color, Element }; // ── helpers ─────────────────────────────────────────────────────────────────── fn extract_pixels( surface: &UiSurface<()>, w: u32, h: u32 ) -> Vec { let mut buf = vec![ 0u8; ( w * h * 4 ) as usize ]; // Software canvas only — `Canvas::Gles::write_to_wayland_buf` is a no-op // (presentation goes through `eglSwapBuffers` elsewhere). UiSurface::new // returns a software surface. assert!( matches!( surface.canvas(), Canvas::Software( _ ) ) ); surface.canvas().write_to_wayland_buf( &mut buf, false ); buf } fn fnv1a_64( bytes: &[u8] ) -> u64 { let mut h: u64 = 0xcbf29ce484222325; for &b in bytes { h ^= b as u64; h = h.wrapping_mul( 0x100000001b3 ); } h } // Approximate equality on a single u8 channel — accounts for AA / rounding // drift on the boundary of background-vs-content fills. fn near( a: u8, b: u8, tol: u8 ) -> bool { a.abs_diff( b ) <= tol } // ── background colour ───────────────────────────────────────────────────────── #[ test ] fn fully_transparent_background_yields_all_zero_alpha() { let mut surface = UiSurface::<()>::new( 64, 64 ); let view: Element<()> = column().into(); let _ = surface.render( &view, RenderOptions::full_canvas( 64, 64 ).background( Color::TRANSPARENT ), ); let buf = extract_pixels( &surface, 64, 64 ); for chunk in buf.chunks_exact( 4 ) { // Every pixel ends up with alpha = 0; RGB after premultiplication is // also 0 because the background fill is the only paint. assert_eq!( chunk[ 3 ], 0, "transparent bg must produce zero alpha" ); } } #[ test ] fn opaque_background_paints_canonical_rgb_in_corner_pixel() { let mut surface = UiSurface::<()>::new( 64, 64 ); let view: Element<()> = column().into(); let _ = surface.render( &view, RenderOptions::full_canvas( 64, 64 ).background( Color::rgb( 1.0, 0.25, 0.0 ) ), ); let buf = extract_pixels( &surface, 64, 64 ); // Top-left pixel: nothing draws there beyond the background fill, so the // channels should match (255, ~64, 0, 255) with mild rounding tolerance. assert!( near( buf[ 0 ], 255, 1 ) ); assert!( near( buf[ 1 ], 64, 2 ) ); assert!( near( buf[ 2 ], 0, 1 ) ); assert_eq!( buf[ 3 ], 255 ); } #[ test ] fn background_fills_every_pixel_when_view_is_empty() { let mut surface = UiSurface::<()>::new( 32, 32 ); let view: Element<()> = column().into(); let _ = surface.render( &view, RenderOptions::full_canvas( 32, 32 ).background( Color::rgb( 0.0, 0.5, 0.0 ) ), ); let buf = extract_pixels( &surface, 32, 32 ); for chunk in buf.chunks_exact( 4 ) { assert!( near( chunk[ 0 ], 0, 1 ) ); assert!( near( chunk[ 1 ], 128, 2 ) ); assert!( near( chunk[ 2 ], 0, 1 ) ); assert_eq!( chunk[ 3 ], 255 ); } } // ── determinism ─────────────────────────────────────────────────────────────── fn counter_view( count: u32 ) -> Element<()> { column::<()>() .padding( 16.0 ) .spacing( 8.0 ) .push( text( format!( "Count: {count}" ) ) ) .push( button( "Increment" ) ) .into() } #[ test ] fn rendering_the_same_view_twice_produces_identical_pixel_buffers() { let mut surface = UiSurface::<()>::new( 240, 120 ); let opts = RenderOptions::full_canvas( 240, 120 ).background( Color::rgb( 0.05, 0.05, 0.05 ) ); let _ = surface.render( &counter_view( 0 ), opts ); let buf_a = extract_pixels( &surface, 240, 120 ); // Force a clean re-paint via mark_content_dirty so both passes hit the // same code path (full redraw rather than partial-damage clip). surface.mark_content_dirty(); let _ = surface.render( &counter_view( 0 ), opts ); let buf_b = extract_pixels( &surface, 240, 120 ); assert_eq!( buf_a, buf_b, "two identical renders must produce identical pixels" ); } #[ test ] fn render_hash_is_stable_across_two_back_to_back_passes() { let mut surface = UiSurface::<()>::new( 320, 200 ); let opts = RenderOptions::full_canvas( 320, 200 ).background( Color::rgb( 0.1, 0.1, 0.1 ) ); let _ = surface.render( &counter_view( 7 ), opts ); let h1 = fnv1a_64( &extract_pixels( &surface, 320, 200 ) ); surface.mark_content_dirty(); let _ = surface.render( &counter_view( 7 ), opts ); let h2 = fnv1a_64( &extract_pixels( &surface, 320, 200 ) ); assert_eq!( h1, h2, "a deterministic render pass must produce a stable hash" ); } // ── content presence ────────────────────────────────────────────────────────── #[ test ] fn rendering_a_button_produces_pixels_that_differ_from_the_background() { let mut surface = UiSurface::<()>::new( 240, 120 ); let bg = Color::rgb( 0.0, 0.0, 0.0 ); let view: Element<()> = column::<()>().push( button( "tap" ) ).into(); let _ = surface.render( &view, RenderOptions::full_canvas( 240, 120 ).background( bg ), ); let buf = extract_pixels( &surface, 240, 120 ); let differs = buf.chunks_exact( 4 ).any( |chunk| !( chunk[ 0 ] == 0 && chunk[ 1 ] == 0 && chunk[ 2 ] == 0 && chunk[ 3 ] == 255 ) ); assert!( differs, "a button must paint at least one non-background pixel" ); } #[ test ] fn rendering_text_produces_pixels_that_differ_from_the_background() { let mut surface = UiSurface::<()>::new( 240, 120 ); let bg = Color::rgb( 0.0, 0.0, 0.0 ); let view: Element<()> = column::<()>().push( text( "ltk" ).size( 32.0 ) ).into(); let _ = surface.render( &view, RenderOptions::full_canvas( 240, 120 ).background( bg ), ); let buf = extract_pixels( &surface, 240, 120 ); let glyph_pixels = buf.chunks_exact( 4 ) .filter( |chunk| !( chunk[ 0 ] == 0 && chunk[ 1 ] == 0 && chunk[ 2 ] == 0 ) ) .count(); assert!( glyph_pixels > 0, "text rasterisation must produce non-bg pixels" ); } #[ test ] fn container_with_explicit_color_fill_paints_inside_padding() { // Fill a sized container with a known colour, render on a black // background, and confirm a measurable region of red pixels appears. let mut surface = UiSurface::<()>::new( 100, 100 ); let view: Element<()> = column::<()>() .padding( 0.0 ) .push( container::<()>( spacer().width( 40.0 ).height( 40.0 ) ) .background( Color::rgb( 1.0, 0.0, 0.0 ) ), ) .into(); let _ = surface.render( &view, RenderOptions::full_canvas( 100, 100 ).background( Color::BLACK ), ); let bytes = extract_pixels( &surface, 100, 100 ); let red_pixels = bytes.chunks_exact( 4 ) .filter( |c| c[ 0 ] > 200 && c[ 1 ] < 30 && c[ 2 ] < 30 && c[ 3 ] == 255 ) .count(); // A 40×40 fill produces 1600 fully-saturated red pixels; allow a wide // floor to absorb anti-aliased edge pixels that mix with the background. assert!( red_pixels > 500, "container with red fill must produce a substantial red region (got {red_pixels})", ); } // ── focus-state divergence ──────────────────────────────────────────────────── #[ test ] fn focusing_a_button_changes_pixels_inside_its_paint_rect() { // The focus ring is one of the cheapest ways to verify the partial- // redraw path actually paints something different. Render with no focus, // snapshot the pixels, set focus, render again, and confirm the buffer // has changed somewhere. let mut surface = UiSurface::<()>::new( 200, 80 ); let opts = RenderOptions::full_canvas( 200, 80 ).background( Color::rgb( 0.05, 0.05, 0.05 ) ); let view: Element<()> = column::<()>().push( button( "tap" ) ).into(); let _ = surface.render( &view, opts ); let unfocused = extract_pixels( &surface, 200, 80 ); let idx = surface.widget_rects()[ 0 ].flat_idx; surface.set_focused( Some( idx ) ); let _ = surface.render( &view, opts ); let focused = extract_pixels( &surface, 200, 80 ); assert_ne!( unfocused, focused, "focus state change must produce visibly different pixels", ); } // ── invariants ──────────────────────────────────────────────────────────────── #[ test ] fn pixel_buffer_length_matches_canvas_size() { let surface = UiSurface::<()>::new( 100, 50 ); let mut buf = vec![ 0u8; 100 * 50 * 4 ]; surface.canvas().write_to_wayland_buf( &mut buf, false ); // All pixels are zero before render — confirms the buffer is correctly // sized and write_to_wayland_buf doesn't overwrite past the end. assert!( buf.iter().all( |&b| b == 0 ) ); } #[ test ] fn resize_changes_the_required_buffer_length() { let mut surface = UiSurface::<()>::new( 64, 64 ); let view: Element<()> = column().into(); let opts = RenderOptions::full_canvas( 64, 64 ); let _ = surface.render( &view, opts ); surface.resize( 128, 96 ); assert_eq!( surface.size(), ( 128, 96 ) ); // Old buffer would be too short; the new size dictates a 49152-byte // buffer. Confirm `write_to_wayland_buf` accepts and fills it without // panicking. let mut new_buf = vec![ 0u8; 128 * 96 * 4 ]; let _ = surface.render( &view, RenderOptions::full_canvas( 128, 96 ).background( Color::rgb( 0.0, 0.0, 0.5 ) ), ); surface.canvas().write_to_wayland_buf( &mut new_buf, false ); // Last pixel should carry the navy bg. let last = &new_buf[ new_buf.len() - 4 .. ]; assert!( near( last[ 2 ], 128, 2 ), "last pixel blue channel ≈ 128" ); assert_eq!( last[ 3 ], 255 ); } // ── canonical scene byte equality ───────────────────────────────────────────── #[ test ] fn canonical_solid_red_scene_has_exact_byte_layout() { // A scene with NO text (so no font-version drift) and a colour with NO // rounding ambiguity (1.0 → 255, 0.0 → 0). The buffer is portable // across machines because the path is plain rectangle clear → tiny-skia // premul → byte copy, and the values land exactly. If this assertion // drifts, either the renderer or the buffer format changed; investigate // before regenerating. let mut surface = UiSurface::<()>::new( 16, 16 ); let view: Element<()> = column().into(); let _ = surface.render( &view, RenderOptions::full_canvas( 16, 16 ).background( Color::rgb( 1.0, 0.0, 0.0 ) ), ); let actual = extract_pixels( &surface, 16, 16 ); let expected: Vec = ( 0..16 * 16 ).flat_map( |_| [ 255u8, 0, 0, 255 ] ).collect(); assert_eq!( actual, expected, "16×16 solid red must serialise to exact RGBA bytes" ); } #[ test ] fn fnv1a_hash_changes_when_a_single_byte_flips() { // Sanity check on the in-test hash function itself — guards the rest of // the determinism tests against a regression that breaks the hash but // leaves the assertions trivially passing. let a = [ 0u8, 0, 0, 0 ]; let mut b = a; b[ 2 ] = 1; assert_ne!( fnv1a_64( &a ), fnv1a_64( &b ) ); }