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

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
```