docs: reorganize into en/ru and add English translation

This commit is contained in:
Glint Dev
2026-07-24 22:04:29 +03:00
parent 938f05f59d
commit 35f92435a3
20 changed files with 4651 additions and 0 deletions

182
docs/en/modules/01-types.md Normal file
View File

@@ -0,0 +1,182 @@
# Type module: `src/interpreter/types.rs`
Runtime base data types: string interning, Value, DOM elements, flat VDOM, and Document.
---
## `InternedStr(u32)`
Newtype wrapper over `u32`. Compact string identifier for fast comparisons.
```rust
pub struct InternedStr(pub u32);
```
**Methods:**
- `from_raw(id: u32) -> Self` — const constructor
- `raw(&self) -> u32` — raw value
- `eq_str(&self, other: &str) -> bool` — comparison with a string via `Interner::lookup`
**Status:** defined, but not used in hot paths (Element, style matching, properties). Phase 2 not completed.
---
## `Interner`
String pool with unique ID allocation. Each string is interned once.
```rust
pub struct Interner {
strings: Vec<String>,
map: HashMap<String, u32>,
next_id: u32,
}
```
- `new()` — empty interner
- `intern(&mut self, s: &str) -> InternedStr` — get or create an ID
- `lookup(&self, id: InternedStr) -> &str` — get a string by ID
- `intern_or_none(&mut self, s: Option<&str>) -> Option<InternedStr>` — optional interning
Stored in `Document::interner`. Accessible via `with_interner()` (thread_local).
---
## `Value`
Typed variable value. Replaces raw strings to eliminate parse/format round-trip.
```rust
pub enum Value {
Str(CompactString),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<Value>),
None,
}
```
**Methods:**
- `as_str(&self) -> Option<&str>` — borrow a string
- `to_owned_string(&self) -> CompactString` — format to string (used in renderer)
**Implemented:** `From<&str>`, `From<String>`, `From<i64>`, `From<f64>`, `From<bool>`, `From<Vec<T>>`.
`PartialEq` — Float is compared with `f64::EPSILON`.
**Usage:** `Document::variables`, `Value↔Dynamic` conversions in rhei.rs.
**NOT used:** in styles (`ComputedStyle::compute` still takes `&str`, matched_sheets uses `HashMap<String, String>`).
---
## `Element<'a>`
VDOM tree node. The central type — the UI is built from it.
```rust
pub struct Element<'a> {
pub type_name: &'a str, // "Button", "Panel", "Text", etc.
pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>, // (key, value)
pub children: Vec<Element<'a>>,
pub computed_style: ComputedStyle,
pub element_id: ElementId,
pub content_hash: u64, // Phase 9.1: hash for widget cache
}
```
**Methods (in types.rs):**
- `id(&self) -> Option<&str>` — value of the HTML attribute `id`
- `new(type_name)` — fresh element with `element_id: u32::MAX`
- `new_with_id(type_name, id)` — with a given ElementId
**Methods (in renderer.rs):**
- `get_prop(&self, key: &str) -> Option<&str>` — property value by key
- `push_prop(key, val)` — add a property
- `set_prop(key, val)` — set or overwrite a property
---
## `ComponentDef<'a>`
Component definition from a template.
```rust
pub struct ComponentDef<'a> {
pub name: String,
pub params: Vec<(String, String)>,
pub children: Vec<Element<'a>>,
}
```
Stored in `Document::components`. Used in `evaluate_vdom` when expanding
components: parameters are passed via variables, the component body is inserted as children.
---
## `Document<'a>`
Complete application state after bytecode loading.
```rust
pub struct Document<'a> {
pub roots: Vec<Element<'a>>,
pub components: HashMap<String, ComponentDef<'a>>,
pub variables: HashMap<String, Value>,
pub rhei_scripts: Vec<String>,
pub stylesheet: StyleSheet,
pub interner: Interner,
pub tracker: ReactiveTracker,
}
```
Created in `Interpreter::run()`. Cloned at Iced application startup (boot function).
Contains everything needed for interpretation: tree, styles, variables, reactivity.
---
## `VNode<'a>` and `FlatVDom<'a>`
Flat VDOM representation for efficient serialization/deserialization.
```rust
pub struct VNode<'a> {
pub id: NodeId,
pub type_name: &'a str,
pub properties: Range<PropertyIdx>,
pub children_range: Range<NodeIdx>,
pub computed_style: ComputedStyle,
pub element_id: ElementId,
}
pub struct FlatVDom<'a> {
pub nodes: Vec<VNode<'a>>,
pub properties: Vec<(String, String)>,
root_indices: Vec<NodeIdx>,
}
```
**Methods:**
- `new()` — empty
- `from_elements(elements)` — recursively flattens an Element tree into VNodes
- `into_elements(self) -> Vec<Element>` — reverse assembly
- `get_node(idx)`, `node_count()`, `root_count()`, `root_indices()`
**Usage:** in tests (`test_flat_vdom_roundtrip`, `test_flat_vdom_nested`, `test_flat_vdom_empty`).
Not in the hot path — VDOM is passed as `Vec<Element>`.
---
## `InterpError`
Bytecode loading errors.
```rust
pub enum InterpError {
BadMagic,
UnexpectedEof,
InvalidUtf8,
UnexpectedPop,
}
```
Implements `Display` and `std::error::Error`. Returned from `Interpreter::run()`.

482
docs/en/modules/02-style.md Normal file
View File

