# Модуль рендерера: `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> = ...; } 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>, V: Into>>(&mut self, key: K, val: V) ``` Добавляет пару `(key, val)` в `self.properties`. ### `set_prop()` ```rust pub fn set_prop>, V: Into>>(&mut self, key: K, val: V) ``` Устанавливает свойство: если ключ уже существует — заменяет значение, иначе — добавляет новую пару. --- ## `extract_var_binding()` ```rust fn extract_var_binding(el: &Element, prop: &str) -> Option ``` Ищет свойство вида `__bind:` и возвращает его значение. Используется для реактивной привязки переменных: `__bind:value` для `Input`, `Toggle`, `Slider`. --- ## `collect_hover_active()` ```rust pub fn collect_hover_active<'a>( el: &'a Element, stylesheet: &StyleSheet, ) -> (HashMap, HashMap) ``` Собирает 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, active_props: &HashMap, 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, parent_font_size: Option, parent_direction: Option, fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, scroll_positions: &HashMap, container_id: u64, stylesheet: &StyleSheet, ) -> Option> ``` **Главная публичная функция рендеринга.** Возвращает `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, parent_font_size: Option, parent_direction: Option, fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, scroll_positions: &HashMap, container_id: u64, stylesheet: &StyleSheet, ) -> Vec> ``` Рекурсивно вызывает `render_element()` для каждого ребёнка. Фильтрует `None` (display: none). Возвращает вектор отрендеренных элементов. --- ## `render_panel()` ```rust fn render_panel<'a>( el: &'a Element, cs: ComputedStyle, parent_color: Option, parent_font_size: Option, fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, scroll_positions: &HashMap, 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, parent_font_size: Option, fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, scroll_positions: &HashMap, 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, parent_font_size: Option, ) -> 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, _parent_font_size: Option, hover_props: &HashMap, active_props: &HashMap, ) -> 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>, cs: &ComputedStyle, is_window: bool, default_padding: f32, scrollable_id: Option, ) -> 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 ```