use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use iced::widget::canvas as iced_canvas;
use iced::widget::{combo_box, markdown, pane_grid, text_editor};
use serde_json::Value;
use crate::protocol::TreeNode;
pub(crate) const MAX_TREE_DEPTH: usize = 256;
const MAX_HASH_DEPTH: usize = 256;
macro_rules! define_caches {
($($(#[$meta:meta])* $field:ident : $value:ty),* $(,)?) => {
pub struct WidgetCaches {
$($(#[$meta])* pub(crate) $field: HashMap<String, $value>,)*
pub extension: crate::extensions::ExtensionCaches,
}
impl WidgetCaches {
pub fn new() -> Self {
Self {
$($field: HashMap::new(),)*
extension: crate::extensions::ExtensionCaches::new(),
}
}
pub fn clear_builtin(&mut self) {
$(self.$field.clear();)*
}
fn prune_stale(&mut self, live_ids: &HashSet<String>) {
$(self.$field.retain(|id, _| live_ids.contains(id));)*
}
}
};
}
define_caches! {
editor_contents: text_editor::Content,
editor_content_hashes: u64,
markdown_items: (u64, Vec<markdown::Item>),
combo_states: combo_box::State<String>,
combo_options: Vec<String>,
pane_grid_states: pane_grid::State<String>,
canvas_caches: HashMap<String, (u64, iced_canvas::Cache)>,
canvas_interactions: Vec<super::canvas::InteractiveShape>,
qr_code_caches: (u64, iced_canvas::Cache),
themer_themes: iced::Theme,
style_overrides: (u64, super::helpers::StyleOverrides),
}
impl Default for WidgetCaches {
fn default() -> Self {
Self::new()
}
}
impl WidgetCaches {
pub fn clear(&mut self) {
self.clear_builtin();
self.extension.clear();
}
pub fn editor_content_mut(&mut self, id: &str) -> Option<&mut text_editor::Content> {
self.editor_contents.get_mut(id)
}
pub fn pane_grid_state_mut(&mut self, id: &str) -> Option<&mut pane_grid::State<String>> {
self.pane_grid_states.get_mut(id)
}
pub fn pane_grid_state(&self, id: &str) -> Option<&pane_grid::State<String>> {
self.pane_grid_states.get(id)
}
}
pub fn ensure_caches(node: &TreeNode, caches: &mut WidgetCaches) {
let mut live_ids = HashSet::new();
ensure_caches_walk(node, caches, &mut live_ids, 0);
caches.prune_stale(&live_ids);
}
fn ensure_caches_walk(
node: &TreeNode,
caches: &mut WidgetCaches,
live_ids: &mut HashSet<String>,
depth: usize,
) {
if depth > MAX_TREE_DEPTH {
log::warn!(
"[id={}] ensure_caches depth exceeds {MAX_TREE_DEPTH}, skipping subtree",
node.id
);
return;
}
live_ids.insert(node.id.clone());
match node.type_name.as_str() {
"text_editor" => super::input::ensure_text_editor_cache(node, caches),
"markdown" => super::display::ensure_markdown_cache(node, caches),
"combo_box" => super::input::ensure_combo_box_cache(node, caches),
"pane_grid" => super::layout::ensure_pane_grid_cache(node, caches),
"canvas" => super::canvas::ensure_canvas_cache(node, caches),
"themer" => super::interactive::ensure_themer_cache(node, caches),
"qr_code" => super::display::ensure_qr_code_cache(node, caches),
_ => {}
}
ensure_style_overrides_cache(node, caches);
for child in &node.children {
ensure_caches_walk(child, caches, live_ids, depth + 1);
}
}
pub(crate) fn canvas_layer_map(
props: Option<&serde_json::Map<String, Value>>,
) -> std::collections::BTreeMap<String, &Value> {
let mut map = std::collections::BTreeMap::new();
if let Some(layers_obj) = props
.and_then(|p| p.get("layers"))
.and_then(|v| v.as_object())
{
for (name, shapes_val) in layers_obj {
map.insert(name.clone(), shapes_val);
}
} else if let Some(shapes_arr) = props.and_then(|p| p.get("shapes")) {
map.insert("default".to_string(), shapes_arr);
}
map
}
fn ensure_style_overrides_cache(node: &TreeNode, caches: &mut WidgetCaches) {
let style_val = match node.props.get("style").and_then(|v| v.as_object()) {
Some(obj) => obj,
None => return,
};
let mut hasher = DefaultHasher::new();
hash_json_value(&serde_json::Value::Object(style_val.clone()), &mut hasher);
let hash = hasher.finish();
if let Some((cached_hash, _)) = caches.style_overrides.get(&node.id)
&& *cached_hash == hash
{
return;
}
let overrides = super::helpers::parse_style_overrides(style_val);
caches
.style_overrides
.insert(node.id.clone(), (hash, overrides));
}
pub(crate) fn cached_style_overrides<'a>(
caches: &'a WidgetCaches,
node_id: &str,
) -> Option<&'a super::helpers::StyleOverrides> {
caches.style_overrides.get(node_id).map(|(_, ov)| ov)
}
pub(crate) fn hash_json_value(v: &serde_json::Value, h: &mut impl std::hash::Hasher) {
hash_json_value_inner(v, h, 0);
}
fn hash_json_value_inner(v: &serde_json::Value, h: &mut impl std::hash::Hasher, depth: usize) {
if depth > MAX_HASH_DEPTH {
6u8.hash(h);
return;
}
match v {
serde_json::Value::Null => 0u8.hash(h),
serde_json::Value::Bool(b) => {
1u8.hash(h);
b.hash(h);
}
serde_json::Value::Number(n) => {
2u8.hash(h);
if let Some(f) = n.as_f64() {
f.to_bits().hash(h);
} else if let Some(i) = n.as_i64() {
i.hash(h);
} else if let Some(u) = n.as_u64() {
u.hash(h);
}
}
serde_json::Value::String(s) => {
3u8.hash(h);
s.hash(h);
}
serde_json::Value::Array(arr) => {
4u8.hash(h);
arr.len().hash(h);
for item in arr {
hash_json_value_inner(item, h, depth + 1);
}
}
serde_json::Value::Object(obj) => {
5u8.hash(h);
obj.len().hash(h);
for (k, v) in obj {
k.hash(h);
hash_json_value_inner(v, h, depth + 1);
}
}
}
}
pub(crate) fn hash_str(s: &str) -> u64 {
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn widget_caches_new_is_empty() {
let c = WidgetCaches::new();
assert!(c.editor_contents.is_empty());
assert!(c.markdown_items.is_empty());
assert!(c.combo_states.is_empty());
assert!(c.combo_options.is_empty());
assert!(c.pane_grid_states.is_empty());
}
#[test]
fn widget_caches_clear_empties_maps() {
let mut c = WidgetCaches::new();
c.combo_options.insert("x".into(), vec!["a".into()]);
c.clear();
assert!(c.combo_options.is_empty());
}
#[test]
fn clear_builtin_preserves_extension_caches() {
let mut caches = WidgetCaches::new();
caches
.editor_contents
.insert("ed1".to_string(), iced::widget::text_editor::Content::new());
caches.extension.insert("ext", "key", 42u32);
caches.clear_builtin();
assert!(caches.editor_contents.is_empty());
assert_eq!(caches.extension.get::<u32>("ext", "key"), Some(&42));
}
#[test]
fn clear_wipes_both_builtin_and_extension() {
let mut caches = WidgetCaches::new();
caches
.editor_contents
.insert("ed1".to_string(), iced::widget::text_editor::Content::new());
caches.extension.insert("ext", "key", 42u32);
caches.clear();
assert!(caches.editor_contents.is_empty());
assert!(!caches.extension.contains("ext", "key"));
}
#[test]
fn hash_json_value_same_input_same_hash() {
use std::collections::hash_map::DefaultHasher;
let val = serde_json::json!({"shapes": [{"type": "rect", "x": 0, "y": 0}]});
let h1 = {
let mut h = DefaultHasher::new();
hash_json_value(&val, &mut h);
h.finish()
};
let h2 = {
let mut h = DefaultHasher::new();
hash_json_value(&val, &mut h);
h.finish()
};
assert_eq!(h1, h2);
}
#[test]
fn hash_json_value_different_input_different_hash() {
use std::collections::hash_map::DefaultHasher;
let a = serde_json::json!({"type": "rect"});
let b = serde_json::json!({"type": "circle"});
let hash_a = {
let mut h = DefaultHasher::new();
hash_json_value(&a, &mut h);
h.finish()
};
let hash_b = {
let mut h = DefaultHasher::new();
hash_json_value(&b, &mut h);
h.finish()
};
assert_ne!(hash_a, hash_b);
}
#[test]
fn hash_json_value_type_discrimination() {
use std::collections::hash_map::DefaultHasher;
let vals = [
serde_json::json!(null),
serde_json::json!(false),
serde_json::json!(0),
serde_json::json!(""),
serde_json::json!([]),
serde_json::json!({}),
];
let hashes: Vec<u64> = vals
.iter()
.map(|v| {
let mut h = DefaultHasher::new();
hash_json_value(v, &mut h);
h.finish()
})
.collect();
for (i, h1) in hashes.iter().enumerate() {
for (j, h2) in hashes.iter().enumerate() {
if i != j {
assert_ne!(h1, h2, "type {i} and {j} should hash differently");
}
}
}
}
}