@@ -0,0 +1,482 @@
# Style Module: `src/interpreter/style.rs`
A CSS-like style system for Glint: selector parsing, index building, cascading property resolution, and computed style caching.
---
## Helper Enums
### `SizeValue`
```rust
pub enum SizeValue {
Px(f32),
Percent(f32),
}
```
An absolute (`Px`) or relative (`Percent`) size value.
```rust
impl SizeValue {
pub fn resolve(self, relative_to: Option<f32>) -> f32
}
```
`Percent` resolves relative to `relative_to`; `Px` returns as-is. When `Percent` and `relative_to = None`, the percentage is returned as a number.
### `Overflow`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Overflow {
#[default] Visible,
Hidden,
Scroll,
Auto,
}
```
Used for `overflow-x`, `overflow-y`.
### `Position`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Position {
#[default] Static,
Relative,
Absolute,
Sticky,
Fixed,
}
```
Defines the element positioning scheme.
### `Display`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Display {
#[default] Block,
Flex,
Grid,
Inline,
None,
}
```
### `LayoutDirection`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutDirection {
Column,
Row,
Grid,
}
```
### `ContentAlign`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentAlign {
Start,
Center,
End,
}
```
### `TextAlign`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
}
impl From<TextAlign> for iced::alignment::Horizontal
```
---
## Selector Parsing
### `AttributeSelector`
```rust
pub enum AttributeSelector {
Exists(String),
Equals(String, String),
}
```
Attribute selector: `[attr]` or `[attr=value]`.
### `Combinator`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Combinator {
Descendant, // space
Child, // >
NextSibling, // +
Subsequent, // ~
}
```
### `split_selectors(input: &str) -> Vec<String>`
Splits a selector group by comma, accounting for nested brackets. For example, `"Button, Label:hover"``["Button", "Label:hover"]`.
### `CompoundSelector`
```rust
#[derive(Debug, Clone)]
pub struct CompoundSelector {
pub tag: Option<String>,
pub id: Option<String>,
pub classes: Vec<String>,
pub pseudo_classes: Vec<String>,
pub attributes: Vec<AttributeSelector>,
}
```
**Methods:**
- `CompoundSelector::parse(input: &str) -> Self` — parses a simple selector like `Button#id.primary:hover[named=val]`. Processes character by character, grouping parts by the first symbol (`#`, `.`, `:`, `[`).
- `fn specificity(&self) -> (u32, u32, u32)` — returns specificity per CSS rule: (id, class+attr+pseudo, tag).
- `pub fn matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) -> bool` — checks whether an element matches this simple selector. Considers:
- `tag` match (or `*`)
- `id` match
- Presence of all `classes`
- Presence of all `attributes` (Exists / Equals)
- Pseudo-classes: `first-child`, `last-child`, `first-of-type`, `empty`, `root`, `nth-child(...)`; others are compared against `active_pseudo`.
### `ComplexSelector`
```rust
#[derive(Debug, Clone)]
pub struct ComplexSelector {
pub compounds: Vec<CompoundSelector>,
pub combinators: Vec<Combinator>,
}
```
**Methods:**
- `ComplexSelector::parse(input: &str) -> Self` — parses a complex selector (e.g. `Panel > Button.primary`). Splits into parts by combinators (`>`, `+`, `~`, space), parses each as a `CompoundSelector`.
- `fn check_compound_against(&self, i, info: &AncestorInfo) -> bool` — checks whether the `i`-th compound matches the provided ancestor info.
- `pub fn matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) -> bool` — full complex selector check: the last compound is the target element, the rest are ancestors/siblings according to combinators.
- `pub fn specificity(&self) -> (u32, u32, u32)` — sum of all compounds' specificities.
- `pub fn as_simple(&self) -> Option<&CompoundSelector>` — if compounds contains exactly one element, returns it; otherwise `None`.
- `pub fn has_pseudo_class(&self, pc: &str) -> bool` — whether any compound has the given pseudo-class.
---
## Helper Structs
### `AncestorInfo`
```rust
#[derive(Debug, Clone)]
pub struct AncestorInfo {
pub type_name: String,
pub id: Option<String>,
pub classes: Vec<String>,
}
```
Methods:
- `AncestorInfo::new(type_name, classes) -> Self`
- `AncestorInfo::new_with_id(type_name, id, classes) -> Self`
Used when checking complex selectors — describes an ancestor or a sibling element.
### `StructuralContext`
```rust
#[derive(Debug, Clone, Default)]
pub struct StructuralContext {
pub sibling_index: usize, // 0-based
pub sibling_total: usize,
pub type_index: usize, // among elements of the same type
pub type_total: usize,
pub has_children: bool,
pub is_root: bool,
}
```
Used for resolving structural pseudo-classes (`first-child`, `nth-child`, `empty`, `root`).
---
## `StyleRule`
```rust
#[derive(Debug, Clone)]
pub struct StyleRule {
pub selector: ComplexSelector,
pub properties: HashMap<String, String>,
}
impl StyleRule {
pub fn build(selector_str: String, properties: HashMap<String, String>) -> Self
}
```
---
## `StyleIndex`
```rust
pub type RuleId = usize;
#[derive(Debug, Clone)]
pub struct StyleIndex {
pub by_tag: HashMap<String, Vec<RuleId>>,
pub by_class: HashMap<String, Vec<RuleId>>,
pub by_id: HashMap<String, Vec<RuleId>>,
pub by_tag_class: HashMap<(String, String), Vec<RuleId>>,
pub by_tag_id: HashMap<(String, String), Vec<RuleId>>,
pub complex_rules: Vec<(RuleId, RuleId)>,
pub universal_rules: Vec<RuleId>,
pub rule_specificities: Vec<(u32, u32, u32)>,
pub rules: Vec<StyleRule>,
pub epoch: u64,
}
impl StyleIndex {
pub fn new() -> Self
}
```
An index for fast rule lookup. Built in `StyleSheet::build_index`:
- `by_tag` / `by_class` / `by_id` / `by_tag_class` / `by_tag_id` — indexes for simple selectors
- `complex_rules` — rules with complex selectors (always checked by brute force)
- `universal_rules` — rules with `*`
- `rule_specificities` — specificity cache
- `epoch` — monotonically increasing counter for cache invalidation
---
## `StyleCache`
```rust
#[derive(Debug, Clone)]
pub struct StyleCache {
entries: HashMap<u64, ComputedStyle>,
max_entries: usize,
}
impl StyleCache {
pub fn new(max_entries: usize) -> Self
pub fn get_or_compute(
&mut self,
type_name: &str,
props: &[(Cow<'_, str>, Cow<'_, str>)],
epoch: u64,
matched_sheets: &[&HashMap<String, String>],
) -> ComputedStyle
pub fn clear(&mut self)
}
```
Computed style cache. Key is a hash of `type_name`, inline properties, and `epoch`. When `max_entries` is exceeded, the cache is fully cleared.
---
## `StyleSheet`
```rust
#[derive(Debug)]
pub struct StyleSheet {
rules: Vec<StyleRule>,
index: Option<StyleIndex>,
epoch: u64,
cache: Mutex<StyleCache>,
}
```
The main type of the module. Contains a list of rules, an optional index, and a cache. Implements `Clone` (with a new empty cache) and `Default`.
**Methods:**
- `StyleSheet::new() -> Self` — creates an empty sheet.
- `pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>)` — adds a rule. Passes the selector through `split_selectors` (supports comma-separated groups). Resets `index = None`.
- `pub fn build_index(&mut self)` — rebuilds the `StyleIndex`. Increments `epoch`, clears the cache. For each rule:
- Computes specificity
- If the selector is simple (1 compound) — indexes by tag/class/id/attributes
- If complex — marks as `complex_rules`
- Universal (`*`) go into `universal_rules`
- `pub fn has_index(&self) -> bool`
- `pub fn query_index(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) -> Option<Vec<&HashMap<String, String>>>` — uses the index for fast lookup: collects candidates from `universal_rules`, `by_tag`, `by_class`, `by_id`, `complex_rules`; filters via `ComplexSelector::matches`; sorts by specificity.
- `pub fn matching_rules(...) -> Vec<&HashMap<String, String>>` — finds matching rules. Attempts `query_index`; if no index — linear scan of all `self.rules` with sorting.
- `pub fn matching_pseudo_rules(pseudo, ...) -> HashMap<String, String>` — finds rules containing the specified pseudo-class (e.g. `:hover`). Uses `query_index_for_pseudo` or linear scan.
- `pub fn compute_cached(type_name, props, matched_sheets) -> ComputedStyle` — computes the final style through the cache (wrapper around `StyleCache::get_or_compute`). Includes `PerfScope::new("style")`.
- `pub fn clear_cache(&self)` — clears the cache.
- `pub fn is_empty(&self) -> bool``self.rules.is_empty()`.
**Methods with `#[cfg(feature = "parallel")]`:**
- `matching_rules_batch(type_names, el_ids, el_classes_list, ...) -> Vec<Vec<&HashMap<String, String>>>` — parallel batch search via `rayon::par_iter`.
---
## `ComputedStyle`
```rust
#[derive(Debug, Clone, Default)]
pub struct ComputedStyle {
pub font_size: Option<SizeValue>,
pub color: Option<iced::Color>,
pub padding: Option<SizeValue>,
pub padding_top: Option<SizeValue>,
pub padding_right: Option<SizeValue>,
pub padding_bottom: Option<SizeValue>,
pub padding_left: Option<SizeValue>,
pub margin: Option<SizeValue>,
pub margin_top: Option<SizeValue>,
pub margin_right: Option<SizeValue>,
pub margin_bottom: Option<SizeValue>,
pub margin_left: Option<SizeValue>,
pub background: Option<iced::Color>,
pub spacing: Option<SizeValue>,
pub border_radius: Option<SizeValue>,
pub border_width: Option<SizeValue>,
pub border_color: Option<iced::Color>,
pub width: Option<iced::Length>,
pub height: Option<iced::Length>,
pub min_width: Option<SizeValue>,
pub max_width: Option<SizeValue>,
pub min_height: Option<SizeValue>,
pub max_height: Option<SizeValue>,
pub direction: Option<LayoutDirection>,
pub align_items: Option<iced::Alignment>,
pub content_align: Option<ContentAlign>,
pub flex_grow: Option<u16>,
pub position: Option<Position>,
pub top: Option<SizeValue>,
pub right: Option<SizeValue>,
pub bottom: Option<SizeValue>,
pub left: Option<SizeValue>,
pub overflow_x: Option<Overflow>,
pub overflow_y: Option<Overflow>,
pub display: Option<Display>,
pub opacity: Option<f32>,
pub font_weight: Option<u16>,
pub line_height: Option<SizeValue>,
pub text_align: Option<TextAlign>,
}
```
The final computed style of an element. All fields are `Option`; a missing property means "not set / inherited from parent".
### `ComputedStyle::compute()`
```rust
pub fn compute(
inline: &[(Cow<'_, str>, Cow<'_, str>)],
matched_sheets: &[&HashMap<String, String>],
) -> Self
```
Assembles the style via `lookup()`: for each field, `lookup(key, inline, matched_sheets)` is called, then parsed by the corresponding function. Notable details:
- `padding`/`margin` — individual properties (`-top`, `-right`, etc.) are checked first, then the shorthand.
- `spacing` — alternate name for `gap`.
- `background` — first `background`, then `background-color`.
- `overflow-x`/`overflow-y` — if the individual property is not found, the general `overflow` is applied.
- `flex_grow` — parsed as `f32`, cast to `u16`.
### `ComputedStyle::compute_batch()`
```rust
#[cfg(feature = "parallel")]
pub fn compute_batch<'a>(
pairs: &[(&[(Cow<'_, str>, Cow<'_, str>)], &[&'a HashMap<String, String>])],
) -> Vec<ComputedStyle>
```
Parallel batch variant via `rayon::par_iter`.
### `ComputedStyle::apply_overrides()`
```rust
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>)
```
Applies (overwrites) a given set of properties on top of the existing style. Used for dynamic changes (e.g. `:hover` rules, inline overrides).
### How `lookup()` works
```rust
fn lookup<'a>(
key: &str,
inline: &'a [(Cow<'_, str>, Cow<'_, str>)],
matched_sheets: &[&'a HashMap<String, String>],
) -> Option<&'a str>
```
Property resolution order:
1. **Inline properties** — iterates over `(key, value)` pairs. Supports the `style:` prefix (i.e. `style:color` is equivalent to `color`).
2. **matched_sheets** — a list of dictionaries from matching CSS rules, sorted by specificity. Iterated from the end (last is most specific).
3. Returns the first found value.
---
## Parsing Functions
| Function | Signature | Description |
|---|---|---|
| `parse_size` | `(s: &str) -> Option<SizeValue>` | Parses a size: `"10"``Px(10)`, `"50%"``Percent(50)`. `auto`, `fill`, `stretch``None` |
| `parse_color` | `(s: &str) -> Option<iced::Color>` | Parses a color: `#rgb`, `#rrggbb`, `#rrggbbaa`, names (`white`, `black`, `transparent`) |
| `parse_length` | `(s: &str) -> Option<iced::Length>` | Parses an Iced length: `"fill"`/`"100%"`, `"shrink"`/`"auto"`, `"50"``Fixed(50)` |
| `parse_overflow` | `(s: &str) -> Option<Overflow>` | `visible`, `hidden`, `scroll`, `auto` |
| `parse_position` | `(s: &str) -> Option<Position>` | `static`, `relative`, `absolute`, `sticky`, `fixed` |
| `parse_display` | `(s: &str) -> Option<Display>` | `none`, `block`, `flex`, `grid`, `inline` |
| `parse_direction` | `(s: &str) -> Option<LayoutDirection>` | `row`/`horizontal`, `column`/`vertical`, `grid` |
| `parse_alignment` | `(s: &str) -> Option<iced::Alignment>` | `start`, `center`, `end` |
| `parse_content_align` | `(s: &str) -> Option<ContentAlign>` | `start`/`left`/`top`, `center`, `end`/`right`/`bottom` |
| `parse_opacity` | `(s: &str) -> Option<f32>` | Number 0.01.0, clamped |
| `parse_font_weight` | `(s: &str) -> Option<u16>` | Names: `normal`→400, `bold`→700, `lighter`→300, `bolder`→900; numeric values |
| `parse_text_align` | `(s: &str) -> Option<TextAlign>` | `left`, `center`, `right` |
### `resolve_size`
```rust
pub fn resolve_size(v: Option<SizeValue>, relative_to: Option<f32>) -> Option<f32>
```
A convenience wrapper around `SizeValue::resolve`, returns `Option<f32>`.
---
## Usage in `renderer.rs`
`ComputedStyle` fields are actively used in `/home/faynot/software/glint-runtime/src/renderer.rs`:
| Field | Where used |
|---|---|
| `.color` | Text color in buttons, text fields, Label |
| `.background` | Background of containers, buttons, text fields |
| `.padding*` | `iced::Padding` for buttons, input fields, containers |
| `.margin*` | Spacing around elements |
| `.border_radius`, `.border_width`, `.border_color` | Borders of buttons, input fields, containers |
| `.width`, `.height` | Sizes of Scrollable, Column, Row, Image |
| `.min_width`, `.max_width`, `.min_height`, `.max_height` | Size constraints |
| `.direction` | Flex direction (Row / Column) |
| `.align_items` | Child element alignment |
| `.content_align` | Content alignment |
| `.flex_grow` | Flex-grow with `FillPortion` |
| `.spacing` | `iced::container::Style` spacing, gap in Row / Column |
| `.position` | Static / Fixed / Absolute / Sticky |
| `.top`, `.right`, `.bottom`, `.left` | Positioning |
| `.overflow_x`, `.overflow_y` | Scrolling (`Scrollable`) |
| `.display` | `Display::None` — element hiding |
| `.opacity` | Transparency |
| `.font_weight` | Font weight in Text |
| `.line_height` | Line spacing |
| `.text_align` | Horizontal text alignment |
| `.font_size` | Font size (inherited from parent) |
---
## `nth_matches(expr: &str, n: usize) -> bool`
Internal function for resolving `:nth-child(an+b)`, `:nth-child(odd)`, `:nth-child(even)`, and `:nth-child(<number>)`. Supports negative `a` and `b`.
---
## Relationships with other modules
- `types.rs` — each `DomNode` (both element and text node) contains `computed_style: ComputedStyle`.
- `renderer.rs` — imports `{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign, resolve_size}`.
- `mod.rs` — uses `AncestorInfo`, `ComputedStyle`, `StructuralContext` when traversing the DOM.

