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

537 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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()`
```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<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()`
```rust
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()`
```rust
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()`
```rust
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()`
```rust
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`:
- `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<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 `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 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.
- `.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<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()`
```rust
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
```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<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()`
```rust
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()`
```rust
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()`
```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<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 = 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
```