mod interaction;
mod program;
mod shapes;
mod types;
mod validation;
#[cfg(test)]
mod tests;
use iced::{Element, Point, Theme};
use plushie_core::types::{self as core_types, PlushieType};
use plushie_core::types::{CanvasShape, extract_canvas_layers};
use crate::PlushieRenderer;
use crate::canvas_engine::CanvasLayerCaches;
use crate::iced_convert;
use crate::message::Message;
use crate::protocol::TreeNode;
use crate::render_ctx::RenderCtx;
use crate::widget::helpers::*;
pub(crate) use interaction::{collect_interactive_elements, validate_interactive_elements};
pub(crate) use types::{InteractiveElement, TransformMatrix};
pub(crate) use validation::validate_canvas_shape_tree;
#[cfg(test)]
pub(crate) use shapes::{parse_canvas_fill, parse_canvas_stroke, resolve_color};
#[cfg(test)]
pub(crate) use types::{ArrowMode, CanvasState, DragAxis, HitRegion};
pub(crate) fn canvas_layers_from_node(
node: &TreeNode,
) -> std::collections::BTreeMap<String, Vec<CanvasShape>> {
let has_layers = node.children.iter().any(|c| c.type_name == "__layer__");
if has_layers {
extract_canvas_layers(node)
} else if !node.children.is_empty() {
let shapes: Vec<CanvasShape> = node
.children
.iter()
.filter_map(CanvasShape::from_node)
.collect();
let mut map = std::collections::BTreeMap::new();
map.insert("default".to_string(), shapes);
map
} else {
std::collections::BTreeMap::new()
}
}
struct CanvasProps {
width: Option<core_types::Length>,
height: Option<core_types::Length>,
background: Option<core_types::Color>,
alt: Option<String>,
description: Option<String>,
role: Option<String>,
arrow_mode: Option<core_types::ArrowMode>,
}
impl CanvasProps {
fn from_node(node: &TreeNode) -> Self {
let p = &node.props;
Self {
width: core_types::Length::extract(p, "width"),
height: core_types::Length::extract(p, "height"),
background: core_types::Color::extract(p, "background"),
alt: String::extract(p, "alt"),
description: String::extract(p, "description"),
role: String::extract(p, "role"),
arrow_mode: core_types::ArrowMode::extract(p, "arrow_mode"),
}
}
}
pub(crate) fn render_canvas_with_state<'a, R: PlushieRenderer>(
node: &'a TreeNode,
ctx: RenderCtx<'a, R>,
node_caches: Option<&'a CanvasLayerCaches<R>>,
interactive_elements: &'a [InteractiveElement],
pending_focus: Option<String>,
) -> Element<'a, Message, Theme, R> {
let cp = CanvasProps::from_node(node);
let props = &node.props;
let width = cp
.width
.as_ref()
.map(iced_convert::length)
.unwrap_or(iced::Length::Fill);
let height = cp
.height
.as_ref()
.map(iced_convert::length)
.unwrap_or(iced::Length::Fixed(200.0));
let layer_map = canvas_layers_from_node(node);
let layers: Vec<(String, Vec<CanvasShape>)> = layer_map
.into_iter()
.map(|(name, layer_shapes)| {
let truncated = shapes::truncate_shapes(&name, layer_shapes);
(name, truncated)
})
.collect();
let background = cp.background.as_ref().map(iced_convert::color);
let on_press = prop_bool_default(props, "on_press", false);
let on_release = prop_bool_default(props, "on_release", false);
let on_move = prop_bool_default(props, "on_move", false);
let on_scroll = prop_bool_default(props, "on_scroll", false);
let interactive = prop_bool_default(props, "interactive", false);
let has_interactive_elements = !interactive_elements.is_empty();
let mut c = iced::widget::Canvas::<_, Message, iced::Theme, R>::new(program::CanvasProgram {
layers,
caches: node_caches,
background,
window_id: ctx.window_id.to_string(),
id: node.id.clone(),
on_press: on_press || interactive || has_interactive_elements,
on_release: on_release || interactive || has_interactive_elements,
on_move: on_move || interactive || has_interactive_elements,
on_scroll: on_scroll || interactive,
images: ctx.images,
interactive_elements,
arrow_mode: cp
.arrow_mode
.map(types::ArrowMode::from)
.unwrap_or_default(),
pending_focus,
})
.width(width)
.height(height);
c = c.id(iced::widget::Id::from(node.id.clone()));
if let Some(alt) = cp.alt {
c = c.alt(alt);
}
if let Some(desc) = cp.description {
c = c.description(desc);
}
if let Some(ref role_str) = cp.role {
if let Some(role) = crate::a11y::parse_role_str(role_str) {
c = c.role(role);
} else {
log::warn!("canvas '{}': unknown role '{role_str}'", node.id);
}
} else if has_interactive_elements {
c = c.role(iced::advanced::widget::operation::accessible::Role::Group);
}
c.into()
}
pub fn canvas_hit_test(node: &crate::protocol::TreeNode, x: f32, y: f32) -> Option<String> {
let layer_map = canvas_layers_from_node(node);
let mut interactive_elements = Vec::new();
for (layer_name, shapes) in &layer_map {
collect_interactive_elements(
shapes,
layer_name,
TransformMatrix::identity(),
None,
None,
"",
&mut interactive_elements,
);
}
interaction::find_hit_element(Point::new(x, y), &interactive_elements).map(|e| e.id.clone())
}
pub fn canvas_find_element_by_id(node: &crate::protocol::TreeNode, element_id: &str) -> bool {
let layer_map = canvas_layers_from_node(node);
let mut interactive_elements = Vec::new();
for (layer_name, shapes) in &layer_map {
collect_interactive_elements(
shapes,
layer_name,
TransformMatrix::identity(),
None,
None,
"",
&mut interactive_elements,
);
}
interactive_elements.iter().any(|e| e.id == element_id)
}
pub fn canvas_has_on_press(node: &crate::protocol::TreeNode) -> bool {
node.props
.get("on_press")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}