cranpose-render-common 0.1.162

Common rendering contracts for Cranpose
Documentation
use cranpose_core::NodeId;
use cranpose_ui_graphics::Rect;
use smallvec::SmallVec;

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

pub trait HitGraphSink {
    fn push_hit(
        &mut self,
        node_id: NodeId,
        capture_path: &[NodeId],
        geometry: HitGeometry<'_>,
        hit: &HitTestNode,
    );
}

impl HitGraphSink for Scene {
    fn push_hit(
        &mut self,
        node_id: NodeId,
        capture_path: &[NodeId],
        geometry: HitGeometry<'_>,
        hit: &HitTestNode,
    ) {
        Scene::push_hit(
            self,
            node_id,
            capture_path,
            geometry,
            HitTargetSpec {
                shape: hit.shape,
                click_actions: hit
                    .click_actions
                    .iter()
                    .cloned()
                    .map(ClickAction::WithPoint),
                pointer_inputs: &hit.pointer_inputs,
                pointer_icon: hit.pointer_icon.as_ref(),
            },
        );
    }
}

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();
    let mut capture_path = SmallVec::<[NodeId; 8]>::new();
    collect_hits_from_graph_inner(
        layer,
        parent_transform,
        sink,
        parent_hit_clip,
        &mut hit_clips,
        &mut pointer_input_ancestors,
        &mut capture_path,
    );
}

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>,
    capture_path: &mut SmallVec<[NodeId; 8]>,
) {
    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 resolved_clip_bounds = resolve_clip(parent_hit_clip, Some(clip_bounds));
        if resolved_clip_bounds.is_some_and(|clip| clip.is_empty()) {
            return;
        }
        hit_clip_bounds = 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) {
        capture_path.clear();
        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,
        );
    }

    let pointer_input_ancestor = layer
        .hit_test
        .as_ref()
        .filter(|hit| !hit.pointer_inputs.is_empty())
        .and(layer.node_id);
    if let Some(node_id) = pointer_input_ancestor {
        pointer_input_ancestors.push(node_id);
    }

    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,
                capture_path,
            );
        }
    }

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

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

#[cfg(test)]
#[path = "tests/hit_graph_tests.rs"]
mod tests;