use oxiui_core::geometry::{Point, Rect};
use oxiui_core::paint::RenderBackend;
use oxiui_core::{Color, DrawList, UiError};
use oxiui_render_wgpu::WgpuBackend;
fn try_backend(width: u32, height: u32) -> Option<WgpuBackend> {
match WgpuBackend::headless(width, height) {
Ok(b) => Some(b),
Err(UiError::Unsupported(msg)) => {
println!("skip: no GPU adapter ({msg})");
None
}
Err(e) => {
panic!("unexpected headless init error: {e}");
}
}
}
const CLEAR: Color = Color(0, 0, 0, 255); const RED: Color = Color(255, 0, 0, 255);
const GREEN: Color = Color(0, 255, 0, 255);
fn pixel(buf: &[u8], width: u32, x: u32, y: u32) -> (u8, u8, u8, u8) {
let idx = ((y * width + x) * 4) as usize;
(buf[idx], buf[idx + 1], buf[idx + 2], buf[idx + 3])
}
#[test]
fn readback_is_tightly_packed() {
let backend = match try_backend(40, 24) {
Some(b) => b,
None => return,
};
let buf = backend.readback_rgba().expect("readback must succeed");
assert_eq!(
buf.len(),
(40 * 24 * 4) as usize,
"readback must be tightly packed width*height*4 with row padding stripped"
);
}
#[test]
fn surface_size_reports_dimensions() {
let backend = match try_backend(64, 48) {
Some(b) => b,
None => return,
};
let sz = backend.surface_size();
assert!((sz.width - 64.0).abs() < f32::EPSILON);
assert!((sz.height - 48.0).abs() < f32::EPSILON);
assert_eq!(backend.width(), 64);
assert_eq!(backend.height(), 48);
}
#[test]
fn empty_list_clears_to_clear_color() {
let mut backend = match try_backend(16, 16) {
Some(b) => b,
None => return,
};
backend.set_clear_color(CLEAR);
let list = DrawList::new();
backend.execute(&list).expect("execute must succeed");
let buf = backend.readback_rgba().expect("readback");
for y in 0..16 {
for x in 0..16 {
assert_eq!(
pixel(&buf, 16, x, y),
(0, 0, 0, 255),
"pixel ({x},{y}) should be the clear colour"
);
}
}
}
#[test]
fn fill_rect_paints_interior_and_leaves_corner_clear() {
let mut backend = match try_backend(64, 64) {
Some(b) => b,
None => return,
};
backend.set_clear_color(CLEAR);
let mut list = DrawList::new();
list.push_rect(Rect::new(10.0, 10.0, 20.0, 20.0), RED);
backend.execute(&list).expect("execute");
let buf = backend.readback_rgba().expect("readback");
assert_eq!(
pixel(&buf, 64, 15, 15),
(255, 0, 0, 255),
"centre of FillRect must be solid red"
);
assert_eq!(pixel(&buf, 64, 11, 11), (255, 0, 0, 255));
assert_eq!(pixel(&buf, 64, 28, 28), (255, 0, 0, 255));
assert_eq!(
pixel(&buf, 64, 0, 0),
(0, 0, 0, 255),
"corner outside the rect must remain the clear colour"
);
assert_eq!(pixel(&buf, 64, 40, 15), (0, 0, 0, 255));
}
#[test]
fn fill_circle_fills_center_and_clears_outside_radius() {
let mut backend = match try_backend(64, 64) {
Some(b) => b,
None => return,
};
backend.set_clear_color(CLEAR);
let mut list = DrawList::new();
list.push_circle(Point::new(32.0, 32.0), 15.0, GREEN);
backend.execute(&list).expect("execute");
let buf = backend.readback_rgba().expect("readback");
assert_eq!(
pixel(&buf, 64, 32, 32),
(0, 255, 0, 255),
"circle centre must be solid green"
);
let (r_in, g_in, _b_in, a_in) = pixel(&buf, 64, 32 + 8, 32);
assert!(
g_in > 200 && r_in < 40 && a_in > 200,
"point inside the circle should be green (got r={r_in}, g={g_in}, a={a_in})"
);
assert_eq!(
pixel(&buf, 64, 2, 2),
(0, 0, 0, 255),
"corner outside the circle radius must remain the clear colour"
);
assert_eq!(
pixel(&buf, 64, 52, 32),
(0, 0, 0, 255),
"point beyond the circle radius must be the clear colour"
);
}
#[test]
fn clip_restricts_fill_to_clip_rect() {
let mut backend = match try_backend(64, 64) {
Some(b) => b,
None => return,
};
backend.set_clear_color(CLEAR);
let mut list = DrawList::new();
list.push_clip(Rect::new(0.0, 0.0, 32.0, 64.0));
list.push_rect(Rect::new(0.0, 0.0, 64.0, 64.0), RED);
list.pop_clip();
backend.execute(&list).expect("execute");
let buf = backend.readback_rgba().expect("readback");
assert_eq!(
pixel(&buf, 64, 10, 32),
(255, 0, 0, 255),
"pixel inside the clip rect must be red"
);
assert_eq!(
pixel(&buf, 64, 50, 32),
(0, 0, 0, 255),
"pixel outside the clip rect must remain the clear colour"
);
}
#[test]
fn nested_clip_intersects() {
let mut backend = match try_backend(64, 64) {
Some(b) => b,
None => return,
};
backend.set_clear_color(CLEAR);
let mut list = DrawList::new();
list.push_clip(Rect::new(0.0, 0.0, 40.0, 40.0));
list.push_clip(Rect::new(20.0, 20.0, 40.0, 40.0));
list.push_rect(Rect::new(0.0, 0.0, 64.0, 64.0), RED);
list.pop_clip();
list.pop_clip();
backend.execute(&list).expect("execute");
let buf = backend.readback_rgba().expect("readback");
assert_eq!(
pixel(&buf, 64, 30, 30),
(255, 0, 0, 255),
"pixel inside the clip intersection must be red"
);
assert_eq!(
pixel(&buf, 64, 10, 10),
(0, 0, 0, 255),
"pixel outside the inner clip must remain the clear colour"
);
assert_eq!(
pixel(&buf, 64, 50, 50),
(0, 0, 0, 255),
"pixel outside the outer clip must remain the clear colour"
);
}
#[test]
fn sequential_rects_paint_distinct_regions() {
let mut backend = match try_backend(64, 32) {
Some(b) => b,
None => return,
};
backend.set_clear_color(CLEAR);
let mut list = DrawList::new();
list.push_rect(Rect::new(0.0, 0.0, 32.0, 32.0), RED);
list.push_rect(Rect::new(32.0, 0.0, 32.0, 32.0), GREEN);
backend.execute(&list).expect("execute");
let buf = backend.readback_rgba().expect("readback");
assert_eq!(
pixel(&buf, 64, 10, 16),
(255, 0, 0, 255),
"left region must be red"
);
assert_eq!(
pixel(&buf, 64, 50, 16),
(0, 255, 0, 255),
"right region must be green"
);
}
#[test]
fn capabilities_reflect_implemented_features() {
let backend = match try_backend(8, 8) {
Some(b) => b,
None => return,
};
assert!(!backend.supports_text());
assert!(backend.supports_images());
assert!(backend.supports_blur());
assert!(backend.supports_gradients());
assert!(backend.supports_paths());
}
#[test]
fn device_init_headless_succeeds_or_skips() {
match WgpuBackend::headless(64, 64) {
Ok(_) => { }
Err(UiError::Unsupported(_)) => { }
Err(e) => panic!("headless() returned unexpected error: {e:?}"),
}
}
#[test]
fn all_pipelines_compile_without_error() {
let Some(_b) = try_backend(4, 4) else { return };
}
#[test]
fn balanced_quality_pipelines_compile() {
match WgpuBackend::headless_with_quality(4, 4, &oxiui_render_wgpu::RenderQuality::balanced()) {
Ok(_) => { }
Err(UiError::Unsupported(_)) => { }
Err(e) => panic!("headless_with_quality(balanced) failed: {e:?}"),
}
}
#[test]
fn scissor_clip_restricts_draw_to_intersection() {
let Some(mut b) = try_backend(64, 64) else {
return;
};
let mut list = DrawList::new();
list.push_rect(Rect::new(0.0, 0.0, 64.0, 64.0), Color(0, 0, 255, 255));
list.push_clip(Rect::new(0.0, 0.0, 64.0, 32.0)); list.push_rect(Rect::new(0.0, 0.0, 64.0, 64.0), Color(0, 255, 0, 255));
list.pop_clip();
b.execute(&list).expect("execute");
let top = b.read_pixel(32, 16).expect("read").expect("pixel");
assert_eq!(
(top.0, top.1, top.2, top.3),
(0, 255, 0, 255),
"top half should be green (inside clip)"
);
let bot = b.read_pixel(32, 48).expect("read").expect("pixel");
assert_eq!(
(bot.0, bot.1, bot.2, bot.3),
(0, 0, 255, 255),
"bottom half should be blue (outside clip)"
);
}
#[test]
fn nested_scissor_clips_intersect() {
let Some(mut b) = try_backend(64, 64) else {
return;
};
let mut list = DrawList::new();
list.push_clip(Rect::new(0.0, 0.0, 48.0, 48.0));
list.push_clip(Rect::new(16.0, 16.0, 48.0, 48.0));
list.push_rect(Rect::new(0.0, 0.0, 64.0, 64.0), Color(255, 0, 0, 255));
list.pop_clip();
list.pop_clip();
b.execute(&list).expect("execute");
let inside = b.read_pixel(32, 32).expect("read").expect("pixel");
assert_eq!(inside.3, 255, "pixel in intersection must be opaque (red)");
let outside = b.read_pixel(5, 5).expect("read").expect("pixel");
assert_eq!(
outside.3, 0,
"pixel outside intersection must be transparent"
);
}
#[test]
fn many_solid_rects_coalesce_into_few_draws() {
let Some(mut b) = try_backend(256, 256) else {
return;
};
let mut list = DrawList::new();
for row in 0u32..10 {
for col in 0u32..10 {
let x = col as f32 * 25.0 + 2.0;
let y = row as f32 * 25.0 + 2.0;
let color = if col % 2 == 0 {
Color(255, 0, 0, 255)
} else {
Color(0, 0, 255, 255)
};
list.push_rect(Rect::new(x, y, 20.0, 20.0), color);
}
}
b.execute(&list).expect("execute");
let red_px = b.read_pixel(12, 12).expect("read red").expect("pixel");
assert_eq!(
(red_px.0, red_px.1, red_px.2),
(255, 0, 0),
"col=0 (red column) centre should be red"
);
let blue_px = b.read_pixel(37, 12).expect("read blue").expect("pixel");
assert_eq!(
(blue_px.0, blue_px.1, blue_px.2),
(0, 0, 255),
"col=1 (blue column) centre should be blue"
);
let stats = b.frame_stats();
assert!(
stats.draw_calls <= 3,
"100 unclipped same-kind rects should coalesce into ≤3 draws, got {}",
stats.draw_calls
);
}
#[test]
fn different_size_backends_work_independently() {
let Some(mut b_small) = try_backend(32, 32) else {
return;
};
let Some(mut b_large) = try_backend(128, 128) else {
return;
};
let mut list = DrawList::new();
list.push_rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color(0, 255, 0, 255));
b_small.execute(&list).expect("small execute");
b_large.execute(&list).expect("large execute");
let s = b_small
.read_pixel(16, 16)
.expect("read small")
.expect("pixel");
assert_eq!(
(s.0, s.1, s.2, s.3),
(0, 255, 0, 255),
"small backend centre should be green"
);
let l = b_large
.read_pixel(50, 50)
.expect("read large")
.expect("pixel");
assert_eq!(
(l.0, l.1, l.2, l.3),
(0, 255, 0, 255),
"large backend centre should be green"
);
}
#[test]
fn resize_updates_surface_dimensions() {
let Some(mut b) = try_backend(32, 32) else {
return;
};
b.resize(128, 64).expect("resize must succeed");
assert_eq!(b.width(), 128, "width must be updated after resize");
assert_eq!(b.height(), 64, "height must be updated after resize");
let sz = b.surface_size();
assert!(
(sz.width - 128.0).abs() < f32::EPSILON,
"surface_size width must be 128 after resize"
);
assert!(
(sz.height - 64.0).abs() < f32::EPSILON,
"surface_size height must be 64 after resize"
);
}
#[test]
fn resize_then_render_produces_correct_pixels() {
let Some(mut b) = try_backend(32, 32) else {
return;
};
b.resize(64, 64).expect("resize");
let mut list = DrawList::new();
list.push_rect(Rect::new(0.0, 0.0, 64.0, 64.0), Color(255, 0, 0, 255));
b.execute(&list).expect("execute after resize");
let px = b.read_pixel(32, 32).expect("read").expect("pixel");
assert_eq!(
(px.0, px.1, px.2, px.3),
(255, 0, 0, 255),
"pixel at (32,32) must be red after resize+render"
);
}
#[test]
fn resize_zero_dimension_returns_error() {
let Some(mut b) = try_backend(32, 32) else {
return;
};
assert!(
b.resize(0, 32).is_err(),
"resize(0, 32) must return an error"
);
assert!(
b.resize(32, 0).is_err(),
"resize(32, 0) must return an error"
);
assert_eq!(b.width(), 32, "width must be unchanged after failed resize");
}