538 lines
20 KiB
Markdown
538 lines
20 KiB
Markdown
# Модуль рендерера: `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
|
||
```
|