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

16 KiB
Raw Blame History

Renderer Module: src/renderer.rs

Transforms the Element tree into Iced widgets. Responsible for building the widget hierarchy, applying the box model, handling :hover/:active pseudo-classes, positioning (fixed, absolute, sticky), and rendering all built-in element types.


Imports

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

A thread_local cache for iced::widget::Id. The key is a tuple (element_id, prefix). The string is formatted as "{prefix}:{key}" and "leaked" via Box::leak to obtain a &'static str. Used for scrollable::id (prefix "sc") and text_input::id (prefix "ti").


Element Methods

get_prop()

impl<'a> Element<'a> {
    #[inline]
    pub fn get_prop(&self, key: &str) -> Option<&str>
}

Looks up a property by key in self.properties. Returns the value or None.

push_prop()

pub fn push_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V)

Adds a (key, val) pair to self.properties.

set_prop()

pub fn set_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V)

Sets a property: if the key already exists — replaces the value, otherwise — adds a new pair.


extract_var_binding()

fn extract_var_binding(el: &Element, prop: &str) -> Option<String>

Looks for a property of the form __bind:<prop> and returns its value. Used for reactive variable binding: __bind:value for Input, Toggle, Slider.


collect_hover_active()

pub fn collect_hover_active<'a>(
    el: &'a Element,
    stylesheet: &StyleSheet,
) -> (HashMap<String, String>, HashMap<String, String>)

Collects CSS properties for the :hover and :active pseudo-classes for an element. Calls stylesheet.matching_pseudo_rules() twice — for "hover" and "active". Returns a tuple (hover_props, active_props). Used in render_element() and 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>

Wraps an arbitrary widget in a button to support :hover/:active styles. Trigger conditions:

  • At least one hover or active style exists;
  • The element has an __on:click handler.

The button is assigned on_press(Message::EventTriggered(...)). In the style() closure, apply_overrides are substituted depending on button::Status:

  • Hoveredhover_props;
  • Pressedactive_props, or hover_props if none exist.

