20 KiB
Модуль рендерера: src/renderer.rs
Преобразует дерево Element в виджеты Iced. Отвечает за построение иерархии виджетов, применение боксовой модели, обработку псевдоклассов :hover/:active, позиционирование (fixed, absolute, sticky) и рендеринг всех встроенных типов элементов.
Импорты
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()
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()
impl<'a> Element<'a> {
#[inline]
pub fn get_prop(&self, key: &str) -> Option<&str>
}
Ищет свойство по ключу в self.properties. Возвращает значение или None.
push_prop()
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()
pub fn set_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V)
Устанавливает свойство: если ключ уже существует — заменяет значение, иначе — добавляет новую пару.
extract_var_binding()
fn extract_var_binding(el: &Element, prop: &str) -> Option<String>
Ищет свойство вида __bind:<prop> и возвращает его значение. Используется для реактивной привязки переменных: __bind:value для Input, Toggle, Slider.
collect_hover_active()
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()
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()
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.
Общая логика для всех элементов
- Собираются
hover_props,active_propsчерезcollect_hover_active(). - Определяется тип позиционирования:
is_fixed,is_absolute,is_sticky. - Если заданы
left+rightбезwidth—width = Fill. Еслиtop+bottomбезheight—height = Fill. flex-growпреобразуется вFillPortion(grow_value)по оси родителя.- Наследуются
current_colorиcurrent_font_size.
Ветка "Window"
if el.type_name == "Window"
- Создаётся
columnсоspacing(по умолчанию 12px). - Рендерятся дети через
render_children(). - Применяется
apply_universal_box_model(is_window = true, scrollable_id = window_id)— окно всегда скроллируемое. - Формируется
iced::widget::stack:- Основной поток (main_flow)
abs_layerssticky_layersfixed_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"
"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"
progress_bar(0.0..=100.0, value)
Свойство value парсится как f32.
"Divider", "Separator"
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
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()
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()
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
LayoutDirection::Row
iced::widget::row с spacing (10px). align_y из cs.align_items или Alignment::Center по умолчанию.
Column
LayoutDirection::Column
iced::widget::column с spacing. align_x из cs.align_items.
Grid
LayoutDirection::Grid
Столбцы (column), внутри каждого — строка (row). Количество колонок из свойства columns (по умолчанию 3). Каждый ряд — чанк по cols детей.
Во всех режимах:
- Если есть
abs_layers— они оборачиваются вstackвнутри контента. - После контента применяется
apply_universal_box_model(). sticky_layersнакладываются поверх черезstack.
render_button()
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()
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()
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()
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()
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()
fn get_margin(cs: &ComputedStyle) -> iced::Padding
Собирает margin из ComputedStyle с учётом margin-top/right/bottom/left. База — cs.margin (по умолчанию 0).
get_button_style()
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()
fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style
Формирует text_input::Style из ComputedStyle: background, border, icon, placeholder, value, selection. Значения по умолчанию — тёмная тема.
estimate_element_height()
fn estimate_element_height(cs: &ComputedStyle) -> f32
Приблизительно вычисляет высоту элемента для sticky-спейсера: padding_top + padding_bottom + border_width * 2 + font_size * line_height.
wrap_sticky_position()
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()
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()
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
Этапы
- Padding:
container(widget).padding(get_padding(cs, default_padding)). - Overflow: если
overflow-y= Scroll/Auto — добавляетсяscrollableс направлениемVertical(илиBothеслиoverflow-xтоже Scroll/Auto). Для Windowoverflow-yпо умолчаниюAuto. Скроллу назначаетсяidиon_scroll. - Clip: если
overflow = Hidden—container.clip(true). - Фон, рамка, скругление:
container.style(...)сBackground,Border. - Ширина/высота: для Window —
Fill/Fill; иначе — изcs.width,cs.height,cs.max_width,cs.max_height. - Выравнивание контента:
align_xизcs.content_align. - 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