Phase 3.1-3.3: ReactiveTracker with ElementId assignment during parsing, dependency scanning, variable change notification in app.rs

This commit is contained in:
Glint Dev
2026-07-21 20:38:41 +03:00
parent a46c9bc9e7
commit d8d3f57a21
4 changed files with 249 additions and 20 deletions

View File

@@ -19,23 +19,36 @@ impl GlintApp {
pub fn update(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::WindowScrolled(y) => {
self.doc.variables.insert("__scroll_y".to_string(), Value::Float(y as f64));
let var = "__scroll_y".to_string();
self.doc.variables.insert(var.clone(), Value::Float(y as f64));
self.doc.tracker.on_variable_changed(&var);
}
Message::EventTriggered(script) => {
if !script.is_empty() {
let old_vars: Vec<String> = self.doc.variables.keys().cloned().collect();
self.rhei.execute_action(&script, &mut self.doc.variables);
let new_vars: Vec<String> = self.doc.variables.keys().cloned().collect();
for v in &new_vars {
if !old_vars.contains(v) || self.doc.variables.get(v) != old_vars.iter().find_map(|k| if k == v { self.doc.variables.get(k) } else { None }) {
self.doc.tracker.on_variable_changed(v);
}
}
}
}
Message::InputChanged(Some(var), val) => {
self.doc.variables.insert(var, Value::from(val));
self.doc.variables.insert(var.clone(), Value::from(val));
self.doc.tracker.on_variable_changed(&var);
}
Message::ToggleChanged(Some(var), val) => {
self.doc.variables.insert(var, Value::Bool(val));
self.doc.variables.insert(var.clone(), Value::Bool(val));
self.doc.tracker.on_variable_changed(&var);
}
Message::SliderChanged(Some(var), val) => {
self.doc.variables.insert(var.clone(), Value::Float(val));
self.doc.tracker.on_variable_changed(&var);
if var == "volume_level" {
self.doc.variables.insert("age".to_string(), Value::Float(val));
self.doc.tracker.on_variable_changed("age");
}
}
_ => {}

View File

@@ -1,5 +1,6 @@
pub mod opcodes;
pub mod reactive;
pub mod reader;
pub mod rhei;
pub mod style;
@@ -9,6 +10,7 @@ use std::borrow::Cow;
pub use rhei::RheiContext;
use style::StyleSheet as SS;
pub use types::{ComponentDef, Document, Element, Interner, InterpError};
pub use reactive::{ElementId, ReactiveTracker};
use compact_str::CompactString;
use opcodes::*;
@@ -31,6 +33,7 @@ impl Interpreter {
let mut components = HashMap::new();
let mut rhei_scripts: Vec<String> = Vec::new();
let mut stylesheet = SS::new();
let mut tracker = ReactiveTracker::new();
let roots = Self::parse_block_elements(
&mut r,
@@ -38,13 +41,14 @@ impl Interpreter {
&mut components,
&mut rhei_scripts,
&mut stylesheet,
&mut tracker,
true,
)?;
//let rhei_ctx = RheiContext::new(&rhei_scripts);
//rhei_ctx.initialize(&mut variables);
Ok(Document { roots, components, variables, rhei_scripts, stylesheet, interner: Interner::new() })
Ok(Document { roots, components, variables, rhei_scripts, stylesheet, interner: Interner::new(), tracker })
}
fn parse_block_elements<'a>(
@@ -53,6 +57,7 @@ impl Interpreter {
components: &mut HashMap<String, ComponentDef<'a>>,
rhei_scripts: &mut Vec<String>,
stylesheet: &mut SS,
tracker: &mut ReactiveTracker,
is_root: bool,
) -> Result<Vec<Element<'a>>, InterpError> {
let mut roots: Vec<Element> = Vec::new();
@@ -63,8 +68,12 @@ impl Interpreter {
if !is_root && op == OP_END_BLOCK { break; }
match op {
OP_ELEM_PUSH => stack.push(Element::new(r.read_str_ref()?)),
OP_ELEM_PUSH => {
let elem_id = tracker.alloc_id();
let mut el = Element::new(r.read_str_ref()?);
el.element_id = elem_id;
stack.push(el);
}
OP_ELEM_POP => {
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
Self::attach(&mut stack, &mut roots, finished);
@@ -96,10 +105,12 @@ impl Interpreter {
val.push_str(RHEI_PREFIX);
val.push_str(expr_ref);
if let Some(el) = stack.last_mut() {
tracker.scan_value(el.element_id, &val);
el.push_prop("text".to_string(), val);
}
} else if let Some(value) = r.read_value_as_string(vop)? {
if let Some(el) = stack.last_mut() {
tracker.scan_value(el.element_id, &value);
el.push_prop("text".to_string(), value);
}
}
@@ -108,7 +119,10 @@ impl Interpreter {
OP_PROP_STR => {
let key = r.read_str_ref()?;
let val = r.read_str_ref()?;
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
if let Some(el) = stack.last_mut() {
tracker.scan_value(el.element_id, val);
el.push_prop(key, val);
}
}
OP_PROP_VAR => {
let key = r.read_str_ref()?;
@@ -116,22 +130,31 @@ impl Interpreter {
let mut val = String::with_capacity(val_ref.len() + 1);
val.push('$');
val.push_str(val_ref);
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
if let Some(el) = stack.last_mut() {
tracker.scan_value(el.element_id, &val);
el.push_prop(key, val);
}
}
OP_PROP_INT => {
let key = r.read_str_ref()?;
let val = r.read_i64()?.to_string();
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
if let Some(el) = stack.last_mut() {
el.push_prop(key, val);
}
}
OP_PROP_FLOAT => {
let key = r.read_str_ref()?;
let val = r.read_f64()?.to_string();
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
if let Some(el) = stack.last_mut() {
el.push_prop(key, val);
}
}
OP_PROP_BOOL => {
let key = r.read_str_ref()?;
let val = (r.read_byte()? != 0).to_string();
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
if let Some(el) = stack.last_mut() {
el.push_prop(key, val);
}
}
OP_PROP_RHEI => {
let key = r.read_str_ref()?;
@@ -140,13 +163,16 @@ impl Interpreter {
val.push_str(RHEI_PREFIX);
val.push_str(expr_ref);
if let Some(el) = stack.last_mut() {
tracker.scan_value(el.element_id, &val);
el.push_prop(key, val);
}
}
OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
let key = r.read_str_ref()?;
if let Some(val) = r.read_value_as_string(op)? {
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
if let Some(el) = stack.last_mut() {
el.push_prop(key, val);
}
}
}
@@ -156,10 +182,10 @@ impl Interpreter {
rhei_scripts.push(script);
} else {
let mut text_el = Element::new("#text");
text_el.push_prop(
"text".to_string(),
format!("{RHEI_PREFIX}{script}"),
);
text_el.element_id = tracker.alloc_id();
let full_val = format!("{RHEI_PREFIX}{script}");
tracker.scan_value(text_el.element_id, &full_val);
text_el.push_prop("text".to_string(), full_val);
Self::attach(&mut stack, &mut roots, text_el);
}
}
@@ -171,7 +197,7 @@ impl Interpreter {
.map(|_| Ok((r.read_string()?, r.read_string()?)))
.collect::<Result<Vec<_>, InterpError>>()?;
let children = Self::parse_block_elements(
r, variables, components, rhei_scripts, stylesheet, false,
r, variables, components, rhei_scripts, stylesheet, tracker, false,
)?;
components.insert(name.clone(), ComponentDef { name, params, children });
}
@@ -186,23 +212,26 @@ impl Interpreter {
};
let true_children = Self::parse_block_elements(
r, variables, components, rhei_scripts, stylesheet, false,
r, variables, components, rhei_scripts, stylesheet, tracker, false,
)?;
let has_else = r.read_byte()? == 1;
let false_children = if has_else {
Self::parse_block_elements(
r, variables, components, rhei_scripts, stylesheet, false,
r, variables, components, rhei_scripts, stylesheet, tracker, false,
)?
} else {
Vec::new()
};
let mut if_el = Element::new("@if");
if_el.element_id = tracker.alloc_id();
tracker.scan_value(if_el.element_id, &cond_val);
if_el.push_prop("condition", cond_val);
if_el.children = true_children;
if !false_children.is_empty() {
let mut else_el = Element::new("@else");
else_el.element_id = tracker.alloc_id();
else_el.children = false_children;
if_el.children.push(else_el);
}
@@ -221,10 +250,12 @@ impl Interpreter {
};
let block_children = Self::parse_block_elements(
r, variables, components, rhei_scripts, stylesheet, false,
r, variables, components, rhei_scripts, stylesheet, tracker, false,
)?;
let mut each_el = Element::new("@each");
each_el.element_id = tracker.alloc_id();
tracker.scan_value(each_el.element_id, &source_val);
each_el.push_prop("var_name".to_string(), var_name);
each_el.push_prop("source".to_string(), source_val);
each_el.children = block_children;

171
src/interpreter/reactive.rs Normal file
View File

@@ -0,0 +1,171 @@
use std::collections::{HashMap, HashSet};
/// Unique identifier for an element in the VDOM tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ElementId(pub u32);
/// Tracks which variables affect which elements for incremental VDOM updates.
#[derive(Debug, Clone)]
pub struct ReactiveTracker {
/// For each variable name, which elements depend on it
subscribers: HashMap<String, HashSet<ElementId>>,
/// For each element, which variables it depends on
dependencies: HashMap<ElementId, HashSet<String>>,
/// Elements that need to be re-evaluated
dirty_set: HashSet<ElementId>,
next_id: u32,
}
impl ReactiveTracker {
pub fn new() -> Self {
Self {
subscribers: HashMap::new(),
dependencies: HashMap::new(),
dirty_set: HashSet::new(),
next_id: 0,
}
}
pub fn alloc_id(&mut self) -> ElementId {
let id = ElementId(self.next_id);
self.next_id += 1;
id
}
/// Register that `element` depends on `var_name`.
/// Call this during template parsing for each `$var` or `!rhee:{expr}` reference.
pub fn add_dependency(&mut self, element: ElementId, var_name: &str) {
self.subscribers.entry(var_name.to_string())
.or_default()
.insert(element);
self.dependencies.entry(element)
.or_default()
.insert(var_name.to_string());
}
/// Register multiple dependencies for an element from a property value.
pub fn scan_value(&mut self, element: ElementId, value: &str) {
// Scan for `$var` patterns
let mut chars = value.char_indices().peekable();
while let Some((_, c)) = chars.next() {
if c == '$' {
let mut var_name = String::new();
while let Some(&(_, next_c)) = chars.peek() {
if next_c.is_ascii_alphanumeric() || next_c == '_' {
var_name.push(chars.next().unwrap().1);
} else {
break;
}
}
if !var_name.is_empty() {
self.add_dependency(element, &var_name);
}
}
}
}
/// Called when a variable changes. Marks affected elements as dirty.
pub fn on_variable_changed(&mut self, name: &str) -> &HashSet<ElementId> {
if let Some(affected) = self.subscribers.get(name) {
self.dirty_set.extend(affected.iter());
}
&self.dirty_set
}
/// Returns the current dirty set and clears it.
pub fn take_dirty_set(&mut self) -> HashSet<ElementId> {
std::mem::take(&mut self.dirty_set)
}
/// Returns true if the element needs re-evaluation.
pub fn is_dirty(&self, id: ElementId) -> bool {
self.dirty_set.contains(&id)
}
/// Clear all tracking data (e.g., on template reload).
pub fn reset(&mut self) {
self.subscribers.clear();
self.dependencies.clear();
self.dirty_set.clear();
self.next_id = 0;
}
}
impl Default for ReactiveTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_dependency_tracking() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
let e2 = tracker.alloc_id();
tracker.add_dependency(e1, "volume");
tracker.add_dependency(e2, "volume");
tracker.add_dependency(e2, "brightness");
let affected = tracker.on_variable_changed("volume");
assert!(affected.contains(&e1));
assert!(affected.contains(&e2));
let affected = tracker.on_variable_changed("brightness");
assert!(!affected.contains(&e1));
assert!(affected.contains(&e2));
}
#[test]
fn test_scan_value() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
tracker.scan_value(e1, "Hello $name, you are $age years old");
let affected = tracker.on_variable_changed("name");
assert!(affected.contains(&e1));
let affected = tracker.on_variable_changed("age");
assert!(affected.contains(&e1));
}
#[test]
fn test_scan_no_vars() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
tracker.scan_value(e1, "Hello world");
let affected = tracker.on_variable_changed("name");
assert!(!affected.contains(&e1));
}
#[test]
fn test_take_dirty_set() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
tracker.add_dependency(e1, "x");
tracker.on_variable_changed("x");
let dirty = tracker.take_dirty_set();
assert_eq!(dirty.len(), 1);
assert!(tracker.dirty_set.is_empty());
}
#[test]
fn test_reset() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
tracker.add_dependency(e1, "x");
tracker.reset();
assert!(tracker.subscribers.is_empty());
assert!(tracker.dependencies.is_empty());
assert!(tracker.dirty_set.is_empty());
}
}

View File

@@ -3,6 +3,7 @@ use std::collections::HashMap;
use std::fmt;
use super::reactive::{ElementId, ReactiveTracker};
use super::style::{ComputedStyle, StyleSheet};
use compact_str::CompactString;
@@ -164,6 +165,7 @@ pub struct Element<'a> {
pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>,
pub children: Vec<Element<'a>>,
pub computed_style: ComputedStyle,
pub element_id: ElementId,
}
impl<'a> Element<'a> {
@@ -179,6 +181,17 @@ impl<'a> Element<'a> {
properties: Vec::with_capacity(8),
children: Vec::with_capacity(4),
computed_style: ComputedStyle::default(),
element_id: ElementId(u32::MAX),
}
}
pub fn new_with_id(type_name: &'a str, id: ElementId) -> Self {
Self {
type_name,
properties: Vec::with_capacity(8),
children: Vec::with_capacity(4),
computed_style: ComputedStyle::default(),
element_id: id,
}
}
}
@@ -198,6 +211,7 @@ pub struct Document<'a> {
pub rhei_scripts: Vec<String>,
pub stylesheet: StyleSheet,
pub interner: Interner,
pub tracker: ReactiveTracker,
}
#[derive(Debug)]