172 lines
5.1 KiB
Rust
172 lines
5.1 KiB
Rust
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());
|
|
}
|
|
}
|