cranpose-render-common 0.0.60

Common rendering contracts for Cranpose
Documentation
use std::rc::Rc;

use cranpose_core::NodeId;
use cranpose_foundation::PointerEvent;
use cranpose_ui::Point;
use cranpose_ui_graphics::{Rect, RoundedCornerShape};

use crate::graph::quad_bounds;
use crate::graph::{LayerNode, ProjectiveTransform, RenderNode};
use crate::graph_scene::{HitClip, HitGeometry};
use crate::primitive_emit::resolve_clip;

pub trait HitGraphSink {
    fn push_hit(
        &mut self,
        node_id: NodeId,
        capture_path: &[NodeId],
        geometry: HitGeometry,
        shape: Option<RoundedCornerShape>,
        click_actions: &[Rc<dyn Fn(Point)>],
        pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
    );
}

pub fn collect_hits_from_graph<S: HitGraphSink>(
    layer: &LayerNode,
    parent_transform: ProjectiveTransform,
    sink: &mut S,
    parent_hit_clip: Option<Rect>,
) {
    if !layer.has_hit_targets {
        return;
    }
    let mut hit_clips = Vec::new();
    let mut pointer_input_ancestors = Vec::new();
    collect_hits_from_graph_inner(
        layer,
        parent_transform,
        sink,
        parent_hit_clip,
        &mut hit_clips,
        &mut pointer_input_ancestors,
    );
}

