cranpose-app-shell 0.9.27

App lifecycle, frame cadence and input dispatch for Cranpose
Documentation
use super::*;

type LayerCanvasState = (MutableState<f32>, MutableState<Color>, NodeId);

thread_local! {
    static LAYER_CANVAS_STATE: RefCell<Option<LayerCanvasState>> = const { RefCell::new(None) };
    static LAYER_CANVAS_DRAWS: Cell<usize> = const { Cell::new(0) };
}

const FIRST_COLOR: Color = Color(0.9, 0.1, 0.2, 1.0);
const SECOND_COLOR: Color = Color(0.1, 0.7, 0.3, 1.0);

#[composable]
fn LayerPropertyCanvas() {
    let rotation = rememberMutableStateOf(|| 0.0_f32);
    let color = rememberMutableStateOf(|| FIRST_COLOR);
    let layer_rotation = rotation;
    let draw_color = color;
    let node_id = cranpose_ui::Canvas(
        Modifier::empty()
            .size_points(80.0, 40.0)
            .graphics_layer(move || GraphicsLayer {
                rotation_z: layer_rotation.get(),
                ..GraphicsLayer::default()
            }),
        move |scope| {
            LAYER_CANVAS_DRAWS.with(|draws| draws.set(draws.get() + 1));
            scope.draw_rect(Brush::solid(draw_color.get()));
        },
    );
    LAYER_CANVAS_STATE.with(|slot| *slot.borrow_mut() = Some((rotation, color, node_id)));
}

fn canvas_layer_position(
    layer: &cranpose_render_common::graph::LayerNode,
    node_id: NodeId,
) -> Option<Point> {
    if layer.node_id == Some(node_id) {
        return Some(layer.transform_to_parent.map_point(Point::default()));
    }
    layer.children.iter().find_map(|child| match child {
        cranpose_render_common::graph::RenderNode::Layer(child) => {
            canvas_layer_position(child, node_id)
        }
        cranpose_render_common::graph::RenderNode::Primitive(_)
        | cranpose_render_common::graph::RenderNode::DrawRun(_) => None,
    })
}

fn canvas_rect_colors(
    layer: &cranpose_render_common::graph::LayerNode,
    node_id: NodeId,
    colors: &mut Vec<Color>,
) {
    for child in &layer.children {
        match child {
            cranpose_render_common::graph::RenderNode::Layer(child) => {
                canvas_rect_colors(child, node_id, colors);
            }
            cranpose_render_common::graph::RenderNode::DrawRun(run)
                if run
                    .command
                    .is_some_and(|command| command.node_id == node_id) =>
            {
                colors.extend(run.primitives().filter_map(|primitive| match primitive {
                    DrawPrimitive::Rect {
                        brush: Brush::Solid(color),
                        ..
                    } => Some(color),
                    _ => None,
                }));
            }
            cranpose_render_common::graph::RenderNode::Primitive(_)
            | cranpose_render_common::graph::RenderNode::DrawRun(_) => {}
        }
    }
}

fn canvas_output(
    shell: &AppShell<ScopedUpdateCountingRenderer>,
    node_id: NodeId,
) -> (Point, Vec<Color>) {
    let graph = shell.surfaces[0]
        .renderer
        .scene()
        .graph
        .as_ref()
        .expect("the initial frame builds a graph");
    let position =
        canvas_layer_position(&graph.root, node_id).expect("Canvas remains in the retained graph");
    let mut colors = Vec::new();
    canvas_rect_colors(&graph.root, node_id, &mut colors);
    (position, colors)
}

#[test]
fn layer_only_changes_keep_canvas_recording_until_its_own_state_changes() {
    let _guard = test_guard();
    LAYER_CANVAS_DRAWS.with(|draws| draws.set(0));
    LAYER_CANVAS_STATE.with(|slot| *slot.borrow_mut() = None);

    let mut shell = AppShell::new_with_size(
        ScopedUpdateCountingRenderer::new(
            Rc::new(Cell::new(0)),
            Rc::new(Cell::new(0)),
            Rc::new(RefCell::new(Vec::new())),
        ),
        location_key(file!(), line!(), column!()),
        LayerPropertyCanvas,
        (320, 240),
        (320.0, 240.0),
    );
    shell.update();
    shell.update();

    let (rotation, color, node_id) = LAYER_CANVAS_STATE
        .with(|slot| slot.borrow().as_ref().copied())
        .expect("Canvas publishes both independent state values");
    let (initial_position, initial_colors) = canvas_output(&shell, node_id);
    assert_eq!(initial_colors, vec![FIRST_COLOR]);
    let initial_draws = LAYER_CANVAS_DRAWS.with(Cell::get);
    assert!(initial_draws > 0, "the Canvas draw closure ran");

    rotation.set_value(90.0);
    shell.update();
    let (rotated_position, rotated_colors) = canvas_output(&shell, node_id);
    assert_ne!(
        rotated_position, initial_position,
        "rotation changes painted position"
    );
    assert_eq!(rotated_colors, initial_colors);
    assert_eq!(
        LAYER_CANVAS_DRAWS.with(Cell::get),
        initial_draws,
        "a graphics-layer-only update should retain Canvas output"
    );

    color.set_value(SECOND_COLOR);
    shell.update();
    let (_, recolored) = canvas_output(&shell, node_id);
    assert_eq!(recolored, vec![SECOND_COLOR]);
    let recolored_draws = LAYER_CANVAS_DRAWS.with(Cell::get);
    assert!(
        recolored_draws > initial_draws,
        "Canvas state change re-records it"
    );

    rotation.set_value(180.0);
    color.set_value(FIRST_COLOR);
    shell.update();
    let (combined_position, combined_colors) = canvas_output(&shell, node_id);
    assert_ne!(combined_position, rotated_position);
    assert_eq!(combined_colors, vec![FIRST_COLOR]);
    assert!(
        LAYER_CANVAS_DRAWS.with(Cell::get) > recolored_draws,
        "simultaneous layer and Canvas changes still re-record content"
    );
}