483 lines
20 KiB
Markdown
483 lines
20 KiB
Markdown
# Модуль стилей: `src/interpreter/style.rs`
|
||
|
||
Система CSS-подобных стилей для Glint: парсинг селекторов, построение индекса, каскадное разрешение свойств и кеширование вычисленных стилей.
|
||
|
||
---
|
||
|
||
## Перечисления-помощники
|
||
|
||
### `SizeValue`
|
||
```rust
|
||
pub enum SizeValue {
|
||
Px(f32),
|
||
Percent(f32),
|
||
}
|
||
```
|
||
Абсолютное (`Px`) или относительное (`Percent`) значение размера.
|
||
|
||
```rust
|
||
impl SizeValue {
|
||
pub fn resolve(self, relative_to: Option<f32>) -> f32
|
||
}
|
||
```
|
||
`Percent` разрешается относительно `relative_to`; `Px` возвращается как есть. При `Percent` и `relative_to = None` возвращается процент как число.
|
||
|
||
### `Overflow`
|
||
```rust
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||
pub enum Overflow {
|
||
#[default] Visible,
|
||
Hidden,
|
||
Scroll,
|
||
Auto,
|
||
}
|
||
```
|
||
Используется для `overflow-x`, `overflow-y`.
|
||
|
||
### `Position`
|
||
```rust
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||
pub enum Position {
|
||
#[default] Static,
|
||
Relative,
|
||
Absolute,
|
||
Sticky,
|
||
Fixed,
|
||
}
|
||
```
|
||
Определяет схему позиционирования элемента.
|
||
|
||
### `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
|
||
```
|
||
|
||
---
|
||
|
||
## Парсинг селекторов
|
||
|
||
### `AttributeSelector`
|
||
```rust
|
||
pub enum AttributeSelector {
|
||
Exists(String),
|
||
Equals(String, String),
|
||
}
|
||
```
|
||
Селектор атрибута: `[attr]` или `[attr=value]`.
|
||
|
||
### `Combinator`
|
||
```rust
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum Combinator {
|
||
Descendant, // пробел
|
||
Child, // >
|
||
NextSibling, // +
|
||
Subsequent, // ~
|
||
}
|
||
```
|
||
|
||
### `split_selectors(input: &str) -> Vec<String>`
|
||
Разделяет группу селекторов по запятой с учётом вложенности скобок. Например, `"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>,
|
||
}
|
||
```
|
||
|
||
**Методы:**
|
||
|
||
- `CompoundSelector::parse(input: &str) -> Self` — парсит простой селектор вида `Button#id.primary:hover[named=val]`. Разбирает посимвольно, группируя части по первому символу (`#`, `.`, `:`, `[`).
|
||
|
||
- `fn specificity(&self) -> (u32, u32, u32)` — возвращает специфичность по правилу CSS: (id, class+attr+pseudo, tag).
|
||
|
||
- `pub fn matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) -> bool` — проверяет, соответствует ли элемент данному простому селектору. Учитывает:
|
||
- Совпадение `tag` (или `*`)
|
||
- Совпадение `id`
|
||
- Наличие всех `classes`
|
||
- Наличие всех `attributes` (Exists / Equals)
|
||
- Псевдоклассы: `first-child`, `last-child`, `first-of-type`, `empty`, `root`, `nth-child(...)`; остальные сравниваются с `active_pseudo`.
|
||
|
||
### `ComplexSelector`
|
||
```rust
|
||
#[derive(Debug, Clone)]
|
||
pub struct ComplexSelector {
|
||
pub compounds: Vec<CompoundSelector>,
|
||
pub combinators: Vec<Combinator>,
|
||
}
|
||
```
|
||
|
||
**Методы:**
|
||
|
||
- `ComplexSelector::parse(input: &str) -> Self` — парсит сложный селектор (например `Panel > Button.primary`). Разбивает на части по комбинаторам (`>`, `+`, `~`, пробел), парсит каждую как `CompoundSelector`.
|
||
|
||
- `fn check_compound_against(&self, i, info: &AncestorInfo) -> bool` — проверяет, соответствует ли `i`-й compound переданной информации о предке.
|
||
|
||
- `pub fn matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) -> bool` — полная проверка сложного селектора: последний compound — целевой элемент, остальные — предки/соседи в соответствии с комбинаторами.
|
||
|
||
- `pub fn specificity(&self) -> (u32, u32, u32)` — сумма специфичностей всех compounds.
|
||
|
||
- `pub fn as_simple(&self) -> Option<&CompoundSelector>` — если compounds содержит ровно один элемент, возвращает его; иначе `None`.
|
||
|
||
- `pub fn has_pseudo_class(&self, pc: &str) -> bool` — есть ли среди compounds указанный псевдокласс.
|
||
|
||
---
|
||
|
||
## Вспомогательные структуры
|
||
|
||
### `AncestorInfo`
|
||
```rust
|
||
#[derive(Debug, Clone)]
|
||
pub struct AncestorInfo {
|
||
pub type_name: String,
|
||
pub id: Option<String>,
|
||
pub classes: Vec<String>,
|
||
}
|
||
```
|
||
|
||
Методы:
|
||
- `AncestorInfo::new(type_name, classes) -> Self`
|
||
- `AncestorInfo::new_with_id(type_name, id, classes) -> Self`
|
||
|
||
Используется при проверке сложных селекторов — описывает предка или соседний элемент.
|
||
|
||
### `StructuralContext`
|
||
```rust
|
||
#[derive(Debug, Clone, Default)]
|
||
pub struct StructuralContext {
|
||
pub sibling_index: usize, // 0-based
|
||
pub sibling_total: usize,
|
||
pub type_index: usize, // среди элементов того же типа
|
||
pub type_total: usize,
|
||
pub has_children: bool,
|
||
pub is_root: bool,
|
||
}
|
||
```
|
||
|
||
Используется для разрешения структурных псевдоклассов (`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
|
||
}
|
||
```
|
||
|
||
Индекс для быстрого поиска правил. Строится в `StyleSheet::build_index`:
|
||
- `by_tag` / `by_class` / `by_id` / `by_tag_class` / `by_tag_id` — индексы для простых селекторов
|
||
- `complex_rules` — правила со сложными селекторами (всегда проверяются в лоб)
|
||
- `universal_rules` — правила с `*`
|
||
- `rule_specificities` — кеш специфичностей
|
||
- `epoch` — монотонно возрастающий счётчик для инвалидации кеша
|
||
|
||
---
|
||
|
||
## `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)
|
||
}
|
||
```
|
||
|
||
Кеш вычисленных стилей. Ключ — хеш от `type_name`, inline-свойств и `epoch`. При превышении `max_entries` кеш полностью очищается.
|
||
|
||
---
|
||
|
||
## `StyleSheet`
|
||
```rust
|
||
#[derive(Debug)]
|
||
pub struct StyleSheet {
|
||
rules: Vec<StyleRule>,
|
||
index: Option<StyleIndex>,
|
||
epoch: u64,
|
||
cache: Mutex<StyleCache>,
|
||
}
|
||
```
|
||
|
||
Главный тип модуля. Содержит список правил, опциональный индекс и кеш. Реализует `Clone` (с новым пустым кешем) и `Default`.
|
||
|
||
**Методы:**
|
||
|
||
- `StyleSheet::new() -> Self` — создаёт пустой лист.
|
||
- `pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>)` — добавляет правило. Пропускает селектор через `split_selectors` (поддержка групп через запятую). Сбрасывает `index = None`.
|
||
- `pub fn build_index(&mut self)` — перестраивает `StyleIndex`. Увеличивает `epoch`, очищает кеш. Для каждого правила:
|
||
- Вычисляет специфичность
|
||
- Если селектор простой (1 compound) — индексирует по tag/class/id/атрибутам
|
||
- Если сложный — помечает как `complex_rules`
|
||
- Универсальные (`*`) попадают в `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>>>` — использует индекс для быстрого поиска: собирает кандидатов из `universal_rules`, `by_tag`, `by_class`, `by_id`, `complex_rules`; фильтрует через `ComplexSelector::matches`; сортирует по специфичности.
|
||
- `pub fn matching_rules(...) -> Vec<&HashMap<String, String>>` — поиск подходящих правил. Пытается `query_index`; если индекса нет — линейный перебор всех `self.rules` с сортировкой.
|
||
- `pub fn matching_pseudo_rules(pseudo, ...) -> HashMap<String, String>` — ищет правила, содержащие указанный псевдокласс (например `:hover`). Использует `query_index_for_pseudo` или линейный перебор.
|
||
- `pub fn compute_cached(type_name, props, matched_sheets) -> ComputedStyle` — вычисляет итоговый стиль через кеш (обёртка над `StyleCache::get_or_compute`). Включает `PerfScope::new("style")`.
|
||
- `pub fn clear_cache(&self)` — очищает кеш.
|
||
- `pub fn is_empty(&self) -> bool` — `self.rules.is_empty()`.
|
||
|
||
**Методы с `#[cfg(feature = "parallel")]`:**
|
||
|
||
- `matching_rules_batch(type_names, el_ids, el_classes_list, ...) -> Vec<Vec<&HashMap<String, String>>>` — параллельный batch-поиск через `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>,
|
||
}
|
||
```
|
||
|
||
Итоговый вычисленный стиль элемента. Все поля — `Option`; отсутствующее свойство означает «не задано / наследуется от родителя».
|
||
|
||
### `ComputedStyle::compute()`
|
||
```rust
|
||
pub fn compute(
|
||
inline: &[(Cow<'_, str>, Cow<'_, str>)],
|
||
matched_sheets: &[&HashMap<String, String>],
|
||
) -> Self
|
||
```
|
||
|
||
Собирает стиль через `lookup()`: для каждого поля вызывается `lookup(key, inline, matched_sheets)`, затем парсится соответствующей функцией. Особенности:
|
||
- `padding`/`margin` — сначала ищутся индивидуальные (`-top`, `-right`, и т.д.), потом общие.
|
||
- `spacing` — альтернативное имя `gap`.
|
||
- `background` — сначала `background`, затем `background-color`.
|
||
- `overflow-x`/`overflow-y` — если индивидуальный не найден, применяется общий `overflow`.
|
||
- `flex_grow` — парсится как `f32`, кастуется в `u16`.
|
||
|
||
### `ComputedStyle::compute_batch()`
|
||
```rust
|
||
#[cfg(feature = "parallel")]
|
||
pub fn compute_batch<'a>(
|
||
pairs: &[(&[(Cow<'_, str>, Cow<'_, str>)], &[&'a HashMap<String, String>])],
|
||
) -> Vec<ComputedStyle>
|
||
```
|
||
Параллельный batch-вариант через `rayon::par_iter`.
|
||
|
||
### `ComputedStyle::apply_overrides()`
|
||
```rust
|
||
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>)
|
||
```
|
||
Применяет (перезаписывает) заданный набор свойств поверх существующего стиля. Используется для динамических изменений (например `:hover`-правила, inline-переопределения).
|
||
|
||
### Как работает `lookup()`
|
||
```rust
|
||
fn lookup<'a>(
|
||
key: &str,
|
||
inline: &'a [(Cow<'_, str>, Cow<'_, str>)],
|
||
matched_sheets: &[&'a HashMap<String, String>],
|
||
) -> Option<&'a str>
|
||
```
|
||
|
||
Порядок разрешения свойства:
|
||
1. **Inline-свойства** — перебор пар `(key, value)`. Поддерживает префикс `style:` (т.е. `style:color` эквивалентен `color`).
|
||
2. **matched_sheets** — список словарей от подходящих CSS-правил, отсортированный по специфичности. Перебирается с конца (последний — самый специфичный).
|
||
3. Возвращается первое найденное значение.
|
||
|
||
---
|
||
|
||
## Функции парсинга
|
||
|
||
| Функция | Сигнатура | Описание |
|
||
|---|---|---|
|
||
| `parse_size` | `(s: &str) -> Option<SizeValue>` | Парсит размер: `"10"` → `Px(10)`, `"50%"` → `Percent(50)`. `auto`, `fill`, `stretch` → `None` |
|
||
| `parse_color` | `(s: &str) -> Option<iced::Color>` | Парсит цвет: `#rgb`, `#rrggbb`, `#rrggbbaa`, имена (`white`, `black`, `transparent`) |
|
||
| `parse_length` | `(s: &str) -> Option<iced::Length>` | Парсит длину Iced: `"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>` | Число 0.0–1.0, clamp |
|
||
| `parse_font_weight` | `(s: &str) -> Option<u16>` | Имена: `normal`→400, `bold`→700, `lighter`→300, `bolder`→900; числовые значения |
|
||
| `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>
|
||
```
|
||
Удобная обёртка над `SizeValue::resolve`, возвращает `Option<f32>`.
|
||
|
||
---
|
||
|
||
## Использование в `renderer.rs`
|
||
|
||
Поля `ComputedStyle` активно используются в `/home/faynot/software/glint-runtime/src/renderer.rs`:
|
||
|
||
| Поле | Где используется |
|
||
|---|---|
|
||
| `.color` | Цвет текста в кнопках, текстовых полях, Label |
|
||
| `.background` | Фон контейнеров, кнопок, текстовых полей |
|
||
| `.padding*` | `iced::Padding` для кнопок, полей ввода, контейнеров |
|
||
| `.margin*` | Отступы вокруг элементов |
|
||
| `.border_radius`, `.border_width`, `.border_color` | Рамки кнопок, полей ввода, контейнеров |
|
||
| `.width`, `.height` | Размеры Scrollable, Column, Row, Image |
|
||
| `.min_width`, `.max_width`, `.min_height`, `.max_height` | Ограничения размеров |
|
||
| `.direction` | Направление флекса (Row/Column) |
|
||
| `.align_items` | Выравнивание дочерних элементов |
|
||
| `.content_align` | Выравнивание контента |
|
||
| `.flex_grow` | Flex-grow с `FillPortion` |
|
||
| `.spacing` | `iced::container::Style` spacing, gap в Row/Column |
|
||
| `.position` | Static / Fixed / Absolute / Sticky |
|
||
| `.top`, `.right`, `.bottom`, `.left` | Позиционирование |
|
||
| `.overflow_x`, `.overflow_y` | Скроллинг (`Scrollable`) |
|
||
| `.display` | `Display::None` — скрытие элемента |
|
||
| `.opacity` | Прозрачность |
|
||
| `.font_weight` | Вес шрифта в Text |
|
||
| `.line_height` | Межстрочный интервал |
|
||
| `.text_align` | Горизонтальное выравнивание текста |
|
||
| `.font_size` | Размер шрифта (передаётся от родителя) |
|
||
|
||
---
|
||
|
||
## `nth_matches(expr: &str, n: usize) -> bool`
|
||
|
||
Внутренняя функция для разрешения `:nth-child(an+b)`, `:nth-child(odd)`, `:nth-child(even)` и `:nth-child(<число>)`. Поддерживает отрицательные `a` и `b`.
|
||
|
||
---
|
||
|
||
## Связи с другими модулями
|
||
|
||
- `types.rs` — каждый `DomNode` (как элемент, так и текстовый узел) содержит `computed_style: ComputedStyle`.
|
||
- `renderer.rs` — импортирует `{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign, resolve_size}`.
|
||
- `mod.rs` — использует `AncestorInfo`, `ComputedStyle`, `StructuralContext` при обходе DOM.
|