use std::cell::Cell;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use iced::widget::canvas;
use iced::{Element, Theme};
use crate::PlushieRenderer;
use crate::message::Message;
use crate::protocol::OutgoingEvent;
use crate::protocol::TreeNode;
use crate::render_ctx::RenderCtx;
use crate::widget::canvas as canvas_widgets;
pub(crate) type CanvasLayerCaches<R> = HashMap<String, CanvasLayerCache<R>>;
pub(crate) struct CanvasLayerCache<R: PlushieRenderer> {
content_hash: u64,
theme_hash: Cell<u64>,
pub(crate) cache: canvas::Cache<R>,
}
impl<R: PlushieRenderer> CanvasLayerCache<R> {
fn new(content_hash: u64) -> Self {
Self {
content_hash,
theme_hash: Cell::new(0),
cache: canvas::Cache::new(),
}
}
fn update_content_hash(&mut self, content_hash: u64) {
if self.content_hash != content_hash {
self.cache.clear();
self.content_hash = content_hash;
}
}
pub(crate) fn ensure_theme_hash(&self, theme_hash: u64) {
if self.theme_hash.get() != theme_hash {
self.cache.clear();
self.theme_hash.set(theme_hash);
}
}
}
pub(crate) fn canvas_theme_hash(theme: &Theme) -> u64 {
fn hash_color(color: iced::Color, state: &mut DefaultHasher) {
color.r.to_bits().hash(state);
color.g.to_bits().hash(state);
color.b.to_bits().hash(state);
color.a.to_bits().hash(state);
}
let palette = theme.palette();
let mut hasher = DefaultHasher::new();
palette.is_dark.hash(&mut hasher);
hash_color(palette.primary.base.color, &mut hasher);
hash_color(palette.background.base.color, &mut hasher);
hash_color(palette.background.base.text, &mut hasher);
hash_color(palette.success.base.color, &mut hasher);
hash_color(palette.danger.base.color, &mut hasher);
hash_color(palette.warning.base.color, &mut hasher);
hasher.finish()
}
pub struct CanvasEngine<R: PlushieRenderer> {
layer_caches: HashMap<(String, String), CanvasLayerCaches<R>>,
interactions: HashMap<(String, String), Vec<canvas_widgets::InteractiveElement>>,
pending_focus: HashMap<(String, String), String>,
}
impl<R: PlushieRenderer> CanvasEngine<R> {
pub fn new() -> Self {
Self {
layer_caches: HashMap::new(),
interactions: HashMap::new(),
pending_focus: HashMap::new(),
}
}
pub fn prepare(&mut self, node: &TreeNode, window_id: &str) {
use crate::widget::canvas::canvas_layers_from_node;
let key = (window_id.to_string(), node.id.clone());
let layer_map = canvas_layers_from_node(node);
if crate::validate::current_validate_props_enabled() {
for warning in canvas_widgets::validate_canvas_shape_tree(node) {
log::warn!("[canvas {}] {}", node.id, warning);
}
}
let mut interactive_elements = Vec::new();
for (layer_name, shapes) in &layer_map {
canvas_widgets::collect_interactive_elements(
shapes,
layer_name,
canvas_widgets::TransformMatrix::identity(),
None,
None,
"",
&mut interactive_elements,
);
}
let diags = canvas_widgets::validate_interactive_elements(&node.id, &interactive_elements);
for diag in &diags {
if let Some(msg) = diag
.value
.as_ref()
.and_then(|d| d.get("message"))
.and_then(|m| m.as_str())
{
log::warn!("[canvas {}] {}", node.id, msg);
}
}
self.interactions.insert(key.clone(), interactive_elements);
let node_caches = self.layer_caches.entry(key).or_default();
for (layer_name, shapes) in &layer_map {
let hash = {
let mut hasher = DefaultHasher::new();
shapes.hash(&mut hasher);
hasher.finish()
};
match node_caches.get_mut(layer_name) {
Some(record) => record.update_content_hash(hash),
None => {
node_caches.insert(layer_name.clone(), CanvasLayerCache::new(hash));
}
}
}
node_caches.retain(|name, _| layer_map.contains_key(name));
}
pub fn render<'a>(
&'a self,
node: &'a TreeNode,
ctx: &RenderCtx<'a, R>,
extra_pending_focus: Option<String>,
) -> Element<'a, Message, Theme, R> {
let key = (ctx.window_id.to_string(), node.id.clone());
let pending = self
.pending_focus
.get(&key)
.cloned()
.or(extra_pending_focus);
canvas_widgets::render_canvas_with_state(
node,
*ctx,
self.layer_caches.get(&key),
self.interactions
.get(&key)
.map(|v| v.as_slice())
.unwrap_or(&[]),
pending,
)
}
pub fn handle_message(&mut self, msg: &Message) -> crate::registry::HandleResult {
use crate::registry::HandleResult;
match msg {
Message::CanvasElementFocusChanged {
old_element_id,
new_element_id,
..
} => {
let mut events = Vec::with_capacity(2);
if let Some(old_id) = old_element_id {
events.push(OutgoingEvent::generic("blurred", old_id.clone(), None));
}
if let Some(new_id) = new_element_id {
events.push(OutgoingEvent::generic("focused", new_id.clone(), None));
}
HandleResult::emit(events)
}
_ => HandleResult::Fallthrough,
}
}
pub fn set_pending_focus(&mut self, element_id: &str) {
if let Some(key) = self
.interactions
.keys()
.find(|(_, nid)| element_id.starts_with(nid.as_str()))
.cloned()
{
self.pending_focus.insert(key, element_id.to_string());
}
}
pub fn prune_stale(&mut self, live_ids: &std::collections::HashSet<(String, String)>) {
self.layer_caches.retain(|k, _| live_ids.contains(k));
self.interactions.retain(|k, _| live_ids.contains(k));
self.pending_focus.retain(|k, _| live_ids.contains(k));
}
}
impl<R: PlushieRenderer> Default for CanvasEngine<R> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::{Props, TreeNode};
use serde_json::{Value, json};
fn node(id: &str, type_name: &str, props: Value, children: Vec<TreeNode>) -> TreeNode {
TreeNode {
id: id.to_string(),
type_name: type_name.to_string(),
props: Props::from_json(props),
children,
}
}
fn canvas_node(id: &str) -> TreeNode {
node(
id,
"canvas",
json!({}),
vec![node(
&format!("{id}-button"),
"group",
json!({
"on_click": true,
"focusable": true,
"a11y": {
"role": "button",
"label": "Button"
}
}),
vec![node(
&format!("{id}-button-hitbox"),
"rect",
json!({
"width": 20,
"height": 20
}),
vec![],
)],
)],
)
}
#[test]
fn canvas_theme_hash_changes_with_palette_colors() {
let light = iced::Theme::Light;
let dark = iced::Theme::Dark;
assert_ne!(
light.palette().background.base.color,
dark.palette().background.base.color
);
assert_ne!(canvas_theme_hash(&light), canvas_theme_hash(&dark));
}
#[test]
fn layer_cache_tracks_theme_hash_separately_from_content_hash() {
let cache = CanvasLayerCache::<iced::Renderer>::new(10);
assert_eq!(cache.theme_hash.get(), 0);
cache.ensure_theme_hash(20);
assert_eq!(cache.theme_hash.get(), 20);
cache.ensure_theme_hash(20);
assert_eq!(cache.theme_hash.get(), 20);
}
#[test]
fn prune_stale_removes_cached_state_for_missing_canvas_nodes() {
let mut engine = CanvasEngine::<iced::Renderer>::new();
let stale = canvas_node("canvas-stale");
let live = canvas_node("canvas-live");
let stale_key = ("window-a".to_string(), stale.id.clone());
let live_key = ("window-a".to_string(), live.id.clone());
engine.prepare(&stale, "window-a");
engine.prepare(&live, "window-a");
engine.set_pending_focus("canvas-stale-button");
engine.set_pending_focus("canvas-live-button");
assert!(engine.layer_caches.contains_key(&stale_key));
assert!(engine.layer_caches.contains_key(&live_key));
assert!(!engine.interactions[&stale_key].is_empty());
assert!(!engine.interactions[&live_key].is_empty());
assert_eq!(
engine.pending_focus.get(&stale_key),
Some(&"canvas-stale-button".to_string())
);
engine.prune_stale(&std::collections::HashSet::from([live_key.clone()]));
assert!(!engine.layer_caches.contains_key(&stale_key));
assert!(!engine.interactions.contains_key(&stale_key));
assert!(!engine.pending_focus.contains_key(&stale_key));
assert!(engine.layer_caches.contains_key(&live_key));
assert!(!engine.interactions[&live_key].is_empty());
assert_eq!(
engine.pending_focus.get(&live_key),
Some(&"canvas-live-button".to_string())
);
}
}