cranpose-render-wgpu 0.1.14

WGPU renderer backend for Cranpose
Documentation
mod support;

use cranpose_app_shell::AppShell;
use cranpose_core::{location_key, MutableState};
use cranpose_render_wgpu::CapturedFrame;
use cranpose_ui::text::{SpanStyle, TextUnit};
use cranpose_ui::{composable, Box, BoxSpec, Color, Modifier, Text, TextStyle};
use std::cell::RefCell;
use std::rc::Rc;

const FRAME_WIDTH: u32 = 400;
const FRAME_HEIGHT: u32 = 300;

#[composable]
#[allow(non_snake_case)]
fn AbsoluteOffsetTextRedrawProbe(start: MutableState<i32>) {
    let style = TextStyle::from_span_style(SpanStyle {
        color: Some(Color(1.0, 0.78, 0.42, 1.0)),
        font_size: TextUnit::Sp(11.0),
        ..Default::default()
    });
    let pattern = if start.get() == 0 {
        "IIIIIIIIIIII"
    } else {
        "WWWWWWWWWWWW"
    };

    Box(
        Modifier::empty()
            .size_points(FRAME_WIDTH as f32, FRAME_HEIGHT as f32)
            .background(Color(0.015, 0.02, 0.03, 1.0)),
        BoxSpec::default(),
        move || {
            for row in 0..14 {
                Text(
                    format!("{pattern} row {:02}", start.get() + row),
                    Modifier::empty()
                        .size_points(380.0, 16.0)
                        .absolute_offset(8.0, row as f32 * 16.0),
                    style.clone(),
                );
            }
        },
    );
}

fn bright_pixel_count(frame: &CapturedFrame) -> usize {
    frame
        .pixels
        .chunks_exact(4)
        .filter(|pixel| pixel[0] > 120 && pixel[1] > 80 && pixel[2] > 35)
        .count()
}

fn changed_pixel_count(before: &CapturedFrame, after: &CapturedFrame, threshold: u8) -> usize {
    assert_eq!(before.width, after.width);
    assert_eq!(before.height, after.height);
    before
        .pixels
        .chunks_exact(4)
        .zip(after.pixels.chunks_exact(4))
        .filter(|(before, after)| {
            before[0].abs_diff(after[0]) > threshold
                || before[1].abs_diff(after[1]) > threshold
                || before[2].abs_diff(after[2]) > threshold
                || before[3].abs_diff(after[3]) > threshold
        })
        .count()
}

#[test]
fn absolute_offset_text_pixels_redraw_after_state_only_change() {
    let (_lock, renderer) = match support::headless_renderer_parts() {
        Ok(parts) => parts,
        Err(err) => {
            eprintln!("skipping absolute-offset text redraw assertion because headless WGPU init failed: {err}");
            return;
        }
    };

    let root_key = location_key(file!(), line!(), column!());
    let state_holder: Rc<RefCell<Option<MutableState<i32>>>> = Rc::new(RefCell::new(None));
    let state_holder_for_app = Rc::clone(&state_holder);
    let mut shell = AppShell::new(renderer, root_key, move || {
        let start = cranpose_core::useState(|| 0i32);
        *state_holder_for_app.borrow_mut() = Some(start);
        AbsoluteOffsetTextRedrawProbe(start);
    });
    shell.set_viewport(FRAME_WIDTH as f32, FRAME_HEIGHT as f32);
    shell.set_buffer_size(FRAME_WIDTH, FRAME_HEIGHT);
    shell.update();

    let before = shell
        .renderer()
        .capture_frame(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("initial frame capture should succeed");
    let before_bright = bright_pixel_count(&before);
    assert!(
        before_bright > 250,
        "initial absolute-offset text should render visibly, bright pixels={before_bright}"
    );

    let start = state_holder
        .borrow()
        .as_ref()
        .cloned()
        .expect("start state should be captured");
    start.set(30);
    shell.update();

    let after = shell
        .renderer()
        .capture_frame(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("updated frame capture should succeed");
    let after_bright = bright_pixel_count(&after);
    assert!(
        after_bright > 250,
        "updated absolute-offset text should remain visibly rendered, bright pixels={after_bright}"
    );

    let changed_pixels = changed_pixel_count(&before, &after, 24);
    assert!(
        changed_pixels > 300,
        "state-only text recomposition should change the rasterized text without waiting for input, changed pixels={changed_pixels}"
    );
}