docs: reorganize into en/ru and add English translation

This commit is contained in:
Glint Dev
2026-07-24 22:04:29 +03:00
parent 938f05f59d
commit 35f92435a3
20 changed files with 4651 additions and 0 deletions

182
docs/ru/modules/01-types.md Normal file
View 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
View 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.01.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.

View 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
View 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)` как строка

View 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 400599, Bold 600799, 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
View 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>>,
}
```

View 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()` |