Applied only to non-Button and non-Input elements (line 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>>

Main public rendering function. Returns None if display: none.

Common logic for all elements

  1. hover_props, active_props are collected via collect_hover_active().
  2. Positioning type is determined: is_fixed, is_absolute, is_sticky.
  3. If left + right are set without widthwidth = Fill. If top + bottom without heightheight = Fill.
  4. flex-grow is converted to FillPortion(grow_value) along the parent axis.
  5. current_color and current_font_size are inherited.

"Window" branch

if el.type_name == "Window"
  • Creates a column with spacing (default 12px).
  • Renders children via render_children().
  • Applies apply_universal_box_model(is_window = true, scrollable_id = window_id) — the window is always scrollable.
  • Builds an iced::widget::stack:
    1. Main flow (main_flow)
    2. abs_layers
    3. sticky_layers
    4. fixed_layers

Final structure:

stack[
  container[ scrollable[ container[ column[...] ] ] ]
  ...abs layers
  ...sticky layers
  ...fixed layers
]

"Panel" branch

Delegates to render_panel(). If there are abs_layers — wraps in a stack.

"Button" branch

Delegates to render_button(). If there are abs_layers or sticky_layers — wraps in a stack.

"Input" branch

Delegates to render_input() passing hover_props and active_props.

Text widgets: "Title", "Header", "Text", "Label", "#text"

"Title" | "Header" => ...
"Text" | "Label" | "#text" => ...
  • Read the text property (or empty string).
  • Create iced::widget::text with font size (24 for Title/Header, 16 for Text).
  • Apply color, font_weight (Light ≤399, Normal 400599, Bold 600799, ExtraBold ≥800), text_align, line_height (with Wrapping::Word).
  • For Title/Header the default size is 24px, for Text — 16px.

"Image"

  • Reads src. If the path starts with fs: — strips the prefix.
  • .svgsvg::Handle, otherwise image::Viewer.
  • Image size is calculated subtracting padding and border-width.
  • For raster images border_radius is applied.

"Icon"

Renders the character 🔹 as text of size 18px (or current_font_size). Placeholder.

"Toggle"

Delegates to render_toggle().

"Slider"

Delegates to render_slider().

"ProgressBar"

progress_bar(0.0..=100.0, value)

The value property is parsed as f32.

"Divider", "Separator"

iced::widget::rule::horizontal(1)

Horizontal line with thickness 1px.

Default branch (unknown type)

  • Creates a column with spacing (10px).
  • Renders children.
  • If there are abs_layers — wraps in a stack.
  • Applies apply_universal_box_model().
  • If there are sticky_layers — overlays them via stack.
  • Returns None (the element is already written into final_widget_opt).

Post-processing for non-Button and non-Input

if el.type_name != "Button" && el.type_name != "Input" {
    final_widget_opt = make_hoverable(...);
}

Positioning handling

After obtaining final_widget:

  • Fixedwrap_fixed_position() → placed into fixed_layers, returns None.
  • Absolutewrap_fixed_position() → placed into abs_layers, returns None.
  • Sticky → if scroll_y > threshold, the element is moved to sticky_layers, and an empty spacer with height estimate_element_height() is inserted in its place. Otherwise the element stays in place.

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

Recursively calls render_element() for each child. Filters out None (display: none). Returns a vector of rendered elements.


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>

Renders Panel — a container with three layout modes:

Row

LayoutDirection::Row

iced::widget::row with spacing (10px). align_y from cs.align_items or Alignment::Center by default.

Column

LayoutDirection::Column

iced::widget::column with spacing. align_x from cs.align_items.

Grid

LayoutDirection::Grid

Columns (column), inside each — a row (row). Number of columns from the columns property (default 3). Each row is a chunk of cols children.

In all modes:

  • If there are abs_layers — they are wrapped in a stack inside the content.
  • After content, apply_universal_box_model() is applied.
  • sticky_layers are overlaid on top via 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>
  • If there are no children — reads label (default "Button") and creates text.
  • If there are children — renders them in a row with spacing = 8.
  • Padding: default 8px vertical, 16px horizontal. With auto-shrink if padding + border > width/height.
  • on_press from __on:click.
  • Style: via get_button_style() with dynamic hover/active overrides from 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>
  • Reads label (optional) and value (parsed as bool, default false).
  • Extracts __bind:value for reactive binding.
  • Creates a checkbox, on on_toggle sends Message::ToggleChanged.
  • If there is a label — wraps in row![checkbox, label] with spacing=8 and 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>
  • Reads placeholder (default "Type here…") and value.
  • Extracts __bind:value.
  • Creates text_input with padding from get_padding(cs, 10.0).
  • Assigns id via get_or_create_widget_id(el.element_id.0, "ti").
  • If there are hover/active styles — applies dynamic style() with apply_overrides.
  • Otherwise — static style via get_text_input_style().

render_slider()

fn render_slider<'a>(
    el: &'a Element,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
  • Reads value (parsed as f32, default 0.0).
  • Extracts __bind:value.
  • Creates a slider in the range 0.0..=100.0.
  • On change sends Message::SliderChanged.

Helper Functions

get_padding()

fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding

Gathers padding from ComputedStyle considering padding-top/right/bottom/left. If padding + border * 2 exceeds fixed width/height — scales padding proportionally (auto-shrink).

get_margin()

fn get_margin(cs: &ComputedStyle) -> iced::Padding

Gathers margin from ComputedStyle considering margin-top/right/bottom/left. Base is cs.margin (default 0).

get_button_style()

fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style

Builds button::Style from ComputedStyle. Depending on status:

  • Hovered — background 15% lighter (* 1.15);
  • Pressed — background 15% darker (* 0.85);
  • Active — unchanged.

get_text_input_style()

fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style

Builds text_input::Style from ComputedStyle: background, border, icon, placeholder, value, selection. Default values use a dark theme.

estimate_element_height()

fn estimate_element_height(cs: &ComputedStyle) -> f32

Approximately calculates element height for sticky spacer: 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>

Wraps a widget in a container with padding-top from cs.top. Width is Fill. Used for sticky elements when 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>

Wraps a widget in a container with width = Fill, height = Fill and alignment (align_x, align_y) based on set top/bottom/left/right. Padding is set accordingly. Used for fixed and absolute positioning.

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>

Applies the box model to any widget. Wrapping order:

outer_container (margin)         ← if margin exists and !is_window
  inner_container (bg, border, clip)
    scrollable                   ← if overflow_x/overflow_y = Scroll/Auto
      container (padding)
        widget

Stages

  1. Padding: container(widget).padding(get_padding(cs, default_padding)).
  2. Overflow: if overflow-y = Scroll/Auto — adds scrollable with direction Vertical (or Both if overflow-x is also Scroll/Auto). For Window overflow-y defaults to Auto. Scroll gets an id and on_scroll.
  3. Clip: if overflow = Hiddencontainer.clip(true).
  4. Background, border, rounding: container.style(...) with Background, Border.
  5. Width/height: for Window — Fill/Fill; otherwise — from cs.width, cs.height, cs.max_width, cs.max_height.
  6. Content alignment: align_x from cs.content_align.
  7. Margin: if margin exists — outer container with padding = margin.

Widget Tree Structure

For a typical window (Window) the tree looks like:

stack[
  container (Window) [Fill, Fill]
    scrollable [id="sc:window_id"]
      container [bg, border, padding]
        column [spacing]
          ...child elements...
  container (fixed)     ← fixed layer
  container (absolute)  ← absolute layer
  container (sticky)    ← sticky layer
]

For Panel:

stack[
  container [bg, border, margin]
    scrollable (if overflow)
      container [padding]
        row | column | grid
          ...child elements...
  container (sticky)    ← sticky layer, on top of boxed content
]

For unknown elements — similar to Panel, but abs-layers inside scroll, sticky on top.

For simple elements (Text, Image, Toggle, Slider, ProgressBar, Divider):

container [bg, border, margin, padding]
  scrollable (if overflow)
    container [padding]
      text | image | checkbox | slider | progress_bar | rule