From b6fa841d11a0a503160c90deb81d7a1d91337619 Mon Sep 17 00:00:00 2001 From: Glint Dev Date: Fri, 24 Jul 2026 19:53:02 +0300 Subject: [PATCH] Phase 9: Keyed widgets (9.1 + 9.2) 9.1: Add content_hash field to Element, computed during VDOM evaluation from type_name + properties + children content_hashes via DefaultHasher. 9.2: Assign stable iced::widget::Id to text_input and scrollable widgets so Iced preserves widget state (cursor position, scroll offset) across frames. Ids are cached in a thread_local HashMap to avoid leaks. 9.3 skipped: iced::Element is not Clone and contains Box, making element-level caching impractical without unsafe lifetime hacks. --- src/interpreter/mod.rs | 17 +++++++++++++++++ src/interpreter/types.rs | 3 +++ src/renderer.rs | 21 ++++++++++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 66cbed3..a1c61c9 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -7,6 +7,7 @@ pub mod style; pub mod types; use std::borrow::Cow; +use std::hash::{Hash, Hasher}; pub use rhei::RheiContext; use style::StyleSheet as SS; pub use types::{ComponentDef, Document, Element, Interner, InterpError}; @@ -476,6 +477,7 @@ impl Interpreter { vcomp.children = Self::evaluate_vdom_incr( &comp_child_refs, variables, components, rhei, stylesheet, &child_ancestors, dirty_set, ); + vcomp.content_hash = Self::compute_content_hash(&vcomp); output.push(vcomp); for (k, old) in old_vals.into_iter().rev() { @@ -509,6 +511,7 @@ impl Interpreter { vnode.children = Self::evaluate_vdom_incr( &child_refs, variables, components, rhei, stylesheet, &child_ancestors, dirty_set, ); + vnode.content_hash = Self::compute_content_hash(&vnode); output.push(vnode); } } @@ -518,6 +521,20 @@ impl Interpreter { output } + fn compute_content_hash(el: &Element) -> u64 { + use std::collections::hash_map::DefaultHasher; + let mut h = DefaultHasher::new(); + el.type_name.hash(&mut h); + for (k, v) in &el.properties { + k.hash(&mut h); + v.hash(&mut h); + } + for child in &el.children { + child.content_hash.hash(&mut h); + } + h.finish() + } + fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec { let mut chain = ancestors.to_vec(); let id = el.id().map(String::from); diff --git a/src/interpreter/types.rs b/src/interpreter/types.rs index 17b3e8b..30ae190 100644 --- a/src/interpreter/types.rs +++ b/src/interpreter/types.rs @@ -167,6 +167,7 @@ pub struct Element<'a> { pub children: Vec>, pub computed_style: ComputedStyle, pub element_id: ElementId, + pub content_hash: u64, } impl<'a> Element<'a> { @@ -183,6 +184,7 @@ impl<'a> Element<'a> { children: Vec::with_capacity(4), computed_style: ComputedStyle::default(), element_id: ElementId(u32::MAX), + content_hash: 0, } } @@ -193,6 +195,7 @@ impl<'a> Element<'a> { children: Vec::with_capacity(4), computed_style: ComputedStyle::default(), element_id: id, + content_hash: 0, } } } diff --git a/src/renderer.rs b/src/renderer.rs index af60c1b..d0dc12a 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -9,8 +9,25 @@ use iced::widget::{ use iced::{Alignment, Background, Border, Length, Theme}; use iced::font::Weight; use std::borrow::Cow; +use std::cell::RefCell; use std::collections::HashMap; +thread_local! { + static WIDGET_ID_CACHE: RefCell> = RefCell::new(HashMap::new()); +} + +fn get_or_create_widget_id(key: u32, prefix: &'static str) -> iced::widget::Id { + WIDGET_ID_CACHE.with(|cache| { + cache.borrow_mut() + .entry((key, prefix)) + .or_insert_with(|| { + let s = Box::leak(format!("{prefix}:{key}").into_boxed_str()); + iced::widget::Id::new(s) + }) + .clone() + }) +} + fn extract_var_binding(el: &Element, prop: &str) -> Option { el.properties.iter() .find_map(|(k, v)| { @@ -378,7 +395,8 @@ fn apply_universal_box_model<'a>( } if let Some(sid) = scrollable_id { - s = s.on_scroll(move |viewport| Message::ScrollableScrolled(sid, viewport.absolute_offset().y)); + s = s.id(get_or_create_widget_id(sid as u32, "sc")) + .on_scroll(move |viewport| Message::ScrollableScrolled(sid, viewport.absolute_offset().y)); } s.into() @@ -1167,6 +1185,7 @@ fn render_input<'a>( let mut input = text_input(&placeholder, &value) .on_input(move |v| Message::InputChanged(var_name.clone(), v)) + .id(get_or_create_widget_id(el.element_id.0, "ti")) .padding(padding); if let Some(w) = cs.width {