diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 0000000..7faf717 --- /dev/null +++ b/docs/en/README.md @@ -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 | 2–3× | +| 2 | String interning for hot paths | 1.5–2× | +| 9.3 | Iced widget cache by content_hash | 1.5–2× (render) | diff --git a/docs/en/architecture/01-data-flow.md b/docs/en/architecture/01-data-flow.md new file mode 100644 index 0000000..0dae9bf --- /dev/null +++ b/docs/en/architecture/01-data-flow.md @@ -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, // 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. diff --git a/docs/en/architecture/02-performance.md b/docs/en/architecture/02-performance.md new file mode 100644 index 0000000..7342dd7 --- /dev/null +++ b/docs/en/architecture/02-performance.md @@ -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. diff --git a/docs/en/modules/01-types.md b/docs/en/modules/01-types.md new file mode 100644 index 0000000..18e1bd6 --- /dev/null +++ b/docs/en/modules/01-types.md @@ -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, + map: HashMap, + 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` — 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), + 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`, `From`, `From`, `From`, `From>`. +`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`). + +--- + +## `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>, + 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>, +} +``` + +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>, + pub components: HashMap>, + pub variables: HashMap, + pub rhei_scripts: Vec, + 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, + pub children_range: Range, + pub computed_style: ComputedStyle, + pub element_id: ElementId, +} + +pub struct FlatVDom<'a> { + pub nodes: Vec>, + pub properties: Vec<(String, String)>, + root_indices: Vec, +} +``` + +**Methods:** +- `new()` — empty +- `from_elements(elements)` — recursively flattens an Element tree into VNodes +- `into_elements(self) -> Vec` — 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`. + +--- + +## `InterpError` + +Bytecode loading errors. + +```rust +pub enum InterpError { + BadMagic, + UnexpectedEof, + InvalidUtf8, + UnexpectedPop, +} +``` + +Implements `Display` and `std::error::Error`. Returned from `Interpreter::run()`. \ No newline at end of file diff --git a/docs/en/modules/02-style.md b/docs/en/modules/02-style.md new file mode 100644 index 0000000..8d17b51 --- /dev/null +++ b/docs/en/modules/02-style.md @@ -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 +} +``` +`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 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` +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, + pub id: Option, + pub classes: Vec, + pub pseudo_classes: Vec, + pub attributes: Vec, +} +``` + +**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, + pub combinators: Vec, +} +``` + +**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, + pub classes: Vec, +} +``` + +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, +} + +impl StyleRule { + pub fn build(selector_str: String, properties: HashMap) -> Self +} +``` + +--- + +## `StyleIndex` +```rust +pub type RuleId = usize; + +#[derive(Debug, Clone)] +pub struct StyleIndex { + pub by_tag: HashMap>, + pub by_class: HashMap>, + pub by_id: HashMap>, + pub by_tag_class: HashMap<(String, String), Vec>, + pub by_tag_id: HashMap<(String, String), Vec>, + pub complex_rules: Vec<(RuleId, RuleId)>, + pub universal_rules: Vec, + pub rule_specificities: Vec<(u32, u32, u32)>, + pub rules: Vec, + 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, + 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], + ) -> 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, + index: Option, + epoch: u64, + cache: Mutex, +} +``` + +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)` — 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>>` — 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>` — finds matching rules. Attempts `query_index`; if no index — linear scan of all `self.rules` with sorting. +- `pub fn matching_pseudo_rules(pseudo, ...) -> HashMap` — 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>>` — parallel batch search via `rayon::par_iter`. + +--- + +## `ComputedStyle` +```rust +#[derive(Debug, Clone, Default)] +pub struct ComputedStyle { + pub font_size: Option, + pub color: Option, + pub padding: Option, + pub padding_top: Option, + pub padding_right: Option, + pub padding_bottom: Option, + pub padding_left: Option, + + pub margin: Option, + pub margin_top: Option, + pub margin_right: Option, + pub margin_bottom: Option, + pub margin_left: Option, + + pub background: Option, + pub spacing: Option, + pub border_radius: Option, + pub border_width: Option, + pub border_color: Option, + + pub width: Option, + pub height: Option, + pub min_width: Option, + pub max_width: Option, + pub min_height: Option, + pub max_height: Option, + + pub direction: Option, + pub align_items: Option, + pub content_align: Option, + + pub flex_grow: Option, + + pub position: Option, + pub top: Option, + pub right: Option, + pub bottom: Option, + pub left: Option, + + pub overflow_x: Option, + pub overflow_y: Option, + pub display: Option, + + pub opacity: Option, + pub font_weight: Option, + pub line_height: Option, + pub text_align: Option, +} +``` + +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], +) -> 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])], +) -> Vec +``` +Parallel batch variant via `rayon::par_iter`. + +### `ComputedStyle::apply_overrides()` +```rust +pub fn apply_overrides(&mut self, sheet: &HashMap) +``` +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], +) -> 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` | Parses a size: `"10"` → `Px(10)`, `"50%"` → `Percent(50)`. `auto`, `fill`, `stretch` → `None` | +| `parse_color` | `(s: &str) -> Option` | Parses a color: `#rgb`, `#rrggbb`, `#rrggbbaa`, names (`white`, `black`, `transparent`) | +| `parse_length` | `(s: &str) -> Option` | Parses an Iced length: `"fill"`/`"100%"`, `"shrink"`/`"auto"`, `"50"` → `Fixed(50)` | +| `parse_overflow` | `(s: &str) -> Option` | `visible`, `hidden`, `scroll`, `auto` | +| `parse_position` | `(s: &str) -> Option` | `static`, `relative`, `absolute`, `sticky`, `fixed` | +| `parse_display` | `(s: &str) -> Option` | `none`, `block`, `flex`, `grid`, `inline` | +| `parse_direction` | `(s: &str) -> Option` | `row`/`horizontal`, `column`/`vertical`, `grid` | +| `parse_alignment` | `(s: &str) -> Option` | `start`, `center`, `end` | +| `parse_content_align` | `(s: &str) -> Option` | `start`/`left`/`top`, `center`, `end`/`right`/`bottom` | +| `parse_opacity` | `(s: &str) -> Option` | Number 0.0–1.0, clamped | +| `parse_font_weight` | `(s: &str) -> Option` | Names: `normal`→400, `bold`→700, `lighter`→300, `bolder`→900; numeric values | +| `parse_text_align` | `(s: &str) -> Option` | `left`, `center`, `right` | + +### `resolve_size` +```rust +pub fn resolve_size(v: Option, relative_to: Option) -> Option +``` +A convenience wrapper around `SizeValue::resolve`, returns `Option`. + +--- + +## 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()`. 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. diff --git a/docs/en/modules/03-reactive.md b/docs/en/modules/03-reactive.md new file mode 100644 index 0000000..b812ce2 --- /dev/null +++ b/docs/en/modules/03-reactive.md @@ -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` +- Key in `dependencies: HashMap>` +- 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>, + dependencies: HashMap>, + dirty_set: HashSet, + 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` | 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 | diff --git a/docs/en/modules/04-rhei.md b/docs/en/modules/04-rhei.md new file mode 100644 index 0000000..6b0636c --- /dev/null +++ b/docs/en/modules/04-rhei.md @@ -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>, + action_cache: RefCell>, + expr_cache: RefCell>, +} +``` + +| 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) +``` + +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) +``` + +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) -> 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::()`. +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) -> 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) +``` + +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 +``` + +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 +``` + +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::()` — integer +2. `s.parse::()` — float +3. `s.parse::()` — boolean +4. Otherwise — `Dynamic::from(s)` as a string diff --git a/docs/en/modules/05-renderer.md b/docs/en/modules/05-renderer.md new file mode 100644 index 0000000..ef1fd42 --- /dev/null +++ b/docs/en/modules/05-renderer.md @@ -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> = ...; +} + +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>, V: Into>>(&mut self, key: K, val: V) +``` +Adds a `(key, val)` pair to `self.properties`. + +### `set_prop()` +```rust +pub fn set_prop>, V: Into>>(&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 +``` + +Looks for a property of the form `__bind:` 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, HashMap) +``` + +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, + active_props: &HashMap, + 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, + parent_font_size: Option, + parent_direction: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + container_id: u64, + stylesheet: &StyleSheet, +) -> Option> +``` + +**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, + parent_font_size: Option, + parent_direction: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + container_id: u64, + stylesheet: &StyleSheet, +) -> Vec> +``` + +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, + parent_font_size: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + 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, + parent_font_size: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + 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, + parent_font_size: Option, +) -> 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, + _parent_font_size: Option, + hover_props: &HashMap, + active_props: &HashMap, +) -> 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>, + cs: &ComputedStyle, + is_window: bool, + default_padding: f32, + scrollable_id: Option, +) -> 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 +``` \ No newline at end of file diff --git a/docs/en/modules/06-mod.md b/docs/en/modules/06-mod.md new file mode 100644 index 0000000..beade36 --- /dev/null +++ b/docs/en/modules/06-mod.md @@ -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, 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, + components: &mut HashMap>, + rhei_scripts: &mut Vec, + stylesheet: &mut SS, + tracker: &mut ReactiveTracker, + is_root: bool, +) -> Result>, InterpError> +``` + +Reads opcodes in a loop, using a stack for nesting (`stack: Vec`). 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>, roots: &mut Vec>, 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, + components: &HashMap>, + rhei: &RheiContext, + stylesheet: &SS, + ancestors: &[AncestorInfo], +) -> Vec> +``` + +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, + components: &HashMap>, + rhei: &RheiContext, + stylesheet: &SS, + ancestors: &[AncestorInfo], + dirty_set: &HashSet, +) -> Vec> +``` + +### 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 +``` + +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> +``` + +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) -> 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, 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, 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>, + pub components: HashMap>, + pub variables: HashMap, + pub rhei_scripts: Vec, + 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>, + pub element_id: ElementId, + pub computed_style: Option, + pub content_hash: u64, +} +``` + +## `ComponentDef` + +```rust +pub struct ComponentDef<'a> { + pub name: String, + pub params: Vec<(String, String)>, + pub children: Vec>, +} +``` diff --git a/docs/en/modules/07-app-perf.md b/docs/en/modules/07-app-perf.md new file mode 100644 index 0000000..ca893e5 --- /dev/null +++ b/docs/en/modules/07-app-perf.md @@ -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>, + 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` + +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`. + +### `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` | 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()` | diff --git a/docs/ru/README.md b/docs/ru/README.md new file mode 100644 index 0000000..4ae41a1 --- /dev/null +++ b/docs/ru/README.md @@ -0,0 +1,48 @@ +# Glint Runtime — документация +(да, пока-что написанное нейросетью. Её объяснений достаточно) + +Бэкенд для исполнения скомпилированного байткода (.glbc) Glint UI-фреймворка. Загружает байткод, интерпретирует VDOM, применяет CSS-подобные стили, выполняет Rhai-скрипты и рендерит результат через Iced (native GPU-ускоренный GUI). + +## Структура проекта + +- `src/main.rs` — точка входа, CLI парсинг +- `src/lib.rs` — публичное API crate'а +- `src/app.rs` — GlintApp: Iced-приложение, update/view +- `src/cli.rs` — CLI команды compile/run +- `src/renderer.rs` — Iced-виджеты: преобразование Element → iced::Element +- `src/perf.rs` — PerfScope: замер производительности по фазам +- `src/interpreter/mod.rs` — Interpreter: загрузка bytecode, 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: граф зависимостей +- `src/interpreter/rhei.rs` — RheiContext: интеграция Rhai скриптов +- `src/interpreter/reader.rs` — Reader: чтение .glbc bytecode +- `src/interpreter/opcodes.rs` — OP_* константы байткода + +## Связанные репозитории + +- `glt` (компилятор .gltm/.glts → .glbc) + +## Ключевые концепции + +- **Document** — загруженный .glbc файл: дерево Element'ов, таблица стилей, переменные +- **Element** — узел VDOM: type_name, properties, children, computed_style, element_id, content_hash +- **VDOM** — виртуальное дерево, результат интерпретации байткода +- **StyleSheet** — таблица стилей с индексированным поиском и кэшем computed style +- **ReactiveTracker** — отслеживает зависимости element → variable, dirty_set +- **RheiContext** — компилирует и выполняет Rhai-скрипты, кэширует AST +- **ComputedStyle** — результат применения CSS-правил: ~40 полей (color, padding, font-size, etc.) + +## Фазы оптимизации + +См. [ARCHITECTURE-IMPROVEMENT-PLAN.md](../ARCHITECTURE-IMPROVEMENT-PLAN.md). + +**Реализовано:** фазы 3–8, 10, 9.1–9.2 (content_hash, стабильные Iced widget ID). + +**Осталось:** +| Фаза | Описание | Оценка ускорения | +|------|----------|-----------------| +| 0 | Профилирование (bench, flamegraph) | — | +| 1 | Типизированные Value в стилях вместо HashMap | 2–3× | +| 2 | String interning для горячих путей | 1.5–2× | +| 9.3 | Кэш Iced-виджетов по content_hash | 1.5–2× (render) | diff --git a/docs/ru/architecture/01-data-flow.md b/docs/ru/architecture/01-data-flow.md new file mode 100644 index 0000000..e60fc45 --- /dev/null +++ b/docs/ru/architecture/01-data-flow.md @@ -0,0 +1,303 @@ +# Поток данных в Glint Runtime + +Как исходный код превращается в пиксели на экране. + +```mermaid +flowchart TD + subgraph "Компиляция" + A1[".gltm markup"] --> P["Parser: glt crate"] + A2[".glts style"] --> P + P --> M["ModuleSoA — плоские массивы"] + M --> AST + AST --> C["Compiler: glt crate"] + C --> BC[".glbc bytecode"] + end + + subgraph "Загрузка" + BC --> IR["Interpreter::run"] + IR --> R["Reader: парсит бинарник"] + R --> DOC["Document: дерево + стили + переменные"] + end + + subgraph "Инициализация" + DOC --> BOOT["iced::application boot"] + BOOT --> RC["RheiContext: компилирует Rhai-скрипты"] + BOOT --> EV0["evaluate_vdom: собирает VDOM целиком"] + EV0 --> APP["GlintApp: готов к работе"] + end + + subgraph "Круг жизни: каждый кадр" + APP --> LOOP{"iced event loop"} + + LOOP -->|пришло событие| MSG[Message] + MSG --> UPD["GlintApp::update"] + UPD --> SET["меняет переменную"] + SET --> TV["tracker помечает зависимые элементы как dirty"] + TV --> DIRTY["забрать dirty_set"] + DIRTY --> VDOM["evaluate_vdom_incr: пересчитать только dirty"] + VDOM --> STYLE["StyleSheet: применить стили (с кэшем)"] + STYLE --> NEW_VDOM["новый VDOM"] + + LOOP -->|по таймеру| VIEW["GlintApp::view"] + VIEW --> REND["render_element: Element → Iced-виджет"] + REND --> ICED["iced::Element дерево"] + ICED --> DIFF["Iced: сравнивает с предыдущим кадром"] + DIFF --> LAYOUT[Layout] + LAYOUT --> DRAW["GPU рисует"] + end + + subgraph "Стили — отдельно" + STYLE --> SI["StyleIndex: ищет правила за O(1)\u2013O(K)"] + SI --> SC["StyleCache: не парсит одно и то же дважды"] + end +``` + +--- + +## Этап 1: Компиляция — из текста в байткод + +Всё начинается с двух типов файлов: + +- **`.gltm`** — разметка: кнопки, панели, тексты, слайдеры и так далее. +- **`.glts`** — стили: CSS-подобные правила, селекторы, цвета, отступы. + +Их компилирует внешний crate **`glt`** (не часть этого репозитория). Он делает три вещи: + +### 1.1 Парсинг + +`Parser` читает `.gltm` и `.glts` и складывает всё в **`ModuleSoA`**. + +**Что такое ModuleSoA?** SoA = Structure of Arrays (структура массивов). Вместо того чтобы хранить элементы как список структур: + +```text +// Array of Structures (AoS) — как мы привыкли +Element { name: "Button", props: [...], children: [...] } +Element { name: "Text", props: [...], children: [...] } +``` + +компилятор хранит их как структуру с параллельными массивами: + +```text +// Structure of Arrays (SoA) — эффективнее для компилятора +ModuleSoA { + type_names: ["Button", "Text", ...], + properties_vec: [ [...], [...], ...], + hierarchy: [parent_id, parent_id, ...], +} +``` + +Так компилятор проходит по всем именам разом (кэш процессора не простаивает), +быстрее ищет родительские связи и легче применяет оптимизации. + +### 1.2 Построение AST + +Из `ModuleSoA` строится AST-дерево. Здесь раскрываются компоненты, if/each ветки, +подставляются параметры. + +### 1.3 Генерация байткода + +`Compiler` обходит AST и превращает его в бинарный формат **`.glbc`**: +- заголовок с magic-байтами (`"glBc"`) +- пул строк (все имена, классы, тексты — одним блоком) +- байт-кодированные опкоды (см. `opcodes.rs`: `OP_ELEM_PUSH`, `OP_PROP`, `OP_IF`, `OP_EACH` и т.д.) + +Результат — компактный бинарник, который можно быстро загрузить и скормить рантайму. + +--- + +## Этап 2: Загрузка — из байткода в Document + +Рантайм берёт `.glbc` и превращает его в структуры данных, с которыми можно работать. + +### `Interpreter::run(bytecode) → Document` + +Внутри `Reader` последовательно читает байткоп: +1. Проверяет magic-байты (это точно `.glbc`?) +2. Читает пул строк +3. Исполняет опкоды, на лету собирая дерево `Element` + +Параллельно происходят две важные вещи: + +**Стили:** каждая встреченная стилевая директива парсится в `StyleRule`, +потом из всех правил строится `StyleIndex` — каталог: «вот все правила для тэга Button, +вот для класса primary, вот для элемента с id=submit». Так поиск стиля +потом будет занимать не O(все правила), а O(пара штук). + +**Зависимости:** каждое свойство вида `"text": "Hello $name"` — это подсказка: +элемент зависит от переменной `name`. `ReactiveTracker` сканирует все свойства, +находит `$var` и запоминает: «элемент ElementId(5) зависит от переменной "name"». + +В итоге получается **`Document`**: +```rust +Document { + roots: Vec, // корневые элементы + components: HashMap, // компоненты + variables: HashMap, // начальные значения + stylesheet: StyleSheet, // таблица стилей + rhei_scripts: Vec, // init-скрипты + tracker: ReactiveTracker, // кто от чего зависит + interner: Interner, // пул уникальных строк +} +``` + +--- + +## Этап 3: Инициализация — подготовка к жизни + +`Document` готов, но его надо «завести». Это делает boot-функция Iced. + +### 3.1 Клонирование + +`doc.clone()` — все строки внутри Element имеют тип `&'a str` с исходным +временем жизни. После клонирования они становятся `&'static str` (рантайм +делает `Box::leak`, чтобы строки жили вечно — приложение работает до закрытия окна). + +### 3.2 Компиляция Rhai + +`RheiContext::new(scripts)`: +- Создаёт Rhai-движок (`Engine`) +- Компилирует все init-скрипты в AST и сохраняет их +- Собирает все функции из скриптов в глобальный модуль +- Потом `precompile_all_from_doc()` проходит по всему дереву Element и компилирует + каждый `__on:click { ... }` и каждое `!rhei:expr` в кэш. + **Теперь при клике не надо компилировать заново** — достаточно взять AST из кэша. + +### 3.3 Запуск init-скриптов + +`initialize()`: синхронизирует переменные с Rhai-скопом, выполняет init-скрипты, +забирает из скопа всё, что изменилось. + +### 3.4 Первый VDOM + +`evaluate_vdom()` — полный проход по дереву: +- Подставляет переменные в строки (`$name` → реальное значение) +- Вычисляет условия `@if` +- Раскрывает `@each` в реальное количество элементов +- Для каждого элемента находит подходящие стили и вычисляет `ComputedStyle` +- Присваивает `content_hash` + +Результат: `GlintApp { doc, rhei, vdom_roots }`. Первый кадр готов к показу. + +--- + +## Этап 4: Круг жизни — каждый кадр + +Iced работает в цикле: событие → `update()` → `view()` → отрисовка. + +### 4.1 Пришло событие: update() + +Пользователь нажал кнопку, подвигал слайдер, ввёл текст — Iced присылает `Message`. + +```rust +enum Message { + SliderChanged(Option, f64), // слайдер: (привязанная переменная, новое значение) + InputChanged(Option, String), // текстовое поле + ToggleChanged(Option, bool), // чекбокс + EventTriggered(String), // клик по кнопке: запустить Rhai-скрипт + WindowScrolled(f32), // скролл окна + ScrollableScrolled(u64, f32), // скролл внутри контейнера +} +``` + +**GlintApp::update()** делает так: + +1. **Меняет переменную.** Например, `SliderChanged("volume", 75)` → `variables["volume"] = 75.0`. +2. **Сообщает трекеру:** `tracker.on_variable_changed("volume")`. Трекер смотрит: + «от этой переменной зависят элементы с ID = 5, 12, 18». Он помечает их как dirty. +3. **Забирает dirty_set:** `tracker.take_dirty_set()`. +4. **Пересчитывает VDOM:** `evaluate_vdom_incr(roots, &dirty_set)`. Она проходит по дереву. + Если элемент в dirty_set — пересчитывает его (подстановка переменных, вычисление стилей). + Если нет — оставляет как есть. **Дети dirty-элемента тоже пересчитываются** (каскад). + +### 4.2 По таймеру: view() + +Даже если ничего не произошло, Iced вызывает `view()` каждый кадр (60 раз в секунду). +Нужно вернуть Iced-виджеты, которые он нарисует. + +**render_element()** — рекурсивная функция, которая превращает Element в Iced-виджет: + +- `Button` → `iced::button(...).on_press(...)` +- `Text` → `iced::text("...").size(16).color(...)` +- `Panel` → `iced::column[...].spacing(10)`, обёрнутый в контейнер с фоном и рамкой +- `Input` → `iced::text_input("placeholder", "value").on_input(...)` +- `Image` → `iced::image(path)` или `iced::svg(path)` +- Неизвестный тип → просто колонка с детьми + +Каждый виджет оборачивается в **`apply_universal_box_model`**: +```text +контейнер [margin] + контейнер [padding, border, background] + scrollable (если overflow: scroll/auto) + контейнер [padding] + сам виджет +``` + +**Проблема:** `render_element` создаёт **все** виджеты с нуля каждый кадр, даже если +Element не изменился. Iced потом диффит новое дерево со старым — но само построение +дерева стоит ~7ms. Это главный резерв оптимизации. + +### 4.3 Iced делает своё дело + +Iced получает дерево `iced::Element`, сравнивает с предыдущим (diff), вычисляет +раскладку (layout) и рисует через GPU (wgpu). Всё это без участия нашего кода. + +--- + +## Анатомия Element + +```rust +Element { + type_name: "Button", // что это за элемент + properties: [("label", "Click"), ("color", "red"), ...], // его свойства + computed_style: ComputedStyle { color: Some(Red), padding: Some(8px), ... }, // вычисленный стиль + element_id: ElementId(42), // уникальный ID в дереве + content_hash: 0xABCD1234, // хэш содержимого (для кэша виджетов) + children: [Element, ...], // дочерние элементы +} +``` + +## Анатомия стилей + +Стили хранятся в `StyleSheet` и работают в три этапа: + +**1. Индекс (`StyleIndex`):** при загрузке все CSS-правила раскладываются по полочкам: +```text +Правило: "Button.primary#submit { color: red; padding: 10px }" +→ by_tag["Button"] = { RuleId(1) } +→ by_class["primary"] = { RuleId(1) } +→ by_id["submit"] = { RuleId(1) } +``` + +**2. Поиск:** когда нужно найти стили для элемента `Button.primary#submit`, +мы берём пересечение множеств из всех трёх полок. Вместо проверки 500 правил — 3 lookup'а. + +**3. Кэш:** даже если стили найдены, `ComputedStyle::compute()` парсит все свойства +(цвет, отступы, шрифты — около 40 полей). Это дорого. `StyleCache` запоминает +результат: `hash(type_name, properties, эпоха) → ComputedStyle`. Если элемент +не менялся — берём готовый стиль из кэша, не парсим. + +--- + +## Событийный цикл на примере слайдера + +``` +1. Пользователь двигает слайдер громкости +2. Iced: SliderChanged(Some("volume"), 75.0) +3. GlintApp::update: + a. variables["volume"] = Float(75.0) + b. tracker.on_variable_changed("volume") + → грязные: ElementId(5) — текст с "$volume", ElementId(12) — ширина от "$volume" + c. evaluate_vdom_incr(roots, &{5, 12}) + → Element 5: пересчитать текст (новая громкость) + → Element 12: пересчитать ширину + → остальные 48 элементов: не трогать +4. GlintApp::view: + → render_element для всех 50 root-элементов + → рекурсивно для всех детей (даже для тех 48, что не менялись) + → Iced получает полностью новое дерево из 200+ виджетов +5. Iced: диффит → находит 2 изменения → перерисовывает 2 области +``` + +**Узкое место:** шаг 4. VDOM пересчитал только 2 элемента из 50 (спасибо ReactiveTracker). +Но render_element создаёт виджеты для всех 200+ узлов. Iced потом всё равно диффит +и ничего не делает с 198 из них, но время на их создание уже потрачено. diff --git a/docs/ru/architecture/02-performance.md b/docs/ru/architecture/02-performance.md new file mode 100644 index 0000000..6f0c184 --- /dev/null +++ b/docs/ru/architecture/02-performance.md @@ -0,0 +1,68 @@ +# Анализ производительности + +Измерения с флагом `--perf` на `desktop.glbc`. + +## Результаты замеров + +**Steady state** (нет событий, idle): + +``` +VDOM: 6.6ms | Style: 0.3ms | Render: 7.2ms | Total: 14.1ms +``` + +**При событиях** (перетаскивание слайдера, пиковые значения): + +``` +VDOM: 17.4ms | Style: 0.8ms | Render: 15.2ms | Total: 33.5ms ⚠️ +``` + +**60 FPS frame budget: 16ms.** В покое укладываемся (14ms), при событиях — нет (до 33ms). + +## Анализ bottleneck'ов + +### Style matching — НЕ bottleneck (0.3-0.8ms) + +Style matching занимает менее 1ms даже на пике. Это результат работы: +- **Phase 4** (StyleIndex) — O(K) вместо O(N×M) +- **Phase 8** (StyleCache) — мемоизация computed style + +### VDOM eval — основной потребитель (6-17ms) + +В покое ~6.6ms — это полный проход по дереву Element'ов. При событиях до 17ms: +- `evaluate_vdom_incr` пересчитывает dirty-элементы (Phase 3) +- Каждое событие может делать dirty целые поддеревья +- Внутри: `resolve_string`, `resolve_prop`, `compute_cached`, рекурсивный проход + +### Render — второй потребитель (7-15ms) + +**Здесь главный резерв оптимизации.** `render_element` создаёт ВСЕ Iced-виджеты +каждый кадр с нуля, даже если Element не изменился. Iced затем диффит новое дерево +со старым — но само построение дерева стоит ~7ms. + +### Сценарий: слайдер + +1. `SliderChanged` → `age` и `volume_level` меняются +2. `tracker.on_variable_changed` → dirty_set для зависимых элементов +3. `evaluate_vdom_incr` пересчитывает dirty-элементы и их детей +4. `view()` → `render_element` для ВСЕХ элементов (полный перерендер) +5. Итог: VDOM 13ms + Render 14ms = 27ms — пропуск кадра + +Первые несколько кадров после события — самые тяжёлые (VDOM ~13ms), затем +стабилизируются (~7ms), так как dirty_set постепенно очищается. + +## Рекомендации + +1. **Phase 9.3 — кэш виджетов** — сократит Render с 7ms до ~0ms для неизменившихся + элементов. Если изменился 1 элемент из 50, перерендеривать нужно только его. + Это снизит общее время с 14ms до ~7ms в покое. + +2. **Phase 1 — Value enum в стилях** — `ComputedStyle::compute()` и `parse_*()` + принимают `&str` и парсят каждое свойство. Если передавать `&Value` — парсинг + не нужен. Потенциально ускорит и style matching, и VDOM eval. + +3. **Phase 2 — InternedStr** — сравнения строк (`type_name == "Button"`, + `key == "padding-top"`) происходят тысячами за кадр. Замена на сравнение u32 + даст 1.5-2× в VDOM и render путях. + +4. **Phase 0.3 — flamegraph** — подтвердить гипотезы замерами профилировщика + (`perf record`), прежде чем вкладываться в оптимизацию. diff --git a/docs/ru/modules/01-types.md b/docs/ru/modules/01-types.md new file mode 100644 index 0000000..d4ad782 --- /dev/null +++ b/docs/ru/modules/01-types.md @@ -0,0 +1,182 @@ +# Модуль типов: `src/interpreter/types.rs` + +Базовые типы данных рантайма: интернирование строк, Value, DOM-элементы, плоский VDOM и Document. + +--- + +## `InternedStr(u32)` + +Новыйтип-обёртка над `u32`. Компактный идентификатор строки для быстрых сравнений. + +```rust +pub struct InternedStr(pub u32); +``` + +**Методы:** +- `from_raw(id: u32) -> Self` — константный конструктор +- `raw(&self) -> u32` — сырое значение +- `eq_str(&self, other: &str) -> bool` — сравнение со строкой через `Interner::lookup` + +**Используется:** определён, но в горячих путях (Element, style matching, properties) не применяется. Фаза 2 не завершена. + +--- + +## `Interner` + +Пул строк с выделением уникальных ID. Каждая строка интернируется один раз. + +```rust +pub struct Interner { + strings: Vec, + map: HashMap, + next_id: u32, +} +``` + +- `new()` — пустой интернер +- `intern(&mut self, s: &str) -> InternedStr` — получить/создать ID +- `lookup(&self, id: InternedStr) -> &str` — получить строку по ID +- `intern_or_none(&mut self, s: Option<&str>) -> Option` — опциональное интернирование + +Хранится в `Document::interner`. Доступен через `with_interner()` (thread_local). + +--- + +## `Value` + +Типизированное значение переменной. Замена сырым строкам для устранения parse/format round-trip. + +```rust +pub enum Value { + Str(CompactString), + Int(i64), + Float(f64), + Bool(bool), + Array(Vec), + None, +} +``` + +**Методы:** +- `as_str(&self) -> Option<&str>` — заимствование строки +- `to_owned_string(&self) -> CompactString` — форматирование в строку (используется в renderer) + +**Реализовано:** `From<&str>`, `From`, `From`, `From`, `From`, `From>`. +`PartialEq` — Float сравнивается с `f64::EPSILON`. + +**Используется:** `Document::variables`, конверсии `Value↔Dynamic` в rhei.rs. +**НЕ используется:** в стилях (`ComputedStyle::compute` всё ещё принимает `&str`, matched_sheets — `HashMap`). + +--- + +## `Element<'a>` + +Узел VDOM-дерева. Центральный тип — из него строится UI. + +```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>, + pub computed_style: ComputedStyle, + pub element_id: ElementId, + pub content_hash: u64, // Phase 9.1: хэш для кэша виджетов +} +``` + +**Методы (в types.rs):** +- `id(&self) -> Option<&str>` — значение HTML-атрибута `id` +- `new(type_name)` — свежий элемент с `element_id: u32::MAX` +- `new_with_id(type_name, id)` — с заданным ElementId + +**Методы (в renderer.rs):** +- `get_prop(&self, key: &str) -> Option<&str>` — значение свойства по ключу +- `push_prop(key, val)` — добавить свойство +- `set_prop(key, val)` — установить/перезаписать свойство + +--- + +## `ComponentDef<'a>` + +Определение компонента из шаблона. + +```rust +pub struct ComponentDef<'a> { + pub name: String, + pub params: Vec<(String, String)>, + pub children: Vec>, +} +``` + +Хранится в `Document::components`. Используется в `evaluate_vdom` при разворачивании +компонентов: параметры передаются через переменные, тело компонента вставляется как children. + +--- + +## `Document<'a>` + +Полное состояние приложения после загрузки байткода. + +```rust +pub struct Document<'a> { + pub roots: Vec>, + pub components: HashMap>, + pub variables: HashMap, + pub rhei_scripts: Vec, + pub stylesheet: StyleSheet, + pub interner: Interner, + pub tracker: ReactiveTracker, +} +``` + +Создаётся в `Interpreter::run()`. Клонируется при старте Iced-приложения (boot function). +Содержит всё необходимое для интерпретации: дерево, стили, переменные, реактивность. + +--- + +## `VNode<'a>` и `FlatVDom<'a>` + +Плоское представление VDOM для эффективной сериализации/десериализации. + +```rust +pub struct VNode<'a> { + pub id: NodeId, + pub type_name: &'a str, + pub properties: Range, + pub children_range: Range, + pub computed_style: ComputedStyle, + pub element_id: ElementId, +} + +pub struct FlatVDom<'a> { + pub nodes: Vec>, + pub properties: Vec<(String, String)>, + root_indices: Vec, +} +``` + +**Методы:** +- `new()` — пустой +- `from_elements(elements)` — рекурсивно уплощает дерево Element'ов в VNode'ы +- `into_elements(self) -> Vec` — обратная сборка +- `get_node(idx)`, `node_count()`, `root_count()`, `root_indices()` + +**Используется:** в тестах (`test_flat_vdom_roundtrip`, `test_flat_vdom_nested`, `test_flat_vdom_empty`). +В горячем пути не участвует — VDOM передаётся как `Vec`. + +--- + +## `InterpError` + +Ошибки загрузки байткода. + +```rust +pub enum InterpError { + BadMagic, + UnexpectedEof, + InvalidUtf8, + UnexpectedPop, +} +``` + +Реализует `Display` и `std::error::Error`. Возвращается из `Interpreter::run()`. diff --git a/docs/ru/modules/02-style.md b/docs/ru/modules/02-style.md new file mode 100644 index 0000000..698aa86 --- /dev/null +++ b/docs/ru/modules/02-style.md @@ -0,0 +1,482 @@ +# Модуль стилей: `src/interpreter/style.rs` + +Система CSS-подобных стилей для Glint: парсинг селекторов, построение индекса, каскадное разрешение свойств и кеширование вычисленных стилей. + +--- + +## Перечисления-помощники + +### `SizeValue` +```rust +pub enum SizeValue { + Px(f32), + Percent(f32), +} +``` +Абсолютное (`Px`) или относительное (`Percent`) значение размера. + +```rust +impl SizeValue { + pub fn resolve(self, relative_to: Option) -> f32 +} +``` +`Percent` разрешается относительно `relative_to`; `Px` возвращается как есть. При `Percent` и `relative_to = None` возвращается процент как число. + +### `Overflow` +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Overflow { + #[default] Visible, + Hidden, + Scroll, + Auto, +} +``` +Используется для `overflow-x`, `overflow-y`. + +### `Position` +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Position { + #[default] Static, + Relative, + Absolute, + Sticky, + Fixed, +} +``` +Определяет схему позиционирования элемента. + +### `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 for iced::alignment::Horizontal +``` + +--- + +## Парсинг селекторов + +### `AttributeSelector` +```rust +pub enum AttributeSelector { + Exists(String), + Equals(String, String), +} +``` +Селектор атрибута: `[attr]` или `[attr=value]`. + +### `Combinator` +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Combinator { + Descendant, // пробел + Child, // > + NextSibling, // + + Subsequent, // ~ +} +``` + +### `split_selectors(input: &str) -> Vec` +Разделяет группу селекторов по запятой с учётом вложенности скобок. Например, `"Button, Label:hover"` → `["Button", "Label:hover"]`. + +### `CompoundSelector` +```rust +#[derive(Debug, Clone)] +pub struct CompoundSelector { + pub tag: Option, + pub id: Option, + pub classes: Vec, + pub pseudo_classes: Vec, + pub attributes: Vec, +} +``` + +**Методы:** + +- `CompoundSelector::parse(input: &str) -> Self` — парсит простой селектор вида `Button#id.primary:hover[named=val]`. Разбирает посимвольно, группируя части по первому символу (`#`, `.`, `:`, `[`). + +- `fn specificity(&self) -> (u32, u32, u32)` — возвращает специфичность по правилу CSS: (id, class+attr+pseudo, tag). + +- `pub fn matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) -> bool` — проверяет, соответствует ли элемент данному простому селектору. Учитывает: + - Совпадение `tag` (или `*`) + - Совпадение `id` + - Наличие всех `classes` + - Наличие всех `attributes` (Exists / Equals) + - Псевдоклассы: `first-child`, `last-child`, `first-of-type`, `empty`, `root`, `nth-child(...)`; остальные сравниваются с `active_pseudo`. + +### `ComplexSelector` +```rust +#[derive(Debug, Clone)] +pub struct ComplexSelector { + pub compounds: Vec, + pub combinators: Vec, +} +``` + +**Методы:** + +- `ComplexSelector::parse(input: &str) -> Self` — парсит сложный селектор (например `Panel > Button.primary`). Разбивает на части по комбинаторам (`>`, `+`, `~`, пробел), парсит каждую как `CompoundSelector`. + +- `fn check_compound_against(&self, i, info: &AncestorInfo) -> bool` — проверяет, соответствует ли `i`-й compound переданной информации о предке. + +- `pub fn matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) -> bool` — полная проверка сложного селектора: последний compound — целевой элемент, остальные — предки/соседи в соответствии с комбинаторами. + +- `pub fn specificity(&self) -> (u32, u32, u32)` — сумма специфичностей всех compounds. + +- `pub fn as_simple(&self) -> Option<&CompoundSelector>` — если compounds содержит ровно один элемент, возвращает его; иначе `None`. + +- `pub fn has_pseudo_class(&self, pc: &str) -> bool` — есть ли среди compounds указанный псевдокласс. + +--- + +## Вспомогательные структуры + +### `AncestorInfo` +```rust +#[derive(Debug, Clone)] +pub struct AncestorInfo { + pub type_name: String, + pub id: Option, + pub classes: Vec, +} +``` + +Методы: +- `AncestorInfo::new(type_name, classes) -> Self` +- `AncestorInfo::new_with_id(type_name, id, classes) -> Self` + +Используется при проверке сложных селекторов — описывает предка или соседний элемент. + +### `StructuralContext` +```rust +#[derive(Debug, Clone, Default)] +pub struct StructuralContext { + pub sibling_index: usize, // 0-based + pub sibling_total: usize, + pub type_index: usize, // среди элементов того же типа + pub type_total: usize, + pub has_children: bool, + pub is_root: bool, +} +``` + +Используется для разрешения структурных псевдоклассов (`first-child`, `nth-child`, `empty`, `root`). + +--- + +## `StyleRule` +```rust +#[derive(Debug, Clone)] +pub struct StyleRule { + pub selector: ComplexSelector, + pub properties: HashMap, +} + +impl StyleRule { + pub fn build(selector_str: String, properties: HashMap) -> Self +} +``` + +--- + +## `StyleIndex` +```rust +pub type RuleId = usize; + +#[derive(Debug, Clone)] +pub struct StyleIndex { + pub by_tag: HashMap>, + pub by_class: HashMap>, + pub by_id: HashMap>, + pub by_tag_class: HashMap<(String, String), Vec>, + pub by_tag_id: HashMap<(String, String), Vec>, + pub complex_rules: Vec<(RuleId, RuleId)>, + pub universal_rules: Vec, + pub rule_specificities: Vec<(u32, u32, u32)>, + pub rules: Vec, + pub epoch: u64, +} + +impl StyleIndex { + pub fn new() -> Self +} +``` + +Индекс для быстрого поиска правил. Строится в `StyleSheet::build_index`: +- `by_tag` / `by_class` / `by_id` / `by_tag_class` / `by_tag_id` — индексы для простых селекторов +- `complex_rules` — правила со сложными селекторами (всегда проверяются в лоб) +- `universal_rules` — правила с `*` +- `rule_specificities` — кеш специфичностей +- `epoch` — монотонно возрастающий счётчик для инвалидации кеша + +--- + +## `StyleCache` +```rust +#[derive(Debug, Clone)] +pub struct StyleCache { + entries: HashMap, + 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], + ) -> ComputedStyle + pub fn clear(&mut self) +} +``` + +Кеш вычисленных стилей. Ключ — хеш от `type_name`, inline-свойств и `epoch`. При превышении `max_entries` кеш полностью очищается. + +--- + +## `StyleSheet` +```rust +#[derive(Debug)] +pub struct StyleSheet { + rules: Vec, + index: Option, + epoch: u64, + cache: Mutex, +} +``` + +Главный тип модуля. Содержит список правил, опциональный индекс и кеш. Реализует `Clone` (с новым пустым кешем) и `Default`. + +**Методы:** + +- `StyleSheet::new() -> Self` — создаёт пустой лист. +- `pub fn add_rule(&mut self, selector: String, properties: HashMap)` — добавляет правило. Пропускает селектор через `split_selectors` (поддержка групп через запятую). Сбрасывает `index = None`. +- `pub fn build_index(&mut self)` — перестраивает `StyleIndex`. Увеличивает `epoch`, очищает кеш. Для каждого правила: + - Вычисляет специфичность + - Если селектор простой (1 compound) — индексирует по tag/class/id/атрибутам + - Если сложный — помечает как `complex_rules` + - Универсальные (`*`) попадают в `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>>` — использует индекс для быстрого поиска: собирает кандидатов из `universal_rules`, `by_tag`, `by_class`, `by_id`, `complex_rules`; фильтрует через `ComplexSelector::matches`; сортирует по специфичности. +- `pub fn matching_rules(...) -> Vec<&HashMap>` — поиск подходящих правил. Пытается `query_index`; если индекса нет — линейный перебор всех `self.rules` с сортировкой. +- `pub fn matching_pseudo_rules(pseudo, ...) -> HashMap` — ищет правила, содержащие указанный псевдокласс (например `:hover`). Использует `query_index_for_pseudo` или линейный перебор. +- `pub fn compute_cached(type_name, props, matched_sheets) -> ComputedStyle` — вычисляет итоговый стиль через кеш (обёртка над `StyleCache::get_or_compute`). Включает `PerfScope::new("style")`. +- `pub fn clear_cache(&self)` — очищает кеш. +- `pub fn is_empty(&self) -> bool` — `self.rules.is_empty()`. + +**Методы с `#[cfg(feature = "parallel")]`:** + +- `matching_rules_batch(type_names, el_ids, el_classes_list, ...) -> Vec>>` — параллельный batch-поиск через `rayon::par_iter`. + +--- + +## `ComputedStyle` +```rust +#[derive(Debug, Clone, Default)] +pub struct ComputedStyle { + pub font_size: Option, + pub color: Option, + pub padding: Option, + pub padding_top: Option, + pub padding_right: Option, + pub padding_bottom: Option, + pub padding_left: Option, + + pub margin: Option, + pub margin_top: Option, + pub margin_right: Option, + pub margin_bottom: Option, + pub margin_left: Option, + + pub background: Option, + pub spacing: Option, + pub border_radius: Option, + pub border_width: Option, + pub border_color: Option, + + pub width: Option, + pub height: Option, + pub min_width: Option, + pub max_width: Option, + pub min_height: Option, + pub max_height: Option, + + pub direction: Option, + pub align_items: Option, + pub content_align: Option, + + pub flex_grow: Option, + + pub position: Option, + pub top: Option, + pub right: Option, + pub bottom: Option, + pub left: Option, + + pub overflow_x: Option, + pub overflow_y: Option, + pub display: Option, + + pub opacity: Option, + pub font_weight: Option, + pub line_height: Option, + pub text_align: Option, +} +``` + +Итоговый вычисленный стиль элемента. Все поля — `Option`; отсутствующее свойство означает «не задано / наследуется от родителя». + +### `ComputedStyle::compute()` +```rust +pub fn compute( + inline: &[(Cow<'_, str>, Cow<'_, str>)], + matched_sheets: &[&HashMap], +) -> Self +``` + +Собирает стиль через `lookup()`: для каждого поля вызывается `lookup(key, inline, matched_sheets)`, затем парсится соответствующей функцией. Особенности: +- `padding`/`margin` — сначала ищутся индивидуальные (`-top`, `-right`, и т.д.), потом общие. +- `spacing` — альтернативное имя `gap`. +- `background` — сначала `background`, затем `background-color`. +- `overflow-x`/`overflow-y` — если индивидуальный не найден, применяется общий `overflow`. +- `flex_grow` — парсится как `f32`, кастуется в `u16`. + +### `ComputedStyle::compute_batch()` +```rust +#[cfg(feature = "parallel")] +pub fn compute_batch<'a>( + pairs: &[(&[(Cow<'_, str>, Cow<'_, str>)], &[&'a HashMap])], +) -> Vec +``` +Параллельный batch-вариант через `rayon::par_iter`. + +### `ComputedStyle::apply_overrides()` +```rust +pub fn apply_overrides(&mut self, sheet: &HashMap) +``` +Применяет (перезаписывает) заданный набор свойств поверх существующего стиля. Используется для динамических изменений (например `:hover`-правила, inline-переопределения). + +### Как работает `lookup()` +```rust +fn lookup<'a>( + key: &str, + inline: &'a [(Cow<'_, str>, Cow<'_, str>)], + matched_sheets: &[&'a HashMap], +) -> Option<&'a str> +``` + +Порядок разрешения свойства: +1. **Inline-свойства** — перебор пар `(key, value)`. Поддерживает префикс `style:` (т.е. `style:color` эквивалентен `color`). +2. **matched_sheets** — список словарей от подходящих CSS-правил, отсортированный по специфичности. Перебирается с конца (последний — самый специфичный). +3. Возвращается первое найденное значение. + +--- + +## Функции парсинга + +| Функция | Сигнатура | Описание | +|---|---|---| +| `parse_size` | `(s: &str) -> Option` | Парсит размер: `"10"` → `Px(10)`, `"50%"` → `Percent(50)`. `auto`, `fill`, `stretch` → `None` | +| `parse_color` | `(s: &str) -> Option` | Парсит цвет: `#rgb`, `#rrggbb`, `#rrggbbaa`, имена (`white`, `black`, `transparent`) | +| `parse_length` | `(s: &str) -> Option` | Парсит длину Iced: `"fill"`/`"100%"`, `"shrink"`/`"auto"`, `"50"` → `Fixed(50)` | +| `parse_overflow` | `(s: &str) -> Option` | `visible`, `hidden`, `scroll`, `auto` | +| `parse_position` | `(s: &str) -> Option` | `static`, `relative`, `absolute`, `sticky`, `fixed` | +| `parse_display` | `(s: &str) -> Option` | `none`, `block`, `flex`, `grid`, `inline` | +| `parse_direction` | `(s: &str) -> Option` | `row`/`horizontal`, `column`/`vertical`, `grid` | +| `parse_alignment` | `(s: &str) -> Option` | `start`, `center`, `end` | +| `parse_content_align` | `(s: &str) -> Option` | `start`/`left`/`top`, `center`, `end`/`right`/`bottom` | +| `parse_opacity` | `(s: &str) -> Option` | Число 0.0–1.0, clamp | +| `parse_font_weight` | `(s: &str) -> Option` | Имена: `normal`→400, `bold`→700, `lighter`→300, `bolder`→900; числовые значения | +| `parse_text_align` | `(s: &str) -> Option` | `left`, `center`, `right` | + +### `resolve_size` +```rust +pub fn resolve_size(v: Option, relative_to: Option) -> Option +``` +Удобная обёртка над `SizeValue::resolve`, возвращает `Option`. + +--- + +## Использование в `renderer.rs` + +Поля `ComputedStyle` активно используются в `/home/faynot/software/glint-runtime/src/renderer.rs`: + +| Поле | Где используется | +|---|---| +| `.color` | Цвет текста в кнопках, текстовых полях, Label | +| `.background` | Фон контейнеров, кнопок, текстовых полей | +| `.padding*` | `iced::Padding` для кнопок, полей ввода, контейнеров | +| `.margin*` | Отступы вокруг элементов | +| `.border_radius`, `.border_width`, `.border_color` | Рамки кнопок, полей ввода, контейнеров | +| `.width`, `.height` | Размеры Scrollable, Column, Row, Image | +| `.min_width`, `.max_width`, `.min_height`, `.max_height` | Ограничения размеров | +| `.direction` | Направление флекса (Row/Column) | +| `.align_items` | Выравнивание дочерних элементов | +| `.content_align` | Выравнивание контента | +| `.flex_grow` | Flex-grow с `FillPortion` | +| `.spacing` | `iced::container::Style` spacing, gap в Row/Column | +| `.position` | Static / Fixed / Absolute / Sticky | +| `.top`, `.right`, `.bottom`, `.left` | Позиционирование | +| `.overflow_x`, `.overflow_y` | Скроллинг (`Scrollable`) | +| `.display` | `Display::None` — скрытие элемента | +| `.opacity` | Прозрачность | +| `.font_weight` | Вес шрифта в Text | +| `.line_height` | Межстрочный интервал | +| `.text_align` | Горизонтальное выравнивание текста | +| `.font_size` | Размер шрифта (передаётся от родителя) | + +--- + +## `nth_matches(expr: &str, n: usize) -> bool` + +Внутренняя функция для разрешения `:nth-child(an+b)`, `:nth-child(odd)`, `:nth-child(even)` и `:nth-child(<число>)`. Поддерживает отрицательные `a` и `b`. + +--- + +## Связи с другими модулями + +- `types.rs` — каждый `DomNode` (как элемент, так и текстовый узел) содержит `computed_style: ComputedStyle`. +- `renderer.rs` — импортирует `{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign, resolve_size}`. +- `mod.rs` — использует `AncestorInfo`, `ComputedStyle`, `StructuralContext` при обходе DOM. diff --git a/docs/ru/modules/03-reactive.md b/docs/ru/modules/03-reactive.md new file mode 100644 index 0000000..cde5652 --- /dev/null +++ b/docs/ru/modules/03-reactive.md @@ -0,0 +1,92 @@ +# Модуль реактивности: `src/interpreter/reactive.rs` + +Система отслеживания зависимостей между переменными и элементами VDOM. +Позволяет пересчитывать только изменившиеся элементы (Phase 3). + +--- + +## `ElementId(u32)` + +Уникальный идентификатор элемента в VDOM-дереве. Раздаётся `ReactiveTracker::alloc_id()` +во время загрузки шаблона. + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ElementId(pub u32); +``` + +**Используется как:** +- Ключ в `dirty_set: HashSet` +- Ключ в `dependencies: HashMap>` +- Поле в `Element::element_id` (связь VDOM → tracker) +- Источник для `iced::widget::Id` (Phase 9.2: `format!("ti:{}", id.0)`) +- Источник для scrollable_id (`el.element_id.0 as u64`) + +--- + +## `ReactiveTracker` + +Граф зависимостей: переменная → список элементов, которые её используют. + +```rust +pub struct ReactiveTracker { + subscribers: HashMap>, + dependencies: HashMap>, + dirty_set: HashSet, + next_id: u32, +} +``` + +**Поля:** +- `subscribers` — для каждой переменной: какие ElementId от неё зависят +- `dependencies` — для каждого элемента: от каких переменных он зависит (обратная связь) +- `dirty_set` — элементы, которые нужно пересчитать в следующем фрейме +- `next_id` — счётчик для `alloc_id()` + +### Методы + +| Метод | Описание | +|-------|----------| +| `new()` | Пустой tracker | +| `alloc_id() -> ElementId` | Выделить новый ID, увеличить счётчик | +| `add_dependency(element, var_name)` | Зарегистрировать зависимость | +| `scan_value(element, value)` | Сканировать строку на `$var` и добавить зависимости | +| `on_variable_changed(name)` | Пометить все зависимые элементы как dirty | +| `take_dirty_set() -> HashSet` | Забрать dirty_set и очистить | +| `is_dirty(id) -> bool` | Проверить, помечен ли элемент | +| `reset()` | Очистить все данные | + +### scan_value() + +Парсит строку в поиске `$var_name` паттернов: + +```rust +"Hello $name, you are $age years old" +// → add_dependency(element, "name") +// → add_dependency(element, "age") +``` + +Используется во время загрузки шаблона для каждой properties строки, +содержащей `$`. В результате каждый элемент знает, от каких переменных зависит. + +### Цикл обновления + +``` +Событие → update() → on_variable_changed("var") + → dirty_set = {element_A, element_B, ...} + → take_dirty_set() → evaluate_vdom_incr(roots, &dirty_set) + → для элементов в dirty_set: пересчитать + → для остальных: вернуть как есть +``` + +--- + +## Тесты + +| Тест | Что проверяет | +|------|--------------| +| `test_basic_dependency_tracking` | Два элемента, две переменные, правильность dirty_set | +| `test_scan_value` | Парсинг `$var` из строки | +| `test_scan_no_vars` | Строка без `$` не создаёт зависимостей | +| `test_take_dirty_set` | `take_dirty_set()` очищает внутренний set | +| `test_reset` | `reset()` очищает всё | diff --git a/docs/ru/modules/04-rhei.md b/docs/ru/modules/04-rhei.md new file mode 100644 index 0000000..223e7cc --- /dev/null +++ b/docs/ru/modules/04-rhei.md @@ -0,0 +1,200 @@ +# Модуль Rhai: `src/interpreter/rhei.rs` + +Интеграция скриптового движка [Rhai](https://rhai.rs/) — компиляция, кеширование AST и выполнение выражений/скриптов. + +--- + +## `RHEI_PREFIX` — префикс Rhai-выражений + +```rust +pub const RHEI_PREFIX: &str = "__rhei:"; +``` + +Константа-маркер для свойств элементов, содержимое которых должно интерпретироваться как Rhai-выражение. Используется в `collect_and_precompile()` для выборки свойств вида `!rhei:...`. + +--- + +## `RheiContext` — контекст выполнения Rhai + +```rust +pub struct RheiContext { + engine: Engine, + init_ast: AST, + scope: RefCell>, + action_cache: RefCell>, + expr_cache: RefCell>, +} +``` + +| Поле | Назначение | +|---|---| +| `engine` | Настроенный экземпляр `rhai::Engine` | +| `init_ast` | Объединённое AST всех скриптов инициализации (включая определения функций) | +| `scope` | Общая область видимости (`Scope`), разделяемая между вызовами; обёрнута в `RefCell` для interior mutability | +| `action_cache` | Кеш скомпилированных скриптов (действий), ключ — исходный код | +| `expr_cache` | Кеш скомпилированных выражений, ключ — исходный код | + +--- + +## Конструкторы + +### `new(scripts)` + +```rust +pub fn new(scripts: &[String]) -> Self +``` + +Создаёт контекст через `new_empty()` и сразу прекомпилирует все переданные скрипты вызовом `precompile_scripts()`. + +### `new_empty(scripts)` + +```rust +fn new_empty(scripts: &[String]) -> Self +``` + +1. Создаёт `Engine::new()`. +2. Настраивает обработчики `on_print` (вывод в stdout с префиксом `[rhei]`) и `on_debug` (вывод в stderr). +3. Компилирует все скрипты и сливает их AST в единое дерево через `merge()`. Ошибки компиляции отдельных блоков логируются, но не прерывают процесс. +4. Из объединённого AST создаёт модуль (`Module::eval_ast_as_new`) с пустым скопом — в нём регистрируются глобальные функции, определённые в скриптах. Модуль регистрируется в движке как глобальный (`register_global_module`). +5. Инициализирует пустой `Scope`, пустые кеши `action_cache` и `expr_cache`. + +--- + +## `sync_scope()` — синхронизация переменных + +```rust +pub fn sync_scope(&self, variables: &HashMap) +``` + +Синхронизирует значения из внешнего `HashMap` в Rhai `Scope`: +- Если переменная уже есть в скопе и её значение не изменилось — пропускает. +- Если переменная есть — обновляет через `set_value()`. +- Если переменной нет — добавляет через `push_dynamic()`. + +Преобразование `Value → Dynamic` выполняется через `value_to_dynamic()`. + +--- + +## `initialize()` — инициализация + +```rust +pub fn initialize(&self, variables: &mut HashMap) +``` + +1. Синхронизирует переменные через `sync_scope()`. +2. Запускает `init_ast` (объединённое AST всех скриптов) через `run_ast_with_scope()`. +3. Обходит все переменные скопа через `iter_raw()` и записывает обратно в `HashMap` те, чьи значения изменились. + +--- + +## `eval_expr()` — вычисление выражения + +```rust +pub fn eval_expr(&self, expr: &str, variables: &HashMap) -> Value +``` + +1. Синхронизирует переменные. +2. Получает (компилирует или берёт из кеша) AST выражения через `get_or_compile_expr()`. +3. Выполняет через `eval_ast_with_scope::()`. +4. Преобразует результат `Dynamic → Value` через `dynamic_to_value()`. +5. При ошибке возвращает `Value::None`. + +--- + +## `eval_condition()` — вычисление условия + +```rust +pub fn eval_condition(&self, expr: &str, variables: &HashMap) -> bool +``` + +Аналогичен `eval_expr()`, но типизирован как `bool`. При ошибке возвращает `false`. + +--- + +## `execute_action()` — выполнение скрипта + +```rust +pub fn execute_action(&self, script: &str, variables: &mut HashMap) +``` + +1. Синхронизирует переменные. +2. Получает AST скрипта через `get_or_compile_action()`. +3. Выполняет через `run_ast_with_scope()`. +4. После выполнения обходит скоп и записывает обратно в `HashMap` изменившиеся переменные. + +--- + +## Кеширование AST + +### `get_or_compile_action(script)` + +```rust +fn get_or_compile_action(&self, script: &str) -> Option +``` + +Проверяет `action_cache`. При промахе компилирует через `engine.compile()`, сохраняет в кеш. + +### `get_or_compile_expr(expr)` + +```rust +fn get_or_compile_expr(&self, expr: &str) -> Option +``` + +Проверяет `expr_cache`. При промахе компилирует через `engine.compile_expression()`, сохраняет в кеш. + +Оба метода при ошибке компиляции логируют её и возвращают `None`. + +--- + +## Пакетная прекомпиляция + +```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) +``` + +| Метод | Действие | +|---|---| +| `precompile_scripts` | Компилирует каждый скрипт как действие | +| `precompile_actions` | То же, что `precompile_scripts` (алиас) | +| `precompile_exprs` | Компилирует каждое выражение | +| `precompile_all_from_doc` | Компилирует все `doc.rhei_scripts` и рекурсивно обходит дерево элементов | + +### `collect_and_precompile()` + +```rust +fn collect_and_precompile(el: &super::Element, ctx: &RheiContext) +``` + +Рекурсивно обходит дерево `Element`: +- Для свойств, начинающихся с `__on:*` и непустых — компилирует как действие. +- Для свойств, начинающихся с `RHEI_PREFIX` (`!rhei:`) — компилирует оставшуюся часть как выражение. + +--- + +## Конвертация типов + +### `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 каждого элемента) +``` + +### `dynamic_to_value(d: &Dynamic) -> Value` + +Проверяет тип через `is_string()`, `is_int()`, `is_float()`, `is_bool()`, `is_array()` в порядке приоритета. Если тип не распознан — возвращает `Value::None`. + +### `str_to_dynamic(s: &str) -> Dynamic` + +Эвристический парсер строки, пробует последовательно: +1. `s.parse::()` — целое число +2. `s.parse::()` — дробное число +3. `s.parse::()` — булево значение +4. Иначе — `Dynamic::from(s)` как строка diff --git a/docs/ru/modules/05-renderer.md b/docs/ru/modules/05-renderer.md new file mode 100644 index 0000000..630a56f --- /dev/null +++ b/docs/ru/modules/05-renderer.md @@ -0,0 +1,537 @@ +# Модуль рендерера: `src/renderer.rs` + +Преобразует дерево `Element` в виджеты Iced. Отвечает за построение иерархии виджетов, применение боксовой модели, обработку псевдоклассов `:hover`/`:active`, позиционирование (fixed, absolute, sticky) и рендеринг всех встроенных типов элементов. + +--- + +## Импорты + +```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` и `get_or_create_widget_id()` + +```rust +thread_local! { + static WIDGET_ID_CACHE: RefCell> = ...; +} + +fn get_or_create_widget_id(key: u32, prefix: &'static str) -> iced::widget::Id +``` + +`thread_local`-кеш для `iced::widget::Id`. Ключ — кортеж `(element_id, префикс)`. Строка формируется как `"{prefix}:{key}"` и «протекает» через `Box::leak`, чтобы получить `&'static str`. Используется для `scrollable::id` (префикс `"sc"`) и `text_input::id` (префикс `"ti"`). + +--- + +## Методы `Element` + +### `get_prop()` +```rust +impl<'a> Element<'a> { + #[inline] + pub fn get_prop(&self, key: &str) -> Option<&str> +} +``` +Ищет свойство по ключу в `self.properties`. Возвращает значение или `None`. + +### `push_prop()` +```rust +pub fn push_prop>, V: Into>>(&mut self, key: K, val: V) +``` +Добавляет пару `(key, val)` в `self.properties`. + +### `set_prop()` +```rust +pub fn set_prop>, V: Into>>(&mut self, key: K, val: V) +``` +Устанавливает свойство: если ключ уже существует — заменяет значение, иначе — добавляет новую пару. + +--- + +## `extract_var_binding()` + +```rust +fn extract_var_binding(el: &Element, prop: &str) -> Option +``` + +Ищет свойство вида `__bind:` и возвращает его значение. Используется для реактивной привязки переменных: `__bind:value` для `Input`, `Toggle`, `Slider`. + +--- + +## `collect_hover_active()` + +```rust +pub fn collect_hover_active<'a>( + el: &'a Element, + stylesheet: &StyleSheet, +) -> (HashMap, HashMap) +``` + +Собирает CSS-свойства для псевдоклассов `:hover` и `:active` для элемента. Вызывает `stylesheet.matching_pseudo_rules()` дважды — для `"hover"` и `"active"`. Возвращает кортеж `(hover_props, active_props)`. Используется в `render_element()` и `make_hoverable()`. + +--- + +## `make_hoverable()` + +```rust +fn make_hoverable<'a>( + widget: iced::Element<'a, crate::Message, Theme, iced::Renderer>, + el: &Element, + hover_props: &HashMap, + active_props: &HashMap, + base_cs: &ComputedStyle, +) -> iced::Element<'a, crate::Message, Theme, iced::Renderer> +``` + +Оборачивает произвольный виджет в `button` для поддержки `:hover`/`:active` стилей. Условия срабатывания: +- Есть хотя бы один `hover` или `active` стиль; +- У элемента есть обработчик `__on:click`. + +Кнопке назначается `on_press(Message::EventTriggered(...))`. В замыкании `style()` подставляются `apply_overrides` в зависимости от `button::Status`: +- `Hovered` → `hover_props`; +- `Pressed` → `active_props`, если их нет — `hover_props`. + +Применяется **только для не-Button и не-Input** элементов (строка 815). + +--- + +## `render_element()` + +```rust +pub fn render_element<'a>( + el: &'a Element, + parent_color: Option, + parent_font_size: Option, + parent_direction: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + container_id: u64, + stylesheet: &StyleSheet, +) -> Option> +``` + +**Главная публичная функция рендеринга.** Возвращает `None` если `display: none`. + +### Общая логика для всех элементов + +1. Собираются `hover_props`, `active_props` через `collect_hover_active()`. +2. Определяется тип позиционирования: `is_fixed`, `is_absolute`, `is_sticky`. +3. Если заданы `left` + `right` без `width` — `width = Fill`. Если `top` + `bottom` без `height` — `height = Fill`. +4. `flex-grow` преобразуется в `FillPortion(grow_value)` по оси родителя. +5. Наследуются `current_color` и `current_font_size`. + +### Ветка `"Window"` + +```rust +if el.type_name == "Window" +``` + +- Создаётся `column` со `spacing` (по умолчанию 12px). +- Рендерятся дети через `render_children()`. +- Применяется `apply_universal_box_model(is_window = true, scrollable_id = window_id)` — окно всегда скроллируемое. +- Формируется `iced::widget::stack`: + 1. Основной поток (main_flow) + 2. `abs_layers` + 3. `sticky_layers` + 4. `fixed_layers` + +Итоговая структура: +``` +stack[ + container[ scrollable[ container[ column[...] ] ] ] + ...abs-слои + ...sticky-слои + ...fixed-слои +] +``` + +### Ветка `"Panel"` + +Делегирует `render_panel()`. Если есть `abs_layers` — оборачивает в `stack`. + +### Ветка `"Button"` + +Делегирует `render_button()`. Если есть `abs_layers` или `sticky_layers` — оборачивает в `stack`. + +### Ветка `"Input"` + +Делегирует `render_input()` с передачей `hover_props` и `active_props`. + +### Текстовые виджеты: `"Title"`, `"Header"`, `"Text"`, `"Label"`, `"#text"` + +```rust +"Title" | "Header" => ... +"Text" | "Label" | "#text" => ... +``` + +- Читают свойство `text` (или пустая строка). +- Создают `iced::widget::text` с размером шрифта (24 для Title/Header, 16 для Text). +- Применяют `color`, `font_weight` (Light ≤399, Normal 400–599, Bold 600–799, ExtraBold ≥800), `text_align`, `line_height` (с `Wrapping::Word`). +- Для Title/Header размер по умолчанию 24px, для Text — 16px. + +### `"Image"` + +- Читает `src`. Если путь начинается с `fs:` — обрезает префикс. +- `.svg` → `svg::Handle`, иначе `image::Viewer`. +- Размер изображения вычисляется с вычетом padding и border-width. +- Для растровых изображений применяется `border_radius`. + +### `"Icon"` + +Выводит символ `🔹` как текст размера 18px (или `current_font_size`). Заглушка. + +### `"Toggle"` + +Делегирует `render_toggle()`. + +### `"Slider"` + +Делегирует `render_slider()`. + +### `"ProgressBar"` + +```rust +progress_bar(0.0..=100.0, value) +``` +Свойство `value` парсится как `f32`. + +### `"Divider"`, `"Separator"` + +```rust +iced::widget::rule::horizontal(1) +``` +Горизонтальная линия толщиной 1px. + +### Ветка по умолчанию (неизвестный тип) + +- Создаётся `column` со `spacing` (10px). +- Рендерятся дети. +- Если есть `abs_layers` — оборачиваются в `stack`. +- Применяется `apply_universal_box_model()`. +- Если есть `sticky_layers` — накладываются поверх через `stack`. +- Возвращается `None` (элемент уже записан в `final_widget_opt`). + +### Постобработка для не-Button и не-Input + +```rust +if el.type_name != "Button" && el.type_name != "Input" { + final_widget_opt = make_hoverable(...); +} +``` + +### Обработка позиционирования + +После получения `final_widget`: + +- **Fixed** → `wrap_fixed_position()` → кладётся в `fixed_layers`, возвращается `None`. +- **Absolute** → `wrap_fixed_position()` → кладётся в `abs_layers`, возвращается `None`. +- **Sticky** → если `scroll_y > threshold`, элемент перекладывается в `sticky_layers`, а на его место вставляется пустой `spacer` высотой `estimate_element_height()`. Иначе элемент остаётся на месте. + +--- + +## `render_children()` + +```rust +fn render_children<'a>( + children: &'a [Element], + parent_color: Option, + parent_font_size: Option, + parent_direction: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + container_id: u64, + stylesheet: &StyleSheet, +) -> Vec> +``` + +Рекурсивно вызывает `render_element()` для каждого ребёнка. Фильтрует `None` (display: none). Возвращает вектор отрендеренных элементов. + +--- + +## `render_panel()` + +```rust +fn render_panel<'a>( + el: &'a Element, + cs: ComputedStyle, + parent_color: Option, + parent_font_size: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + container_id: u64, + stylesheet: &StyleSheet, +) -> iced::Element<'a, Message, Theme, iced::Renderer> +``` + +Рендерит `Panel` — контейнер с тремя режимами раскладки: + +### Row + +```rust +LayoutDirection::Row +``` +`iced::widget::row` с `spacing` (10px). `align_y` из `cs.align_items` или `Alignment::Center` по умолчанию. + +### Column + +```rust +LayoutDirection::Column +``` +`iced::widget::column` с `spacing`. `align_x` из `cs.align_items`. + +### Grid + +```rust +LayoutDirection::Grid +``` +Столбцы (`column`), внутри каждого — строка (`row`). Количество колонок из свойства `columns` (по умолчанию 3). Каждый ряд — чанк по `cols` детей. + +Во всех режимах: +- Если есть `abs_layers` — они оборачиваются в `stack` внутри контента. +- После контента применяется `apply_universal_box_model()`. +- `sticky_layers` накладываются поверх через `stack`. + +--- + +## `render_button()` + +```rust +fn render_button<'a>( + el: &'a Element, + cs: ComputedStyle, + parent_color: Option, + parent_font_size: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + container_id: u64, + stylesheet: &StyleSheet, +) -> iced::Element<'a, Message, Theme, iced::Renderer> +``` + +- Если нет детей — читает `label` (по умолчанию `"Button"`) и создаёт `text`. +- Если есть дети — рендерит их в `row` со `spacing = 8`. +- Padding: по умолчанию 8px по вертикали, 16px по горизонтали. С авто-сжатием если `padding + border > width/height`. +- `on_press` из `__on:click`. +- Стиль: через `get_button_style()` с динамическими `hover`/`active` переопределениями из `stylesheet.matching_rules()`. + +--- + +## `render_toggle()` + +```rust +fn render_toggle<'a>( + el: &'a Element, + parent_color: Option, + parent_font_size: Option, +) -> iced::Element<'a, Message, Theme, iced::Renderer> +``` + +- Читает `label` (опционально) и `value` (парсится как `bool`, по умолчанию `false`). +- Извлекает `__bind:value` для реактивного биндинга. +- Создаёт `checkbox`, на `on_toggle` отправляет `Message::ToggleChanged`. +- Если есть label — оборачивает `row![checkbox, label]` с `spacing=8` и `align_y=Center`. + +--- + +## `render_input()` + +```rust +fn render_input<'a>( + el: &'a Element, + cs: ComputedStyle, + parent_color: Option, + _parent_font_size: Option, + hover_props: &HashMap, + active_props: &HashMap, +) -> iced::Element<'a, Message, Theme, iced::Renderer> +``` + +- Читает `placeholder` (по умолчанию `"Type here…"`) и `value`. +- Извлекает `__bind:value`. +- Создаёт `text_input` с `padding` из `get_padding(cs, 10.0)`. +- Назначает `id` через `get_or_create_widget_id(el.element_id.0, "ti")`. +- Если есть hover/active стили — применяет динамический `style()` с `apply_overrides`. +- Иначе — статический стиль через `get_text_input_style()`. + +--- + +## `render_slider()` + +```rust +fn render_slider<'a>( + el: &'a Element, +) -> iced::Element<'a, Message, Theme, iced::Renderer> +``` + +- Читает `value` (парсится как `f32`, по умолчанию 0.0). +- Извлекает `__bind:value`. +- Создаёт `slider` в диапазоне `0.0..=100.0`. +- На изменение отправляет `Message::SliderChanged`. + +--- + +## Вспомогательные функции + +### `get_padding()` + +```rust +fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding +``` + +Собирает `padding` из `ComputedStyle` с учётом `padding-top/right/bottom/left`. Если `padding + border * 2` превышает фиксированные `width`/`height` — масштабирует padding пропорционально (авто-сжатие). + +### `get_margin()` + +```rust +fn get_margin(cs: &ComputedStyle) -> iced::Padding +``` + +Собирает `margin` из `ComputedStyle` с учётом `margin-top/right/bottom/left`. База — `cs.margin` (по умолчанию 0). + +### `get_button_style()` + +```rust +fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style +``` + +Формирует `button::Style` из `ComputedStyle`. В зависимости от `status`: +- `Hovered` — фон светлее на 15% (`* 1.15`); +- `Pressed` — фон темнее на 15% (`* 0.85`); +- `Active` — без изменений. + +### `get_text_input_style()` + +```rust +fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style +``` + +Формирует `text_input::Style` из `ComputedStyle`: `background`, `border`, `icon`, `placeholder`, `value`, `selection`. Значения по умолчанию — тёмная тема. + +### `estimate_element_height()` + +```rust +fn estimate_element_height(cs: &ComputedStyle) -> f32 +``` + +Приблизительно вычисляет высоту элемента для sticky-спейсера: `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> +``` + +Оборачивает виджет в `container` с `padding-top` из `cs.top`. Ширина `Fill`. Используется для sticky-элементов, когда `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> +``` + +Оборачивает виджет в `container` с `width = Fill`, `height = Fill` и выравниванием (`align_x`, `align_y`) на основе установленных `top`/`bottom`/`left`/`right`. Padding выставляется соответственно. Используется для `fixed` и `absolute` позиционирования. + +### `apply_universal_box_model()` + +```rust +fn apply_universal_box_model<'a>( + widget: impl Into>, + cs: &ComputedStyle, + is_window: bool, + default_padding: f32, + scrollable_id: Option, +) -> iced::Element<'a, Message, Theme, iced::Renderer> +``` + +Применяет боксовую модель к любому виджету. **Порядок обёртки:** + +``` +outer_container (margin) ← если есть margin и !is_window + inner_container (bg, border, clip) + scrollable ← если overflow_x/overflow_y = Scroll/Auto + container (padding) + widget +``` + +#### Этапы + +1. **Padding**: `container(widget).padding(get_padding(cs, default_padding))`. +2. **Overflow**: если `overflow-y` = Scroll/Auto — добавляется `scrollable` с направлением `Vertical` (или `Both` если `overflow-x` тоже Scroll/Auto). Для Window `overflow-y` по умолчанию `Auto`. Скроллу назначается `id` и `on_scroll`. +3. **Clip**: если `overflow = Hidden` — `container.clip(true)`. +4. **Фон, рамка, скругление**: `container.style(...)` с `Background`, `Border`. +5. **Ширина/высота**: для Window — `Fill`/`Fill`; иначе — из `cs.width`, `cs.height`, `cs.max_width`, `cs.max_height`. +6. **Выравнивание контента**: `align_x` из `cs.content_align`. +7. **Margin**: если есть margin — внешний `container` с `padding = margin`. + +--- + +## Структура дерева виджетов (Widget Tree) + +Для типичного окна (`Window`) дерево выглядит так: + +``` +stack[ + container (Window) [Fill, Fill] + scrollable [id="sc:window_id"] + container [bg, border, padding] + column [spacing] + ...дочерние элементы... + container (fixed) ← слой fixed + container (absolute) ← слой absolute + container (sticky) ← слой sticky +] +``` + +Для `Panel`: + +``` +stack[ + container [bg, border, margin] + scrollable (если overflow) + container [padding] + row | column | grid + ...дочерние элементы... + container (sticky) ← слой sticky, поверх boxed-контента +] +``` + +Для неизвестных элементов — аналогично Panel, но abs-слои внутри скролла, sticky — поверх. + +Для простых элементов (Text, Image, Toggle, Slider, ProgressBar, Divider): + +``` +container [bg, border, margin, padding] + scrollable (если overflow) + container [padding] + text | image | checkbox | slider | progress_bar | rule +``` diff --git a/docs/ru/modules/06-mod.md b/docs/ru/modules/06-mod.md new file mode 100644 index 0000000..a1d3f62 --- /dev/null +++ b/docs/ru/modules/06-mod.md @@ -0,0 +1,305 @@ +# Модуль `interpreter` — ядро интерпретатора байткода + +## Структура модуля + +``` +interpreter/ +├── mod.rs — Интерпретатор: парсинг байткода, VDOM, стили +├── opcodes.rs — Определения opcode (OP_ELEM_PUSH, OP_IF, OP_EACH, …) +├── reactive.rs — ReactiveTracker, ElementId — отслеживание грязных узлов +├── reader.rs — Reader — чтение байткода (str_ref, i64, f64, value, …) +├── rhei.rs — RheiContext — выполнение Rhai-скриптов и выражений +├── style.rs — StyleSheet, AncestorInfo, ComputedStyle, StructuralContext +└── types.rs — Element, Document, ComponentDef, Value, FlatVDom, InterpError +``` + +## `Interpreter` (пустая структура) + +```rust +pub struct Interpreter; +``` + +Структура не содержит полей — все методы статические. Служит пространством имён для функций интерпретации. + +## `Interpreter::run()` — первичный парсинг байткода + +```rust +pub fn run<'a>(bytecode: &'a [u8]) -> Result, InterpError> +``` + +1. Проверяет магическое число (`bytecode[..4] == MAGIC`). +2. Создаёт `Reader`, `HashMap` для `variables` и `components`, `Vec` для `rhei_scripts`, пустой `StyleSheet` и `ReactiveTracker`. +3. Вызывает `parse_block_elements()` для корневого уровня, который читает поток опкодов и строит дерево `Element`. +4. После парсинга вызывает `stylesheet.build_index()`. +5. Возвращает `Document { roots, components, variables, rhei_scripts, stylesheet, interner, tracker }`. + +## `parse_block_elements()` — рекурсивный парсинг блока + +```rust +fn parse_block_elements<'a>( + r: &mut Reader<'a>, + variables: &mut HashMap, + components: &mut HashMap>, + rhei_scripts: &mut Vec, + stylesheet: &mut SS, + tracker: &mut ReactiveTracker, + is_root: bool, +) -> Result>, InterpError> +``` + +Читает опкоды в цикле, используя стек для построения вложенности (`stack: Vec`). На `OP_END_BLOCK` завершает текущий блок (если `!is_root`). + +### Обрабатываемые опкоды + +| Опкод | Действие | +|---|---| +| `OP_ELEM_PUSH` | Создаёт `Element` с `tracker.alloc_id()`, кладёт на стек | +| `OP_ELEM_POP` | Снимает элемент со стека, вызывает `attach()` | +| `OP_GLOBAL` / `OP_LET` | Читает имя и значение, вставляет в `variables` | +| `OP_SINGLETON` | Пропускает данные синглтона | +| `OP_CONTENT` | Текстовое содержимое — свойство `text`. Если `OP_PROP_RHEI` — префикс `!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` | Читает ключ и значение, вызывает `el.push_prop()` | +| `OP_RHEI_BLK` | На корневом уровне без родителя — скрипт; иначе — элемент `#text` с `!rhei:` | +| `OP_COMPONENT` | Читает имя, параметры, рекурсивно парсит дочерний блок; сохраняет `ComponentDef` | +| `OP_IF` | Читает условие, парсит true-блок, проверяет `has_else`, парсит false-блок. Создаёт `@if` с `@else` как последним child | +| `OP_EACH` | Читает имя переменной, источник (массив или `$var` или Rhai), парсит блок-шаблон. Создаёт `@each` | +| `OP_ON` | Читает имя события, аргументы, ищет `OP_RHEI_BLK` — скрипт обработчика; устанавливает `__on:{event}` | +| `OP_STYLE_RULE` | Читает селектор и свойства, вызывает `stylesheet.add_rule()` | + +### `attach()` + +```rust +fn attach<'a>(stack: &mut Vec>, roots: &mut Vec>, el: Element<'a>) +``` + +Если стек не пуст — добавляет в `parent.children`, иначе в `roots`. + +--- + +## `evaluate_vdom()` — сборка VDOM (точка входа) + +```rust +pub fn evaluate_vdom<'a>( + templates: &[Element<'a>], + variables: &mut HashMap, + components: &HashMap>, + rhei: &RheiContext, + stylesheet: &SS, + ancestors: &[AncestorInfo], +) -> Vec> +``` + +Делегирует `evaluate_vdom_incr()` с пустым `dirty_set` — полный пересчёт. + +### `evaluate_vdom_flat()` + +```rust +pub fn evaluate_vdom_flat<'a>(...) -> FlatVDom<'a> +``` + +Оборачивает `evaluate_vdom()` и конвертирует результат через `FlatVDom::from_elements()`. + +--- + +## `evaluate_vdom_incr()` — инкрементальная сборка VDOM + +```rust +pub fn evaluate_vdom_incr<'a>( + templates: &[&Element<'a>], + variables: &mut HashMap, + components: &HashMap>, + rhei: &RheiContext, + stylesheet: &SS, + ancestors: &[AncestorInfo], + dirty_set: &HashSet, +) -> Vec> +``` + +### Предварительная обработка + +1. **`sibling_infos`** — для каждого элемента из `templates` создаётся `AncestorInfo` (type_name, id, classes). Нужен для структурных псевдоклассов (CSS `:nth-child`, `:first-of-type` и т.д.). + +2. **`type_counts`** / **`type_seen`** — подсчёт общего числа элементов каждого типа и счётчик для `StructuralContext`. + +### Основной цикл по `templates` + +Для каждого элемента вычисляется `StructuralContext`: + +```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` + +- Читает `condition`, вызывает `evaluate_condition()`. +- Проходит по `child` элементам: если `@else` — активна когда условие ложно; иначе активна когда истинно. +- Рекурсивно вызывает `evaluate_vdom_incr()` для активной ветки. + +#### `@each` + +- Получает `var_name` и `source`. +- Разрешает источник: если с префиксом `!rhei:` — вызывает `normalize_rhai_array()`, иначе — `resolve_string()`. +- Разбивает результат по `,`, для каждого элемента: + - Вставляет `var_name` в `variables`, рекурсивно обходит шаблон, восстанавливает предыдущее значение переменной. + +#### Обычный элемент / Компонент + +- Если `el.type_name` найден в `components`: + 1. Собирает аргументы из параметров компонента через `resolve_prop()`. + 2. Сохраняет старые значения переменных, вставляет новые. + 3. Создаёт `vcomp`, копирует свойства, разрешая их через `resolve_prop()` и `resolve_string()`. + 4. Вычисляет стили: `collect_matching_styles()` → `stylesheet.compute_cached()`. + 5. Строит chain предков: `build_ancestor_chain()`. + 6. Рекурсивно обходит `comp.children`. + 7. Вычисляет `content_hash`. + 8. Восстанавливает переменные. +- Иначе (обычный элемент): + 1. Создаёт `vnode`. + 2. Разрешает свойства: `__on:` → `resolve_string()`, `$var` → `__bind:{key}`, остальные → `resolve_prop()`. + 3. Вычисляет стили через `collect_matching_styles()` + `compute_cached()`. + 4. Строит chain предков, рекурсивно обходит `el.children`. + 5. Вычисляет `content_hash`. + +--- + +## `compute_content_hash()` + +```rust +fn compute_content_hash(el: &Element) -> u64 +``` + +Хеш элемента для кэширования. Учитывает: +- `type_name` +- Все пары ключ-значение из `properties` +- Рекурсивно `content_hash` дочерних элементов (`children`) + +Использует `DefaultHasher`. + +--- + +## `build_ancestor_chain()` + +```rust +fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec +``` + +Копирует текущий `ancestors`, добавляет `AncestorInfo` для текущего элемента (type_name, id, classes). Возвращает расширенную цепочку для передачи при рекурсивном обходе детей. + +--- + +## `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> +``` + +Извлекает `id`, `classes`, все атрибуты элемента, делегирует `stylesheet.matching_rules()` с полным контекстом для CSS-селекторов. + +--- + +## `resolve_string()` — подстановка `$var` + +```rust +pub fn resolve_string<'a>(val: &'a str, scope: &HashMap) -> Cow<'a, str> +``` + +- Если в строке нет `$` — возвращает `Cow::Borrowed(val)` (без аллокаций). +- Иначе обходит строку посимвольно. После `$` собирает имя переменной (буквы, цифры, `_`), ищет в `scope`, подставляет значение. Если переменная не найдена — оставляет `$var` как есть. + +## `resolve_prop()` — разрешение значения свойства + +```rust +fn resolve_prop(v: &str, variables: &HashMap, rhei: &RheiContext) -> String +``` + +- Если начинается с `!rhei:` — вызывает `rhei.eval_expr()`. +- Иначе — `resolve_string()`. + +## `evaluate_condition()` — вычисление условия `@if` + +```rust +fn evaluate_condition(cond: &str, variables: &HashMap, rhei: &RheiContext) -> bool +``` + +- Если `!rhei:` — `rhei.eval_condition()`. +- Иначе: + 1. Удаляет фигурные скобки `{...}`. + 2. Выполняет `resolve_string()`. + 3. Проверяет `is_truthy_str()`, затем `false`/`0`/пусто. + 4. Пытается разобрать числовые операторы (`>=`, `<=`, `>`, `<`, `==`, `!=`). + +## `is_truthy()` / `is_truthy_str()` — приведение к bool + +```rust +fn is_truthy(v: &Value) -> bool +fn is_truthy_str(s: &str) -> bool +``` + +- `Value::Bool` — по значению. +- `Value::Int` — ненулевой. +- `Value::Float` — ненулевой. +- `Value::Str` — делегирует `is_truthy_str()`. +- `Value::None` — `false`. +- `Value::Array` — не пустой. +- Строка: `""`, `"false"`, `"0"`, `"null"` → `false`; `"true"`, `"1"` → `true`; иначе парсит как `f64`. + +## `normalize_rhai_array()` — нормализация Rhai-массива + +```rust +fn normalize_rhai_array(s: &str) -> String +``` + +Обрезает `[...]`, разбивает по `,`, обрезает пробелы, соединяет через `,`. Используется в `@each` для приведения Rhai-массива к формату, ожидаемому циклом. + +--- + +## `Document` + +```rust +pub struct Document<'a> { + pub roots: Vec>, + pub components: HashMap>, + pub variables: HashMap, + pub rhei_scripts: Vec, + 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>, + pub element_id: ElementId, + pub computed_style: Option, + pub content_hash: u64, +} +``` + +## `ComponentDef` + +```rust +pub struct ComponentDef<'a> { + pub name: String, + pub params: Vec<(String, String)>, + pub children: Vec>, +} +``` diff --git a/docs/ru/modules/07-app-perf.md b/docs/ru/modules/07-app-perf.md new file mode 100644 index 0000000..1a95872 --- /dev/null +++ b/docs/ru/modules/07-app-perf.md @@ -0,0 +1,109 @@ +# App и Perf модули + +## `src/app.rs` — GlintApp + +Точка входа Iced-приложения. Держит состояние и реализует `update`/`view`. + +### `GlintApp` + +```rust +pub struct GlintApp { + pub doc: Document<'static>, + pub vdom_roots: Vec>, + pub rhei: RheiContext, +} +``` + +- `doc` — оригинальный Document (стили, переменные, tracker) +- `vdom_roots` — результат последнего evaluate_vdom (_incr) +- `rhei` — Rhai-движок с кэшированными AST + +### `update(&mut self, message: Message) -> iced::Task` + +Обработка сообщений от Iced: + +| Message | Действие | +|---------|----------| +| `WindowScrolled(y)` | Установить `__scroll_y`, вызвать `on_variable_changed` | +| `ScrollableScrolled(id, y)` | Установить `__scroll_{id}`, вызвать `on_variable_changed` | +| `EventTriggered(script)` | `execute_action()`, затем diff переменных для поиска изменившихся | +| `InputChanged(var, val)` | Установить переменную, вызвать `on_variable_changed` | +| `ToggleChanged(var, val)` | Установить `Bool`, вызвать `on_variable_changed` | +| `SliderChanged(var, val)` | Установить `Float`, вызвать `on_variable_changed` | + +После обработки сообщения: +1. `tracker.take_dirty_set()` — получить dirty элементы +2. `evaluate_vdom_incr()` — пересчитать VDOM +3. Сохранить в `self.vdom_roots` + +**Perf:** VDOM фаза замеряется `PerfScope::new("vdom")`. + +### `view(&self) -> iced::Element` + +Построить Iced-виджеты из `self.vdom_roots`: + +1. Собрать scroll_positions из `__scroll_*` переменных +2. Для каждого root: `render_element()` → push в column +3. Наложить global fixed/absolute/sticky слои +4. Вернуть `container(layout).into()` + +**Perf:** Render фаза замеряется `PerfScope::new("render")`. +В конце `perf::print_frame()` — вывод в stderr. + +--- + +## `src/perf.rs` — Performance Monitoring + +Система замера времени по фазам (VDOM, Style, Render). Включение: флаг `--perf`. + +### `PerfReport` + +```rust +pub struct PerfReport { + pub vdom_eval: f64, // ms + pub style: f64, // ms + pub render: f64, // ms + pub total: f64, // ms +} +``` + +Хранится в `thread_local!` `RefCell`. + +### `PerfScope` + +Drop-guard: замеряет время от создания до разрушения. + +```rust +pub struct PerfScope { + name: &'static str, + start: Instant, +} +``` + +При `drop()`: добавляет elapsed ms к соответствующему полю `PerfReport`. +Имена: `"vdom"`, `"style"`, `"render"`. + +### Функции + +| Функция | Описание | +|---------|----------| +| `set_enabled(bool)` | Включить/выключить замеры | +| `is_enabled() -> bool` | Проверить состояние | +| `report_and_reset() -> Option` | Собрать отчёт, проверить >16ms threshold | +| `reset()` | Обнулить PerfReport | +| `print_frame()` | Вызвать report_and_reset + сброс, напечатать в stderr | + +### Пример вывода + +``` +[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 +``` + +### Точки инструментирования + +| Файл | Фаза | Место | +|------|------|-------| +| `app.rs:update` | vdom | Вокруг `evaluate_vdom_incr()` | +| `app.rs:view` | render | Вся функция `view()` | +| `style.rs:compute_cached` | style | Внутри `StyleSheet::compute_cached()` |