Phase 3.1-3.3: ReactiveTracker with ElementId assignment during parsing, dependency scanning, variable change notification in app.rs
This commit is contained in:
@@ -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
171
src/interpreter/reactive.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user