docs: reorganize into en/ru and add English translation
This commit is contained in:
47
docs/en/README.md
Normal file
47
docs/en/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Glint Runtime — Documentation
|
||||
|
||||
Backend for executing compiled bytecode (.glbc) of the Glint UI framework. Loads bytecode, interprets VDOM, applies CSS-like styles, executes Rhai scripts, and renders the result via Iced (native GPU-accelerated GUI).
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/main.rs` — entry point, CLI parsing
|
||||
- `src/lib.rs` — public API of the crate
|
||||
- `src/app.rs` — GlintApp: Iced application, update/view
|
||||
- `src/cli.rs` — CLI commands compile/run
|
||||
- `src/renderer.rs` — Iced widgets: Element → iced::Element conversion
|
||||
- `src/perf.rs` — PerfScope: performance measurement by phases
|
||||
- `src/interpreter/mod.rs` — Interpreter: bytecode loading, VDOM eval
|
||||
- `src/interpreter/types.rs` — Element, VNode, FlatVDom, Value, InternedStr, Document
|
||||
- `src/interpreter/style.rs` — StyleSheet, StyleRule, StyleIndex, StyleCache, ComputedStyle
|
||||
- `src/interpreter/reactive.rs` — ReactiveTracker: dependency graph
|
||||
- `src/interpreter/rhei.rs` — RheiContext: Rhai script integration
|
||||
- `src/interpreter/reader.rs` — Reader: .glbc bytecode reading
|
||||
- `src/interpreter/opcodes.rs` — OP_* bytecode constants
|
||||
|
||||
## Related Repositories
|
||||
|
||||
- `glt` (compiler .gltm/.glts → .glbc)
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Document** — loaded .glbc file: Element tree, style sheet, variables
|
||||
- **Element** — VDOM node: type_name, properties, children, computed_style, element_id, content_hash
|
||||
- **VDOM** — virtual tree, result of bytecode interpretation
|
||||
- **StyleSheet** — style sheet with indexed lookup and computed style cache
|
||||
- **ReactiveTracker** — tracks element → variable dependencies, dirty_set
|
||||
- **RheiContext** — compiles and executes Rhai scripts, caches AST
|
||||
- **ComputedStyle** — result of applying CSS rules: ~40 fields (color, padding, font-size, etc.)
|
||||
|
||||
## Optimization Phases
|
||||
|
||||
See [ARCHITECTURE-IMPROVEMENT-PLAN.md](../ARCHITECTURE-IMPROVEMENT-PLAN.md).
|
||||
|
||||
**Implemented:** phases 3–8, 10, 9.1–9.2 (content_hash, stable Iced widget IDs).
|
||||
|
||||
**Remaining:**
|
||||
| Phase | Description | Estimated Speedup |
|
||||
|-------|-------------|-------------------|
|
||||
| 0 | Profiling (bench, flamegraph) | — |
|
||||
| 1 | Typed Values in styles instead of HashMap<String, String> | 2–3× |
|
||||
| 2 | String interning for hot paths | 1.5–2× |
|
||||
| 9.3 | Iced widget cache by content_hash | 1.5–2× (render) |
|
||||
303
docs/en/architecture/01-data-flow.md
Normal file
303
docs/en/architecture/01-data-flow.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Data Flow in Glint Runtime
|
||||
|
||||
How source code turns into pixels on the screen.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "Compilation"
|
||||
A1[".gltm markup"] --> P["Parser: glt crate"]
|
||||
A2[".glts style"] --> P
|
||||
P --> M["ModuleSoA — flat arrays"]
|
||||
M --> AST
|
||||
AST --> C["Compiler: glt crate"]
|
||||
C --> BC[".glbc bytecode"]
|
||||
end
|
||||
|
||||
subgraph "Loading"
|
||||
BC --> IR["Interpreter::run"]
|
||||
IR --> R["Reader: parses the binary"]
|
||||
R --> DOC["Document: tree + styles + variables"]
|
||||
end
|
||||
|
||||
subgraph "Initialization"
|
||||
DOC --> BOOT["iced::application boot"]
|
||||
BOOT --> RC["RheiContext: compiles Rhai scripts"]
|
||||
BOOT --> EV0["evaluate_vdom: builds the full VDOM"]
|
||||
EV0 --> APP["GlintApp: ready to run"]
|
||||
end
|
||||
|
||||
subgraph "Lifecycle: each frame"
|
||||
APP --> LOOP{"iced event loop"}
|
||||
|
||||
LOOP -->|event received| MSG[Message]
|
||||
MSG --> UPD["GlintApp::update"]
|
||||
UPD --> SET["changes a variable"]
|
||||
SET --> TV["tracker marks dependent elements as dirty"]
|
||||
TV --> DIRTY["collect dirty_set"]
|
||||
DIRTY --> VDOM["evaluate_vdom_incr: recalculate only dirty"]
|
||||
VDOM --> STYLE["StyleSheet: apply styles (with cache)"]
|
||||
STYLE --> NEW_VDOM["new VDOM"]
|
||||
|
||||
LOOP -->|on timer| VIEW["GlintApp::view"]
|
||||
VIEW --> REND["render_element: Element → Iced widget"]
|
||||
REND --> ICED["iced::Element tree"]
|
||||
ICED --> DIFF["Iced: compares with previous frame"]
|
||||
DIFF --> LAYOUT[Layout]
|
||||
LAYOUT --> DRAW["GPU draws"]
|
||||
end
|
||||
|
||||
subgraph "Styles — separate"
|
||||
STYLE --> SI["StyleIndex: looks up rules in O(1)–O(K)"]
|
||||
SI --> SC["StyleCache: doesn't parse the same thing twice"]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 1: Compilation — from text to bytecode
|
||||
|
||||
It all starts with two file types:
|
||||
|
||||
- **`.gltm`** — markup: buttons, panels, texts, sliders, and so on.
|
||||
- **`.glts`** — styles: CSS-like rules, selectors, colors, margins.
|
||||
|
||||
They are compiled by the external **`glt`** crate (not part of this repository). It does three things:
|
||||
|
||||
### 1.1 Parsing
|
||||
|
||||
`Parser` reads `.gltm` and `.glts` and stores everything in **`ModuleSoA`**.
|
||||
|
||||
**What is ModuleSoA?** SoA = Structure of Arrays. Instead of storing elements as a list of structs:
|
||||
|
||||
```text
|
||||
// Array of Structures (AoS) — the usual way
|
||||
Element { name: "Button", props: [...], children: [...] }
|
||||
Element { name: "Text", props: [...], children: [...] }
|
||||
```
|
||||
|
||||
the compiler stores them as a struct with parallel arrays:
|
||||
|
||||
```text
|
||||
// Structure of Arrays (SoA) — more efficient for the compiler
|
||||
ModuleSoA {
|
||||
type_names: ["Button", "Text", ...],
|
||||
properties_vec: [ [...], [...], ...],
|
||||
hierarchy: [parent_id, parent_id, ...],
|
||||
}
|
||||
```
|
||||
|
||||
This way the compiler iterates over all names at once (CPU cache stays hot),
|
||||
finds parent relationships faster, and applies optimizations more easily.
|
||||
|
||||
### 1.2 Building the AST
|
||||
|
||||
An AST tree is built from `ModuleSoA`. Components, if/each branches,
|
||||
and parameters are resolved here.
|
||||
|
||||
### 1.3 Bytecode generation
|
||||
|
||||
`Compiler` walks the AST and turns it into the **`.glbc`** binary format:
|
||||
- header with magic bytes (`"glBc"`)
|
||||
- string pool (all names, classes, texts — one contiguous block)
|
||||
- byte-encoded opcodes (see `opcodes.rs`: `OP_ELEM_PUSH`, `OP_PROP`, `OP_IF`, `OP_EACH`, etc.)
|
||||
|
||||
The result is a compact binary that can be loaded quickly and fed to the runtime.
|
||||
|
||||
---
|
||||
|
||||
## Stage 2: Loading — from bytecode to Document
|
||||
|
||||
The runtime takes `.glbc` and turns it into data structures that can be worked with.
|
||||
|
||||
### `Interpreter::run(bytecode) → Document`
|
||||
|
||||
Internally, `Reader` reads the bytecode sequentially:
|
||||
1. Checks magic bytes (is this really `.glbc`?)
|
||||
2. Reads the string pool
|
||||
3. Executes opcodes, building the `Element` tree on the fly
|
||||
|
||||
Two important things happen in parallel:
|
||||
|
||||
**Styles:** each encountered style directive is parsed into a `StyleRule`,
|
||||
then all rules are built into a `StyleIndex` — a catalog: "here are all rules for tag Button,
|
||||
here for class primary, here for element with id=submit". This way style lookup
|
||||
will later take not O(all rules), but O(a couple of items).
|
||||
|
||||
**Dependencies:** every property like `"text": "Hello $name"` is a hint:
|
||||
the element depends on the variable `name`. `ReactiveTracker` scans all properties,
|
||||
finds `$var` and remembers: "element ElementId(5) depends on variable 'name'".
|
||||
|
||||
The result is a **`Document`**:
|
||||
```rust
|
||||
Document {
|
||||
roots: Vec<Element>, // root elements
|
||||
components: HashMap<String, ComponentDef>, // components
|
||||
variables: HashMap<String, Value>, // initial values
|
||||
stylesheet: StyleSheet, // style sheet
|
||||
rhei_scripts: Vec<String>, // init scripts
|
||||
tracker: ReactiveTracker, // who depends on what
|
||||
interner: Interner, // unique string pool
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 3: Initialization — preparing for life
|
||||
|
||||
`Document` is ready, but it needs to be "started". The Iced boot function does this.
|
||||
|
||||
### 3.1 Cloning
|
||||
|
||||
`doc.clone()` — all strings inside Element have type `&'a str` with the original
|
||||
lifetime. After cloning they become `&'static str` (the runtime
|
||||
calls `Box::leak` so strings live forever — the application runs until the window is closed).
|
||||
|
||||
### 3.2 Rhai compilation
|
||||
|
||||
`RheiContext::new(scripts)`:
|
||||
- Creates a Rhai engine (`Engine`)
|
||||
- Compiles all init scripts into AST and saves them
|
||||
- Collects all functions from the scripts into a global module
|
||||
- Then `precompile_all_from_doc()` walks the entire Element tree and compiles
|
||||
every `__on:click { ... }` and every `!rhei:expr` into cache.
|
||||
**Now on click there's no need to recompile** — just grab the AST from cache.
|
||||
|
||||
### 3.3 Running init scripts
|
||||
|
||||
`initialize()`: synchronizes variables with the Rhai scope, executes init scripts,
|
||||
pulls everything that changed from the scope.
|
||||
|
||||
### 3.4 First VDOM
|
||||
|
||||
`evaluate_vdom()` — a full traversal of the tree:
|
||||
- Substitutes variables into strings (`$name` → actual value)
|
||||
- Evaluates `@if` conditions
|
||||
- Expands `@each` into the actual number of elements
|
||||
- For each element, finds matching styles and computes `ComputedStyle`
|
||||
- Assigns `content_hash`
|
||||
|
||||
Result: `GlintApp { doc, rhei, vdom_roots }`. The first frame is ready to display.
|
||||
|
||||
---
|
||||
|
||||
## Stage 4: Lifecycle — each frame
|
||||
|
||||
Iced runs in a loop: event → `update()` → `view()` → rendering.
|
||||
|
||||
### 4.1 Event received: update()
|
||||
|
||||
The user clicked a button, moved a slider, entered text — Iced sends a `Message`.
|
||||
|
||||
```rust
|
||||
enum Message {
|
||||
SliderChanged(Option<String>, f64), // slider: (bound variable, new value)
|
||||
InputChanged(Option<String>, String), // text input
|
||||
ToggleChanged(Option<String>, bool), // checkbox
|
||||
EventTriggered(String), // button click: run Rhai script
|
||||
WindowScrolled(f32), // window scroll
|
||||
ScrollableScrolled(u64, f32), // scroll inside container
|
||||
}
|
||||
```
|
||||
|
||||
**GlintApp::update()** does the following:
|
||||
|
||||
1. **Changes the variable.** For example, `SliderChanged("volume", 75)` → `variables["volume"] = 75.0`.
|
||||
2. **Notifies the tracker:** `tracker.on_variable_changed("volume")`. The tracker checks:
|
||||
"elements with IDs 5, 12, 18 depend on this variable". It marks them as dirty.
|
||||
3. **Collects the dirty_set:** `tracker.take_dirty_set()`.
|
||||
4. **Recalculates VDOM:** `evaluate_vdom_incr(roots, &dirty_set)`. It walks the tree.
|
||||
If an element is in dirty_set — recalculates it (variable substitution, style computation).
|
||||
If not — leaves it as is. **Children of dirty elements are also recalculated** (cascade).
|
||||
|
||||
### 4.2 On timer: view()
|
||||
|
||||
Even if nothing happened, Iced calls `view()` every frame (60 times per second).
|
||||
It needs to return Iced widgets for rendering.
|
||||
|
||||
**render_element()** — a recursive function that turns an Element into an Iced widget:
|
||||
|
||||
- `Button` → `iced::button(...).on_press(...)`
|
||||
- `Text` → `iced::text("...").size(16).color(...)`
|
||||
- `Panel` → `iced::column[...].spacing(10)`, wrapped in a container with background and border
|
||||
- `Input` → `iced::text_input("placeholder", "value").on_input(...)`
|
||||
- `Image` → `iced::image(path)` or `iced::svg(path)`
|
||||
- Unknown type → just a column with children
|
||||
|
||||
Each widget is wrapped in **`apply_universal_box_model`**:
|
||||
```text
|
||||
container [margin]
|
||||
container [padding, border, background]
|
||||
scrollable (if overflow: scroll/auto)
|
||||
container [padding]
|
||||
the widget itself
|
||||
```
|
||||
|
||||
**Problem:** `render_element` creates **all** widgets from scratch every frame, even if
|
||||
the Element hasn't changed. Iced then diffs the new tree against the old one — but building
|
||||
the tree itself takes ~7ms. This is the main optimization opportunity.
|
||||
|
||||
### 4.3 Iced does its thing
|
||||
|
||||
Iced receives the `iced::Element` tree, compares it with the previous one (diff), computes
|
||||
the layout, and renders via GPU (wgpu). All of this happens without our code.
|
||||
|
||||
---
|
||||
|
||||
## Element anatomy
|
||||
|
||||
```rust
|
||||
Element {
|
||||
type_name: "Button", // what kind of element
|
||||
properties: [("label", "Click"), ("color", "red"), ...], // its properties
|
||||
computed_style: ComputedStyle { color: Some(Red), padding: Some(8px), ... }, // computed style
|
||||
element_id: ElementId(42), // unique ID in the tree
|
||||
content_hash: 0xABCD1234, // content hash (for widget cache)
|
||||
children: [Element, ...], // child elements
|
||||
}
|
||||
```
|
||||
|
||||
## Style anatomy
|
||||
|
||||
Styles are stored in `StyleSheet` and work in three stages:
|
||||
|
||||
**1. Index (`StyleIndex`):** at load time, all CSS rules are sorted into buckets:
|
||||
```text
|
||||
Rule: "Button.primary#submit { color: red; padding: 10px }"
|
||||
→ by_tag["Button"] = { RuleId(1) }
|
||||
→ by_class["primary"] = { RuleId(1) }
|
||||
→ by_id["submit"] = { RuleId(1) }
|
||||
```
|
||||
|
||||
**2. Lookup:** when we need to find styles for a `Button.primary#submit` element,
|
||||
we take the intersection of sets from all three buckets. Instead of checking 500 rules — 3 lookups.
|
||||
|
||||
**3. Cache:** even when styles are found, `ComputedStyle::compute()` parses all properties
|
||||
(color, margins, fonts — about 40 fields). This is expensive. `StyleCache` remembers
|
||||
the result: `hash(type_name, properties, epoch) → ComputedStyle`. If the element
|
||||
hasn't changed — we get the ready-made style from cache, no parsing.
|
||||
|
||||
---
|
||||
|
||||
## Event loop with a slider example
|
||||
|
||||
```
|
||||
1. User moves the volume slider
|
||||
2. Iced: SliderChanged(Some("volume"), 75.0)
|
||||
3. GlintApp::update:
|
||||
a. variables["volume"] = Float(75.0)
|
||||
b. tracker.on_variable_changed("volume")
|
||||
→ dirty: ElementId(5) — text with "$volume", ElementId(12) — width from "$volume"
|
||||
c. evaluate_vdom_incr(roots, &{5, 12})
|
||||
→ Element 5: recalculate text (new volume)
|
||||
→ Element 12: recalculate width
|
||||
→ remaining 48 elements: don't touch
|
||||
4. GlintApp::view:
|
||||
→ render_element for all 50 root elements
|
||||
→ recursively for all children (even for those 48 that didn't change)
|
||||
→ Iced receives a completely new tree of 200+ widgets
|
||||
5. Iced: diffs → finds 2 changes → redraws 2 areas
|
||||
```
|
||||
|
||||
**Bottleneck:** step 4. VDOM recalculated only 2 out of 50 elements (thanks to ReactiveTracker).
|
||||
But render_element creates widgets for all 200+ nodes. Iced then diffs anyway
|
||||
and does nothing with 198 of them, but the time to create them has already been spent.
|
||||
68
docs/en/architecture/02-performance.md
Normal file
68
docs/en/architecture/02-performance.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Performance Analysis
|
||||
|
||||
Measurements with the `--perf` flag on `desktop.glbc`.
|
||||
|
||||
## Measurement Results
|
||||
|
||||
**Steady state** (no events, idle):
|
||||
|
||||
```
|
||||
VDOM: 6.6ms | Style: 0.3ms | Render: 7.2ms | Total: 14.1ms
|
||||
```
|
||||
|
||||
**Under events** (slider drag, peak values):
|
||||
|
||||
```
|
||||
VDOM: 17.4ms | Style: 0.8ms | Render: 15.2ms | Total: 33.5ms ⚠️
|
||||
```
|
||||
|
||||
**60 FPS frame budget: 16ms.** At idle we fit (14ms), under events — not (up to 33ms).
|
||||
|
||||
## Bottleneck Analysis
|
||||
|
||||
### Style matching — NOT a bottleneck (0.3-0.8ms)
|
||||
|
||||
Style matching takes less than 1ms even at peak. This is the result of:
|
||||
- **Phase 4** (StyleIndex) — O(K) instead of O(N×M)
|
||||
- **Phase 8** (StyleCache) — computed style memoization
|
||||
|
||||
### VDOM eval — main consumer (6-17ms)
|
||||
|
||||
At idle ~6.6ms — a full traversal of the Element tree. Under events up to 17ms:
|
||||
- `evaluate_vdom_incr` recalculates dirty elements (Phase 3)
|
||||
- Each event can dirty entire subtrees
|
||||
- Internally: `resolve_string`, `resolve_prop`, `compute_cached`, recursive traversal
|
||||
|
||||
### Render — second consumer (7-15ms)
|
||||
|
||||
**This is where the main optimization headroom lies.** `render_element` creates ALL Iced widgets
|
||||
from scratch every frame, even if the Element hasn't changed. Iced then diffs the new tree
|
||||
against the old one — but the tree construction itself costs ~7ms.
|
||||
|
||||
### Scenario: slider
|
||||
|
||||
1. `SliderChanged` → `age` and `volume_level` change
|
||||
2. `tracker.on_variable_changed` → dirty_set for dependent elements
|
||||
3. `evaluate_vdom_incr` recalculates dirty elements and their children
|
||||
4. `view()` → `render_element` for ALL elements (full re-render)
|
||||
5. Result: VDOM 13ms + Render 14ms = 27ms — frame drop
|
||||
|
||||
The first few frames after an event are the heaviest (VDOM ~13ms), then
|
||||
stabilize (~7ms) as dirty_set gradually clears.
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Phase 9.3 — widget cache** — would reduce Render from 7ms to ~0ms for unchanged
|
||||
elements. If 1 out of 50 elements changes, only that one needs re-rendering.
|
||||
This would lower total time from 14ms to ~7ms at idle.
|
||||
|
||||
2. **Phase 1 — Value enum in styles** — `ComputedStyle::compute()` and `parse_*()`
|
||||
take `&str` and parse each property. Passing `&Value` instead would skip parsing.
|
||||
Potentially speeds up both style matching and VDOM eval.
|
||||
|
||||
3. **Phase 2 — InternedStr** — string comparisons (`type_name == "Button"`,
|
||||
`key == "padding-top"`) happen thousands of times per frame. Replacing with u32
|
||||
comparison would give 1.5-2× in VDOM and render paths.
|
||||
|
||||
4. **Phase 0.3 — flamegraph** — confirm hypotheses with profiler measurements
|
||||
(`perf record`) before investing in optimization.
|
||||
182
docs/en/modules/01-types.md
Normal file
182
docs/en/modules/01-types.md
Normal 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
482
docs/en/modules/02-style.md
Normal 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.0–1.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.
|
||||
92
docs/en/modules/03-reactive.md
Normal file
92
docs/en/modules/03-reactive.md
Normal 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
200
docs/en/modules/04-rhei.md
Normal 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
|
||||
537
docs/en/modules/05-renderer.md
Normal file
537
docs/en/modules/05-renderer.md
Normal 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 400–599, Bold 600–799, 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
305
docs/en/modules/06-mod.md
Normal 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>>,
|
||||
}
|
||||
```
|
||||
109
docs/en/modules/07-app-perf.md
Normal file
109
docs/en/modules/07-app-perf.md
Normal 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()` |
|
||||
Reference in New Issue
Block a user