Files
Glint-Runtime/docs/ru/modules/05-renderer.md

20 KiB
Raw Permalink Blame History

Модуль рендерера: 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:

  • Hoveredhover_props;
  • Pressedactive_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.

Общая логика для всех элементов

  1. Собираются hover_props, active_props через collect_hover_active().
  2. Определяется тип позиционирования: is_fixed, is_absolute, is_sticky.
  3. Если заданы left + right без widthwidth = Fill. Если top + bottom без heightheight = Fill.
  4. flex-grow преобразуется в FillPortion(grow_value) по оси родителя.
  5. Наследуются 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:
    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"

"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: — обрезает префикс.
  • .svgsvg::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:

  • Fixedwrap_fixed_position() → кладётся в fixed_layers, возвращается None.
  • Absolutewrap_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

Этапы

  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 = Hiddencontainer.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