fn collect_hits_from_graph_inner<S: HitGraphSink>(
    layer: &LayerNode,
    parent_transform: ProjectiveTransform,
    sink: &mut S,
    parent_hit_clip: Option<Rect>,
    hit_clips: &mut Vec<HitClip>,
    pointer_input_ancestors: &mut Vec<NodeId>,
) {
    if !layer.has_hit_targets {
        return;
    }
    let transform = layer.transform_to_parent.then(parent_transform);
    let transformed_quad = transform.map_rect(layer.local_bounds);
    let transformed_rect = quad_bounds(transformed_quad);

    if transformed_rect.width <= 0.0 || transformed_rect.height <= 0.0 {
        return;
    }

    let Some(world_to_local) = transform.inverse() else {
        return;
    };

    let mut hit_clip_bounds = parent_hit_clip;
    let mut pushed_clip = false;
    if let Some(local_clip) = layer.clip_rect() {
        let clip_quad = transform.map_rect(local_clip);
        let clip_bounds = quad_bounds(clip_quad);
        let Some(resolved_clip_bounds) = resolve_clip(parent_hit_clip, Some(clip_bounds)) else {
            return;
        };
        hit_clip_bounds = Some(resolved_clip_bounds);
        hit_clips.push(HitClip {
            quad: clip_quad,
            bounds: clip_bounds,
        });
        pushed_clip = true;
    }

    if let (Some(node_id), Some(hit)) = (layer.node_id, &layer.hit_test) {
        let mut capture_path = Vec::with_capacity(1 + pointer_input_ancestors.len());
        capture_path.push(node_id);
        capture_path.extend(pointer_input_ancestors.iter().rev().copied());
        sink.push_hit(
            node_id,
            &capture_path,
            HitGeometry {
                rect: transformed_rect,
                quad: transformed_quad,
                local_bounds: layer.local_bounds,
                world_to_local,
                hit_clip_bounds,
                hit_clips: hit_clips.to_vec(),
            },
            hit.shape,
            &hit.click_actions,
            &hit.pointer_inputs,
        );
    }

    let pushes_pointer_input_ancestor = layer
        .hit_test
        .as_ref()
        .is_some_and(|hit| !hit.pointer_inputs.is_empty())
        && layer.node_id.is_some();
    if pushes_pointer_input_ancestor {
        pointer_input_ancestors.push(layer.node_id.expect("checked above"));
    }

    for child in &layer.children {
        if let RenderNode::Layer(child_layer) = child {
            collect_hits_from_graph_inner(
                child_layer,
                transform,
                sink,
                hit_clip_bounds,
                hit_clips,
                pointer_input_ancestors,
            );
        }
    }

    if pushes_pointer_input_ancestor {
        let _ = pointer_input_ancestors.pop();
    }

    if pushed_clip {
        let _ = hit_clips.pop();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::graph::{CachePolicy, HitTestNode, IsolationReasons};
    use crate::raster_cache::LayerRasterCacheHashes;

    type RecordedHit = (
        NodeId,
        Vec<NodeId>,
        Rect,
        [[f32; 2]; 4],
        Option<Rect>,
        usize,
    );

    #[derive(Default)]
    struct TestSink {
        hits: Vec<RecordedHit>,
    }

    impl HitGraphSink for TestSink {
        fn push_hit(
            &mut self,
            node_id: NodeId,
            capture_path: &[NodeId],
            geometry: HitGeometry,
            _shape: Option<RoundedCornerShape>,
            _click_actions: &[Rc<dyn Fn(Point)>],
            _pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
        ) {
            self.hits.push((
                node_id,
                capture_path.to_vec(),
                geometry.rect,
                geometry.quad,
                geometry.hit_clip_bounds,
                geometry.hit_clips.len(),
            ));
        }
    }

    fn test_layer(node_id: NodeId, transform_to_parent: ProjectiveTransform) -> LayerNode {
        LayerNode {
            node_id: Some(node_id),
            local_bounds: Rect {
                x: 0.0,
                y: 0.0,
                width: 30.0,
                height: 18.0,
            },
            transform_to_parent,
            motion_context_animated: false,
            translated_content_context: false,
            graphics_layer: cranpose_ui_graphics::GraphicsLayer::default(),
            clip_to_bounds: true,
            shadow_clip: None,
            hit_test: Some(HitTestNode {
                shape: None,
                click_actions: vec![Rc::new(|_point| {})],
                pointer_inputs: vec![],
                clip: None,
            }),
            has_hit_targets: true,
            isolation: IsolationReasons::default(),
            cache_policy: CachePolicy::None,
            cache_hashes: LayerRasterCacheHashes::default(),
            cache_hashes_valid: false,
            children: vec![],
        }
    }

    #[test]
    fn collect_hits_uses_graph_transform_to_parent() {
        let layer = test_layer(7, ProjectiveTransform::translation(12.0, 9.0));
        let mut sink = TestSink::default();

        collect_hits_from_graph(&layer, ProjectiveTransform::identity(), &mut sink, None);

        assert_eq!(sink.hits.len(), 1);
        let (node_id, capture_path, rect, quad, clip, clip_count) = &sink.hits[0];
        assert_eq!(*node_id, 7);
        assert_eq!(capture_path, &vec![7]);
        assert_eq!(
            *rect,
            Rect {
                x: 12.0,
                y: 9.0,
                width: 30.0,
                height: 18.0,
            }
        );
        assert_eq!(
            *quad,
            [[12.0, 9.0], [42.0, 9.0], [12.0, 27.0], [42.0, 27.0]]
        );
        assert_eq!(*clip, Some(*rect));
        assert_eq!(*clip_count, 1);
    }

    #[test]
    fn collect_hits_composes_nested_graph_transforms() {
        let child = test_layer(9, ProjectiveTransform::translation(4.0, 3.0));
        let mut parent = test_layer(7, ProjectiveTransform::translation(10.0, 6.0));
        parent.hit_test.as_mut().expect("hit test").pointer_inputs = vec![Rc::new(|_event| {})];
        parent.children.push(RenderNode::Layer(Box::new(child)));
        let mut sink = TestSink::default();

        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);

        assert_eq!(sink.hits.len(), 2);
        let (_, child_capture_path, child_rect, child_quad, child_clip, child_clip_count) =
            &sink.hits[1];
        assert_eq!(child_capture_path, &vec![9, 7]);
        assert_eq!(
            *child_rect,
            Rect {
                x: 14.0,
                y: 9.0,
                width: 30.0,
                height: 18.0,
            }
        );
        assert_eq!(
            *child_quad,
            [[14.0, 9.0], [44.0, 9.0], [14.0, 27.0], [44.0, 27.0]]
        );
        assert_eq!(
            *child_clip,
            Some(Rect {
                x: 14.0,
                y: 9.0,
                width: 26.0,
                height: 15.0,
            })
        );
        assert_eq!(*child_clip_count, 2);
    }

    #[test]
    fn collect_hits_retains_transformed_clip_chain() {
        let mut parent = test_layer(1, ProjectiveTransform::translation(20.0, 10.0));
        let mut child = test_layer(
            2,
            ProjectiveTransform::from_rect_to_quad(
                Rect {
                    x: 0.0,
                    y: 0.0,
                    width: 30.0,
                    height: 18.0,
                },
                [[0.0, 0.0], [30.0, 0.0], [4.0, 18.0], [34.0, 18.0]],
            ),
        );
        child.clip_to_bounds = true;
        parent.children.push(RenderNode::Layer(Box::new(child)));

        let mut sink = TestSink::default();
        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);

        let (_, _, _, _, _, child_clip_count) = sink.hits[1];
        assert_eq!(child_clip_count, 2);
    }
}