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

View 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.