Files
Glint-Runtime/docs/en/modules/02-style.md

483 lines
16 KiB
Markdown
Raw 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.
# Style Module: `src/interpreter/style.rs`
A CSS-like style system for Glint: selector parsing, index building, cascading property resolution, and computed style caching.
---
## Helper Enums
### `SizeValue`
```rust
pub enum SizeValue {
Px(f32),
Percent(f32),
}
```
An absolute (`Px`) or relative (`Percent`) size value.
```rust
impl SizeValue {
pub fn resolve(self, relative_to: Option<f32>) -> f32
}
```
`Percent` resolves relative to `relative_to`; `Px` returns as-is. When `Percent` and `relative_to = None`, the percentage is returned as a number.
### `Overflow`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Overflow {
#[default] Visible,
Hidden,
Scroll,
Auto,
}
```
Used for `overflow-x`, `overflow-y`.
### `Position`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Position {
#[default] Static,
Relative,
Absolute,
Sticky,
Fixed,
}
```
Defines the element positioning scheme.
### `Display`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Display {
#[default] Block,
Flex,
Grid,
Inline,
None,
}
```
### `LayoutDirection`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutDirection {
Column,
Row,
Grid,
}
```
### `ContentAlign`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentAlign {
Start,
Center,
End,
}
```
### `TextAlign`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
}
impl From<TextAlign> for iced::alignment::Horizontal
```
---
## Selector Parsing
### `AttributeSelector`
```rust
pub enum AttributeSelector {
Exists(String),
Equals(String, String),
}
```
Attribute selector: `[attr]` or `[attr=value]`.
### `Combinator`
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Combinator {
Descendant, // space
Child, // >
NextSibling, // +
Subsequent, // ~
}
```
### `split_selectors(input: &str) -> Vec<String>`
Splits a selector group by comma, accounting for nested brackets. For example, `"Button, Label:hover"``["Button", "Label:hover"]`.
### `CompoundSelector`
```rust
#[derive(Debug, Clone)]
pub struct CompoundSelector {
pub tag: Option<String>,
pub id: Option<String>,
pub classes: Vec<String>,
pub pseudo_classes: Vec<String>,
pub attributes: Vec<AttributeSelector>,
}
```
**Methods:**
- `CompoundSelector::parse(input: &str) -> Self` — parses a simple selector like `Button#id.primary:hover[named=val]`. Processes character by character, grouping parts by the first symbol (`#`, `.`, `:`, `[`).
- `fn specificity(&self) -> (u32, u32, u32)` — returns specificity per CSS rule: (id, class+attr+pseudo, tag).
- `pub fn matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) -> bool` — checks whether an element matches this simple selector. Considers:
- `tag` match (or `*`)
- `id` match
- Presence of all `classes`
- Presence of all `attributes` (Exists / Equals)
- Pseudo-classes: `first-child`, `last-child`, `first-of-type`, `empty`, `root`, `nth-child(...)`; others are compared against `active_pseudo`.
### `ComplexSelector`
```rust
#[derive(Debug, Clone)]
pub struct ComplexSelector {
pub compounds: Vec<CompoundSelector>,
pub combinators: Vec<Combinator>,
}
```
**Methods:**
- `ComplexSelector::parse(input: &str) -> Self` — parses a complex selector (e.g. `Panel > Button.primary`). Splits into parts by combinators (`>`, `+`, `~`, space), parses each as a `CompoundSelector`.
- `fn check_compound_against(&self, i, info: &AncestorInfo) -> bool` — checks whether the `i`-th compound matches the provided ancestor info.
- `pub fn matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) -> bool` — full complex selector check: the last compound is the target element, the rest are ancestors/siblings according to combinators.
- `pub fn specificity(&self) -> (u32, u32, u32)` — sum of all compounds' specificities.
- `pub fn as_simple(&self) -> Option<&CompoundSelector>` — if compounds contains exactly one element, returns it; otherwise `None`.
- `pub fn has_pseudo_class(&self, pc: &str) -> bool` — whether any compound has the given pseudo-class.
---
## Helper Structs
### `AncestorInfo`
```rust
#[derive(Debug, Clone)]
pub struct AncestorInfo {
pub type_name: String,
pub id: Option<String>,
pub classes: Vec<String>,
}
```
Methods:
- `AncestorInfo::new(type_name, classes) -> Self`
- `AncestorInfo::new_with_id(type_name, id, classes) -> Self`
Used when checking complex selectors — describes an ancestor or a sibling element.
### `StructuralContext`
```rust
#[derive(Debug, Clone, Default)]
pub struct StructuralContext {
pub sibling_index: usize, // 0-based
pub sibling_total: usize,
pub type_index: usize, // among elements of the same type
pub type_total: usize,
pub has_children: bool,
pub is_root: bool,
}
```
Used for resolving structural pseudo-classes (`first-child`, `nth-child`, `empty`, `root`).
---
## `StyleRule`
```rust
#[derive(Debug, Clone)]
pub struct StyleRule {
pub selector: ComplexSelector,
pub properties: HashMap<String, String>,
}
impl StyleRule {
pub fn build(selector_str: String, properties: HashMap<String, String>) -> Self
}
```
---
## `StyleIndex`
```rust
pub type RuleId = usize;
#[derive(Debug, Clone)]
pub struct StyleIndex {
pub by_tag: HashMap<String, Vec<RuleId>>,
pub by_class: HashMap<String, Vec<RuleId>>,
pub by_id: HashMap<String, Vec<RuleId>>,
pub by_tag_class: HashMap<(String, String), Vec<RuleId>>,
pub by_tag_id: HashMap<(String, String), Vec<RuleId>>,
pub complex_rules: Vec<(RuleId, RuleId)>,
pub universal_rules: Vec<RuleId>,
pub rule_specificities: Vec<(u32, u32, u32)>,
pub rules: Vec<StyleRule>,
pub epoch: u64,
}
impl StyleIndex {
pub fn new() -> Self
}
```
An index for fast rule lookup. Built in `StyleSheet::build_index`:
- `by_tag` / `by_class` / `by_id` / `by_tag_class` / `by_tag_id` — indexes for simple selectors
- `complex_rules` — rules with complex selectors (always checked by brute force)
- `universal_rules` — rules with `*`
- `rule_specificities` — specificity cache
- `epoch` — monotonically increasing counter for cache invalidation
---
## `StyleCache`
```rust
#[derive(Debug, Clone)]
pub struct StyleCache {
entries: HashMap<u64, ComputedStyle>,
max_entries: usize,
}
impl StyleCache {
pub fn new(max_entries: usize) -> Self
pub fn get_or_compute(
&mut self,
type_name: &str,
props: &[(Cow<'_, str>, Cow<'_, str>)],
epoch: u64,
matched_sheets: &[&HashMap<String, String>],
) -> ComputedStyle
pub fn clear(&mut self)
}
```
Computed style cache. Key is a hash of `type_name`, inline properties, and `epoch`. When `max_entries` is exceeded, the cache is fully cleared.
---
## `StyleSheet`
```rust
#[derive(Debug)]
pub struct StyleSheet {
rules: Vec<StyleRule>,
index: Option<StyleIndex>,
epoch: u64,
cache: Mutex<StyleCache>,
}
```
The main type of the module. Contains a list of rules, an optional index, and a cache. Implements `Clone` (with a new empty cache) and `Default`.
**Methods:**
- `StyleSheet::new() -> Self` — creates an empty sheet.
- `pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>)` — adds a rule. Passes the selector through `split_selectors` (supports comma-separated groups). Resets `index = None`.
- `pub fn build_index(&mut self)` — rebuilds the `StyleIndex`. Increments `epoch`, clears the cache. For each rule:
- Computes specificity
- If the selector is simple (1 compound) — indexes by tag/class/id/attributes
- If complex — marks as `complex_rules`
- Universal (`*`) go into `universal_rules`
- `pub fn has_index(&self) -> bool`
- `pub fn query_index(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) -> Option<Vec<&HashMap<String, String>>>` — uses the index for fast lookup: collects candidates from `universal_rules`, `by_tag`, `by_class`, `by_id`, `complex_rules`; filters via `ComplexSelector::matches`; sorts by specificity.
- `pub fn matching_rules(...) -> Vec<&HashMap<String, String>>` — finds matching rules. Attempts `query_index`; if no index — linear scan of all `self.rules` with sorting.
- `pub fn matching_pseudo_rules(pseudo, ...) -> HashMap<String, String>` — finds rules containing the specified pseudo-class (e.g. `:hover`). Uses `query_index_for_pseudo` or linear scan.
- `pub fn compute_cached(type_name, props, matched_sheets) -> ComputedStyle` — computes the final style through the cache (wrapper around `StyleCache::get_or_compute`). Includes `PerfScope::new("style")`.
- `pub fn clear_cache(&self)` — clears the cache.
- `pub fn is_empty(&self) -> bool``self.rules.is_empty()`.
**Methods with `#[cfg(feature = "parallel")]`:**
- `matching_rules_batch(type_names, el_ids, el_classes_list, ...) -> Vec<Vec<&HashMap<String, String>>>` — parallel batch search via `rayon::par_iter`.
---
## `ComputedStyle`
```rust
#[derive(Debug, Clone, Default)]
pub struct ComputedStyle {
pub font_size: Option<SizeValue>,
pub color: Option<iced::Color>,
pub padding: Option<SizeValue>,
pub padding_top: Option<SizeValue>,
pub padding_right: Option<SizeValue>,
pub padding_bottom: Option<SizeValue>,
pub padding_left: Option<SizeValue>,
pub margin: Option<SizeValue>,
pub margin_top: Option<SizeValue>,
pub margin_right: Option<SizeValue>,
pub margin_bottom: Option<SizeValue>,
pub margin_left: Option<SizeValue>,
pub background: Option<iced::Color>,
pub spacing: Option<SizeValue>,
pub border_radius: Option<SizeValue>,
pub border_width: Option<SizeValue>,
pub border_color: Option<iced::Color>,
pub width: Option<iced::Length>,
pub height: Option<iced::Length>,
pub min_width: Option<SizeValue>,
pub max_width: Option<SizeValue>,
pub min_height: Option<SizeValue>,
pub max_height: Option<SizeValue>,
pub direction: Option<LayoutDirection>,
pub align_items: Option<iced::Alignment>,
pub content_align: Option<ContentAlign>,
pub flex_grow: Option<u16>,
pub position: Option<Position>,
pub top: Option<SizeValue>,
pub right: Option<SizeValue>,
pub bottom: Option<SizeValue>,
pub left: Option<SizeValue>,
pub overflow_x: Option<Overflow>,
pub overflow_y: Option<Overflow>,
pub display: Option<Display>,
pub opacity: Option<f32>,
pub font_weight: Option<u16>,
pub line_height: Option<SizeValue>,
pub text_align: Option<TextAlign>,
}
```
The final computed style of an element. All fields are `Option`; a missing property means "not set / inherited from parent".
### `ComputedStyle::compute()`
```rust
pub fn compute(
inline: &[(Cow<'_, str>, Cow<'_, str>)],
matched_sheets: &[&HashMap<String, String>],
) -> Self
```
Assembles the style via `lookup()`: for each field, `lookup(key, inline, matched_sheets)` is called, then parsed by the corresponding function. Notable details:
- `padding`/`margin` — individual properties (`-top`, `-right`, etc.) are checked first, then the shorthand.
- `spacing` — alternate name for `gap`.
- `background` — first `background`, then `background-color`.
- `overflow-x`/`overflow-y` — if the individual property is not found, the general `overflow` is applied.
- `flex_grow` — parsed as `f32`, cast to `u16`.
### `ComputedStyle::compute_batch()`
```rust
#[cfg(feature = "parallel")]
pub fn compute_batch<'a>(
pairs: &[(&[(Cow<'_, str>, Cow<'_, str>)], &[&'a HashMap<String, String>])],
) -> Vec<ComputedStyle>
```
Parallel batch variant via `rayon::par_iter`.
### `ComputedStyle::apply_overrides()`
```rust
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>)
```
Applies (overwrites) a given set of properties on top of the existing style. Used for dynamic changes (e.g. `:hover` rules, inline overrides).
### How `lookup()` works
```rust
fn lookup<'a>(
key: &str,
inline: &'a [(Cow<'_, str>, Cow<'_, str>)],
matched_sheets: &[&'a HashMap<String, String>],
) -> Option<&'a str>
```
Property resolution order:
1. **Inline properties** — iterates over `(key, value)` pairs. Supports the `style:` prefix (i.e. `style:color` is equivalent to `color`).
2. **matched_sheets** — a list of dictionaries from matching CSS rules, sorted by specificity. Iterated from the end (last is most specific).
3. Returns the first found value.
---
## Parsing Functions
| Function | Signature | Description |
|---|---|---|
| `parse_size` | `(s: &str) -> Option<SizeValue>` | Parses a size: `"10"``Px(10)`, `"50%"``Percent(50)`. `auto`, `fill`, `stretch``None` |
| `parse_color` | `(s: &str) -> Option<iced::Color>` | Parses a color: `#rgb`, `#rrggbb`, `#rrggbbaa`, names (`white`, `black`, `transparent`) |
| `parse_length` | `(s: &str) -> Option<iced::Length>` | Parses an Iced length: `"fill"`/`"100%"`, `"shrink"`/`"auto"`, `"50"``Fixed(50)` |
| `parse_overflow` | `(s: &str) -> Option<Overflow>` | `visible`, `hidden`, `scroll`, `auto` |
| `parse_position` | `(s: &str) -> Option<Position>` | `static`, `relative`, `absolute`, `sticky`, `fixed` |
| `parse_display` | `(s: &str) -> Option<Display>` | `none`, `block`, `flex`, `grid`, `inline` |
| `parse_direction` | `(s: &str) -> Option<LayoutDirection>` | `row`/`horizontal`, `column`/`vertical`, `grid` |
| `parse_alignment` | `(s: &str) -> Option<iced::Alignment>` | `start`, `center`, `end` |
| `parse_content_align` | `(s: &str) -> Option<ContentAlign>` | `start`/`left`/`top`, `center`, `end`/`right`/`bottom` |
| `parse_opacity` | `(s: &str) -> Option<f32>` | Number 0.01.0, clamped |
| `parse_font_weight` | `(s: &str) -> Option<u16>` | Names: `normal`→400, `bold`→700, `lighter`→300, `bolder`→900; numeric values |
| `parse_text_align` | `(s: &str) -> Option<TextAlign>` | `left`, `center`, `right` |
### `resolve_size`
```rust
pub fn resolve_size(v: Option<SizeValue>, relative_to: Option<f32>) -> Option<f32>
```
A convenience wrapper around `SizeValue::resolve`, returns `Option<f32>`.
---
## Usage in `renderer.rs`
`ComputedStyle` fields are actively used in `/home/faynot/software/glint-runtime/src/renderer.rs`:
| Field | Where used |
|---|---|
| `.color` | Text color in buttons, text fields, Label |
| `.background` | Background of containers, buttons, text fields |
| `.padding*` | `iced::Padding` for buttons, input fields, containers |
| `.margin*` | Spacing around elements |
| `.border_radius`, `.border_width`, `.border_color` | Borders of buttons, input fields, containers |
| `.width`, `.height` | Sizes of Scrollable, Column, Row, Image |
| `.min_width`, `.max_width`, `.min_height`, `.max_height` | Size constraints |
| `.direction` | Flex direction (Row / Column) |
| `.align_items` | Child element alignment |
| `.content_align` | Content alignment |
| `.flex_grow` | Flex-grow with `FillPortion` |
| `.spacing` | `iced::container::Style` spacing, gap in Row / Column |
| `.position` | Static / Fixed / Absolute / Sticky |
| `.top`, `.right`, `.bottom`, `.left` | Positioning |
| `.overflow_x`, `.overflow_y` | Scrolling (`Scrollable`) |
| `.display` | `Display::None` — element hiding |
| `.opacity` | Transparency |
| `.font_weight` | Font weight in Text |
| `.line_height` | Line spacing |
| `.text_align` | Horizontal text alignment |
| `.font_size` | Font size (inherited from parent) |
---
## `nth_matches(expr: &str, n: usize) -> bool`
Internal function for resolving `:nth-child(an+b)`, `:nth-child(odd)`, `:nth-child(even)`, and `:nth-child(<number>)`. Supports negative `a` and `b`.
---
## Relationships with other modules
- `types.rs` — each `DomNode` (both element and text node) contains `computed_style: ComputedStyle`.
- `renderer.rs` — imports `{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign, resolve_size}`.
- `mod.rs` — uses `AncestorInfo`, `ComputedStyle`, `StructuralContext` when traversing the DOM.