makepad/examples/spritelab/tests/ui.rs
Admin 3b4d6a4ff7 platform+widgets: sploded 3D inspect view, the tweaker design-feedback suite, script math-AOT and docs channel, pointer-pin and pixel-probe, modal layout, map warp
Squashed from work; the fine-grained history is under tag archive/work-2026-08-29:
- mp* wave: mpwm window manager + the mp app family, WM API, theme bridge, PDF engine fix
- mpwm polish wave: terminal key focus, focus-history close order, pop-back-to-origin, occupied-workspace cycling, demo
- mpwm: warm-instance pool, flat-luminance opens, flicker-free CEF resize
- work: land the sources the last commits reference
- kenney: catalogue all 50 free 3D kits; Modal dismissed() never fired
- platform: windows check green again — SetWindowTextW binding
- map: exact warp-aware inverse projections — pointer ops work folded
- mpwm: quick-look gap fixes; image cache eviction on preview unload
- tweaker: material thumbnails + vibecode popup + ctrl-space notes, undo/redo over the edit ledger, capture-semantics pi
- tweaker: vibe popup card chrome + dispatch order, ctrl-space notes verified, sploded design v2 chapter
- tweaker: tabbed side panel (Props/Shader/Tree) - shader tab with checkerboard material well + prompt, complete widget-
- sploded v2: nesting-depth z, hairline scope frames, body pass
- sploded: pin the depth convention with a test, kill the draw_depth residue
- sploded: real body-pass split (scene-pass capture, panel flat) + y-convention source of truth with anti-flip gate test
- sploded: hollow outlines, flat-band input, ray-pick unprojection
- tweaker: shader tab defaults to the selection's first draw layer, stale hint trimmed
- sploded: outlines become clipped, antialiased strips; tighter deck
- sploded: merge the lane's v2 (nesting-depth z, clipped AA strip outlines, flat-band input, unproject, SplodedStack bod
- sploded: the exploded view is a LIVE view — pointer events route through the inverse explode transform (ray -> plane -
- tweaker: tabs are real widgets (uid, tree node under the dock, own plane in 3D) and pickable; navigation-class clicks
- sploded: pinned/hover outlines render on the widget's own plane in 3D — per-widget nesting depth lives on the platform
- tweaker: the material well renders the pinned widget's actual shader — the swatch byte-copies the widget's live draw c
- tweaker: the Shader tab shows the shader as written — the layer's pixel/vertex fn source (nearest definition up the co
- sploded: I = true isometric preset (yaw 45°, pitch atan(1/√2))
- tweaker: eyedropper — the colour popover's pick button arms a pixel probe; the next press in the app samples that devi
- tweaker: the shader loop closes — /tweak/apply resolves the pinned widget by uid (anonymous path segments never round-
- vj: responsive DJ mixer + Windows drag-and-drop, cherry-picked from PR #1199 (vjroger)
- tweaker: the material well is a magnifier — the mirrored instance draws at the widget's native size in the well's own
- tweaker: per-layer material thumbnails — the Widget derive emits WidgetNode::layer_areas() (every #[live] Draw… field
- tweaker: the shader source view is the real CodeView (syntax highlighting, selection, editing) when the app registers
- tweaker: Ctrl+Enter sends on every platform (TextInput treated only Cmd as primary on macOS, so Ctrl+Enter inserted a
- Modal claims no layout slot: the DJ page fills its window again
- tweaker: every fn apply recompiles (eval_chunk ran every chunk under ONE synthetic callsite, so the script body — and
- tweaker: an apply whose draw shader fails to compile is rejected — the layer goes back (last live fns / the fn as writ
- tweaker: the Shader tab's source view owns its scrolling (the ScrollYView around the CodeView double-scrolled the care
- widgets: set_visible belongs to every widget, not just View (#1194)
- script: a dead heap's resource handles must not outlive it (#1195)
- Resources: search the executable's directory, not only the working directory (#1196)
- Windows: fit a restored window to the displays that are actually attached (#1197)
- d3d11: a failing GPU call reports the loss instead of killing the process (#1198)

Co-authored-by: Kevin Boos <1139460+kevinaboos@users.noreply.github.com>
2026-08-29 09:26:24 +02:00

168 lines
5.3 KiB
Rust

//! Working-tree sprite-pass lab test (not for commit): screenshot the row of
//! six billboard recipes and say, per recipe, whether pixels arrived.
use makepad_test::{makepad_test, Selector, TestApp};
use makepad_zune_png::makepad_zune_core::bytestream::ZCursor;
use makepad_zune_png::PngDecoder;
struct Image {
width: usize,
height: usize,
rgba: Vec<u8>,
}
impl Image {
fn read(path: &std::path::Path) -> Image {
let bytes = std::fs::read(path)
.unwrap_or_else(|err| panic!("cannot read grab {}: {err}", path.display()));
let mut decoder = PngDecoder::new(ZCursor::new(&bytes));
let pixels = decoder
.decode_raw()
.unwrap_or_else(|err| panic!("cannot decode grab {}: {err:?}", path.display()));
let (width, height) = decoder.dimensions().expect("grab has no dimensions");
let components = decoder
.colorspace()
.expect("grab has no colorspace")
.num_components();
let mut rgba = vec![0u8; width * height * 4];
for i in 0..width * height {
let src = i * components;
rgba[i * 4] = pixels[src];
rgba[i * 4 + 1] = pixels[src + 1];
rgba[i * 4 + 2] = pixels[src + 2];
rgba[i * 4 + 3] = if components == 4 { pixels[src + 3] } else { 255 };
}
Image {
width,
height,
rgba,
}
}
fn pixel(&self, x: usize, y: usize) -> [u8; 3] {
let p = (y.min(self.height - 1) * self.width + x.min(self.width - 1)) * 4;
[self.rgba[p], self.rgba[p + 1], self.rgba[p + 2]]
}
}
fn is_red(p: [u8; 3]) -> bool {
p[0] > 150 && p[1] < 90 && p[2] < 90
}
fn is_yellow(p: [u8; 3]) -> bool {
p[0] > 150 && p[1] > 150 && p[2] < 90
}
fn is_green(p: [u8; 3]) -> bool {
p[1] > 120 && p[0] < 100 && p[2] < 100
}
fn is_magenta(p: [u8; 3]) -> bool {
p[0] > 150 && p[1] < 90 && p[2] > 150
}
fn is_blue(p: [u8; 3]) -> bool {
p[2] > 150 && p[0] < 100 && p[1] < 150
}
#[makepad_test]
fn each_billboard_recipe_puts_pixels_on_screen(app: TestApp) {
app.locator(Selector::id("lab")).wait_visible();
// Give the pass a couple of frames to settle, then grab.
std::thread::sleep(std::time::Duration::from_millis(600));
let path = app.screenshot();
println!("[spritelab] grab: {}", path.display());
let img = Image::read(&path);
// Six equal column bands, one per case, in submission order.
let band_w = img.width / 6;
let names = [
"0 asset-ui recipe (whole uv, zw=0) ",
"1 sandbox barrel (half uv, zw=46x32) ",
"2 sandbox troo (window uv, zw=472x434) ",
"3 troo mirrored (u0>u1, zw=472x434) ",
"4 troo window, ramp OFF (zw=0) ",
"5 barrel recipe facing AWAY (yaw+pi) ",
];
let mut counts = [[0usize; 5]; 6];
for band in 0..6 {
let x0 = band * band_w;
let x1 = (band + 1) * band_w;
for y in 0..img.height {
for x in x0..x1 {
let p = img.pixel(x, y);
if is_red(p) {
counts[band][0] += 1;
}
if is_yellow(p) {
counts[band][1] += 1;
}
if is_green(p) {
counts[band][2] += 1;
}
if is_magenta(p) {
counts[band][3] += 1;
}
if is_blue(p) {
counts[band][4] += 1;
}
}
}
}
println!("[spritelab] band red yellow green magenta blue");
for band in 0..6 {
println!(
"[spritelab] {} {:>7} {:>7} {:>7} {:>7} {:>7}",
names[band],
counts[band][0],
counts[band][1],
counts[band][2],
counts[band][3],
counts[band][4]
);
}
let mut failures: Vec<String> = Vec::new();
if counts[0][0] < 100 || counts[0][1] < 100 {
failures.push(format!(
"case 0 (asset-ui recipe) missing: red {} yellow {}",
counts[0][0], counts[0][1]
));
}
if counts[1][0] < 100 {
failures.push(format!(
"case 1 (sandbox barrel recipe) missing: red {}",
counts[1][0]
));
}
if counts[2][2] < 100 {
failures.push(format!(
"case 2 (SANDBOX TROO RECIPE) missing: green {}",
counts[2][2]
));
}
if counts[3][2] < 100 {
failures.push(format!(
"case 3 (troo mirrored) missing: green {}",
counts[3][2]
));
}
if counts[4][2] < 100 {
failures.push(format!(
"case 4 (troo, ramp off) missing: green {}",
counts[4][2]
));
}
// Case 5 (facing away) is report-only: culling it or showing it are both
// defensible; the row above says which one this build does.
println!(
"[spritelab] case 5 (facing away) red pixels: {} -> {}",
counts[5][0],
if counts[5][0] > 100 {
"drawn from behind (no backface cull)"
} else {
"NOT drawn (culled or degenerate when facing away)"
}
);
assert!(
failures.is_empty(),
"invisible billboard recipes:\n{}",
failures.join("\n")
);
}