use std::collections::HashMap;
use std::rc::Rc;
use cranpose_core::{MemoryApplier, NodeError, NodeId};
use cranpose_foundation::PointerEvent;
use cranpose_render_common::graph::ProjectiveTransform;
use cranpose_render_common::graph_scene::HitGeometry;
use cranpose_render_common::hit_graph::{collect_hits_from_graph, HitGraphSink};
use cranpose_render_common::scene_builder::build_graph_from_applier;
use cranpose_ui::{
build_layout_tree_from_applier, build_semantics_tree_from_applier, LayoutBox, LayoutEngine,
Point, Rect, SemanticsAction, SemanticsNode, SemanticsRole, SemanticsWidgetRole, Size,
};
use cranpose_ui_graphics::RoundedCornerShape;
#[derive(Clone, Debug, PartialEq)]
pub struct PlacedSemanticsNode {
pub node_id: NodeId,
pub role: SemanticsRole,
pub widget_role: Option<SemanticsWidgetRole>,
pub label: Option<String>,
pub state_description: Option<String>,
pub clickable: bool,
pub toggled: Option<bool>,
pub enabled: bool,
pub layout_bounds: Rect,
pub touch_bounds: Option<Rect>,
pub children: Vec<PlacedSemanticsNode>,
}
impl PlacedSemanticsNode {
pub fn target_bounds(&self) -> Rect {
self.touch_bounds.unwrap_or(self.layout_bounds)
}
pub fn visit(&self, visitor: &mut impl FnMut(&PlacedSemanticsNode)) {
visitor(self);
for child in &self.children {
child.visit(visitor);
}
}
pub fn flatten(&self) -> Vec<&PlacedSemanticsNode> {
let mut all = Vec::new();
self.collect(&mut all);
all
}
fn collect<'a>(&'a self, out: &mut Vec<&'a PlacedSemanticsNode>) {
out.push(self);
for child in &self.children {
child.collect(out);
}
}
pub fn controls(&self) -> Vec<&PlacedSemanticsNode> {
self.flatten()
.into_iter()
.filter(|node| node.clickable)
.collect()
}
pub fn describe(&self) -> String {
match &self.label {
Some(label) => format!("{label:?}"),
None => format!("{:?}#{}", self.role, self.node_id),
}
}
}
pub fn placed_semantics_from_applier(
applier: &mut MemoryApplier,
root: NodeId,
size: Size,
) -> Result<Option<PlacedSemanticsNode>, NodeError> {
applier.compute_layout(root, size)?;
let Some(layout) = build_layout_tree_from_applier(applier, root)? else {
return Ok(None);
};
let Some(semantics) = build_semantics_tree_from_applier(applier, root)? else {
return Ok(None);
};
let mut layout_bounds = HashMap::new();
index_layout_bounds(layout.root(), &mut layout_bounds);
let mut touch_bounds = HashMap::new();
if let Some(graph) = build_graph_from_applier(applier, root, 1.0) {
let mut sink = TouchBoundsSink {
bounds: &mut touch_bounds,
};
collect_hits_from_graph(
&graph.root,
ProjectiveTransform::identity(),
&mut sink,
None,
);
}
join(semantics.root(), &layout_bounds, &touch_bounds).map(Some)
}
fn index_layout_bounds(layout_box: &LayoutBox, out: &mut HashMap<NodeId, Rect>) {
out.insert(layout_box.node_id, layout_box.rect);
for child in &layout_box.children {
index_layout_bounds(child, out);
}
}
struct TouchBoundsSink<'a> {
bounds: &'a mut HashMap<NodeId, Rect>,
}
impl HitGraphSink for TouchBoundsSink<'_> {
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.bounds.entry(node_id).or_insert(geometry.rect);
}
}
fn join(
node: &SemanticsNode,
layout_bounds: &HashMap<NodeId, Rect>,
touch_bounds: &HashMap<NodeId, Rect>,
) -> Result<PlacedSemanticsNode, NodeError> {
let bounds = layout_bounds
.get(&node.node_id)
.copied()
.ok_or(NodeError::MissingContext {
id: node.node_id,
reason: "semantics node has no layout box: the semantics walk and the \
layout walk disagree about what was placed",
})?;
let mut children = Vec::with_capacity(node.children.len());
for child in &node.children {
children.push(join(child, layout_bounds, touch_bounds)?);
}
Ok(PlacedSemanticsNode {
node_id: node.node_id,
role: node.role.clone(),
widget_role: node.widget_role,
label: match &node.role {
SemanticsRole::Text { value } => Some(value.clone()),
_ => node.description.clone(),
},
state_description: node.state_description.clone(),
clickable: node
.actions
.iter()
.any(|action| matches!(action, SemanticsAction::Click { .. })),
toggled: node.toggled,
enabled: node.enabled,
layout_bounds: bounds,
touch_bounds: touch_bounds.get(&node.node_id).copied(),
children,
})
}