# Поток данных в 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 из них, но время на их создание уже потрачено.