docs: reorganize into en/ru and add English translation
This commit is contained in:
48
docs/ru/README.md
Normal file
48
docs/ru/README.md
Normal file
@@ -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<String, String> | 2–3× |
|
||||
| 2 | String interning для горячих путей | 1.5–2× |
|
||||
| 9.3 | Кэш Iced-виджетов по content_hash | 1.5–2× (render) |
|
||||
303
docs/ru/architecture/01-data-flow.md
Normal file
303
docs/ru/architecture/01-data-flow.md
Normal file
@@ -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<Element>, // корневые элементы
|
||||
components: HashMap<String, ComponentDef>, // компоненты
|
||||
variables: HashMap<String, Value>, // начальные значения
|
||||
stylesheet: StyleSheet, // таблица стилей
|
||||
rhei_scripts: Vec<String>, // 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<String>, f64), // слайдер: (привязанная переменная, новое значение)
|
||||
InputChanged(Option<String>, String), // текстовое поле
|
||||
ToggleChanged(Option<String>, 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 из них, но время на их создание уже потрачено.
|
||||
68
docs/ru/architecture/02-performance.md
Normal file
68
docs/ru/architecture/02-performance.md
Normal file
@@ -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`), прежде чем вкладываться в оптимизацию.
|
||||
182
docs/ru/modules/01-types.md
Normal file
182
docs/ru/modules/01-types.md
Normal file
@@ -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<String>,
|
||||
map: HashMap<String, u32>,
|
||||
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<InternedStr>` — опциональное интернирование
|
||||
|
||||
Хранится в `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<Value>),
|
||||
None,
|
||||
}
|
||||
```
|
||||
|
||||
**Методы:**
|
||||
- `as_str(&self) -> Option<&str>` — заимствование строки
|
||||
- `to_owned_string(&self) -> CompactString` — форматирование в строку (используется в renderer)
|
||||
|
||||
**Реализовано:** `From<&str>`, `From<String>`, `From<i64>`, `From<f64>`, `From<bool>`, `From<Vec<T>>`.
|
||||
`PartialEq` — Float сравнивается с `f64::EPSILON`.
|
||||
|
||||
**Используется:** `Document::variables`, конверсии `Value↔Dynamic` в rhei.rs.
|
||||
**НЕ используется:** в стилях (`ComputedStyle::compute` всё ещё принимает `&str`, matched_sheets — `HashMap<String, String>`).
|
||||
|
||||
---
|
||||
|
||||
## `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<Element<'a>>,
|
||||
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<Element<'a>>,
|
||||
}
|
||||
```
|
||||
|
||||
Хранится в `Document::components`. Используется в `evaluate_vdom` при разворачивании
|
||||
компонентов: параметры передаются через переменные, тело компонента вставляется как children.
|
||||
|
||||
---
|
||||
|
||||
## `Document<'a>`
|
||||
|
||||
Полное состояние приложения после загрузки байткода.
|
||||
|
||||
```rust
|
||||
pub struct Document<'a> {
|
||||
pub roots: Vec<Element<'a>>,
|
||||
pub components: HashMap<String, ComponentDef<'a>>,
|
||||
pub variables: HashMap<String, Value>,
|
||||
pub rhei_scripts: Vec<String>,
|
||||
pub stylesheet: StyleSheet,
|
||||
pub interner: Interner,
|
||||
pub tracker: ReactiveTracker,
|
||||
}
|
||||
```
|
||||
|
||||
Создаётся в `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<PropertyIdx>,
|
||||
pub children_range: Range<NodeIdx>,
|
||||
pub computed_style: ComputedStyle,
|
||||
pub element_id: ElementId,
|
||||
}
|
||||
|
||||
pub struct FlatVDom<'a> {
|
||||
pub nodes: Vec<VNode<'a>>,
|
||||
pub properties: Vec<(String, String)>,
|
||||
root_indices: Vec<NodeIdx>,
|
||||
}
|
||||
```
|
||||
|
||||
**Методы:**
|
||||
- `new()` — пустой
|
||||
- `from_elements(elements)` — рекурсивно уплощает дерево Element'ов в VNode'ы
|
||||
- `into_elements(self) -> Vec<Element>` — обратная сборка
|
||||
- `get_node(idx)`, `node_count()`, `root_count()`, `root_indices()`
|
||||
|
||||
**Используется:** в тестах (`test_flat_vdom_roundtrip`, `test_flat_vdom_nested`, `test_flat_vdom_empty`).
|
||||
В горячем пути не участвует — VDOM передаётся как `Vec<Element>`.
|
||||
|
||||
---
|
||||
|
||||
## `InterpError`
|
||||
|
||||
Ошибки загрузки байткода.
|
||||
|
||||
```rust
|
||||
pub enum InterpError {
|
||||
BadMagic,
|
||||
UnexpectedEof,
|
||||
InvalidUtf8,
|
||||
UnexpectedPop,
|
||||
}
|
||||
```
|
||||
|
||||
Реализует `Display` и `std::error::Error`. Возвращается из `Interpreter::run()`.
|
||||
482
docs/ru/modules/02-style.md
Normal file
482
docs/ru/modules/02-style.md
Normal file
@@ -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>) -> 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<TextAlign> 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<String>`
|
||||
Разделяет группу селекторов по запятой с учётом вложенности скобок. Например, `"Button, Label:hover"` → `["Button", "Label:hover"]`.
|
||||
|
||||
### `CompoundSelector`
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompoundSelector {
|
||||
pub tag: Option<String>,
|
||||
pub id: Option<String>,
|
||||
pub classes: Vec<String>,
|
||||
pub pseudo_classes: Vec<String>,
|
||||
pub attributes: Vec<AttributeSelector>,
|
||||
}
|
||||
```
|
||||
|
||||
**Методы:**
|
||||
|
||||
- `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<CompoundSelector>,
|
||||
pub combinators: Vec<Combinator>,
|
||||
}
|
||||
```
|
||||
|
||||
**Методы:**
|
||||
|
||||
- `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<String>,
|
||||
pub classes: Vec<String>,
|
||||
}
|
||||
```
|
||||
|
||||
Методы:
|
||||
- `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<String, String>,
|
||||
}
|
||||
|
||||
impl StyleRule {
|
||||
pub fn build(selector_str: String, properties: HashMap<String, String>) -> Self
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `StyleIndex`
|
||||
```rust
|
||||
pub type RuleId = usize;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyleIndex {
|
||||
pub by_tag: HashMap<String, Vec<RuleId>>,
|
||||
pub by_class: HashMap<String, Vec<RuleId>>,
|
||||
pub by_id: HashMap<String, Vec<RuleId>>,
|
||||
pub by_tag_class: HashMap<(String, String), Vec<RuleId>>,
|
||||
pub by_tag_id: HashMap<(String, String), Vec<RuleId>>,
|
||||
pub complex_rules: Vec<(RuleId, RuleId)>,
|
||||
pub universal_rules: Vec<RuleId>,
|
||||
pub rule_specificities: Vec<(u32, u32, u32)>,
|
||||
pub rules: Vec<StyleRule>,
|
||||
pub epoch: u64,
|
||||
}
|
||||
|
||||
impl StyleIndex {
|
||||
pub fn new() -> Self
|
||||
}
|
||||
```
|
||||
|
||||
Индекс для быстрого поиска правил. Строится в `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<u64, ComputedStyle>,
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl StyleCache {
|
||||
pub fn new(max_entries: usize) -> Self
|
||||
pub fn get_or_compute(
|
||||
&mut self,
|
||||
type_name: &str,
|
||||
props: &[(Cow<'_, str>, Cow<'_, str>)],
|
||||
epoch: u64,
|
||||
matched_sheets: &[&HashMap<String, String>],
|
||||
) -> ComputedStyle
|
||||
pub fn clear(&mut self)
|
||||
}
|
||||
```
|
||||
|
||||
Кеш вычисленных стилей. Ключ — хеш от `type_name`, inline-свойств и `epoch`. При превышении `max_entries` кеш полностью очищается.
|
||||
|
||||
---
|
||||
|
||||
## `StyleSheet`
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct StyleSheet {
|
||||
rules: Vec<StyleRule>,
|
||||
index: Option<StyleIndex>,
|
||||
epoch: u64,
|
||||
cache: Mutex<StyleCache>,
|
||||
}
|
||||
```
|
||||
|
||||
Главный тип модуля. Содержит список правил, опциональный индекс и кеш. Реализует `Clone` (с новым пустым кешем) и `Default`.
|
||||
|
||||
**Методы:**
|
||||
|
||||
- `StyleSheet::new() -> Self` — создаёт пустой лист.
|
||||
- `pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>)` — добавляет правило. Пропускает селектор через `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<Vec<&HashMap<String, String>>>` — использует индекс для быстрого поиска: собирает кандидатов из `universal_rules`, `by_tag`, `by_class`, `by_id`, `complex_rules`; фильтрует через `ComplexSelector::matches`; сортирует по специфичности.
|
||||
- `pub fn matching_rules(...) -> Vec<&HashMap<String, String>>` — поиск подходящих правил. Пытается `query_index`; если индекса нет — линейный перебор всех `self.rules` с сортировкой.
|
||||
- `pub fn matching_pseudo_rules(pseudo, ...) -> HashMap<String, String>` — ищет правила, содержащие указанный псевдокласс (например `: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<Vec<&HashMap<String, String>>>` — параллельный batch-поиск через `rayon::par_iter`.
|
||||
|
||||
---
|
||||
|
||||
## `ComputedStyle`
|
||||
```rust
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ComputedStyle {
|
||||
pub font_size: Option<SizeValue>,
|
||||
pub color: Option<iced::Color>,
|
||||
pub padding: Option<SizeValue>,
|
||||
pub padding_top: Option<SizeValue>,
|
||||
pub padding_right: Option<SizeValue>,
|
||||
pub padding_bottom: Option<SizeValue>,
|
||||
pub padding_left: Option<SizeValue>,
|
||||
|
||||
pub margin: Option<SizeValue>,
|
||||
pub margin_top: Option<SizeValue>,
|
||||
pub margin_right: Option<SizeValue>,
|
||||
pub margin_bottom: Option<SizeValue>,
|
||||
pub margin_left: Option<SizeValue>,
|
||||
|
||||
pub background: Option<iced::Color>,
|
||||
pub spacing: Option<SizeValue>,
|
||||
pub border_radius: Option<SizeValue>,
|
||||
pub border_width: Option<SizeValue>,
|
||||
pub border_color: Option<iced::Color>,
|
||||
|
||||
pub width: Option<iced::Length>,
|
||||
pub height: Option<iced::Length>,
|
||||
pub min_width: Option<SizeValue>,
|
||||
pub max_width: Option<SizeValue>,
|
||||
pub min_height: Option<SizeValue>,
|
||||
pub max_height: Option<SizeValue>,
|
||||
|
||||
pub direction: Option<LayoutDirection>,
|
||||
pub align_items: Option<iced::Alignment>,
|
||||
pub content_align: Option<ContentAlign>,
|
||||
|
||||
pub flex_grow: Option<u16>,
|
||||
|
||||
pub position: Option<Position>,
|
||||
pub top: Option<SizeValue>,
|
||||
pub right: Option<SizeValue>,
|
||||
pub bottom: Option<SizeValue>,
|
||||
pub left: Option<SizeValue>,
|
||||
|
||||
pub overflow_x: Option<Overflow>,
|
||||
pub overflow_y: Option<Overflow>,
|
||||
pub display: Option<Display>,
|
||||
|
||||
pub opacity: Option<f32>,
|
||||
pub font_weight: Option<u16>,
|
||||
pub line_height: Option<SizeValue>,
|
||||
pub text_align: Option<TextAlign>,
|
||||
}
|
||||
```
|
||||
|
||||
Итоговый вычисленный стиль элемента. Все поля — `Option`; отсутствующее свойство означает «не задано / наследуется от родителя».
|
||||
|
||||
### `ComputedStyle::compute()`
|
||||
```rust
|
||||
pub fn compute(
|
||||
inline: &[(Cow<'_, str>, Cow<'_, str>)],
|
||||
matched_sheets: &[&HashMap<String, String>],
|
||||
) -> 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<String, String>])],
|
||||
) -> Vec<ComputedStyle>
|
||||
```
|
||||
Параллельный batch-вариант через `rayon::par_iter`.
|
||||
|
||||
### `ComputedStyle::apply_overrides()`
|
||||
```rust
|
||||
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>)
|
||||
```
|
||||
Применяет (перезаписывает) заданный набор свойств поверх существующего стиля. Используется для динамических изменений (например `:hover`-правила, inline-переопределения).
|
||||
|
||||
### Как работает `lookup()`
|
||||
```rust
|
||||
fn lookup<'a>(
|
||||
key: &str,
|
||||
inline: &'a [(Cow<'_, str>, Cow<'_, str>)],
|
||||
matched_sheets: &[&'a HashMap<String, String>],
|
||||
) -> Option<&'a str>
|
||||
```
|
||||
|
||||
Порядок разрешения свойства:
|
||||
1. **Inline-свойства** — перебор пар `(key, value)`. Поддерживает префикс `style:` (т.е. `style:color` эквивалентен `color`).
|
||||
2. **matched_sheets** — список словарей от подходящих CSS-правил, отсортированный по специфичности. Перебирается с конца (последний — самый специфичный).
|
||||
3. Возвращается первое найденное значение.
|
||||
|
||||
---
|
||||
|
||||
## Функции парсинга
|
||||
|
||||
| Функция | Сигнатура | Описание |
|
||||
|---|---|---|
|
||||
| `parse_size` | `(s: &str) -> Option<SizeValue>` | Парсит размер: `"10"` → `Px(10)`, `"50%"` → `Percent(50)`. `auto`, `fill`, `stretch` → `None` |
|
||||
| `parse_color` | `(s: &str) -> Option<iced::Color>` | Парсит цвет: `#rgb`, `#rrggbb`, `#rrggbbaa`, имена (`white`, `black`, `transparent`) |
|
||||
| `parse_length` | `(s: &str) -> Option<iced::Length>` | Парсит длину Iced: `"fill"`/`"100%"`, `"shrink"`/`"auto"`, `"50"` → `Fixed(50)` |
|
||||
| `parse_overflow` | `(s: &str) -> Option<Overflow>` | `visible`, `hidden`, `scroll`, `auto` |
|
||||
| `parse_position` | `(s: &str) -> Option<Position>` | `static`, `relative`, `absolute`, `sticky`, `fixed` |
|
||||
| `parse_display` | `(s: &str) -> Option<Display>` | `none`, `block`, `flex`, `grid`, `inline` |
|
||||
| `parse_direction` | `(s: &str) -> Option<LayoutDirection>` | `row`/`horizontal`, `column`/`vertical`, `grid` |
|
||||
| `parse_alignment` | `(s: &str) -> Option<iced::Alignment>` | `start`, `center`, `end` |
|
||||
| `parse_content_align` | `(s: &str) -> Option<ContentAlign>` | `start`/`left`/`top`, `center`, `end`/`right`/`bottom` |
|
||||
| `parse_opacity` | `(s: &str) -> Option<f32>` | Число 0.0–1.0, clamp |
|
||||
| `parse_font_weight` | `(s: &str) -> Option<u16>` | Имена: `normal`→400, `bold`→700, `lighter`→300, `bolder`→900; числовые значения |
|
||||
| `parse_text_align` | `(s: &str) -> Option<TextAlign>` | `left`, `center`, `right` |
|
||||
|
||||
### `resolve_size`
|
||||
```rust
|
||||
pub fn resolve_size(v: Option<SizeValue>, relative_to: Option<f32>) -> Option<f32>
|
||||
```
|
||||
Удобная обёртка над `SizeValue::resolve`, возвращает `Option<f32>`.
|
||||
|
||||
---
|
||||
|
||||
## Использование в `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.
|
||||
92
docs/ru/modules/03-reactive.md
Normal file
92
docs/ru/modules/03-reactive.md
Normal file
@@ -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<ElementId>`
|
||||
- Ключ в `dependencies: HashMap<ElementId, HashSet<String>>`
|
||||
- Поле в `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<String, HashSet<ElementId>>,
|
||||
dependencies: HashMap<ElementId, HashSet<String>>,
|
||||
dirty_set: HashSet<ElementId>,
|
||||
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<ElementId>` | Забрать 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()` очищает всё |
|
||||
200
docs/ru/modules/04-rhei.md
Normal file
200
docs/ru/modules/04-rhei.md
Normal file
@@ -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<Scope<'static>>,
|
||||
action_cache: RefCell<HashMap<String, AST>>,
|
||||
expr_cache: RefCell<HashMap<String, AST>>,
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Назначение |
|
||||
|---|---|
|
||||
| `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<String, Value>)
|
||||
```
|
||||
|
||||
Синхронизирует значения из внешнего `HashMap` в Rhai `Scope`:
|
||||
- Если переменная уже есть в скопе и её значение не изменилось — пропускает.
|
||||
- Если переменная есть — обновляет через `set_value()`.
|
||||
- Если переменной нет — добавляет через `push_dynamic()`.
|
||||
|
||||
Преобразование `Value → Dynamic` выполняется через `value_to_dynamic()`.
|
||||
|
||||
---
|
||||
|
||||
## `initialize()` — инициализация
|
||||
|
||||
```rust
|
||||
pub fn initialize(&self, variables: &mut HashMap<String, Value>)
|
||||
```
|
||||
|
||||
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<String, Value>) -> Value
|
||||
```
|
||||
|
||||
1. Синхронизирует переменные.
|
||||
2. Получает (компилирует или берёт из кеша) AST выражения через `get_or_compile_expr()`.
|
||||
3. Выполняет через `eval_ast_with_scope::<Dynamic>()`.
|
||||
4. Преобразует результат `Dynamic → Value` через `dynamic_to_value()`.
|
||||
5. При ошибке возвращает `Value::None`.
|
||||
|
||||
---
|
||||
|
||||
## `eval_condition()` — вычисление условия
|
||||
|
||||
```rust
|
||||
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, Value>) -> bool
|
||||
```
|
||||
|
||||
Аналогичен `eval_expr()`, но типизирован как `bool`. При ошибке возвращает `false`.
|
||||
|
||||
---
|
||||
|
||||
## `execute_action()` — выполнение скрипта
|
||||
|
||||
```rust
|
||||
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, Value>)
|
||||
```
|
||||
|
||||
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<AST>
|
||||
```
|
||||
|
||||
Проверяет `action_cache`. При промахе компилирует через `engine.compile()`, сохраняет в кеш.
|
||||
|
||||
### `get_or_compile_expr(expr)`
|
||||
|
||||
```rust
|
||||
fn get_or_compile_expr(&self, expr: &str) -> Option<AST>
|
||||
```
|
||||
|
||||
Проверяет `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::<i64>()` — целое число
|
||||
2. `s.parse::<f64>()` — дробное число
|
||||
3. `s.parse::<bool>()` — булево значение
|
||||
4. Иначе — `Dynamic::from(s)` как строка
|
||||
537
docs/ru/modules/05-renderer.md
Normal file
537
docs/ru/modules/05-renderer.md
Normal file
@@ -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<HashMap<(u32, &'static str), iced::widget::Id>> = ...;
|
||||
}
|
||||
|
||||
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<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V)
|
||||
```
|
||||
Добавляет пару `(key, val)` в `self.properties`.
|
||||
|
||||
### `set_prop()`
|
||||
```rust
|
||||
pub fn set_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V)
|
||||
```
|
||||
Устанавливает свойство: если ключ уже существует — заменяет значение, иначе — добавляет новую пару.
|
||||
|
||||
---
|
||||
|
||||
## `extract_var_binding()`
|
||||
|
||||
```rust
|
||||
fn extract_var_binding(el: &Element, prop: &str) -> Option<String>
|
||||
```
|
||||
|
||||
Ищет свойство вида `__bind:<prop>` и возвращает его значение. Используется для реактивной привязки переменных: `__bind:value` для `Input`, `Toggle`, `Slider`.
|
||||
|
||||
---
|
||||
|
||||
## `collect_hover_active()`
|
||||
|
||||
```rust
|
||||
pub fn collect_hover_active<'a>(
|
||||
el: &'a Element,
|
||||
stylesheet: &StyleSheet,
|
||||
) -> (HashMap<String, String>, HashMap<String, String>)
|
||||
```
|
||||
|
||||
Собирает 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<String, String>,
|
||||
active_props: &HashMap<String, String>,
|
||||
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<iced::Color>,
|
||||
parent_font_size: Option<f32>,
|
||||
parent_direction: Option<LayoutDirection>,
|
||||
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
scroll_positions: &HashMap<u64, f32>,
|
||||
container_id: u64,
|
||||
stylesheet: &StyleSheet,
|
||||
) -> Option<iced::Element<'a, Message, Theme, iced::Renderer>>
|
||||
```
|
||||
|
||||
**Главная публичная функция рендеринга.** Возвращает `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<iced::Color>,
|
||||
parent_font_size: Option<f32>,
|
||||
parent_direction: Option<LayoutDirection>,
|
||||
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
scroll_positions: &HashMap<u64, f32>,
|
||||
container_id: u64,
|
||||
stylesheet: &StyleSheet,
|
||||
) -> Vec<iced::Element<'a, Message, Theme, iced::Renderer>>
|
||||
```
|
||||
|
||||
Рекурсивно вызывает `render_element()` для каждого ребёнка. Фильтрует `None` (display: none). Возвращает вектор отрендеренных элементов.
|
||||
|
||||
---
|
||||
|
||||
## `render_panel()`
|
||||
|
||||
```rust
|
||||
fn render_panel<'a>(
|
||||
el: &'a Element,
|
||||
cs: ComputedStyle,
|
||||
parent_color: Option<iced::Color>,
|
||||
parent_font_size: Option<f32>,
|
||||
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
scroll_positions: &HashMap<u64, f32>,
|
||||
container_id: u64,
|
||||
stylesheet: &StyleSheet,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer>
|
||||
```
|
||||
|
||||
Рендерит `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<iced::Color>,
|
||||
parent_font_size: Option<f32>,
|
||||
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
scroll_positions: &HashMap<u64, f32>,
|
||||
container_id: u64,
|
||||
stylesheet: &StyleSheet,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer>
|
||||
```
|
||||
|
||||
- Если нет детей — читает `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<iced::Color>,
|
||||
parent_font_size: Option<f32>,
|
||||
) -> 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<iced::Color>,
|
||||
_parent_font_size: Option<f32>,
|
||||
hover_props: &HashMap<String, String>,
|
||||
active_props: &HashMap<String, String>,
|
||||
) -> 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<iced::Element<'a, Message, Theme, iced::Renderer>>,
|
||||
cs: &ComputedStyle,
|
||||
is_window: bool,
|
||||
default_padding: f32,
|
||||
scrollable_id: Option<u64>,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer>
|
||||
```
|
||||
|
||||
Применяет боксовую модель к любому виджету. **Порядок обёртки:**
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
305
docs/ru/modules/06-mod.md
Normal file
305
docs/ru/modules/06-mod.md
Normal file
@@ -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<Document<'a>, 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<String, Value>,
|
||||
components: &mut HashMap<String, ComponentDef<'a>>,
|
||||
rhei_scripts: &mut Vec<String>,
|
||||
stylesheet: &mut SS,
|
||||
tracker: &mut ReactiveTracker,
|
||||
is_root: bool,
|
||||
) -> Result<Vec<Element<'a>>, InterpError>
|
||||
```
|
||||
|
||||
Читает опкоды в цикле, используя стек для построения вложенности (`stack: Vec<Element>`). На `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<Element<'a>>, roots: &mut Vec<Element<'a>>, el: Element<'a>)
|
||||
```
|
||||
|
||||
Если стек не пуст — добавляет в `parent.children`, иначе в `roots`.
|
||||
|
||||
---
|
||||
|
||||
## `evaluate_vdom()` — сборка VDOM (точка входа)
|
||||
|
||||
```rust
|
||||
pub fn evaluate_vdom<'a>(
|
||||
templates: &[Element<'a>],
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
) -> Vec<Element<'a>>
|
||||
```
|
||||
|
||||
Делегирует `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<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
dirty_set: &HashSet<ElementId>,
|
||||
) -> Vec<Element<'a>>
|
||||
```
|
||||
|
||||
### Предварительная обработка
|
||||
|
||||
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<AncestorInfo>
|
||||
```
|
||||
|
||||
Копирует текущий `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<String, String>>
|
||||
```
|
||||
|
||||
Извлекает `id`, `classes`, все атрибуты элемента, делегирует `stylesheet.matching_rules()` с полным контекстом для CSS-селекторов.
|
||||
|
||||
---
|
||||
|
||||
## `resolve_string()` — подстановка `$var`
|
||||
|
||||
```rust
|
||||
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str>
|
||||
```
|
||||
|
||||
- Если в строке нет `$` — возвращает `Cow::Borrowed(val)` (без аллокаций).
|
||||
- Иначе обходит строку посимвольно. После `$` собирает имя переменной (буквы, цифры, `_`), ищет в `scope`, подставляет значение. Если переменная не найдена — оставляет `$var` как есть.
|
||||
|
||||
## `resolve_prop()` — разрешение значения свойства
|
||||
|
||||
```rust
|
||||
fn resolve_prop(v: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> String
|
||||
```
|
||||
|
||||
- Если начинается с `!rhei:` — вызывает `rhei.eval_expr()`.
|
||||
- Иначе — `resolve_string()`.
|
||||
|
||||
## `evaluate_condition()` — вычисление условия `@if`
|
||||
|
||||
```rust
|
||||
fn evaluate_condition(cond: &str, variables: &HashMap<String, Value>, 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<Element<'a>>,
|
||||
pub components: HashMap<String, ComponentDef<'a>>,
|
||||
pub variables: HashMap<String, Value>,
|
||||
pub rhei_scripts: Vec<String>,
|
||||
pub stylesheet: StyleSheet,
|
||||
pub interner: Interner,
|
||||
pub tracker: ReactiveTracker,
|
||||
}
|
||||
```
|
||||
|
||||
## `Element`
|
||||
|
||||
```rust
|
||||
pub struct Element<'a> {
|
||||
pub type_name: &'a str,
|
||||
pub properties: Vec<(String, String)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
pub element_id: ElementId,
|
||||
pub computed_style: Option<ComputedStyle>,
|
||||
pub content_hash: u64,
|
||||
}
|
||||
```
|
||||
|
||||
## `ComponentDef`
|
||||
|
||||
```rust
|
||||
pub struct ComponentDef<'a> {
|
||||
pub name: String,
|
||||
pub params: Vec<(String, String)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
}
|
||||
```
|
||||
109
docs/ru/modules/07-app-perf.md
Normal file
109
docs/ru/modules/07-app-perf.md
Normal file
@@ -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<Element<'static>>,
|
||||
pub rhei: RheiContext,
|
||||
}
|
||||
```
|
||||
|
||||
- `doc` — оригинальный Document (стили, переменные, tracker)
|
||||
- `vdom_roots` — результат последнего evaluate_vdom (_incr)
|
||||
- `rhei` — Rhai-движок с кэшированными AST
|
||||
|
||||
### `update(&mut self, message: Message) -> iced::Task<Message>`
|
||||
|
||||
Обработка сообщений от 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<PerfReport>`.
|
||||
|
||||
### `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<String>` | Собрать отчёт, проверить >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()` |
|
||||
Reference in New Issue
Block a user