# 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, // root elements components: HashMap, // components variables: HashMap, // initial values stylesheet: StyleSheet, // style sheet rhei_scripts: Vec, // 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, f64), // slider: (bound variable, new value) InputChanged(Option, String), // text input ToggleChanged(Option, 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.