# 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 ```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` and `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 ``` 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()` ```rust 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()` ```rust pub fn push_prop>, V: Into>>(&mut self, key: K, val: V) ``` Adds a `(key, val)` pair to `self.properties`. ### `set_prop()` ```rust pub fn set_prop>, V: Into>>(&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()` ```rust fn extract_var_binding(el: &Element, prop: &str) -> Option ``` Looks for a property of the form `__bind:` and returns its value. Used for reactive variable binding: `__bind:value` for `Input`, `Toggle`, `Slider`. --- ## `collect_hover_active()` ```rust pub fn collect_hover_active<'a>( el: &'a Element, stylesheet: &StyleSheet, ) -> (HashMap, HashMap) ``` 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()` ```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> ``` 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`: - `Hovered` → `hover_props`; - `Pressed` → `active_props`, or `hover_props` if none exist. Applied **only to non-Button and non-Input** elements (line 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> ``` **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 `width` — `width = Fill`. If `top` + `bottom` without `height` — `height = 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 ```rust 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"` ```rust "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 400–599, Bold 600–799, 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. - `.svg` → `svg::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"` ```rust progress_bar(0.0..=100.0, value) ``` The `value` property is parsed as `f32`. ### `"Divider"`, `"Separator"` ```rust 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 ```rust 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 into `fixed_layers`, returns `None`. - **Absolute** → `wrap_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()` ```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> ``` Recursively calls `render_element()` for each child. Filters out `None` (display: none). Returns a vector of rendered elements. --- ## `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> ``` Renders `Panel` — a container with three layout modes: ### Row ```rust LayoutDirection::Row ``` `iced::widget::row` with `spacing` (10px). `align_y` from `cs.align_items` or `Alignment::Center` by default. ### Column ```rust LayoutDirection::Column ``` `iced::widget::column` with `spacing`. `align_x` from `cs.align_items`. ### Grid ```rust 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()` ```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> ``` - 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()` ```rust fn render_toggle<'a>( el: &'a Element, parent_color: Option, parent_font_size: Option, ) -> 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()` ```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> ``` - 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()` ```rust 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()` ```rust 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()` ```rust 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()` ```rust 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()` ```rust 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()` ```rust 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()` ```rust 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()` ```rust 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()` ```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> ``` 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 = Hidden` — `container.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 ```