View File

@@ -0,0 +1,92 @@
# Reactivity Module: `src/interpreter/reactive.rs`
Dependency tracking system between variables and VDOM elements.
Allows recalculation of only changed elements (Phase 3).
---
## `ElementId(u32)`
Unique element identifier in the VDOM tree. Assigned by `ReactiveTracker::alloc_id()`
during template loading.
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ElementId(pub u32);
```
**Used as:**
- Key in `dirty_set: HashSet<ElementId>`
- Key in `dependencies: HashMap<ElementId, HashSet<String>>`
- Field in `Element::element_id` (VDOM → tracker link)
- Source for `iced::widget::Id` (Phase 9.2: `format!("ti:{}", id.0)`)
- Source for scrollable_id (`el.element_id.0 as u64`)
---
## `ReactiveTracker`
Dependency graph: variable → list of elements that reference it.
```rust
pub struct ReactiveTracker {
subscribers: HashMap<String, HashSet<ElementId>>,
dependencies: HashMap<ElementId, HashSet<String>>,
dirty_set: HashSet<ElementId>,
next_id: u32,
}
```
**Fields:**
- `subscribers` — for each variable: which ElementIds depend on it
- `dependencies` — for each element: which variables it depends on (reverse mapping)
- `dirty_set` — elements to recalculate in the next frame
- `next_id` — counter for `alloc_id()`
### Methods
| Method | Description |
|--------|-------------|
| `new()` | Empty tracker |
| `alloc_id() -> ElementId` | Allocate a new ID, increment counter |
| `add_dependency(element, var_name)` | Register a dependency |
| `scan_value(element, value)` | Scan a string for `$var` and add dependencies |
| `on_variable_changed(name)` | Mark all dependent elements as dirty |
| `take_dirty_set() -> HashSet<ElementId>` | Take dirty_set and clear it |
| `is_dirty(id) -> bool` | Check if an element is marked dirty |
| `reset()` | Clear all data |
### scan_value()
Parses a string for `$var_name` patterns:
```rust
"Hello $name, you are $age years old"
// → add_dependency(element, "name")
// → add_dependency(element, "age")
```
Used during template loading for each properties string containing `$`.
As a result, each element knows which variables it depends on.
### Update cycle
```
Event → update() → on_variable_changed("var")
→ dirty_set = {element_A, element_B, ...}
→ take_dirty_set() → evaluate_vdom_incr(roots, &dirty_set)
→ for dirty_set elements: recalculate
→ for others: return as-is
```
---
## Tests
| Test | What it checks |
|------|---------------|
| `test_basic_dependency_tracking` | Two elements, two variables, correct dirty_set |
| `test_scan_value` | Parse `$var` from a string |
| `test_scan_no_vars` | String without `$` creates no dependencies |
| `test_take_dirty_set` | `take_dirty_set()` clears the internal set |
| `test_reset` | `reset()` clears everything |

200
docs/en/modules/04-rhei.md Normal file
View File

@@ -0,0 +1,200 @@
# Rhai Module: `src/interpreter/rhei.rs`
Integration of the [Rhai](https://rhai.rs/) scripting engine — compilation, AST caching, and expression/script execution.
---
## `RHEI_PREFIX` — Rhai expression prefix
```rust
pub const RHEI_PREFIX: &str = "__rhei:";
```
A marker constant for element properties whose content should be interpreted as Rhai expressions. Used in `collect_and_precompile()` to select properties of the form `!rhei:...`.
---
## `RheiContext` — Rhai execution context
```rust
pub struct RheiContext {
engine: Engine,
init_ast: AST,
scope: RefCell<Scope<'static>>,
action_cache: RefCell<HashMap<String, AST>>,
expr_cache: RefCell<HashMap<String, AST>>,
}
```
| Field | Purpose |
|---|---|
| `engine` | Configured `rhai::Engine` instance |
| `init_ast` | Merged AST of all initialization scripts (including function definitions) |
| `scope` | Shared scope (`Scope`) shared across calls; wrapped in `RefCell` for interior mutability |
| `action_cache` | Cache of compiled scripts (actions), keyed by source code |
| `expr_cache` | Cache of compiled expressions, keyed by source code |
---
## Constructors
### `new(scripts)`
```rust
pub fn new(scripts: &[String]) -> Self
```
Creates a context via `new_empty()` and immediately precompiles all provided scripts by calling `precompile_scripts()`.
### `new_empty(scripts)`
```rust
fn new_empty(scripts: &[String]) -> Self
```
1. Creates `Engine::new()`.
2. Configures `on_print` (outputs to stdout with `[rhei]` prefix) and `on_debug` (outputs to stderr) handlers.
3. Compiles all scripts and merges their ASTs into a single tree via `merge()`. Compilation errors for individual blocks are logged but do not abort the process.
4. Creates a module (`Module::eval_ast_as_new`) from the merged AST with an empty scope — this registers global functions defined in the scripts. The module is registered in the engine as a global module (`register_global_module`).
5. Initializes an empty `Scope`, empty `action_cache` and `expr_cache` caches.
---
## `sync_scope()` — variable synchronization
```rust
pub fn sync_scope(&self, variables: &HashMap<String, Value>)
```
Synchronizes values from an external `HashMap` into the Rhai `Scope`:
- If a variable already exists in the scope and its value has not changed — skip.
- If a variable exists — update it via `set_value()`.
- If a variable does not exist — add it via `push_dynamic()`.
Conversion `Value → Dynamic` is performed via `value_to_dynamic()`.
---
## `initialize()` — initialization
```rust
pub fn initialize(&self, variables: &mut HashMap<String, Value>)
```
1. Synchronizes variables via `sync_scope()`.
2. Runs `init_ast` (merged AST of all scripts) via `run_ast_with_scope()`.
3. Iterates over all scope variables via `iter_raw()` and writes back into the `HashMap` those whose values have changed.
---
## `eval_expr()` — expression evaluation
```rust
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, Value>) -> Value
```
1. Synchronizes variables.
2. Obtains (compiles or fetches from cache) the expression AST via `get_or_compile_expr()`.
3. Executes via `eval_ast_with_scope::<Dynamic>()`.
4. Converts the result `Dynamic → Value` via `dynamic_to_value()`.
5. On error returns `Value::None`.
---
## `eval_condition()` — condition evaluation
```rust
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, Value>) -> bool
```
Similar to `eval_expr()`, but typed as `bool`. On error returns `false`.
---
## `execute_action()` — script execution
```rust
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, Value>)
```
1. Synchronizes variables.
2. Obtains the script AST via `get_or_compile_action()`.
3. Executes via `run_ast_with_scope()`.
4. After execution iterates over the scope and writes changed variables back into the `HashMap`.
---
## AST Caching
### `get_or_compile_action(script)`
```rust
fn get_or_compile_action(&self, script: &str) -> Option<AST>
```
Checks `action_cache`. On miss compiles via `engine.compile()`, stores in cache.
### `get_or_compile_expr(expr)`
```rust
fn get_or_compile_expr(&self, expr: &str) -> Option<AST>
```
Checks `expr_cache`. On miss compiles via `engine.compile_expression()`, stores in cache.
Both methods log the error on compilation failure and return `None`.
---
## Batch Precompilation
```rust
pub fn precompile_scripts(&self, scripts: &[String])
pub fn precompile_actions(&self, actions: &[String])
pub fn precompile_exprs(&self, exprs: &[String])
pub fn precompile_all_from_doc(&self, doc: &super::Document)
```
| Method | Action |
|---|---|
| `precompile_scripts` | Compiles each script as an action |
| `precompile_actions` | Same as `precompile_scripts` (alias) |
| `precompile_exprs` | Compiles each expression |
| `precompile_all_from_doc` | Compiles all `doc.rhei_scripts` and recursively traverses the element tree |
### `collect_and_precompile()`
```rust
fn collect_and_precompile(el: &super::Element, ctx: &RheiContext)
```
Recursively traverses the `Element` tree:
- For properties starting with `__on:*` and non-empty — compiles as an action.
- For properties starting with `RHEI_PREFIX` (`!rhei:`) — compiles the remaining part as an expression.
---
## Type Conversion
### `value_to_dynamic(v: &Value) -> Dynamic`
```rust
Value::Int(i) Dynamic::from(*i)
Value::Float(f) Dynamic::from(*f)
Value::Bool(b) Dynamic::from(*b)
Value::Str(s) str_to_dynamic(s)
Value::None Dynamic::UNIT
Value::Array(a) Dynamic::from_iter(value_to_dynamic of each element)
```
### `dynamic_to_value(d: &Dynamic) -> Value`
Checks the type via `is_string()`, `is_int()`, `is_float()`, `is_bool()`, `is_array()` in priority order. If the type is not recognized — returns `Value::None`.
### `str_to_dynamic(s: &str) -> Dynamic`
A heuristic string parser that tries sequentially:
1. `s.parse::<i64>()` — integer
2. `s.parse::<f64>()` — float
3. `s.parse::<bool>()` — boolean
4. Otherwise — `Dynamic::from(s)` as a string

View File

@@ -0,0 +1,537 @@
# Renderer Module: `src/renderer.rs`
Transforms the `Element` tree into Iced widgets. Responsible for building the widget hierarchy, applying the box model, handling `:hover`/`:active` pseudo-classes, positioning (fixed, absolute, sticky), and rendering all built-in element types.
---
## Imports
```rust
use crate::Message;
use crate::interpreter::Element;
use crate::interpreter::style::{ComputedStyle, ContentAlign, Display, LayoutDirection,
Position, Overflow, StructuralContext, StyleSheet, TextAlign, resolve_size};
use iced::widget::container::Style as ContainerStyle;
use iced::widget::{
button, checkbox, column, container, image, progress_bar, row, scrollable, slider, svg, text,
text::Wrapping, text_input,
};
use iced::{Alignment, Background, Border, Length, Theme};
use iced::font::Weight;
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
```
---
## `WIDGET_ID_CACHE` and `get_or_create_widget_id()`
```rust
thread_local! {
static WIDGET_ID_CACHE: RefCell<HashMap<(u32, &'static str), iced::widget::Id>> = ...;
}
fn get_or_create_widget_id(key: u32, prefix: &'static str) -> iced::widget::Id
```
A `thread_local` cache for `iced::widget::Id`. The key is a tuple `(element_id, prefix)`. The string is formatted as `"{prefix}:{key}"` and "leaked" via `Box::leak` to obtain a `&'static str`. Used for `scrollable::id` (prefix `"sc"`) and `text_input::id` (prefix `"ti"`).
---
## `Element` Methods
### `get_prop()`
```rust
impl<'a> Element<'a> {
#[inline]
pub fn get_prop(&self, key: &str) -> Option<&str>
}
```
Looks up a property by key in `self.properties`. Returns the value or `None`.
### `push_prop()`
```rust
pub fn push_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V)
```
Adds a `(key, val)` pair to `self.properties`.
### `set_prop()`
```rust
pub fn set_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V)
```
Sets a property: if the key already exists — replaces the value, otherwise — adds a new pair.
---
## `extract_var_binding()`
```rust
fn extract_var_binding(el: &Element, prop: &str) -> Option<String>
```
Looks for a property of the form `__bind:<prop>` and returns its value. Used for reactive variable binding: `__bind:value` for `Input`, `Toggle`, `Slider`.
---
## `collect_hover_active()`
```rust
pub fn collect_hover_active<'a>(
el: &'a Element,
stylesheet: &StyleSheet,
) -> (HashMap<String, String>, HashMap<String, String>)
```
Collects CSS properties for the `:hover` and `:active` pseudo-classes for an element. Calls `stylesheet.matching_pseudo_rules()` twice — for `"hover"` and `"active"`. Returns a tuple `(hover_props, active_props)`. Used in `render_element()` and `make_hoverable()`.
---
## `make_hoverable()`
```rust
fn make_hoverable<'a>(
widget: iced::Element<'a, crate::Message, Theme, iced::Renderer>,
el: &Element,
hover_props: &HashMap<String, String>,
active_props: &HashMap<String, String>,
base_cs: &ComputedStyle,
) -> iced::Element<'a, crate::Message, Theme, iced::Renderer>
```
Wraps an arbitrary widget in a `button` to support `:hover`/`:active` styles. Trigger conditions:
- At least one `hover` or `active` style exists;
- The element has an `__on:click` handler.
The button is assigned `on_press(Message::EventTriggered(...))`. In the `style()` closure, `apply_overrides` are substituted depending on `button::Status`:
- `Hovered``hover_props`;
- `Pressed``active_props`, or `hover_props` if none exist.
Applied **only to non-Button and non-Input** elements (line 815).
---
## `render_element()`
```rust
pub fn render_element<'a>(
el: &'a Element,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
parent_direction: Option<LayoutDirection>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
scroll_positions: &HashMap<u64, f32>,
container_id: u64,
stylesheet: &StyleSheet,
) -> Option<iced::Element<'a, Message, Theme, iced::Renderer>>
```
**Main public rendering function.** Returns `None` if `display: none`.
### Common logic for all elements
1. `hover_props`, `active_props` are collected via `collect_hover_active()`.
2. Positioning type is determined: `is_fixed`, `is_absolute`, `is_sticky`.
3. If `left` + `right` are set without `width``width = Fill`. If `top` + `bottom` without `height``height = Fill`.
4. `flex-grow` is converted to `FillPortion(grow_value)` along the parent axis.
5. `current_color` and `current_font_size` are inherited.
### `"Window"` branch
```rust
if el.type_name == "Window"
```
- Creates a `column` with `spacing` (default 12px).
- Renders children via `render_children()`.
- Applies `apply_universal_box_model(is_window = true, scrollable_id = window_id)` — the window is always scrollable.
- Builds an `iced::widget::stack`:
1. Main flow (main_flow)
2. `abs_layers`
3. `sticky_layers`
4. `fixed_layers`
Final structure:
```
stack[
container[ scrollable[ container[ column[...] ] ] ]
...abs layers
...sticky layers
...fixed layers
]
```
### `"Panel"` branch
Delegates to `render_panel()`. If there are `abs_layers` — wraps in a `stack`.
### `"Button"` branch
Delegates to `render_button()`. If there are `abs_layers` or `sticky_layers` — wraps in a `stack`.
### `"Input"` branch
Delegates to `render_input()` passing `hover_props` and `active_props`.
### Text widgets: `"Title"`, `"Header"`, `"Text"`, `"Label"`, `"#text"`
```rust
"Title" | "Header" => ...
"Text" | "Label" | "#text" => ...
```
- Read the `text` property (or empty string).
- Create `iced::widget::text` with font size (24 for Title/Header, 16 for Text).
- Apply `color`, `font_weight` (Light ≤399, Normal 400599, Bold 600799, ExtraBold ≥800), `text_align`, `line_height` (with `Wrapping::Word`).
- For Title/Header the default size is 24px, for Text — 16px.
### `"Image"`
- Reads `src`. If the path starts with `fs:` — strips the prefix.
- `.svg``svg::Handle`, otherwise `image::Viewer`.
- Image size is calculated subtracting padding and border-width.
- For raster images `border_radius` is applied.
### `"Icon"`
Renders the character `🔹` as text of size 18px (or `current_font_size`). Placeholder.
### `"Toggle"`
Delegates to `render_toggle()`.
### `"Slider"`
Delegates to `render_slider()`.
### `"ProgressBar"`
```rust
progress_bar(0.0..=100.0, value)
```
The `value` property is parsed as `f32`.
### `"Divider"`, `"Separator"`
```rust
iced::widget::rule::horizontal(1)
```
Horizontal line with thickness 1px.
### Default branch (unknown type)
- Creates a `column` with `spacing` (10px).
- Renders children.
- If there are `abs_layers` — wraps in a `stack`.
- Applies `apply_universal_box_model()`.
- If there are `sticky_layers` — overlays them via `stack`.
- Returns `None` (the element is already written into `final_widget_opt`).
### Post-processing for non-Button and non-Input
```rust
if el.type_name != "Button" && el.type_name != "Input" {
final_widget_opt = make_hoverable(...);
}
```
### Positioning handling
After obtaining `final_widget`:
- **Fixed** → `wrap_fixed_position()` → placed into `fixed_layers`, returns `None`.
- **Absolute** → `wrap_fixed_position()` → placed into `abs_layers`, returns `None`.
- **Sticky** → if `scroll_y > threshold`, the element is moved to `sticky_layers`, and an empty `spacer` with height `estimate_element_height()` is inserted in its place. Otherwise the element stays in place.
---
## `render_children()`
```rust
fn render_children<'a>(
children: &'a [Element],
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
parent_direction: Option<LayoutDirection>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
scroll_positions: &HashMap<u64, f32>,
container_id: u64,
stylesheet: &StyleSheet,
) -> Vec<iced::Element<'a, Message, Theme, iced::Renderer>>
```
Recursively calls `render_element()` for each child. Filters out `None` (display: none). Returns a vector of rendered elements.
---
## `render_panel()`
```rust
fn render_panel<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
scroll_positions: &HashMap<u64, f32>,
container_id: u64,
stylesheet: &StyleSheet,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
Renders `Panel` — a container with three layout modes:
### Row
```rust
LayoutDirection::Row
```
`iced::widget::row` with `spacing` (10px). `align_y` from `cs.align_items` or `Alignment::Center` by default.
### Column
```rust
LayoutDirection::Column
```
`iced::widget::column` with `spacing`. `align_x` from `cs.align_items`.
### Grid
```rust
LayoutDirection::Grid
```
Columns (`column`), inside each — a row (`row`). Number of columns from the `columns` property (default 3). Each row is a chunk of `cols` children.
In all modes:
- If there are `abs_layers` — they are wrapped in a `stack` inside the content.
- After content, `apply_universal_box_model()` is applied.
- `sticky_layers` are overlaid on top via `stack`.
---
## `render_button()`
```rust
fn render_button<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
scroll_positions: &HashMap<u64, f32>,
container_id: u64,
stylesheet: &StyleSheet,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
- If there are no children — reads `label` (default `"Button"`) and creates `text`.
- If there are children — renders them in a `row` with `spacing = 8`.
- Padding: default 8px vertical, 16px horizontal. With auto-shrink if `padding + border > width/height`.
- `on_press` from `__on:click`.
- Style: via `get_button_style()` with dynamic `hover`/`active` overrides from `stylesheet.matching_rules()`.
---
## `render_toggle()`
```rust
fn render_toggle<'a>(
el: &'a Element,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
- Reads `label` (optional) and `value` (parsed as `bool`, default `false`).
- Extracts `__bind:value` for reactive binding.
- Creates a `checkbox`, on `on_toggle` sends `Message::ToggleChanged`.
- If there is a label — wraps in `row![checkbox, label]` with `spacing=8` and `align_y=Center`.
---
## `render_input()`
```rust
fn render_input<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
_parent_font_size: Option<f32>,
hover_props: &HashMap<String, String>,
active_props: &HashMap<String, String>,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
- Reads `placeholder` (default `"Type here…"`) and `value`.
- Extracts `__bind:value`.
- Creates `text_input` with `padding` from `get_padding(cs, 10.0)`.
- Assigns `id` via `get_or_create_widget_id(el.element_id.0, "ti")`.
- If there are hover/active styles — applies dynamic `style()` with `apply_overrides`.
- Otherwise — static style via `get_text_input_style()`.
---
## `render_slider()`
```rust
fn render_slider<'a>(
el: &'a Element,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
- Reads `value` (parsed as `f32`, default 0.0).
- Extracts `__bind:value`.
- Creates a `slider` in the range `0.0..=100.0`.
- On change sends `Message::SliderChanged`.
---
## Helper Functions
### `get_padding()`
```rust
fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding
```
Gathers `padding` from `ComputedStyle` considering `padding-top/right/bottom/left`. If `padding + border * 2` exceeds fixed `width`/`height` — scales padding proportionally (auto-shrink).
### `get_margin()`
```rust
fn get_margin(cs: &ComputedStyle) -> iced::Padding
```
Gathers `margin` from `ComputedStyle` considering `margin-top/right/bottom/left`. Base is `cs.margin` (default 0).
### `get_button_style()`
```rust
fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style
```
Builds `button::Style` from `ComputedStyle`. Depending on `status`:
- `Hovered` — background 15% lighter (`* 1.15`);
- `Pressed` — background 15% darker (`* 0.85`);
- `Active` — unchanged.
### `get_text_input_style()`
```rust
fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style
```
Builds `text_input::Style` from `ComputedStyle`: `background`, `border`, `icon`, `placeholder`, `value`, `selection`. Default values use a dark theme.
### `estimate_element_height()`
```rust
fn estimate_element_height(cs: &ComputedStyle) -> f32
```
Approximately calculates element height for sticky spacer: `padding_top + padding_bottom + border_width * 2 + font_size * line_height`.
### `wrap_sticky_position()`
```rust
fn wrap_sticky_position<'a>(
widget: iced::Element<'a, Message, Theme, iced::Renderer>,
cs: &ComputedStyle,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
Wraps a widget in a `container` with `padding-top` from `cs.top`. Width is `Fill`. Used for sticky elements when `scroll_y > threshold`.
### `wrap_fixed_position()`
```rust
fn wrap_fixed_position<'a>(
widget: iced::Element<'a, Message, Theme, iced::Renderer>,
cs: &ComputedStyle,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
Wraps a widget in a `container` with `width = Fill`, `height = Fill` and alignment (`align_x`, `align_y`) based on set `top`/`bottom`/`left`/`right`. Padding is set accordingly. Used for `fixed` and `absolute` positioning.
### `apply_universal_box_model()`
```rust
fn apply_universal_box_model<'a>(
widget: impl Into<iced::Element<'a, Message, Theme, iced::Renderer>>,
cs: &ComputedStyle,
is_window: bool,
default_padding: f32,
scrollable_id: Option<u64>,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
Applies the box model to any widget. **Wrapping order:**
```
outer_container (margin) ← if margin exists and !is_window
inner_container (bg, border, clip)
scrollable ← if overflow_x/overflow_y = Scroll/Auto
container (padding)
widget
```
#### Stages
1. **Padding**: `container(widget).padding(get_padding(cs, default_padding))`.
2. **Overflow**: if `overflow-y` = Scroll/Auto — adds `scrollable` with direction `Vertical` (or `Both` if `overflow-x` is also Scroll/Auto). For Window `overflow-y` defaults to `Auto`. Scroll gets an `id` and `on_scroll`.
3. **Clip**: if `overflow = Hidden``container.clip(true)`.
4. **Background, border, rounding**: `container.style(...)` with `Background`, `Border`.
5. **Width/height**: for Window — `Fill`/`Fill`; otherwise — from `cs.width`, `cs.height`, `cs.max_width`, `cs.max_height`.
6. **Content alignment**: `align_x` from `cs.content_align`.
7. **Margin**: if margin exists — outer `container` with `padding = margin`.
---
## Widget Tree Structure
For a typical window (`Window`) the tree looks like:
```
stack[
container (Window) [Fill, Fill]
scrollable [id="sc:window_id"]
container [bg, border, padding]
column [spacing]
...child elements...
container (fixed) ← fixed layer
container (absolute) ← absolute layer
container (sticky) ← sticky layer
]
```
For `Panel`:
```
stack[
container [bg, border, margin]
scrollable (if overflow)
container [padding]
row | column | grid
...child elements...
container (sticky) ← sticky layer, on top of boxed content
]
```
For unknown elements — similar to Panel, but abs-layers inside scroll, sticky on top.
For simple elements (Text, Image, Toggle, Slider, ProgressBar, Divider):
```
container [bg, border, margin, padding]
scrollable (if overflow)
container [padding]
text | image | checkbox | slider | progress_bar | rule
```

305
docs/en/modules/06-mod.md Normal file
View File

@@ -0,0 +1,305 @@
# Module `interpreter` — bytecode interpreter core
## Module structure
```
interpreter/
├── mod.rs — Interpreter: bytecode parsing, VDOM, styles
├── opcodes.rs — Opcode definitions (OP_ELEM_PUSH, OP_IF, OP_EACH, …)
├── reactive.rs — ReactiveTracker, ElementId — dirty node tracking
├── reader.rs — Reader — bytecode reading (str_ref, i64, f64, value, …)
├── rhei.rs — RheiContext — Rhai script and expression execution
├── style.rs — StyleSheet, AncestorInfo, ComputedStyle, StructuralContext
└── types.rs — Element, Document, ComponentDef, Value, FlatVDom, InterpError
```
## `Interpreter` (empty struct)
```rust
pub struct Interpreter;
```
The struct has no fields — all methods are static. It serves as a namespace for interpretation functions.
## `Interpreter::run()` — primary bytecode parsing
```rust
pub fn run<'a>(bytecode: &'a [u8]) -> Result<Document<'a>, InterpError>
```
1. Checks the magic number (`bytecode[..4] == MAGIC`).
2. Creates a `Reader`, `HashMap` for `variables` and `components`, `Vec` for `rhei_scripts`, an empty `StyleSheet`, and a `ReactiveTracker`.
3. Calls `parse_block_elements()` for the root level, which reads the opcode stream and builds the `Element` tree.
4. After parsing, calls `stylesheet.build_index()`.
5. Returns `Document { roots, components, variables, rhei_scripts, stylesheet, interner, tracker }`.
## `parse_block_elements()` — recursive block parsing
```rust
fn parse_block_elements<'a>(
r: &mut Reader<'a>,
variables: &mut HashMap<String, Value>,
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>
```
Reads opcodes in a loop, using a stack for nesting (`stack: Vec<Element>`). On `OP_END_BLOCK` it finishes the current block (if `!is_root`).
### Handled opcodes
| Opcode | Action |
|---|---|
| `OP_ELEM_PUSH` | Creates an `Element` with `tracker.alloc_id()`, pushes onto the stack |
| `OP_ELEM_POP` | Pops an element from the stack, calls `attach()` |
| `OP_GLOBAL` / `OP_LET` | Reads name and value, inserts into `variables` |
| `OP_SINGLETON` | Skips singleton data |
| `OP_CONTENT` | Text content — `text` property. If `OP_PROP_RHEI` — prefix `!rhei:` |
| `OP_PROP_STR`, `OP_PROP_VAR`, `OP_PROP_INT`, `OP_PROP_FLOAT`, `OP_PROP_BOOL`, `OP_PROP_RHEI`, `OP_PROP_UNIT`, `OP_PROP_CALL`, `OP_PROP_IDENT`, `OP_PROP_FSPATH`, `OP_PROP_COLOR` | Reads key and value, calls `el.push_prop()` |
| `OP_RHEI_BLK` | At root level without a parent — script; otherwise — `#text` element with `!rhei:` |
| `OP_COMPONENT` | Reads name, parameters, recursively parses child block; stores `ComponentDef` |
| `OP_IF` | Reads condition, parses true-block, checks `has_else`, parses false-block. Creates `@if` with `@else` as the last child |
| `OP_EACH` | Reads variable name, source (array or `$var` or Rhai), parses template block. Creates `@each` |
| `OP_ON` | Reads event name, arguments, looks for `OP_RHEI_BLK` — handler script; sets `__on:{event}` |
| `OP_STYLE_RULE` | Reads selector and properties, calls `stylesheet.add_rule()` |
### `attach()`
```rust
fn attach<'a>(stack: &mut Vec<Element<'a>>, roots: &mut Vec<Element<'a>>, el: Element<'a>)
```
If the stack is not empty — adds to `parent.children`, otherwise to `roots`.
---
## `evaluate_vdom()` — VDOM assembly (entry point)
```rust
pub fn evaluate_vdom<'a>(
templates: &[Element<'a>],
variables: &mut HashMap<String, Value>,
components: &HashMap<String, ComponentDef<'a>>,
rhei: &RheiContext,
stylesheet: &SS,
ancestors: &[AncestorInfo],
) -> Vec<Element<'a>>
```
Delegates to `evaluate_vdom_incr()` with an empty `dirty_set` — full recomputation.
### `evaluate_vdom_flat()`
```rust
pub fn evaluate_vdom_flat<'a>(...) -> FlatVDom<'a>
```
Wraps `evaluate_vdom()` and converts the result via `FlatVDom::from_elements()`.
---
## `evaluate_vdom_incr()` — incremental VDOM assembly
```rust
pub fn evaluate_vdom_incr<'a>(
templates: &[&Element<'a>],
variables: &mut HashMap<String, Value>,
components: &HashMap<String, ComponentDef<'a>>,
rhei: &RheiContext,
stylesheet: &SS,
ancestors: &[AncestorInfo],
dirty_set: &HashSet<ElementId>,
) -> Vec<Element<'a>>
```
### Preprocessing
1. **`sibling_infos`** — for each element from `templates` creates an `AncestorInfo` (type_name, id, classes). Needed for structural pseudo-classes (CSS `:nth-child`, `:first-of-type`, etc.).
2. **`type_counts`** / **`type_seen`** — count of total elements of each type and a counter for `StructuralContext`.
### Main loop over `templates`
For each element, a `StructuralContext` is computed:
```rust
let structural = StructuralContext {
sibling_index: i,
sibling_total: templates.len(),
type_index: *type_idx,
type_total,
has_children: ...,
is_root: ancestors.is_empty(),
};
```
#### `@if`
- Reads `condition`, calls `evaluate_condition()`.
- Iterates over `child` elements: if `@else` — active when condition is false; otherwise active when true.
- Recursively calls `evaluate_vdom_incr()` for the active branch.
#### `@each`
- Gets `var_name` and `source`.
- Resolves the source: if prefixed with `!rhei:` — calls `normalize_rhai_array()`, otherwise — `resolve_string()`.
- Splits the result by `,`, for each element:
- Inserts `var_name` into `variables`, recursively processes the template, restores the previous variable value.
#### Regular element / Component
- If `el.type_name` is found in `components`:
1. Collects arguments from component parameters via `resolve_prop()`.
2. Saves old variable values, inserts new ones.
3. Creates `vcomp`, copies properties, resolving them via `resolve_prop()` and `resolve_string()`.
4. Computes styles: `collect_matching_styles()``stylesheet.compute_cached()`.
5. Builds ancestor chain: `build_ancestor_chain()`.
6. Recursively processes `comp.children`.
7. Computes `content_hash`.
8. Restores variables.
- Otherwise (regular element):
1. Creates `vnode`.
2. Resolves properties: `__on:``resolve_string()`, `$var``__bind:{key}`, others → `resolve_prop()`.
3. Computes styles via `collect_matching_styles()` + `compute_cached()`.
4. Builds ancestor chain, recursively processes `el.children`.
5. Computes `content_hash`.
---
## `compute_content_hash()`
```rust
fn compute_content_hash(el: &Element) -> u64
```
Element hash for caching. Considers:
- `type_name`
- All key-value pairs from `properties`
- Recursively `content_hash` of child elements (`children`)
Uses `DefaultHasher`.
---
## `build_ancestor_chain()`
```rust
fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec<AncestorInfo>
```
Copies the current `ancestors`, adds `AncestorInfo` for the current element (type_name, id, classes). Returns the extended chain for passing during recursive child traversal.
---
## `collect_matching_styles()`
```rust
fn collect_matching_styles<'a>(
el: &Element,
stylesheet: &'a SS,
active_pseudo: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
) -> Vec<&'a HashMap<String, String>>
```
Extracts `id`, `classes`, and all attributes of the element, delegates to `stylesheet.matching_rules()` with the full context for CSS selectors.
---
## `resolve_string()` — `$var` substitution
```rust
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str>
```
- If the string has no `$` — returns `Cow::Borrowed(val)` (no allocations).
- Otherwise, traverses the string character by character. After `$`, collects the variable name (letters, digits, `_`), looks it up in `scope`, substitutes the value. If the variable is not found — leaves `$var` as is.
## `resolve_prop()` — property value resolution
```rust
fn resolve_prop(v: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> String
```
- If it starts with `!rhei:` — calls `rhei.eval_expr()`.
- Otherwise — `resolve_string()`.
## `evaluate_condition()` — `@if` condition evaluation
```rust
fn evaluate_condition(cond: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> bool
```
- If `!rhei:``rhei.eval_condition()`.
- Otherwise:
1. Strips curly braces `{...}`.
2. Performs `resolve_string()`.
3. Checks `is_truthy_str()`, then `false`/`0`/empty.
4. Attempts to parse numeric operators (`>=`, `<=`, `>`, `<`, `==`, `!=`).
## `is_truthy()` / `is_truthy_str()` — conversion to bool
```rust
fn is_truthy(v: &Value) -> bool
fn is_truthy_str(s: &str) -> bool
```
- `Value::Bool` — by value.
- `Value::Int` — non-zero.
- `Value::Float` — non-zero.
- `Value::Str` — delegates to `is_truthy_str()`.
- `Value::None``false`.
- `Value::Array` — not empty.
- String: `""`, `"false"`, `"0"`, `"null"``false`; `"true"`, `"1"``true`; otherwise parses as `f64`.
## `normalize_rhai_array()` — Rhai array normalization
```rust
fn normalize_rhai_array(s: &str) -> String
```
Trims `[...]`, splits by `,`, trims whitespace, joins with `,`. Used in `@each` to convert a Rhai array to the format expected by the loop.
---
## `Document`
```rust
pub struct Document<'a> {
pub roots: Vec<Element<'a>>,
pub components: HashMap<String, ComponentDef<'a>>,
pub variables: HashMap<String, Value>,
pub rhei_scripts: Vec<String>,
pub stylesheet: StyleSheet,
pub interner: Interner,
pub tracker: ReactiveTracker,
}
```
## `Element`
```rust
pub struct Element<'a> {
pub type_name: &'a str,
pub properties: Vec<(String, String)>,
pub children: Vec<Element<'a>>,
pub element_id: ElementId,
pub computed_style: Option<ComputedStyle>,
pub content_hash: u64,
}
```
## `ComponentDef`
```rust
pub struct ComponentDef<'a> {
pub name: String,
pub params: Vec<(String, String)>,
pub children: Vec<Element<'a>>,
}
```

View File

@@ -0,0 +1,109 @@
# App and Perf modules
## `src/app.rs` — GlintApp
Iced application entry point. Holds state and implements `update`/`view`.
### `GlintApp`
```rust
pub struct GlintApp {
pub doc: Document<'static>,
pub vdom_roots: Vec<Element<'static>>,
pub rhei: RheiContext,
}
```
- `doc` — original Document (styles, variables, tracker)
- `vdom_roots` — result of the last evaluate_vdom (_incr)
- `rhei` — Rhai engine with cached ASTs
### `update(&mut self, message: Message) -> iced::Task<Message>`
Processing messages from Iced:
| Message | Action |
|---------|--------|
| `WindowScrolled(y)` | Set `__scroll_y`, call `on_variable_changed` |
| `ScrollableScrolled(id, y)` | Set `__scroll_{id}`, call `on_variable_changed` |
| `EventTriggered(script)` | `execute_action()`, then diff variables to find changed ones |
| `InputChanged(var, val)` | Set variable, call `on_variable_changed` |
| `ToggleChanged(var, val)` | Set `Bool`, call `on_variable_changed` |
| `SliderChanged(var, val)` | Set `Float`, call `on_variable_changed` |
After processing the message:
1. `tracker.take_dirty_set()` — get dirty elements
2. `evaluate_vdom_incr()` — recalculate VDOM
3. Save to `self.vdom_roots`
**Perf:** VDOM phase is measured with `PerfScope::new("vdom")`.
### `view(&self) -> iced::Element`
Build Iced widgets from `self.vdom_roots`:
1. Collect scroll_positions from `__scroll_*` variables
2. For each root: `render_element()` → push into column
3. Overlay global fixed/absolute/sticky layers
4. Return `container(layout).into()`
**Perf:** Render phase is measured with `PerfScope::new("render")`.
At the end `perf::print_frame()` — output to stderr.
---
## `src/perf.rs` — Performance Monitoring
Phase timing system (VDOM, Style, Render). Enabled with `--perf` flag.
### `PerfReport`
```rust
pub struct PerfReport {
pub vdom_eval: f64, // ms
pub style: f64, // ms
pub render: f64, // ms
pub total: f64, // ms
}
```
Stored in `thread_local!` `RefCell<PerfReport>`.
### `PerfScope`
Drop-guard: measures time from creation to destruction.
```rust
pub struct PerfScope {
name: &'static str,
start: Instant,
}
```
On `drop()`: adds elapsed ms to the corresponding `PerfReport` field.
Names: `"vdom"`, `"style"`, `"render"`.
### Functions
| Function | Description |
|----------|-------------|
| `set_enabled(bool)` | Enable/disable measurements |
| `is_enabled() -> bool` | Check state |
| `report_and_reset() -> Option<String>` | Collect report, check >16ms threshold |
| `reset()` | Reset PerfReport |
| `print_frame()` | Call report_and_reset + reset, print to stderr |
### Example output
```
[perf] VDOM: 6.63ms | Style: 0.26ms | Render: 7.19ms | Total: 14.08ms
[perf] VDOM: 13.26ms | Style: 0.52ms | Render: 7.18ms | Total: 20.95ms ⚠️ Frame budget exceeded! 20.95ms > 16ms
```
### Instrumentation points
| File | Phase | Location |
|------|-------|----------|
| `app.rs:update` | vdom | Around `evaluate_vdom_incr()` |
| `app.rs:view` | render | Entire `view()` function |
| `style.rs:compute_cached` | style | Inside `StyleSheet::compute_cached()` |