16 KiB
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
hoveroractivestyle exists; - The element has an
__on:clickhandler.
The button is assigned on_press(Message::EventTriggered(...)). In the style() closure, apply_overrides are substituted depending on button::Status:
Hovered→hover_props;Pressed→active_props, orhover_propsif 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
hover_props,active_propsare collected viacollect_hover_active().- Positioning type is determined:
is_fixed,is_absolute,is_sticky. - If
left+rightare set withoutwidth—width = Fill. Iftop+bottomwithoutheight—height = Fill. flex-growis converted toFillPortion(grow_value)along the parent axis.current_colorandcurrent_font_sizeare inherited.
"Window" branch
if el.type_name == "Window"
- Creates a
columnwithspacing(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:- Main flow (main_flow)
abs_layerssticky_layersfixed_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
textproperty (or empty string). - Create
iced::widget::textwith font size (24 for Title/Header, 16 for Text). - Apply
color,font_weight(Light ≤399, Normal 400–599, Bold 600–799, ExtraBold ≥800),text_align,line_height(withWrapping::Word). - For Title/Header the default size is 24px, for Text — 16px.
"Image"
- Reads
src. If the path starts withfs:— strips the prefix. .svg→svg::Handle, otherwiseimage::Viewer.- Image size is calculated subtracting padding and border-width.
- For raster images
border_radiusis 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
columnwithspacing(10px). - Renders children.
- If there are
abs_layers— wraps in astack. - Applies
apply_universal_box_model(). - If there are
sticky_layers— overlays them viastack. - Returns
None(the element is already written intofinal_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:
- Fixed →
wrap_fixed_position()→ placed intofixed_layers, returnsNone. - Absolute →
wrap_fixed_position()→ placed intoabs_layers, returnsNone. - Sticky → if
scroll_y > threshold, the element is moved tosticky_layers, and an emptyspacerwith heightestimate_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 astackinside the content. - After content,
apply_universal_box_model()is applied. sticky_layersare overlaid on top viastack.
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 createstext. - If there are children — renders them in a
rowwithspacing = 8. - Padding: default 8px vertical, 16px horizontal. With auto-shrink if
padding + border > width/height. on_pressfrom__on:click.- Style: via
get_button_style()with dynamichover/activeoverrides fromstylesheet.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) andvalue(parsed asbool, defaultfalse). - Extracts
__bind:valuefor reactive binding. - Creates a
checkbox, onon_togglesendsMessage::ToggleChanged. - If there is a label — wraps in
row![checkbox, label]withspacing=8andalign_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…") andvalue. - Extracts
__bind:value. - Creates
text_inputwithpaddingfromget_padding(cs, 10.0). - Assigns
idviaget_or_create_widget_id(el.element_id.0, "ti"). - If there are hover/active styles — applies dynamic
style()withapply_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 asf32, default 0.0). - Extracts
__bind:value. - Creates a
sliderin the range0.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
- Padding:
container(widget).padding(get_padding(cs, default_padding)). - Overflow: if
overflow-y= Scroll/Auto — addsscrollablewith directionVertical(orBothifoverflow-xis also Scroll/Auto). For Windowoverflow-ydefaults toAuto. Scroll gets anidandon_scroll. - Clip: if
overflow = Hidden—container.clip(true). - Background, border, rounding:
container.style(...)withBackground,Border. - Width/height: for Window —
Fill/Fill; otherwise — fromcs.width,cs.height,cs.max_width,cs.max_height. - Content alignment:
align_xfromcs.content_align. - Margin: if margin exists — outer
containerwithpadding = 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