Compare commits
43 Commits
06270a2126
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d42e7df8af | |||
|
|
e6523bd35b | ||
|
|
35f92435a3 | ||
|
|
938f05f59d | ||
|
|
b6fa841d11 | ||
|
|
28bd450787 | ||
|
|
906dde1569 | ||
|
|
144ac12107 | ||
|
|
469e87eaf3 | ||
|
|
1ede7d0f7f | ||
|
|
6f12c53b78 | ||
|
|
32beb81ac4 | ||
|
|
415de4d6e6 | ||
|
|
075533db6a | ||
|
|
37a0ea47be | ||
|
|
f51b32ecc3 | ||
|
|
e770d1a68a | ||
|
|
bdb5df2bf2 | ||
|
|
66b684e412 | ||
|
|
9ce650a803 | ||
|
|
ec73dcabb8 | ||
|
|
1f5dc7f2ca | ||
|
|
8784c189f5 | ||
|
|
09e3f5e75d | ||
|
|
4eec003939 | ||
|
|
a798e966f3 | ||
|
|
0d89ff68be | ||
|
|
daba2136c3 | ||
|
|
be6d5c36fc | ||
|
|
fd2863678b | ||
|
|
06aa67b23f | ||
|
|
43c4291ac8 | ||
|
|
cd2cf64e80 | ||
|
|
24fbd17f3e | ||
|
|
26ed1f900e | ||
|
|
bc45e85adf | ||
|
|
87feba394d | ||
|
|
4a6dea6dd8 | ||
|
|
d8d3f57a21 | ||
|
|
a46c9bc9e7 | ||
|
|
f3ea96258c | ||
|
|
30cf2e5e26 | ||
|
|
5597f07787 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
/target
|
||||
*.glbc
|
||||
STYLE-SYSTEM-COMPLETION-PLAN.md
|
||||
ARCHITECTURE-IMPROVEMENT-PLAN.md
|
||||
|
||||
70
ARCHITECTURE-IMPROVEMENT-PLAN.md
Normal file
70
ARCHITECTURE-IMPROVEMENT-PLAN.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# План улучшения архитектуры Glint Runtime — остаток
|
||||
|
||||
Всё, что не реализовано из исходного плана. Реализованные фазы (3-8, 10, 9.1-9.2) удалены.
|
||||
|
||||
---
|
||||
|
||||
## Фаза 0: Бенчмарки и профилирование
|
||||
|
||||
**Цель:** зафиксировать текущие метрики, чтобы объективно оценивать прогресс.
|
||||
|
||||
- [ ] **0.3** Запустить `perf record` / `flamegraph-rs` на горячем пути и выявить bottleneck
|
||||
- [ ] **0.4** Записать baseline в `BENCHMARKS.md` или `README.md`
|
||||
|
||||
---
|
||||
|
||||
## Фаза 1: Типизированные значения (Value enum)
|
||||
|
||||
**Цель:** устранить постоянный round-trip через строки (parse/format на каждое свойство).
|
||||
|
||||
**Статус:** `Value` enum, конверсии `Value↔Dynamic` и `resolve_string` с Value сделаны.
|
||||
**Осталось:** `HashMap<String, String>` в стилях не тронут — `ComputedStyle::compute()` и
|
||||
`parse_*()` всё ещё принимают `&str`. Стили — главный потребитель parse/format.
|
||||
|
||||
- [ ] **1.2** Заменить `HashMap<String, String>` на `HashMap<String, Value>` в стилях:
|
||||
- `StyleRule::properties`
|
||||
- Все matched_sheets
|
||||
- [ ] **1.8** Переписать `parse_color`, `parse_size`, `parse_length` на работу с `&Value`
|
||||
- [ ] **1.9** Написать тесты для Value: конверсии, сравнения, форматирование
|
||||
|
||||
---
|
||||
|
||||
## Фаза 2: Интернирование строк (String Interning)
|
||||
|
||||
**Цель:** ускорить сравнение строк (ключи свойств, названия типов, классы).
|
||||
Заменить миллион `== "padding-top"` на O(1) сравнение ID.
|
||||
|
||||
**Статус:** `InternedStr(u32)` и `Interner` определены в `types.rs`, но нигде не используются
|
||||
в горячем пути. `Element::type_name` — `&str`, ключи пропертей — `Cow<str>`,
|
||||
селекторы — `String`. Сравнения — через `==`.
|
||||
|
||||
- [ ] **2.2** Заменить `&'a str` на `InternedStr` в `Element::type_name` и ключах
|
||||
- [ ] **2.3** `lookup()` в `style.rs` — сравнение через ID вместо `==`
|
||||
- [ ] **2.4** `CompoundSelector::matches_element()` — сравнение через InternedStr
|
||||
- [ ] **2.5** `HashMap<String, String>` → `HashMap<InternedStr, Value>` где ключи повторяются
|
||||
|
||||
---
|
||||
|
||||
## Фаза 9.3: Кэш Iced-виджетов
|
||||
|
||||
**Цель:** не пересоздавать `iced::Element` для элементов с неизменившимся `content_hash`.
|
||||
|
||||
**Статус:** 9.1 (`content_hash` на `Element`) и 9.2 (стабильные `iced::widget::Id`) сделаны.
|
||||
9.3 отложен: `iced::Element` не `Clone`, хранит `Box<dyn Widget>`, кэширование требует
|
||||
unsafe transmute lifetime или перестройки renderer.
|
||||
|
||||
- [ ] **9.3** Реализовать кэш `HashMap<ElementId, (u64, iced::Element)>` в `GlintApp`:
|
||||
- При совпадении `content_hash` — возвращать сохранённый виджет
|
||||
- Иначе — рендерить, кэшировать, обновлять hash
|
||||
- Unsafe transmute `'static → '_` допустим, т.к. данные живут в `Element<'static>`
|
||||
|
||||
---
|
||||
|
||||
## Сводная таблица — остаток
|
||||
|
||||
| Фаза | Описание | Оценка ускорения |
|
||||
|------|----------|-----------------|
|
||||
| 0 | Профилирование | — |
|
||||
| 1 | Value enum в стилях | 2-3× |
|
||||
| 2 | String interning | 1.5-2× |
|
||||
| 9.3 | Кэш виджетов | 1.5-2× (render) |
|
||||
144
Cargo.lock
generated
144
Cargo.lock
generated
@@ -108,6 +108,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
@@ -583,6 +589,12 @@ dependencies = [
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
@@ -629,6 +641,33 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
@@ -910,6 +949,42 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
@@ -1474,13 +1549,16 @@ checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
|
||||
name = "glint-runtime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored",
|
||||
"compact_str",
|
||||
"criterion",
|
||||
"glt",
|
||||
"iced",
|
||||
"indicatif",
|
||||
"rayon",
|
||||
"regex",
|
||||
"rhai",
|
||||
"rustc-hash 2.1.2",
|
||||
@@ -1947,12 +2025,32 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -2868,6 +2966,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.54"
|
||||
@@ -3014,6 +3118,34 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
@@ -3239,7 +3371,7 @@ dependencies = [
|
||||
"built",
|
||||
"cfg-if",
|
||||
"interpolate_name",
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"libfuzzer-sys",
|
||||
"log",
|
||||
@@ -4049,6 +4181,16 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
|
||||
13
Cargo.toml
13
Cargo.toml
@@ -14,3 +14,16 @@ rhai = "1.17.1"
|
||||
rustc-hash = "2"
|
||||
chrono = "0.4.44"
|
||||
compact_str = "0.8"
|
||||
bumpalo = "3"
|
||||
rayon = { version = "1.10", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
parallel = ["rayon"]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
harness = false
|
||||
|
||||
@@ -1,570 +0,0 @@
|
||||
# План завершения стилевой системы Glint
|
||||
|
||||
## Цель
|
||||
|
||||
Достичь полной мощности HTML+CSS в языке Glint. Система разделена на 4 уровня:
|
||||
- **Парсер** (glt/src/style_parser.rs) — чтение `.glts`
|
||||
- **AST + Компилятор** (glt/src/ast.rs, compiler.rs) — представление и байткод
|
||||
- **Интерпретатор** (runtime/src/interpreter/) — VDOM + вычисление стилей
|
||||
- **Рендерер** (runtime/src/renderer.rs) — отрисовка через Iced
|
||||
|
||||
---
|
||||
|
||||
## 1. Селекторы (критично)
|
||||
|
||||
### 1.1 Структурированное представление селектора
|
||||
|
||||
**Сейчас:** Селектор — плоская строка `"Panel.desktop"`, matching по точному совпадению.
|
||||
|
||||
**Нужно:** Разобрать селектор в структуру:
|
||||
|
||||
```rust
|
||||
enum Selector {
|
||||
Simple(SimpleSelector),
|
||||
Compound(CompoundSelector), // без комбинатора
|
||||
Complex {
|
||||
left: Box<Selector>,
|
||||
combinator: Combinator,
|
||||
right: Box<Selector>,
|
||||
},
|
||||
List(Vec<Selector>), // запятые: `Button, .btn, #id`
|
||||
}
|
||||
|
||||
struct SimpleSelector {
|
||||
tag: Option<String>, // `Button` | `*`
|
||||
id: Option<String>, // `#myid`
|
||||
classes: Vec<String>, // `.foo.bar`
|
||||
pseudo_classes: Vec<PseudoClass>, // `:hover`
|
||||
pseudo_element: Option<PseudoElement>, // `::before`
|
||||
attribute: Vec<AttributeSelector>, // `[disabled]`, `[type="text"]`
|
||||
}
|
||||
|
||||
enum Combinator {
|
||||
Descendant, // пробел
|
||||
Child, // >
|
||||
NextSibling, // +
|
||||
Subsequent, // ~
|
||||
}
|
||||
|
||||
enum PseudoClass {
|
||||
Hover, Active, Focus,
|
||||
FirstChild, LastChild,
|
||||
NthChild(i32, i32), // An+B
|
||||
FirstOfType, LastOfType,
|
||||
Disabled, Enabled, Checked,
|
||||
Root, Empty,
|
||||
Not(Box<Selector>),
|
||||
// ...
|
||||
}
|
||||
|
||||
struct AttributeSelector {
|
||||
name: String,
|
||||
op: AttrOp,
|
||||
value: Option<String>,
|
||||
}
|
||||
|
||||
enum AttrOp { Set, Eq, Contain, StartsWith, EndsWith, InList }
|
||||
```
|
||||
|
||||
### 1.2 Парсинг селекторов (style_parser.rs)
|
||||
|
||||
- Заменить `parse_selector()` на полноценный парсер селекторов.
|
||||
- Поддержка: `Button.active:hover`, `Panel > .btn`, `#header`, `[type="text"]`, `*`, `:nth-child(2n+1)`, запятые.
|
||||
- Валидация и сообщения об ошибках.
|
||||
|
||||
### 1.3 Байткод для селекторов
|
||||
|
||||
- Новый опкод `OP_SELECTOR` или расширение `OP_STYLE_RULE`:
|
||||
- Записать дерево селектора в байткод.
|
||||
- На каждый тип узла — свой подопкод (0x30–0x3F).
|
||||
- Компилятор: сериализация структуры селектора.
|
||||
- Читатель (reader.rs): десериализация обратно в структуру.
|
||||
|
||||
### 1.4 Runtime matching (interpreter/mod.rs)
|
||||
|
||||
- Заменить `collect_matching_styles()` на алгоритм обхода дерева селекторов.
|
||||
- **Специфичность:** inline > id > class/attr/pseudo > tag.
|
||||
- При одинаковой специфичности — порядок объявления (последний побеждает).
|
||||
- **Комбинаторы:** при обходе VDOM подниматься вверх по предкам/соседям.
|
||||
- Кэширование результатов matching для производительности.
|
||||
|
||||
---
|
||||
|
||||
## 2. Псевдоклассы (высокий приоритет)
|
||||
|
||||
### 2.1 Интерактивные (`:hover`, `:active`, `:focus`)
|
||||
|
||||
**Сейчас:** Только у Button ховер/нажатие — хардкод в renderer.
|
||||
|
||||
**Нужно:**
|
||||
- Хранить состояние интерактивности в VDOM/Element (`ElementState`).
|
||||
- В renderer передавать статус (hovered/pressed/focused) в `collect_matching_styles`.
|
||||
- `:hover`-стили переопределяют базовые.
|
||||
- Плавный переход между состояниями (см. transitions).
|
||||
|
||||
**Изменения:**
|
||||
- `types.rs`: `Element` может получить поле `state: ElementState`.
|
||||
- `renderer.rs`: у `render_element` появляется доступ к состоянию мыши.
|
||||
- `style.rs`: `ComputedStyle::compute()` принимает псевдоклассы.
|
||||
|
||||
### 2.2 Структурные (`:first-child`, `:nth-child`, `:last-child`, `:first-of-type`, `:last-of-type`, `:empty`, `:root`)
|
||||
|
||||
**Сейчас:** Не поддерживаются.
|
||||
|
||||
**Нужно:**
|
||||
- При matching вычислять позицию элемента среди siblings.
|
||||
- Для `:nth-child(An+B)` — парсить выражение и проверять `(index - B) % A == 0`.
|
||||
- `:empty` — `el.children.is_empty()`.
|
||||
- `:root` — элемент верхнего уровня в VDOM.
|
||||
|
||||
### 2.3 Состояния (`:disabled`, `:enabled`, `:checked`)
|
||||
|
||||
- `:disabled`/`:enabled` — по свойству `disabled`.
|
||||
- `:checked` — по свойству `checked`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Медиа-запросы (`@media`) (средний приоритет)
|
||||
|
||||
### 3.1 Парсер
|
||||
|
||||
```glts
|
||||
@media (max-width: 600px) {
|
||||
Panel { direction: vertical }
|
||||
}
|
||||
```
|
||||
|
||||
- Парсить `@media` в `Directive::MediaQuery { query, child_span }`.
|
||||
- Поддержка: `width`, `height`, `min-width`, `max-width`, `orientation`, `prefers-color-scheme`.
|
||||
- Логические операторы: `and`, `or`, `not`, `,` (or).
|
||||
|
||||
### 3.2 Runtime
|
||||
|
||||
- Собрать информацию об окне (размер, тема ОС).
|
||||
- При изменении размера окна переоценивать media queries.
|
||||
- Хранить в `Document::media_state: MediaState`.
|
||||
- `collect_matching_styles` проверяет media query перед добавлением правил.
|
||||
|
||||
---
|
||||
|
||||
## 4. Анимации (`@anim`) (средний приоритет)
|
||||
|
||||
### 4.1 Парсинг — уже есть
|
||||
|
||||
`@anim name { from { ... } to { ... } }` — парсится и компилируется.
|
||||
|
||||
### 4.2 Runtime — НЕ реализован
|
||||
|
||||
**Сейчас:** `OP_STYLE_ANIM` пропускается.
|
||||
|
||||
**Нужно:**
|
||||
1. Хранить `KeyframeAnimation` в `StyleSheet` (не только в байткоде, но и в runtime-структуре).
|
||||
2. В `Element` поле `animation_state: HashMap<String, AnimationInstance>`.
|
||||
3. Система тиков:
|
||||
- Iced `subscription` на каждый кадр (`on_every_event` → `Event::Window(RedrawRequested)`).
|
||||
- При каждом тике обновлять `elapsed` для активных анимаций.
|
||||
- Интерполировать между keyframes.
|
||||
4. Свойства `animation-name`, `animation-duration`, `animation-timing-function`, `animation-delay`, `animation-iteration-count`, `animation-fill-mode`.
|
||||
5. Поддержка `@keyframes` с процентами: `0% { ... } 50% { ... } 100% { ... }`.
|
||||
|
||||
### 4.3 Timing functions
|
||||
|
||||
- `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out`.
|
||||
- `cubic-bezier(p1x, p1y, p2x, p2y)`.
|
||||
- `steps(n, direction)`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Транзишены (`transition`) (средний приоритет)
|
||||
|
||||
### 5.1 Свойства
|
||||
|
||||
- `transition-property`, `transition-duration`, `transition-timing-function`, `transition-delay`.
|
||||
- Шорткат `transition: all 0.3s ease`.
|
||||
|
||||
### 5.2 Runtime
|
||||
|
||||
- При изменении `ComputedStyle` (например, при ховере) не применять мгновенно, а запускать интерполяцию.
|
||||
- Хранить `TransitionState` на элемент: `HashMap<String, (start_value, end_value, elapsed, duration, easing)>`.
|
||||
- Каждый кадр обновлять transitioning-свойства.
|
||||
|
||||
---
|
||||
|
||||
## 6. Новые CSS-свойства в ComputedStyle
|
||||
|
||||
### 6.1 Типографика
|
||||
|
||||
| Свойство | Тип | Парсер |
|
||||
|----------|-----|--------|
|
||||
| `font-family` | `Option<String>` | список шрифтов |
|
||||
| `font-weight` | `Option<u16>` | `normal(400)`, `bold(700)`, числовое |
|
||||
| `font-style` | `Option<FontStyle>` | `normal`, `italic`, `oblique` |
|
||||
| `line-height` | `Option<f32>` | числовое или `normal` |
|
||||
| `letter-spacing` | `Option<f32>` | px |
|
||||
| `text-align` | `Option<TextAlign>` | `left`, `center`, `right`, `justify` |
|
||||
| `text-decoration` | `Option<TextDecoration>` | `none`, `underline`, `line-through` |
|
||||
| `text-transform` | `Option<TextTransform>` | `none`, `uppercase`, `lowercase`, `capitalize` |
|
||||
| `white-space` | `Option<WhiteSpace>` | `normal`, `nowrap`, `pre` |
|
||||
| `word-break` | `Option<WordBreak>` | `normal`, `break-all`, `keep-all` |
|
||||
| `text-overflow` | `Option<TextOverflow>` | `clip`, `ellipsis` |
|
||||
|
||||
### 6.2 Фон и границы
|
||||
|
||||
| Свойство | Тип | Парсер |
|
||||
|----------|-----|--------|
|
||||
| `opacity` | `Option<f32>` | 0.0–1.0 |
|
||||
| `background-image` | `Option<String>` | `url(...)` |
|
||||
| `background-repeat` | `Option<BgRepeat>` | `repeat`, `no-repeat` |
|
||||
| `background-size` | `Option<BgSize>` | `cover`, `contain`, px, % |
|
||||
| `background-position` | `Option<BgPos>` | `center`, `top left`, px |
|
||||
| `linear-gradient(...)` | `Option<Gradient>` | парсить в `Value::Call` и обрабатывать |
|
||||
| `box-shadow` | `Option<Vec<Shadow>>` | `offset-x offset-y blur spread color` |
|
||||
| `text-shadow` | `Option<Vec<Shadow>>` | то же |
|
||||
| `outline` | `Option<Outline>` | width, style, color |
|
||||
| `border-style` | `Option<BorderStyle>` | `solid`, `dashed`, `dotted` |
|
||||
| `border` (шорткат) | — | разворачивать в width/style/color |
|
||||
|
||||
### 6.3 Flexbox (расширение)
|
||||
|
||||
| Свойство | Тип |
|
||||
|----------|-----|
|
||||
| `justify-content` | `Option<JustifyContent>` | `start`, `center`, `end`, `space-between`, `space-around`, `space-evenly` |
|
||||
| `flex-wrap` | `Option<FlexWrap>` | `nowrap`, `wrap`, `wrap-reverse` |
|
||||
| `flex-direction` | `Option<FlexDirection>` | (расширение `LayoutDirection`) |
|
||||
| `align-self` | `Option<Alignment>` | переопределяет `align-items` для конкретного элемента |
|
||||
| `align-content` | `Option<AlignContent>` | multi-line выравнивание |
|
||||
| `flex` (шорткат) | — | grow, shrink, basis |
|
||||
| `flex-shrink` | `Option<f32>` | |
|
||||
| `flex-basis` | `Option<Length>` | |
|
||||
|
||||
### 6.4 Grid Layout
|
||||
|
||||
| Свойство | Тип |
|
||||
|----------|-----|
|
||||
| `grid-template-columns` | `Option<Vec<GridTrack>>` | `1fr 1fr`, `repeat(3, 1fr)`, `auto` |
|
||||
| `grid-template-rows` | `Option<Vec<GridTrack>>` | |
|
||||
| `grid-gap` / `gap` | уже есть как `spacing` | |
|
||||
| `grid-column` | `Option<(u32, u32)>` | start / end |
|
||||
| `grid-row` | `Option<(u32, u32)>` | |
|
||||
| `grid-auto-flow` | `Option<GridFlow>` | |
|
||||
|
||||
### 6.5 Позиционирование (расширение)
|
||||
|
||||
| Свойство | Тип | Статус |
|
||||
|----------|-----|--------|
|
||||
| `position: absolute` | — | сейчас мэппится в Static |
|
||||
| `position: relative` | — | сейчас мэппится в Static |
|
||||
| `position: sticky` | — | сейчас мэппится в Static |
|
||||
| `z-index` | `Option<i32>` | |
|
||||
| `display` | `Option<Display>` | `block`, `flex`, `grid`, `none`, `inline` |
|
||||
|
||||
### 6.6 Прочее
|
||||
|
||||
| Свойство | Тип |
|
||||
|----------|-----|
|
||||
| `visibility` | `Option<Visibility>` | `visible`, `hidden` |
|
||||
| `cursor` | `Option<Cursor>` | `pointer`, `default`, `text` |
|
||||
| `pointer-events` | `Option<PointerEvents>` | `auto`, `none` |
|
||||
| `transform` | `Option<Vec<Transform>>` | `translate(x,y)`, `scale(s)`, `rotate(a)` |
|
||||
| `transform-origin` | `Option<String>` | |
|
||||
| `backdrop-filter` | `Option<Vec<Filter>>` | `blur(10px)`, `brightness(1.2)` |
|
||||
| `filter` | `Option<Vec<Filter>>` | то же |
|
||||
| `list-style` | `Option<ListStyle>` | |
|
||||
| `isolation` | `Option<Isolation>` | `auto`, `isolate` |
|
||||
| `mix-blend-mode` | `Option<BlendMode>` | `multiply`, `screen` |
|
||||
|
||||
### 6.7 Итого: ~40–60 новых полей в ComputedStyle
|
||||
|
||||
Каждое поле: тип `Option<T>`, значение по умолчанию `None`.
|
||||
Парсинг: новая функция `parse_<property>(s: &str) -> Option<T>` для каждого.
|
||||
Lookup в `ComputedStyle::compute()`.
|
||||
|
||||
**Рендеринг в Iced:**
|
||||
- Iced 0.14 поддерживает Background::Gradient.
|
||||
- Box-shadow пока не поддерживается, но можно эмулировать через container style с offset-тенью.
|
||||
- Transforms и filters — Iced пока не нативно; нужен кастомный widget или пропуск.
|
||||
|
||||
---
|
||||
|
||||
## 7. Система каскада и наследования
|
||||
|
||||
### 7.1 Наследование (сейчас: только `color`, `font-size`)
|
||||
|
||||
Добавить наследование для:
|
||||
- `font-family`, `font-weight`, `font-style`, `line-height`, `letter-spacing`, `text-align`, `white-space`, `word-break`, `text-transform`, `visibility`, `cursor`, `pointer-events`.
|
||||
|
||||
Механизм: в `render_element()` передавать не только `parent_color` и `parent_font_size`, а `&ComputedStyle` родителя или отдельный `InheritedStyle`.
|
||||
|
||||
### 7.2 `inherit` / `initial` / `unset`
|
||||
|
||||
- Специальные значения для любого свойства.
|
||||
|
||||
### 7.3 `!important`
|
||||
|
||||
- Флаг важности в правиле. Переопределяет специфичность.
|
||||
- Парсить `value!important`.
|
||||
|
||||
---
|
||||
|
||||
## 8. CSS-функции и значения
|
||||
|
||||
### 8.1 `calc()`
|
||||
|
||||
```rust
|
||||
enum CalcValue {
|
||||
Number(f32),
|
||||
Percentage(f32),
|
||||
Add(Box<CalcValue>, Box<CalcValue>),
|
||||
Sub(Box<CalcValue>, Box<CalcValue>),
|
||||
Mul(Box<CalcValue>, Box<CalcValue>),
|
||||
Div(Box<CalcValue>, Box<CalcValue>),
|
||||
Var(String),
|
||||
}
|
||||
```
|
||||
|
||||
- Парсить `calc(100% - 20px)`.
|
||||
- Вычислять в runtime с учётом контекста (размер родителя).
|
||||
|
||||
### 8.2 `var()` — CSS custom properties
|
||||
|
||||
- `--my-var: value;` в правилах.
|
||||
- `var(--my-var, fallback)` при использовании.
|
||||
- Хранить custom properties в `ComputedStyle.custom_props: HashMap<String, String>`.
|
||||
- Наследуются по умолчанию.
|
||||
|
||||
### 8.3 `min()`, `max()`, `clamp()`
|
||||
|
||||
- `min(100%, 500px)`, `max(200px, 50%)`, `clamp(200px, 50%, 500px)`.
|
||||
|
||||
### 8.4 `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `oklch()`, `color-mix()`
|
||||
|
||||
- Сейчас только `#hex` и 3 named colors.
|
||||
- Парсить `rgb(255, 0, 0)`, `rgba(255, 0, 0, 0.5)`.
|
||||
- Расширить `parse_color()`.
|
||||
|
||||
### 8.5 Named colors
|
||||
|
||||
- Расширить словарь до 140+ CSS named colors (AliceBlue, ...).
|
||||
|
||||
---
|
||||
|
||||
## 9. @-правила
|
||||
|
||||
### 9.1 `@import`
|
||||
|
||||
```glts
|
||||
@import "theme.glts"
|
||||
```
|
||||
|
||||
- В парсере: обработать `@import`, загрузить файл, влить переменные и миксины.
|
||||
- Может быть сложным (циклы, кэширование). Опционально на раннем этапе.
|
||||
|
||||
### 9.2 `@font-face`
|
||||
|
||||
```glts
|
||||
@font-face {
|
||||
font-family: "MyFont"
|
||||
src: fs:/fonts/myfont.ttf
|
||||
}
|
||||
```
|
||||
|
||||
- Регистрировать в Iced через `font::Family`.
|
||||
- Потребует `iced::font::load()`.
|
||||
|
||||
### 9.3 `@scope` (CSS Cascading Layers)
|
||||
|
||||
- `@scope (.card) { ... }` — ограничение области действия правил.
|
||||
|
||||
---
|
||||
|
||||
## 10. Рендеринг (renderer.rs)
|
||||
|
||||
### 10.1 Псевдоклассы в рендере
|
||||
|
||||
- Для интерактивных элементов передавать `button::Status`.
|
||||
- `:hover`/`:active`-стили должны пересчитываться при изменении статуса.
|
||||
|
||||
### 10.2 Flexbox (полноценный)
|
||||
|
||||
- `justify-content`: Iced `Row`/`Column` не имеют нативного justifyContent; эмулировать через наполнители (`Length::Fill`).
|
||||
- `flex-wrap`: нужен `Flow` widget или эмуляция строками.
|
||||
|
||||
### 10.3 Grid
|
||||
|
||||
- Полноценный CSS Grid: имплементировать кастомный Iced widget или разбивать на строки/колонки вручную.
|
||||
- `grid-template-columns: 1fr 1fr 1fr` → разбивка children на ряды.
|
||||
- `grid-column`/`grid-row`: позиционирование ячеек.
|
||||
|
||||
### 10.4 `display: none`
|
||||
|
||||
- Пропускать элемент при рендеринге (уже частично — `None` возвращается).
|
||||
|
||||
### 10.5 `z-index`
|
||||
|
||||
- Сортировать fixed-слой по z-index внутри stack.
|
||||
|
||||
### 10.6 `position: absolute` / `relative`
|
||||
|
||||
- `relative`: смещение через padding контейнера.
|
||||
- `absolute`: позиционирование относительно предка с `position: relative` (или окна). Эмулировать через stack слои, как `fixed`.
|
||||
|
||||
### 10.7 `position: sticky`
|
||||
|
||||
- Эмулировать через Iced scrollable + перехват событий скролла.
|
||||
- Пока не поддерживается Iced нативно. Можно через subscription на scroll position + ручное позиционирование.
|
||||
|
||||
### 10.8 `opacity`
|
||||
|
||||
- Iced: `widget.opacity(f32)` (iced 0.14+).
|
||||
|
||||
### 10.9 `box-shadow`, `text-shadow`
|
||||
|
||||
- Эмуляция: container с подложкой (offset background) или кастомный шейдер.
|
||||
- Iced 0.14 имеет `Border` только с `color`, `width`, `radius`. Shadow — нет.
|
||||
|
||||
### 10.10 `outline`
|
||||
|
||||
- Через border с `outline-offset` эмуляцией.
|
||||
|
||||
### 10.11 Трансформы (`transform`)
|
||||
|
||||
- Iced не поддерживает transforms нативно.
|
||||
- Потребуется кастомный widget с `lyon` или `vello` для path-трансформаций.
|
||||
- На раннем этапе — документировать как unsupported.
|
||||
|
||||
---
|
||||
|
||||
## 11. Инструментарий разработчика
|
||||
|
||||
### 11.1 Вывод вычисленных стилей
|
||||
|
||||
- Флаг `--debug-styles` в CLI: печатать `ComputedStyle` каждого элемента.
|
||||
|
||||
### 11.2 Инспектор элементов
|
||||
|
||||
- Iced debug overlay: `iced::widget::pane_grid` с инспектором.
|
||||
- Показывать matched selectors, specificity, computed properties.
|
||||
|
||||
### 11.3 Hot-reload стилей
|
||||
|
||||
- Перекомпиляция `.glts` без перезапуска приложения.
|
||||
- Обновление bytecode в runtime.
|
||||
|
||||
---
|
||||
|
||||
## 12. Порядок реализации (приоритеты)
|
||||
|
||||
### Фаза 1: Критическое (MVP+)
|
||||
1. Структурированные селекторы + matching engine со специфичностью
|
||||
2. Комбинаторы (особенно descendant)
|
||||
3. Псевдоклассы `:hover`, `:active`, `:focus`
|
||||
4. Селектор по ID (`#id`)
|
||||
5. Множественные селекторы через запятую
|
||||
6. Псевдоклассы `:first-child`, `:nth-child`, `:last-child`, `:empty`
|
||||
|
||||
### Фаза 2: Визуальное обогащение
|
||||
7. Новые свойства: opacity, font-weight, text-align, justify-content, line-height
|
||||
8. fill-шорткаты для flex (flex-grow/shrink/basis)
|
||||
9. `position: absolute` / `relative` / `sticky`
|
||||
10. `z-index`
|
||||
11. `display: none`
|
||||
12. `calc()`, `min()`, `max()`, `clamp()`
|
||||
13. `rgb()`, `rgba()`, `hsl()` + расширенный словарь named colors
|
||||
14. `!important`
|
||||
|
||||
### Фаза 3: Продвинутое
|
||||
15. `@media` queries
|
||||
16. Animations (playback)
|
||||
17. Transitions
|
||||
18. `box-shadow`, `text-shadow` (эмуляция)
|
||||
19. CSS Grid
|
||||
20. `flex-wrap` + `justify-content` полноценно
|
||||
21. `var()` CSS custom properties
|
||||
22. `@import`
|
||||
23. `@font-face`
|
||||
|
||||
### Фаза 4: Экспертное
|
||||
24. `transform`, `transform-origin`
|
||||
25. `backdrop-filter`, `filter`
|
||||
26. `gradient` (linear, radial)
|
||||
27. `color-mix()`, `oklch()`
|
||||
28. Hot-reload
|
||||
29. DevTools инспектор
|
||||
30. `@scope`
|
||||
|
||||
---
|
||||
|
||||
## 13. Архитектурные изменения
|
||||
|
||||
### 13.1 `ComputedStyle` → разбить на подс-труктуры
|
||||
|
||||
Вместо 40+ плоских полей:
|
||||
|
||||
```rust
|
||||
pub struct ComputedStyle {
|
||||
pub box_model: BoxModel,
|
||||
pub typography: Typography,
|
||||
pub background: BackgroundStyle,
|
||||
pub border: BorderStyle,
|
||||
pub flex: FlexStyle,
|
||||
pub grid: GridStyle,
|
||||
pub position: Positioning,
|
||||
pub effects: Effects,
|
||||
pub overflow: OverflowStyle,
|
||||
pub animation: AnimationStyle,
|
||||
pub custom: HashMap<String, String>,
|
||||
pub display: Option<Display>,
|
||||
pub visibility: Option<Visibility>,
|
||||
}
|
||||
```
|
||||
|
||||
### 13.2 Selector engine — отдельный модуль
|
||||
|
||||
```rust
|
||||
mod selector {
|
||||
pub struct Selector { ... }
|
||||
pub fn parse(input: &str) -> Result<Selector, String>;
|
||||
pub fn specificity(&self) -> (u32, u32, u32);
|
||||
pub fn matches(&self, el: &Element, ctx: &MatchContext) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
### 13.3 StyleSheet — расширение
|
||||
|
||||
```rust
|
||||
pub struct StyleRule {
|
||||
pub selector: Selector,
|
||||
pub properties: HashMap<String, String>,
|
||||
pub source_order: u32,
|
||||
pub media: Option<MediaQuery>,
|
||||
pub important: HashSet<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 13.4 Система тиков для анимаций/транзишенов
|
||||
|
||||
- Новый модуль `animation` в runtime.
|
||||
- `AnimationEngine` с `HashMap<AnimationId, AnimationInstance>`.
|
||||
- Iced subscription: `time::every(Duration::from_millis(16))` (60fps).
|
||||
|
||||
---
|
||||
|
||||
## 14. Оценка сложности
|
||||
|
||||
| Компонент | Новые файлы | Изменённые файлы | Пример LOC |
|
||||
|-----------|-------------|------------------|------------|
|
||||
| Selector parser | `glt/src/selector.rs` | `style_parser.rs` | ~500 |
|
||||
| Selector matching | `runtime/src/selector.rs` | `mod.rs` | ~400 |
|
||||
| ComputedStyle расширение | — | `style.rs` | +400 |
|
||||
| Медиа-запросы | — | `style_parser.rs`, `mod.rs` | ~300 |
|
||||
| Анимации | `runtime/src/animation.rs` | `style.rs`, `mod.rs`, `app.rs` | ~500 |
|
||||
| Транзишены | `runtime/src/transition.rs` | | ~400 |
|
||||
| Псевдоклассы | — | `mod.rs`, `renderer.rs`, `types.rs` | ~300 |
|
||||
| Новые парсеры свойств | — | `style.rs` | +600 |
|
||||
| Рендеринг | — | `renderer.rs` | +500 |
|
||||
| Flex/Grid | `runtime/src/layout.rs` | `renderer.rs` | ~400 |
|
||||
| calc/var | `runtime/src/values.rs` | `style.rs` | ~300 |
|
||||
| @import/@font-face | — | `style_parser.rs`, `renderer.rs` | ~200 |
|
||||
| Инструменты | `runtime/src/inspector.rs` | `cli.rs`, `app.rs` | ~300 |
|
||||
|
||||
**Итого:** ~5–7 новых файлов, ~15 изменённых, ~5000–6000 строк нового кода.
|
||||
99
benches/bench.rs
Normal file
99
benches/bench.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn bench_matching_rules(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("matching_rules");
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function("100_rules", |b| {
|
||||
b.iter(|| {
|
||||
let mut stylesheet = glint_runtime::interpreter::style::StyleSheet::new();
|
||||
for i in 0..100 {
|
||||
stylesheet.add_rule(
|
||||
format!(".class-{}", i),
|
||||
std::collections::HashMap::from([("color".into(), "red".into())]),
|
||||
);
|
||||
}
|
||||
stylesheet.build_index();
|
||||
let _ = stylesheet.matching_rules(
|
||||
"Button",
|
||||
None,
|
||||
&["class-1"],
|
||||
&[],
|
||||
&Default::default(),
|
||||
&[],
|
||||
&[],
|
||||
&std::collections::HashMap::new(),
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_style_compute(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("computed_style");
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function("compute_empty", |b| {
|
||||
b.iter(|| {
|
||||
let _ = glint_runtime::interpreter::style::ComputedStyle::compute(
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function("compute_5_props", |b| {
|
||||
let props: Vec<(std::borrow::Cow<'_, str>, std::borrow::Cow<'_, str>)> = vec![
|
||||
("color".into(), "red".into()),
|
||||
("padding".into(), "10px".into()),
|
||||
("font-size".into(), "16px".into()),
|
||||
("background".into(), "#fff".into()),
|
||||
("width".into(), "100px".into()),
|
||||
];
|
||||
b.iter(|| {
|
||||
let _ = glint_runtime::interpreter::style::ComputedStyle::compute(
|
||||
&props,
|
||||
&[],
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_resolve_string(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("resolve_string");
|
||||
group.sample_size(10);
|
||||
|
||||
let vars = std::collections::HashMap::from([
|
||||
("name".to_string(), glint_runtime::interpreter::Value::Str("world".into())),
|
||||
("count".to_string(), glint_runtime::interpreter::Value::Int(42)),
|
||||
]);
|
||||
|
||||
group.bench_function("no_vars", |b| {
|
||||
b.iter(|| {
|
||||
use glint_runtime::interpreter::Interpreter;
|
||||
let _ = Interpreter::resolve_string("hello world", &vars);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function("1_var", |b| {
|
||||
b.iter(|| {
|
||||
use glint_runtime::interpreter::Interpreter;
|
||||
let _ = Interpreter::resolve_string("Hello $name!", &vars);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function("3_vars", |b| {
|
||||
b.iter(|| {
|
||||
use glint_runtime::interpreter::Interpreter;
|
||||
let _ = Interpreter::resolve_string("$name has count $count and is $name", &vars);
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_matching_rules, bench_style_compute, bench_resolve_string);
|
||||
criterion_main!(benches);
|
||||
BIN
desktop.glbc
BIN
desktop.glbc
Binary file not shown.
BIN
display.glbc
BIN
display.glbc
Binary file not shown.
47
docs/en/README.md
Normal file
47
docs/en/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Glint Runtime — Documentation
|
||||
|
||||
Backend for executing compiled bytecode (.glbc) of the Glint UI framework. Loads bytecode, interprets VDOM, applies CSS-like styles, executes Rhai scripts, and renders the result via Iced (native GPU-accelerated GUI).
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/main.rs` — entry point, CLI parsing
|
||||
- `src/lib.rs` — public API of the crate
|
||||
- `src/app.rs` — GlintApp: Iced application, update/view
|
||||
- `src/cli.rs` — CLI commands compile/run
|
||||
- `src/renderer.rs` — Iced widgets: Element → iced::Element conversion
|
||||
- `src/perf.rs` — PerfScope: performance measurement by phases
|
||||
- `src/interpreter/mod.rs` — Interpreter: bytecode loading, VDOM eval
|
||||
- `src/interpreter/types.rs` — Element, VNode, FlatVDom, Value, InternedStr, Document
|
||||
- `src/interpreter/style.rs` — StyleSheet, StyleRule, StyleIndex, StyleCache, ComputedStyle
|
||||
- `src/interpreter/reactive.rs` — ReactiveTracker: dependency graph
|
||||
- `src/interpreter/rhei.rs` — RheiContext: Rhai script integration
|
||||
- `src/interpreter/reader.rs` — Reader: .glbc bytecode reading
|
||||
- `src/interpreter/opcodes.rs` — OP_* bytecode constants
|
||||
|
||||
## Related Repositories
|
||||
|
||||
- `glt` (compiler .gltm/.glts → .glbc)
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Document** — loaded .glbc file: Element tree, style sheet, variables
|
||||
- **Element** — VDOM node: type_name, properties, children, computed_style, element_id, content_hash
|
||||
- **VDOM** — virtual tree, result of bytecode interpretation
|
||||
- **StyleSheet** — style sheet with indexed lookup and computed style cache
|
||||
- **ReactiveTracker** — tracks element → variable dependencies, dirty_set
|
||||
- **RheiContext** — compiles and executes Rhai scripts, caches AST
|
||||
- **ComputedStyle** — result of applying CSS rules: ~40 fields (color, padding, font-size, etc.)
|
||||
|
||||
## Optimization Phases
|
||||
|
||||
See [ARCHITECTURE-IMPROVEMENT-PLAN.md](../ARCHITECTURE-IMPROVEMENT-PLAN.md).
|
||||
|
||||
**Implemented:** phases 3–8, 10, 9.1–9.2 (content_hash, stable Iced widget IDs).
|
||||
|
||||
**Remaining:**
|
||||
| Phase | Description | Estimated Speedup |
|
||||
|-------|-------------|-------------------|
|
||||
| 0 | Profiling (bench, flamegraph) | — |
|
||||
| 1 | Typed Values in styles instead of HashMap<String, String> | 2–3× |
|
||||
| 2 | String interning for hot paths | 1.5–2× |
|
||||
| 9.3 | Iced widget cache by content_hash | 1.5–2× (render) |
|
||||
303
docs/en/architecture/01-data-flow.md
Normal file
303
docs/en/architecture/01-data-flow.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Data Flow in Glint Runtime
|
||||
|
||||
How source code turns into pixels on the screen.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "Compilation"
|
||||
A1[".gltm markup"] --> P["Parser: glt crate"]
|
||||
A2[".glts style"] --> P
|
||||
P --> M["ModuleSoA — flat arrays"]
|
||||
M --> AST
|
||||
AST --> C["Compiler: glt crate"]
|
||||
C --> BC[".glbc bytecode"]
|
||||
end
|
||||
|
||||
subgraph "Loading"
|
||||
BC --> IR["Interpreter::run"]
|
||||
IR --> R["Reader: parses the binary"]
|
||||
R --> DOC["Document: tree + styles + variables"]
|
||||
end
|
||||
|
||||
subgraph "Initialization"
|
||||
DOC --> BOOT["iced::application boot"]
|
||||
BOOT --> RC["RheiContext: compiles Rhai scripts"]
|
||||
BOOT --> EV0["evaluate_vdom: builds the full VDOM"]
|
||||
EV0 --> APP["GlintApp: ready to run"]
|
||||
end
|
||||
|
||||
subgraph "Lifecycle: each frame"
|
||||
APP --> LOOP{"iced event loop"}
|
||||
|
||||
LOOP -->|event received| MSG[Message]
|
||||
MSG --> UPD["GlintApp::update"]
|
||||
UPD --> SET["changes a variable"]
|
||||
SET --> TV["tracker marks dependent elements as dirty"]
|
||||
TV --> DIRTY["collect dirty_set"]
|
||||
DIRTY --> VDOM["evaluate_vdom_incr: recalculate only dirty"]
|
||||
VDOM --> STYLE["StyleSheet: apply styles (with cache)"]
|
||||
STYLE --> NEW_VDOM["new VDOM"]
|
||||
|
||||
LOOP -->|on timer| VIEW["GlintApp::view"]
|
||||
VIEW --> REND["render_element: Element → Iced widget"]
|
||||
REND --> ICED["iced::Element tree"]
|
||||
ICED --> DIFF["Iced: compares with previous frame"]
|
||||
DIFF --> LAYOUT[Layout]
|
||||
LAYOUT --> DRAW["GPU draws"]
|
||||
end
|
||||
|
||||
subgraph "Styles — separate"
|
||||
STYLE --> SI["StyleIndex: looks up rules in O(1)–O(K)"]
|
||||
SI --> SC["StyleCache: doesn't parse the same thing twice"]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 1: Compilation — from text to bytecode
|
||||
|
||||
It all starts with two file types:
|
||||
|
||||
- **`.gltm`** — markup: buttons, panels, texts, sliders, and so on.
|
||||
- **`.glts`** — styles: CSS-like rules, selectors, colors, margins.
|
||||
|
||||
They are compiled by the external **`glt`** crate (not part of this repository). It does three things:
|
||||
|
||||
### 1.1 Parsing
|
||||
|
||||
`Parser` reads `.gltm` and `.glts` and stores everything in **`ModuleSoA`**.
|
||||
|
||||
**What is ModuleSoA?** SoA = Structure of Arrays. Instead of storing elements as a list of structs:
|
||||
|
||||
```text
|
||||
// Array of Structures (AoS) — the usual way
|
||||
Element { name: "Button", props: [...], children: [...] }
|
||||
Element { name: "Text", props: [...], children: [...] }
|
||||
```
|
||||
|
||||
the compiler stores them as a struct with parallel arrays:
|
||||
|
||||
```text
|
||||
// Structure of Arrays (SoA) — more efficient for the compiler
|
||||
ModuleSoA {
|
||||
type_names: ["Button", "Text", ...],
|
||||
properties_vec: [ [...], [...], ...],
|
||||
hierarchy: [parent_id, parent_id, ...],
|
||||
}
|
||||
```
|
||||
|
||||
This way the compiler iterates over all names at once (CPU cache stays hot),
|
||||
finds parent relationships faster, and applies optimizations more easily.
|
||||
|
||||
### 1.2 Building the AST
|
||||
|
||||
An AST tree is built from `ModuleSoA`. Components, if/each branches,
|
||||
and parameters are resolved here.
|
||||
|
||||
### 1.3 Bytecode generation
|
||||
|
||||
`Compiler` walks the AST and turns it into the **`.glbc`** binary format:
|
||||
- header with magic bytes (`"glBc"`)
|
||||
- string pool (all names, classes, texts — one contiguous block)
|
||||
- byte-encoded opcodes (see `opcodes.rs`: `OP_ELEM_PUSH`, `OP_PROP`, `OP_IF`, `OP_EACH`, etc.)
|
||||
|
||||
The result is a compact binary that can be loaded quickly and fed to the runtime.
|
||||
|
||||
---
|
||||
|
||||
## Stage 2: Loading — from bytecode to Document
|
||||
|
||||
The runtime takes `.glbc` and turns it into data structures that can be worked with.
|
||||
|
||||
### `Interpreter::run(bytecode) → Document`
|
||||
|
||||
Internally, `Reader` reads the bytecode sequentially:
|
||||
1. Checks magic bytes (is this really `.glbc`?)
|
||||
2. Reads the string pool
|
||||
3. Executes opcodes, building the `Element` tree on the fly
|
||||
|
||||
Two important things happen in parallel:
|
||||
|
||||
**Styles:** each encountered style directive is parsed into a `StyleRule`,
|
||||
then all rules are built into a `StyleIndex` — a catalog: "here are all rules for tag Button,
|
||||
here for class primary, here for element with id=submit". This way style lookup
|
||||
will later take not O(all rules), but O(a couple of items).
|
||||
|
||||
**Dependencies:** every property like `"text": "Hello $name"` is a hint:
|
||||
the element depends on the variable `name`. `ReactiveTracker` scans all properties,
|
||||
finds `$var` and remembers: "element ElementId(5) depends on variable 'name'".
|
||||
|
||||
The result is a **`Document`**:
|
||||
```rust
|
||||
Document {
|
||||
roots: Vec<Element>, // root elements
|
||||
components: HashMap<String, ComponentDef>, // components
|
||||
variables: HashMap<String, Value>, // initial values
|
||||
stylesheet: StyleSheet, // style sheet
|
||||
rhei_scripts: Vec<String>, // init scripts
|
||||
tracker: ReactiveTracker, // who depends on what
|
||||
interner: Interner, // unique string pool
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 3: Initialization — preparing for life
|
||||
|
||||
`Document` is ready, but it needs to be "started". The Iced boot function does this.
|
||||
|
||||
### 3.1 Cloning
|
||||
|
||||
`doc.clone()` — all strings inside Element have type `&'a str` with the original
|
||||
lifetime. After cloning they become `&'static str` (the runtime
|
||||
calls `Box::leak` so strings live forever — the application runs until the window is closed).
|
||||
|
||||
### 3.2 Rhai compilation
|
||||
|
||||
`RheiContext::new(scripts)`:
|
||||
- Creates a Rhai engine (`Engine`)
|
||||
- Compiles all init scripts into AST and saves them
|
||||
- Collects all functions from the scripts into a global module
|
||||
- Then `precompile_all_from_doc()` walks the entire Element tree and compiles
|
||||
every `__on:click { ... }` and every `!rhei:expr` into cache.
|
||||
**Now on click there's no need to recompile** — just grab the AST from cache.
|
||||
|
||||
### 3.3 Running init scripts
|
||||
|
||||
`initialize()`: synchronizes variables with the Rhai scope, executes init scripts,
|
||||
pulls everything that changed from the scope.
|
||||
|
||||
### 3.4 First VDOM
|
||||
|
||||
`evaluate_vdom()` — a full traversal of the tree:
|
||||
- Substitutes variables into strings (`$name` → actual value)
|
||||
- Evaluates `@if` conditions
|
||||
- Expands `@each` into the actual number of elements
|
||||
- For each element, finds matching styles and computes `ComputedStyle`
|
||||
- Assigns `content_hash`
|
||||
|
||||
Result: `GlintApp { doc, rhei, vdom_roots }`. The first frame is ready to display.
|
||||
|
||||
---
|
||||
|
||||
## Stage 4: Lifecycle — each frame
|
||||
|
||||
Iced runs in a loop: event → `update()` → `view()` → rendering.
|
||||
|
||||
### 4.1 Event received: update()
|
||||
|
||||
The user clicked a button, moved a slider, entered text — Iced sends a `Message`.
|
||||
|
||||
```rust
|
||||
enum Message {
|
||||
SliderChanged(Option<String>, f64), // slider: (bound variable, new value)
|
||||
InputChanged(Option<String>, String), // text input
|
||||
ToggleChanged(Option<String>, bool), // checkbox
|
||||
EventTriggered(String), // button click: run Rhai script
|
||||
WindowScrolled(f32), // window scroll
|
||||
ScrollableScrolled(u64, f32), // scroll inside container
|
||||
}
|
||||
```
|
||||
|
||||
**GlintApp::update()** does the following:
|
||||
|
||||
1. **Changes the variable.** For example, `SliderChanged("volume", 75)` → `variables["volume"] = 75.0`.
|
||||
2. **Notifies the tracker:** `tracker.on_variable_changed("volume")`. The tracker checks:
|
||||
"elements with IDs 5, 12, 18 depend on this variable". It marks them as dirty.
|
||||
3. **Collects the dirty_set:** `tracker.take_dirty_set()`.
|
||||
4. **Recalculates VDOM:** `evaluate_vdom_incr(roots, &dirty_set)`. It walks the tree.
|
||||
If an element is in dirty_set — recalculates it (variable substitution, style computation).
|
||||
If not — leaves it as is. **Children of dirty elements are also recalculated** (cascade).
|
||||
|
||||
### 4.2 On timer: view()
|
||||
|
||||
Even if nothing happened, Iced calls `view()` every frame (60 times per second).
|
||||
It needs to return Iced widgets for rendering.
|
||||
|
||||
**render_element()** — a recursive function that turns an Element into an Iced widget:
|
||||
|
||||
- `Button` → `iced::button(...).on_press(...)`
|
||||
- `Text` → `iced::text("...").size(16).color(...)`
|
||||
- `Panel` → `iced::column[...].spacing(10)`, wrapped in a container with background and border
|
||||
- `Input` → `iced::text_input("placeholder", "value").on_input(...)`
|
||||
- `Image` → `iced::image(path)` or `iced::svg(path)`
|
||||
- Unknown type → just a column with children
|
||||
|
||||
Each widget is wrapped in **`apply_universal_box_model`**:
|
||||
```text
|
||||
container [margin]
|
||||
container [padding, border, background]
|
||||
scrollable (if overflow: scroll/auto)
|
||||
container [padding]
|
||||
the widget itself
|
||||
```
|
||||
|
||||
**Problem:** `render_element` creates **all** widgets from scratch every frame, even if
|
||||
the Element hasn't changed. Iced then diffs the new tree against the old one — but building
|
||||
the tree itself takes ~7ms. This is the main optimization opportunity.
|
||||
|
||||
### 4.3 Iced does its thing
|
||||
|
||||
Iced receives the `iced::Element` tree, compares it with the previous one (diff), computes
|
||||
the layout, and renders via GPU (wgpu). All of this happens without our code.
|
||||
|
||||
---
|
||||
|
||||
## Element anatomy
|
||||
|
||||
```rust
|
||||
Element {
|
||||
type_name: "Button", // what kind of element
|
||||
properties: [("label", "Click"), ("color", "red"), ...], // its properties
|
||||
computed_style: ComputedStyle { color: Some(Red), padding: Some(8px), ... }, // computed style
|
||||
element_id: ElementId(42), // unique ID in the tree
|
||||
content_hash: 0xABCD1234, // content hash (for widget cache)
|
||||
children: [Element, ...], // child elements
|
||||
}
|
||||
```
|
||||
|
||||
## Style anatomy
|
||||
|
||||
Styles are stored in `StyleSheet` and work in three stages:
|
||||
|
||||
**1. Index (`StyleIndex`):** at load time, all CSS rules are sorted into buckets:
|
||||
```text
|
||||
Rule: "Button.primary#submit { color: red; padding: 10px }"
|
||||
→ by_tag["Button"] = { RuleId(1) }
|
||||
→ by_class["primary"] = { RuleId(1) }
|
||||
→ by_id["submit"] = { RuleId(1) }
|
||||
```
|
||||
|
||||
**2. Lookup:** when we need to find styles for a `Button.primary#submit` element,
|
||||
we take the intersection of sets from all three buckets. Instead of checking 500 rules — 3 lookups.
|
||||
|
||||
**3. Cache:** even when styles are found, `ComputedStyle::compute()` parses all properties
|
||||
(color, margins, fonts — about 40 fields). This is expensive. `StyleCache` remembers
|
||||
the result: `hash(type_name, properties, epoch) → ComputedStyle`. If the element
|
||||
hasn't changed — we get the ready-made style from cache, no parsing.
|
||||
|
||||
---
|
||||
|
||||
## Event loop with a slider example
|
||||
|
||||
```
|
||||
1. User moves the volume slider
|
||||
2. Iced: SliderChanged(Some("volume"), 75.0)
|
||||
3. GlintApp::update:
|
||||
a. variables["volume"] = Float(75.0)
|
||||
b. tracker.on_variable_changed("volume")
|
||||
→ dirty: ElementId(5) — text with "$volume", ElementId(12) — width from "$volume"
|
||||
c. evaluate_vdom_incr(roots, &{5, 12})
|
||||
→ Element 5: recalculate text (new volume)
|
||||
→ Element 12: recalculate width
|
||||
→ remaining 48 elements: don't touch
|
||||
4. GlintApp::view:
|
||||
→ render_element for all 50 root elements
|
||||
→ recursively for all children (even for those 48 that didn't change)
|
||||
→ Iced receives a completely new tree of 200+ widgets
|
||||
5. Iced: diffs → finds 2 changes → redraws 2 areas
|
||||
```
|
||||
|
||||
**Bottleneck:** step 4. VDOM recalculated only 2 out of 50 elements (thanks to ReactiveTracker).
|
||||
But render_element creates widgets for all 200+ nodes. Iced then diffs anyway
|
||||
and does nothing with 198 of them, but the time to create them has already been spent.
|
||||
68
docs/en/architecture/02-performance.md
Normal file
68
docs/en/architecture/02-performance.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Performance Analysis
|
||||
|
||||
Measurements with the `--perf` flag on `desktop.glbc`.
|
||||
|
||||
## Measurement Results
|
||||
|
||||
**Steady state** (no events, idle):
|
||||
|
||||
```
|
||||
VDOM: 6.6ms | Style: 0.3ms | Render: 7.2ms | Total: 14.1ms
|
||||
```
|
||||
|
||||
**Under events** (slider drag, peak values):
|
||||
|
||||
```
|
||||
VDOM: 17.4ms | Style: 0.8ms | Render: 15.2ms | Total: 33.5ms ⚠️
|
||||
```
|
||||
|
||||
**60 FPS frame budget: 16ms.** At idle we fit (14ms), under events — not (up to 33ms).
|
||||
|
||||
## Bottleneck Analysis
|
||||
|
||||
### Style matching — NOT a bottleneck (0.3-0.8ms)
|
||||
|
||||
Style matching takes less than 1ms even at peak. This is the result of:
|
||||
- **Phase 4** (StyleIndex) — O(K) instead of O(N×M)
|
||||
- **Phase 8** (StyleCache) — computed style memoization
|
||||
|
||||
### VDOM eval — main consumer (6-17ms)
|
||||
|
||||
At idle ~6.6ms — a full traversal of the Element tree. Under events up to 17ms:
|
||||
- `evaluate_vdom_incr` recalculates dirty elements (Phase 3)
|
||||
- Each event can dirty entire subtrees
|
||||
- Internally: `resolve_string`, `resolve_prop`, `compute_cached`, recursive traversal
|
||||
|
||||
### Render — second consumer (7-15ms)
|
||||
|
||||
**This is where the main optimization headroom lies.** `render_element` creates ALL Iced widgets
|
||||
from scratch every frame, even if the Element hasn't changed. Iced then diffs the new tree
|
||||
against the old one — but the tree construction itself costs ~7ms.
|
||||
|
||||
### Scenario: slider
|
||||
|
||||
1. `SliderChanged` → `age` and `volume_level` change
|
||||
2. `tracker.on_variable_changed` → dirty_set for dependent elements
|
||||
3. `evaluate_vdom_incr` recalculates dirty elements and their children
|
||||
4. `view()` → `render_element` for ALL elements (full re-render)
|
||||
5. Result: VDOM 13ms + Render 14ms = 27ms — frame drop
|
||||
|
||||
The first few frames after an event are the heaviest (VDOM ~13ms), then
|
||||
stabilize (~7ms) as dirty_set gradually clears.
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Phase 9.3 — widget cache** — would reduce Render from 7ms to ~0ms for unchanged
|
||||
elements. If 1 out of 50 elements changes, only that one needs re-rendering.
|
||||
This would lower total time from 14ms to ~7ms at idle.
|
||||
|
||||
2. **Phase 1 — Value enum in styles** — `ComputedStyle::compute()` and `parse_*()`
|
||||
take `&str` and parse each property. Passing `&Value` instead would skip parsing.
|
||||
Potentially speeds up both style matching and VDOM eval.
|
||||
|
||||
3. **Phase 2 — InternedStr** — string comparisons (`type_name == "Button"`,
|
||||
`key == "padding-top"`) happen thousands of times per frame. Replacing with u32
|
||||
comparison would give 1.5-2× in VDOM and render paths.
|
||||
|
||||
4. **Phase 0.3 — flamegraph** — confirm hypotheses with profiler measurements
|
||||
(`perf record`) before investing in optimization.
|
||||
182
docs/en/modules/01-types.md
Normal file
182
docs/en/modules/01-types.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Type module: `src/interpreter/types.rs`
|
||||
|
||||
Runtime base data types: string interning, Value, DOM elements, flat VDOM, and Document.
|
||||
|
||||
---
|
||||
|
||||
## `InternedStr(u32)`
|
||||
|
||||
Newtype wrapper over `u32`. Compact string identifier for fast comparisons.
|
||||
|
||||
```rust
|
||||
pub struct InternedStr(pub u32);
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `from_raw(id: u32) -> Self` — const constructor
|
||||
- `raw(&self) -> u32` — raw value
|
||||
- `eq_str(&self, other: &str) -> bool` — comparison with a string via `Interner::lookup`
|
||||
|
||||
**Status:** defined, but not used in hot paths (Element, style matching, properties). Phase 2 not completed.
|
||||
|
||||
---
|
||||
|
||||
## `Interner`
|
||||
|
||||
String pool with unique ID allocation. Each string is interned once.
|
||||
|
||||
```rust
|
||||
pub struct Interner {
|
||||
strings: Vec<String>,
|
||||
map: HashMap<String, u32>,
|
||||
next_id: u32,
|
||||
}
|
||||
```
|
||||
|
||||
- `new()` — empty interner
|
||||
- `intern(&mut self, s: &str) -> InternedStr` — get or create an ID
|
||||
- `lookup(&self, id: InternedStr) -> &str` — get a string by ID
|
||||
- `intern_or_none(&mut self, s: Option<&str>) -> Option<InternedStr>` — optional interning
|
||||
|
||||
Stored in `Document::interner`. Accessible via `with_interner()` (thread_local).
|
||||
|
||||
---
|
||||
|
||||
## `Value`
|
||||
|
||||
Typed variable value. Replaces raw strings to eliminate parse/format round-trip.
|
||||
|
||||
```rust
|
||||
pub enum Value {
|
||||
Str(CompactString),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
Array(Vec<Value>),
|
||||
None,
|
||||
}
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `as_str(&self) -> Option<&str>` — borrow a string
|
||||
- `to_owned_string(&self) -> CompactString` — format to string (used in renderer)
|
||||
|
||||
**Implemented:** `From<&str>`, `From<String>`, `From<i64>`, `From<f64>`, `From<bool>`, `From<Vec<T>>`.
|
||||
`PartialEq` — Float is compared with `f64::EPSILON`.
|
||||
|
||||
**Usage:** `Document::variables`, `Value↔Dynamic` conversions in rhei.rs.
|
||||
**NOT used:** in styles (`ComputedStyle::compute` still takes `&str`, matched_sheets uses `HashMap<String, String>`).
|
||||
|
||||
---
|
||||
|
||||
## `Element<'a>`
|
||||
|
||||
VDOM tree node. The central type — the UI is built from it.
|
||||
|
||||
```rust
|
||||
pub struct Element<'a> {
|
||||
pub type_name: &'a str, // "Button", "Panel", "Text", etc.
|
||||
pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>, // (key, value)
|
||||
pub children: Vec<Element<'a>>,
|
||||
pub computed_style: ComputedStyle,
|
||||
pub element_id: ElementId,
|
||||
pub content_hash: u64, // Phase 9.1: hash for widget cache
|
||||
}
|
||||
```
|
||||
|
||||
**Methods (in types.rs):**
|
||||
- `id(&self) -> Option<&str>` — value of the HTML attribute `id`
|
||||
- `new(type_name)` — fresh element with `element_id: u32::MAX`
|
||||
- `new_with_id(type_name, id)` — with a given ElementId
|
||||
|
||||
**Methods (in renderer.rs):**
|
||||
- `get_prop(&self, key: &str) -> Option<&str>` — property value by key
|
||||
- `push_prop(key, val)` — add a property
|
||||
- `set_prop(key, val)` — set or overwrite a property
|
||||
|
||||
---
|
||||
|
||||
## `ComponentDef<'a>`
|
||||
|
||||
Component definition from a template.
|
||||
|
||||
```rust
|
||||
pub struct ComponentDef<'a> {
|
||||
pub name: String,
|
||||
pub params: Vec<(String, String)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
}
|
||||
```
|
||||
|
||||
Stored in `Document::components`. Used in `evaluate_vdom` when expanding
|
||||
components: parameters are passed via variables, the component body is inserted as children.
|
||||
|
||||
---
|
||||
|
||||
## `Document<'a>`
|
||||
|
||||
Complete application state after bytecode loading.
|
||||
|
||||
```rust
|
||||
pub struct Document<'a> {
|
||||
pub roots: Vec<Element<'a>>,
|
||||
pub components: HashMap<String, ComponentDef<'a>>,
|
||||
pub variables: HashMap<String, Value>,
|
||||
pub rhei_scripts: Vec<String>,
|
||||
pub stylesheet: StyleSheet,
|
||||
pub interner: Interner,
|
||||
pub tracker: ReactiveTracker,
|
||||
}
|
||||
```
|
||||
|
||||
Created in `Interpreter::run()`. Cloned at Iced application startup (boot function).
|
||||
Contains everything needed for interpretation: tree, styles, variables, reactivity.
|
||||
|
||||
---
|
||||
|
||||
## `VNode<'a>` and `FlatVDom<'a>`
|
||||
|
||||
Flat VDOM representation for efficient serialization/deserialization.
|
||||
|
||||
```rust
|
||||
pub struct VNode<'a> {
|
||||
pub id: NodeId,
|
||||
pub type_name: &'a str,
|
||||
pub properties: Range<PropertyIdx>,
|
||||
pub children_range: Range<NodeIdx>,
|
||||
pub computed_style: ComputedStyle,
|
||||
pub element_id: ElementId,
|
||||
}
|
||||
|
||||
pub struct FlatVDom<'a> {
|
||||
pub nodes: Vec<VNode<'a>>,
|
||||
pub properties: Vec<(String, String)>,
|
||||
root_indices: Vec<NodeIdx>,
|
||||
}
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `new()` — empty
|
||||
- `from_elements(elements)` — recursively flattens an Element tree into VNodes
|
||||
- `into_elements(self) -> Vec<Element>` — reverse assembly
|
||||
- `get_node(idx)`, `node_count()`, `root_count()`, `root_indices()`
|
||||
|
||||
**Usage:** in tests (`test_flat_vdom_roundtrip`, `test_flat_vdom_nested`, `test_flat_vdom_empty`).
|
||||
Not in the hot path — VDOM is passed as `Vec<Element>`.
|
||||
|
||||
---
|
||||
|
||||
## `InterpError`
|
||||
|
||||
Bytecode loading errors.
|
||||
|
||||
```rust
|
||||
pub enum InterpError {
|
||||
BadMagic,
|
||||
UnexpectedEof,
|
||||
InvalidUtf8,
|
||||
UnexpectedPop,
|
||||
}
|
||||
```
|
||||
|
||||
Implements `Display` and `std::error::Error`. Returned from `Interpreter::run()`.
|
||||
482
docs/en/modules/02-style.md
Normal file
482
docs/en/modules/02-style.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# 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.0–1.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.
|
||||
92
docs/en/modules/03-reactive.md
Normal file
92
docs/en/modules/03-reactive.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Reactivity Module: `src/interpreter/reactive.rs`
|
||||
|
||||
Dependency tracking system between variables and VDOM elements.
|
||||
Allows recalculation of only changed elements (Phase 3).
|
||||
|
||||
---
|
||||
|
||||
## `ElementId(u32)`
|
||||
|
||||
Unique element identifier in the VDOM tree. Assigned by `ReactiveTracker::alloc_id()`
|
||||
during template loading.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ElementId(pub u32);
|
||||
```
|
||||
|
||||
**Used as:**
|
||||
- Key in `dirty_set: HashSet<ElementId>`
|
||||
- Key in `dependencies: HashMap<ElementId, HashSet<String>>`
|
||||
- Field in `Element::element_id` (VDOM → tracker link)
|
||||
- Source for `iced::widget::Id` (Phase 9.2: `format!("ti:{}", id.0)`)
|
||||
- Source for scrollable_id (`el.element_id.0 as u64`)
|
||||
|
||||
---
|
||||
|
||||
## `ReactiveTracker`
|
||||
|
||||
Dependency graph: variable → list of elements that reference it.
|
||||
|
||||
```rust
|
||||
pub struct ReactiveTracker {
|
||||
subscribers: HashMap<String, HashSet<ElementId>>,
|
||||
dependencies: HashMap<ElementId, HashSet<String>>,
|
||||
dirty_set: HashSet<ElementId>,
|
||||
next_id: u32,
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `subscribers` — for each variable: which ElementIds depend on it
|
||||
- `dependencies` — for each element: which variables it depends on (reverse mapping)
|
||||
- `dirty_set` — elements to recalculate in the next frame
|
||||
- `next_id` — counter for `alloc_id()`
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `new()` | Empty tracker |
|
||||
| `alloc_id() -> ElementId` | Allocate a new ID, increment counter |
|
||||
| `add_dependency(element, var_name)` | Register a dependency |
|
||||
| `scan_value(element, value)` | Scan a string for `$var` and add dependencies |
|
||||
| `on_variable_changed(name)` | Mark all dependent elements as dirty |
|
||||
| `take_dirty_set() -> HashSet<ElementId>` | Take dirty_set and clear it |
|
||||
| `is_dirty(id) -> bool` | Check if an element is marked dirty |
|
||||
| `reset()` | Clear all data |
|
||||
|
||||
### scan_value()
|
||||
|
||||
Parses a string for `$var_name` patterns:
|
||||
|
||||
```rust
|
||||
"Hello $name, you are $age years old"
|
||||
// → add_dependency(element, "name")
|
||||
// → add_dependency(element, "age")
|
||||
```
|
||||
|
||||
Used during template loading for each properties string containing `$`.
|
||||
As a result, each element knows which variables it depends on.
|
||||
|
||||
### Update cycle
|
||||
|
||||
```
|
||||
Event → update() → on_variable_changed("var")
|
||||
→ dirty_set = {element_A, element_B, ...}
|
||||
→ take_dirty_set() → evaluate_vdom_incr(roots, &dirty_set)
|
||||
→ for dirty_set elements: recalculate
|
||||
→ for others: return as-is
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
| Test | What it checks |
|
||||
|------|---------------|
|
||||
| `test_basic_dependency_tracking` | Two elements, two variables, correct dirty_set |
|
||||
| `test_scan_value` | Parse `$var` from a string |
|
||||
| `test_scan_no_vars` | String without `$` creates no dependencies |
|
||||
| `test_take_dirty_set` | `take_dirty_set()` clears the internal set |
|
||||
| `test_reset` | `reset()` clears everything |
|
||||
200
docs/en/modules/04-rhei.md
Normal file
200
docs/en/modules/04-rhei.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Rhai Module: `src/interpreter/rhei.rs`
|
||||
|
||||
Integration of the [Rhai](https://rhai.rs/) scripting engine — compilation, AST caching, and expression/script execution.
|
||||
|
||||
---
|
||||
|
||||
## `RHEI_PREFIX` — Rhai expression prefix
|
||||
|
||||
```rust
|
||||
pub const RHEI_PREFIX: &str = "__rhei:";
|
||||
```
|
||||
|
||||
A marker constant for element properties whose content should be interpreted as Rhai expressions. Used in `collect_and_precompile()` to select properties of the form `!rhei:...`.
|
||||
|
||||
---
|
||||
|
||||
## `RheiContext` — Rhai execution context
|
||||
|
||||
```rust
|
||||
pub struct RheiContext {
|
||||
engine: Engine,
|
||||
init_ast: AST,
|
||||
scope: RefCell<Scope<'static>>,
|
||||
action_cache: RefCell<HashMap<String, AST>>,
|
||||
expr_cache: RefCell<HashMap<String, AST>>,
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `engine` | Configured `rhai::Engine` instance |
|
||||
| `init_ast` | Merged AST of all initialization scripts (including function definitions) |
|
||||
| `scope` | Shared scope (`Scope`) shared across calls; wrapped in `RefCell` for interior mutability |
|
||||
| `action_cache` | Cache of compiled scripts (actions), keyed by source code |
|
||||
| `expr_cache` | Cache of compiled expressions, keyed by source code |
|
||||
|
||||
---
|
||||
|
||||
## Constructors
|
||||
|
||||
### `new(scripts)`
|
||||
|
||||
```rust
|
||||
pub fn new(scripts: &[String]) -> Self
|
||||
```
|
||||
|
||||
Creates a context via `new_empty()` and immediately precompiles all provided scripts by calling `precompile_scripts()`.
|
||||
|
||||
### `new_empty(scripts)`
|
||||
|
||||
```rust
|
||||
fn new_empty(scripts: &[String]) -> Self
|
||||
```
|
||||
|
||||
1. Creates `Engine::new()`.
|
||||
2. Configures `on_print` (outputs to stdout with `[rhei]` prefix) and `on_debug` (outputs to stderr) handlers.
|
||||
3. Compiles all scripts and merges their ASTs into a single tree via `merge()`. Compilation errors for individual blocks are logged but do not abort the process.
|
||||
4. Creates a module (`Module::eval_ast_as_new`) from the merged AST with an empty scope — this registers global functions defined in the scripts. The module is registered in the engine as a global module (`register_global_module`).
|
||||
5. Initializes an empty `Scope`, empty `action_cache` and `expr_cache` caches.
|
||||
|
||||
---
|
||||
|
||||
## `sync_scope()` — variable synchronization
|
||||
|
||||
```rust
|
||||
pub fn sync_scope(&self, variables: &HashMap<String, Value>)
|
||||
```
|
||||
|
||||
Synchronizes values from an external `HashMap` into the Rhai `Scope`:
|
||||
- If a variable already exists in the scope and its value has not changed — skip.
|
||||
- If a variable exists — update it via `set_value()`.
|
||||
- If a variable does not exist — add it via `push_dynamic()`.
|
||||
|
||||
Conversion `Value → Dynamic` is performed via `value_to_dynamic()`.
|
||||
|
||||
---
|
||||
|
||||
## `initialize()` — initialization
|
||||
|
||||
```rust
|
||||
pub fn initialize(&self, variables: &mut HashMap<String, Value>)
|
||||
```
|
||||
|
||||
1. Synchronizes variables via `sync_scope()`.
|
||||
2. Runs `init_ast` (merged AST of all scripts) via `run_ast_with_scope()`.
|
||||
3. Iterates over all scope variables via `iter_raw()` and writes back into the `HashMap` those whose values have changed.
|
||||
|
||||
---
|
||||
|
||||
## `eval_expr()` — expression evaluation
|
||||
|
||||
```rust
|
||||
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, Value>) -> Value
|
||||
```
|
||||
|
||||
1. Synchronizes variables.
|
||||
2. Obtains (compiles or fetches from cache) the expression AST via `get_or_compile_expr()`.
|
||||
3. Executes via `eval_ast_with_scope::<Dynamic>()`.
|
||||
4. Converts the result `Dynamic → Value` via `dynamic_to_value()`.
|
||||
5. On error returns `Value::None`.
|
||||
|
||||
---
|
||||
|
||||
## `eval_condition()` — condition evaluation
|
||||
|
||||
```rust
|
||||
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, Value>) -> bool
|
||||
```
|
||||
|
||||
Similar to `eval_expr()`, but typed as `bool`. On error returns `false`.
|
||||
|
||||
---
|
||||
|
||||
## `execute_action()` — script execution
|
||||
|
||||
```rust
|
||||
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, Value>)
|
||||
```
|
||||
|
||||
1. Synchronizes variables.
|
||||
2. Obtains the script AST via `get_or_compile_action()`.
|
||||
3. Executes via `run_ast_with_scope()`.
|
||||
4. After execution iterates over the scope and writes changed variables back into the `HashMap`.
|
||||
|
||||
---
|
||||
|
||||
## AST Caching
|
||||
|
||||
### `get_or_compile_action(script)`
|
||||
|
||||
```rust
|
||||
fn get_or_compile_action(&self, script: &str) -> Option<AST>
|
||||
```
|
||||
|
||||
Checks `action_cache`. On miss compiles via `engine.compile()`, stores in cache.
|
||||
|
||||
### `get_or_compile_expr(expr)`
|
||||
|
||||
```rust
|
||||
fn get_or_compile_expr(&self, expr: &str) -> Option<AST>
|
||||
```
|
||||
|
||||
Checks `expr_cache`. On miss compiles via `engine.compile_expression()`, stores in cache.
|
||||
|
||||
Both methods log the error on compilation failure and return `None`.
|
||||
|
||||
---
|
||||
|
||||
## Batch Precompilation
|
||||
|
||||
```rust
|
||||
pub fn precompile_scripts(&self, scripts: &[String])
|
||||
pub fn precompile_actions(&self, actions: &[String])
|
||||
pub fn precompile_exprs(&self, exprs: &[String])
|
||||
pub fn precompile_all_from_doc(&self, doc: &super::Document)
|
||||
```
|
||||
|
||||
| Method | Action |
|
||||
|---|---|
|
||||
| `precompile_scripts` | Compiles each script as an action |
|
||||
| `precompile_actions` | Same as `precompile_scripts` (alias) |
|
||||
| `precompile_exprs` | Compiles each expression |
|
||||
| `precompile_all_from_doc` | Compiles all `doc.rhei_scripts` and recursively traverses the element tree |
|
||||
|
||||
### `collect_and_precompile()`
|
||||
|
||||
```rust
|
||||
fn collect_and_precompile(el: &super::Element, ctx: &RheiContext)
|
||||
```
|
||||
|
||||
Recursively traverses the `Element` tree:
|
||||
- For properties starting with `__on:*` and non-empty — compiles as an action.
|
||||
- For properties starting with `RHEI_PREFIX` (`!rhei:`) — compiles the remaining part as an expression.
|
||||
|
||||
---
|
||||
|
||||
## Type Conversion
|
||||
|
||||
### `value_to_dynamic(v: &Value) -> Dynamic`
|
||||
|
||||
```rust
|
||||
Value::Int(i) → Dynamic::from(*i)
|
||||
Value::Float(f) → Dynamic::from(*f)
|
||||
Value::Bool(b) → Dynamic::from(*b)
|
||||
Value::Str(s) → str_to_dynamic(s)
|
||||
Value::None → Dynamic::UNIT
|
||||
Value::Array(a) → Dynamic::from_iter(value_to_dynamic of each element)
|
||||
```
|
||||
|
||||
### `dynamic_to_value(d: &Dynamic) -> Value`
|
||||
|
||||
Checks the type via `is_string()`, `is_int()`, `is_float()`, `is_bool()`, `is_array()` in priority order. If the type is not recognized — returns `Value::None`.
|
||||
|
||||
### `str_to_dynamic(s: &str) -> Dynamic`
|
||||
|
||||
A heuristic string parser that tries sequentially:
|
||||
1. `s.parse::<i64>()` — integer
|
||||
2. `s.parse::<f64>()` — float
|
||||
3. `s.parse::<bool>()` — boolean
|
||||
4. Otherwise — `Dynamic::from(s)` as a string
|
||||
537
docs/en/modules/05-renderer.md
Normal file
537
docs/en/modules/05-renderer.md
Normal file
@@ -0,0 +1,537 @@
|
||||
# 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 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<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
|
||||
```
|
||||
305
docs/en/modules/06-mod.md
Normal file
305
docs/en/modules/06-mod.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# Module `interpreter` — bytecode interpreter core
|
||||
|
||||
## Module structure
|
||||
|
||||
```
|
||||
interpreter/
|
||||
├── mod.rs — Interpreter: bytecode parsing, VDOM, styles
|
||||
├── opcodes.rs — Opcode definitions (OP_ELEM_PUSH, OP_IF, OP_EACH, …)
|
||||
├── reactive.rs — ReactiveTracker, ElementId — dirty node tracking
|
||||
├── reader.rs — Reader — bytecode reading (str_ref, i64, f64, value, …)
|
||||
├── rhei.rs — RheiContext — Rhai script and expression execution
|
||||
├── style.rs — StyleSheet, AncestorInfo, ComputedStyle, StructuralContext
|
||||
└── types.rs — Element, Document, ComponentDef, Value, FlatVDom, InterpError
|
||||
```
|
||||
|
||||
## `Interpreter` (empty struct)
|
||||
|
||||
```rust
|
||||
pub struct Interpreter;
|
||||
```
|
||||
|
||||
The struct has no fields — all methods are static. It serves as a namespace for interpretation functions.
|
||||
|
||||
## `Interpreter::run()` — primary bytecode parsing
|
||||
|
||||
```rust
|
||||
pub fn run<'a>(bytecode: &'a [u8]) -> Result<Document<'a>, InterpError>
|
||||
```
|
||||
|
||||
1. Checks the magic number (`bytecode[..4] == MAGIC`).
|
||||
2. Creates a `Reader`, `HashMap` for `variables` and `components`, `Vec` for `rhei_scripts`, an empty `StyleSheet`, and a `ReactiveTracker`.
|
||||
3. Calls `parse_block_elements()` for the root level, which reads the opcode stream and builds the `Element` tree.
|
||||
4. After parsing, calls `stylesheet.build_index()`.
|
||||
5. Returns `Document { roots, components, variables, rhei_scripts, stylesheet, interner, tracker }`.
|
||||
|
||||
## `parse_block_elements()` — recursive block parsing
|
||||
|
||||
```rust
|
||||
fn parse_block_elements<'a>(
|
||||
r: &mut Reader<'a>,
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &mut HashMap<String, ComponentDef<'a>>,
|
||||
rhei_scripts: &mut Vec<String>,
|
||||
stylesheet: &mut SS,
|
||||
tracker: &mut ReactiveTracker,
|
||||
is_root: bool,
|
||||
) -> Result<Vec<Element<'a>>, InterpError>
|
||||
```
|
||||
|
||||
Reads opcodes in a loop, using a stack for nesting (`stack: Vec<Element>`). On `OP_END_BLOCK` it finishes the current block (if `!is_root`).
|
||||
|
||||
### Handled opcodes
|
||||
|
||||
| Opcode | Action |
|
||||
|---|---|
|
||||
| `OP_ELEM_PUSH` | Creates an `Element` with `tracker.alloc_id()`, pushes onto the stack |
|
||||
| `OP_ELEM_POP` | Pops an element from the stack, calls `attach()` |
|
||||
| `OP_GLOBAL` / `OP_LET` | Reads name and value, inserts into `variables` |
|
||||
| `OP_SINGLETON` | Skips singleton data |
|
||||
| `OP_CONTENT` | Text content — `text` property. If `OP_PROP_RHEI` — prefix `!rhei:` |
|
||||
| `OP_PROP_STR`, `OP_PROP_VAR`, `OP_PROP_INT`, `OP_PROP_FLOAT`, `OP_PROP_BOOL`, `OP_PROP_RHEI`, `OP_PROP_UNIT`, `OP_PROP_CALL`, `OP_PROP_IDENT`, `OP_PROP_FSPATH`, `OP_PROP_COLOR` | Reads key and value, calls `el.push_prop()` |
|
||||
| `OP_RHEI_BLK` | At root level without a parent — script; otherwise — `#text` element with `!rhei:` |
|
||||
| `OP_COMPONENT` | Reads name, parameters, recursively parses child block; stores `ComponentDef` |
|
||||
| `OP_IF` | Reads condition, parses true-block, checks `has_else`, parses false-block. Creates `@if` with `@else` as the last child |
|
||||
| `OP_EACH` | Reads variable name, source (array or `$var` or Rhai), parses template block. Creates `@each` |
|
||||
| `OP_ON` | Reads event name, arguments, looks for `OP_RHEI_BLK` — handler script; sets `__on:{event}` |
|
||||
| `OP_STYLE_RULE` | Reads selector and properties, calls `stylesheet.add_rule()` |
|
||||
|
||||
### `attach()`
|
||||
|
||||
```rust
|
||||
fn attach<'a>(stack: &mut Vec<Element<'a>>, roots: &mut Vec<Element<'a>>, el: Element<'a>)
|
||||
```
|
||||
|
||||
If the stack is not empty — adds to `parent.children`, otherwise to `roots`.
|
||||
|
||||
---
|
||||
|
||||
## `evaluate_vdom()` — VDOM assembly (entry point)
|
||||
|
||||
```rust
|
||||
pub fn evaluate_vdom<'a>(
|
||||
templates: &[Element<'a>],
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
) -> Vec<Element<'a>>
|
||||
```
|
||||
|
||||
Delegates to `evaluate_vdom_incr()` with an empty `dirty_set` — full recomputation.
|
||||
|
||||
### `evaluate_vdom_flat()`
|
||||
|
||||
```rust
|
||||
pub fn evaluate_vdom_flat<'a>(...) -> FlatVDom<'a>
|
||||
```
|
||||
|
||||
Wraps `evaluate_vdom()` and converts the result via `FlatVDom::from_elements()`.
|
||||
|
||||
---
|
||||
|
||||
## `evaluate_vdom_incr()` — incremental VDOM assembly
|
||||
|
||||
```rust
|
||||
pub fn evaluate_vdom_incr<'a>(
|
||||
templates: &[&Element<'a>],
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
dirty_set: &HashSet<ElementId>,
|
||||
) -> Vec<Element<'a>>
|
||||
```
|
||||
|
||||
### Preprocessing
|
||||
|
||||
1. **`sibling_infos`** — for each element from `templates` creates an `AncestorInfo` (type_name, id, classes). Needed for structural pseudo-classes (CSS `:nth-child`, `:first-of-type`, etc.).
|
||||
|
||||
2. **`type_counts`** / **`type_seen`** — count of total elements of each type and a counter for `StructuralContext`.
|
||||
|
||||
### Main loop over `templates`
|
||||
|
||||
For each element, a `StructuralContext` is computed:
|
||||
|
||||
```rust
|
||||
let structural = StructuralContext {
|
||||
sibling_index: i,
|
||||
sibling_total: templates.len(),
|
||||
type_index: *type_idx,
|
||||
type_total,
|
||||
has_children: ...,
|
||||
is_root: ancestors.is_empty(),
|
||||
};
|
||||
```
|
||||
|
||||
#### `@if`
|
||||
|
||||
- Reads `condition`, calls `evaluate_condition()`.
|
||||
- Iterates over `child` elements: if `@else` — active when condition is false; otherwise active when true.
|
||||
- Recursively calls `evaluate_vdom_incr()` for the active branch.
|
||||
|
||||
#### `@each`
|
||||
|
||||
- Gets `var_name` and `source`.
|
||||
- Resolves the source: if prefixed with `!rhei:` — calls `normalize_rhai_array()`, otherwise — `resolve_string()`.
|
||||
- Splits the result by `,`, for each element:
|
||||
- Inserts `var_name` into `variables`, recursively processes the template, restores the previous variable value.
|
||||
|
||||
#### Regular element / Component
|
||||
|
||||
- If `el.type_name` is found in `components`:
|
||||
1. Collects arguments from component parameters via `resolve_prop()`.
|
||||
2. Saves old variable values, inserts new ones.
|
||||
3. Creates `vcomp`, copies properties, resolving them via `resolve_prop()` and `resolve_string()`.
|
||||
4. Computes styles: `collect_matching_styles()` → `stylesheet.compute_cached()`.
|
||||
5. Builds ancestor chain: `build_ancestor_chain()`.
|
||||
6. Recursively processes `comp.children`.
|
||||
7. Computes `content_hash`.
|
||||
8. Restores variables.
|
||||
- Otherwise (regular element):
|
||||
1. Creates `vnode`.
|
||||
2. Resolves properties: `__on:` → `resolve_string()`, `$var` → `__bind:{key}`, others → `resolve_prop()`.
|
||||
3. Computes styles via `collect_matching_styles()` + `compute_cached()`.
|
||||
4. Builds ancestor chain, recursively processes `el.children`.
|
||||
5. Computes `content_hash`.
|
||||
|
||||
---
|
||||
|
||||
## `compute_content_hash()`
|
||||
|
||||
```rust
|
||||
fn compute_content_hash(el: &Element) -> u64
|
||||
```
|
||||
|
||||
Element hash for caching. Considers:
|
||||
- `type_name`
|
||||
- All key-value pairs from `properties`
|
||||
- Recursively `content_hash` of child elements (`children`)
|
||||
|
||||
Uses `DefaultHasher`.
|
||||
|
||||
---
|
||||
|
||||
## `build_ancestor_chain()`
|
||||
|
||||
```rust
|
||||
fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec<AncestorInfo>
|
||||
```
|
||||
|
||||
Copies the current `ancestors`, adds `AncestorInfo` for the current element (type_name, id, classes). Returns the extended chain for passing during recursive child traversal.
|
||||
|
||||
---
|
||||
|
||||
## `collect_matching_styles()`
|
||||
|
||||
```rust
|
||||
fn collect_matching_styles<'a>(
|
||||
el: &Element,
|
||||
stylesheet: &'a SS,
|
||||
active_pseudo: &[&str],
|
||||
structural: &StructuralContext,
|
||||
ancestors: &[AncestorInfo],
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
) -> Vec<&'a HashMap<String, String>>
|
||||
```
|
||||
|
||||
Extracts `id`, `classes`, and all attributes of the element, delegates to `stylesheet.matching_rules()` with the full context for CSS selectors.
|
||||
|
||||
---
|
||||
|
||||
## `resolve_string()` — `$var` substitution
|
||||
|
||||
```rust
|
||||
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str>
|
||||
```
|
||||
|
||||
- If the string has no `$` — returns `Cow::Borrowed(val)` (no allocations).
|
||||
- Otherwise, traverses the string character by character. After `$`, collects the variable name (letters, digits, `_`), looks it up in `scope`, substitutes the value. If the variable is not found — leaves `$var` as is.
|
||||
|
||||
## `resolve_prop()` — property value resolution
|
||||
|
||||
```rust
|
||||
fn resolve_prop(v: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> String
|
||||
```
|
||||
|
||||
- If it starts with `!rhei:` — calls `rhei.eval_expr()`.
|
||||
- Otherwise — `resolve_string()`.
|
||||
|
||||
## `evaluate_condition()` — `@if` condition evaluation
|
||||
|
||||
```rust
|
||||
fn evaluate_condition(cond: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> bool
|
||||
```
|
||||
|
||||
- If `!rhei:` — `rhei.eval_condition()`.
|
||||
- Otherwise:
|
||||
1. Strips curly braces `{...}`.
|
||||
2. Performs `resolve_string()`.
|
||||
3. Checks `is_truthy_str()`, then `false`/`0`/empty.
|
||||
4. Attempts to parse numeric operators (`>=`, `<=`, `>`, `<`, `==`, `!=`).
|
||||
|
||||
## `is_truthy()` / `is_truthy_str()` — conversion to bool
|
||||
|
||||
```rust
|
||||
fn is_truthy(v: &Value) -> bool
|
||||
fn is_truthy_str(s: &str) -> bool
|
||||
```
|
||||
|
||||
- `Value::Bool` — by value.
|
||||
- `Value::Int` — non-zero.
|
||||
- `Value::Float` — non-zero.
|
||||
- `Value::Str` — delegates to `is_truthy_str()`.
|
||||
- `Value::None` — `false`.
|
||||
- `Value::Array` — not empty.
|
||||
- String: `""`, `"false"`, `"0"`, `"null"` → `false`; `"true"`, `"1"` → `true`; otherwise parses as `f64`.
|
||||
|
||||
## `normalize_rhai_array()` — Rhai array normalization
|
||||
|
||||
```rust
|
||||
fn normalize_rhai_array(s: &str) -> String
|
||||
```
|
||||
|
||||
Trims `[...]`, splits by `,`, trims whitespace, joins with `,`. Used in `@each` to convert a Rhai array to the format expected by the loop.
|
||||
|
||||
---
|
||||
|
||||
## `Document`
|
||||
|
||||
```rust
|
||||
pub struct Document<'a> {
|
||||
pub roots: Vec<Element<'a>>,
|
||||
pub components: HashMap<String, ComponentDef<'a>>,
|
||||
pub variables: HashMap<String, Value>,
|
||||
pub rhei_scripts: Vec<String>,
|
||||
pub stylesheet: StyleSheet,
|
||||
pub interner: Interner,
|
||||
pub tracker: ReactiveTracker,
|
||||
}
|
||||
```
|
||||
|
||||
## `Element`
|
||||
|
||||
```rust
|
||||
pub struct Element<'a> {
|
||||
pub type_name: &'a str,
|
||||
pub properties: Vec<(String, String)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
pub element_id: ElementId,
|
||||
pub computed_style: Option<ComputedStyle>,
|
||||
pub content_hash: u64,
|
||||
}
|
||||
```
|
||||
|
||||
## `ComponentDef`
|
||||
|
||||
```rust
|
||||
pub struct ComponentDef<'a> {
|
||||
pub name: String,
|
||||
pub params: Vec<(String, String)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
}
|
||||
```
|
||||
109
docs/en/modules/07-app-perf.md
Normal file
109
docs/en/modules/07-app-perf.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# App and Perf modules
|
||||
|
||||
## `src/app.rs` — GlintApp
|
||||
|
||||
Iced application entry point. Holds state and implements `update`/`view`.
|
||||
|
||||
### `GlintApp`
|
||||
|
||||
```rust
|
||||
pub struct GlintApp {
|
||||
pub doc: Document<'static>,
|
||||
pub vdom_roots: Vec<Element<'static>>,
|
||||
pub rhei: RheiContext,
|
||||
}
|
||||
```
|
||||
|
||||
- `doc` — original Document (styles, variables, tracker)
|
||||
- `vdom_roots` — result of the last evaluate_vdom (_incr)
|
||||
- `rhei` — Rhai engine with cached ASTs
|
||||
|
||||
### `update(&mut self, message: Message) -> iced::Task<Message>`
|
||||
|
||||
Processing messages from Iced:
|
||||
|
||||
| Message | Action |
|
||||
|---------|--------|
|
||||
| `WindowScrolled(y)` | Set `__scroll_y`, call `on_variable_changed` |
|
||||
| `ScrollableScrolled(id, y)` | Set `__scroll_{id}`, call `on_variable_changed` |
|
||||
| `EventTriggered(script)` | `execute_action()`, then diff variables to find changed ones |
|
||||
| `InputChanged(var, val)` | Set variable, call `on_variable_changed` |
|
||||
| `ToggleChanged(var, val)` | Set `Bool`, call `on_variable_changed` |
|
||||
| `SliderChanged(var, val)` | Set `Float`, call `on_variable_changed` |
|
||||
|
||||
After processing the message:
|
||||
1. `tracker.take_dirty_set()` — get dirty elements
|
||||
2. `evaluate_vdom_incr()` — recalculate VDOM
|
||||
3. Save to `self.vdom_roots`
|
||||
|
||||
**Perf:** VDOM phase is measured with `PerfScope::new("vdom")`.
|
||||
|
||||
### `view(&self) -> iced::Element`
|
||||
|
||||
Build Iced widgets from `self.vdom_roots`:
|
||||
|
||||
1. Collect scroll_positions from `__scroll_*` variables
|
||||
2. For each root: `render_element()` → push into column
|
||||
3. Overlay global fixed/absolute/sticky layers
|
||||
4. Return `container(layout).into()`
|
||||
|
||||
**Perf:** Render phase is measured with `PerfScope::new("render")`.
|
||||
At the end `perf::print_frame()` — output to stderr.
|
||||
|
||||
---
|
||||
|
||||
## `src/perf.rs` — Performance Monitoring
|
||||
|
||||
Phase timing system (VDOM, Style, Render). Enabled with `--perf` flag.
|
||||
|
||||
### `PerfReport`
|
||||
|
||||
```rust
|
||||
pub struct PerfReport {
|
||||
pub vdom_eval: f64, // ms
|
||||
pub style: f64, // ms
|
||||
pub render: f64, // ms
|
||||
pub total: f64, // ms
|
||||
}
|
||||
```
|
||||
|
||||
Stored in `thread_local!` `RefCell<PerfReport>`.
|
||||
|
||||
### `PerfScope`
|
||||
|
||||
Drop-guard: measures time from creation to destruction.
|
||||
|
||||
```rust
|
||||
pub struct PerfScope {
|
||||
name: &'static str,
|
||||
start: Instant,
|
||||
}
|
||||
```
|
||||
|
||||
On `drop()`: adds elapsed ms to the corresponding `PerfReport` field.
|
||||
Names: `"vdom"`, `"style"`, `"render"`.
|
||||
|
||||
### Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `set_enabled(bool)` | Enable/disable measurements |
|
||||
| `is_enabled() -> bool` | Check state |
|
||||
| `report_and_reset() -> Option<String>` | Collect report, check >16ms threshold |
|
||||
| `reset()` | Reset PerfReport |
|
||||
| `print_frame()` | Call report_and_reset + reset, print to stderr |
|
||||
|
||||
### Example output
|
||||
|
||||
```
|
||||
[perf] VDOM: 6.63ms | Style: 0.26ms | Render: 7.19ms | Total: 14.08ms
|
||||
[perf] VDOM: 13.26ms | Style: 0.52ms | Render: 7.18ms | Total: 20.95ms ⚠️ Frame budget exceeded! 20.95ms > 16ms
|
||||
```
|
||||
|
||||
### Instrumentation points
|
||||
|
||||
| File | Phase | Location |
|
||||
|------|-------|----------|
|
||||
| `app.rs:update` | vdom | Around `evaluate_vdom_incr()` |
|
||||
| `app.rs:view` | render | Entire `view()` function |
|
||||
| `style.rs:compute_cached` | style | Inside `StyleSheet::compute_cached()` |
|
||||
48
docs/ru/README.md
Normal file
48
docs/ru/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Glint Runtime — документация
|
||||
(да, пока-что написанное нейросетью. Её объяснений достаточно)
|
||||
|
||||
Бэкенд для исполнения скомпилированного байткода (.glbc) Glint UI-фреймворка. Загружает байткод, интерпретирует VDOM, применяет CSS-подобные стили, выполняет Rhai-скрипты и рендерит результат через Iced (native GPU-ускоренный GUI).
|
||||
|
||||
## Структура проекта
|
||||
|
||||
- `src/main.rs` — точка входа, CLI парсинг
|
||||
- `src/lib.rs` — публичное API crate'а
|
||||
- `src/app.rs` — GlintApp: Iced-приложение, update/view
|
||||
- `src/cli.rs` — CLI команды compile/run
|
||||
- `src/renderer.rs` — Iced-виджеты: преобразование Element → iced::Element
|
||||
- `src/perf.rs` — PerfScope: замер производительности по фазам
|
||||
- `src/interpreter/mod.rs` — Interpreter: загрузка bytecode, VDOM eval
|
||||
- `src/interpreter/types.rs` — Element, VNode, FlatVDom, Value, InternedStr, Document
|
||||
- `src/interpreter/style.rs` — StyleSheet, StyleRule, StyleIndex, StyleCache, ComputedStyle
|
||||
- `src/interpreter/reactive.rs` — ReactiveTracker: граф зависимостей
|
||||
- `src/interpreter/rhei.rs` — RheiContext: интеграция Rhai скриптов
|
||||
- `src/interpreter/reader.rs` — Reader: чтение .glbc bytecode
|
||||
- `src/interpreter/opcodes.rs` — OP_* константы байткода
|
||||
|
||||
## Связанные репозитории
|
||||
|
||||
- `glt` (компилятор .gltm/.glts → .glbc)
|
||||
|
||||
## Ключевые концепции
|
||||
|
||||
- **Document** — загруженный .glbc файл: дерево Element'ов, таблица стилей, переменные
|
||||
- **Element** — узел VDOM: type_name, properties, children, computed_style, element_id, content_hash
|
||||
- **VDOM** — виртуальное дерево, результат интерпретации байткода
|
||||
- **StyleSheet** — таблица стилей с индексированным поиском и кэшем computed style
|
||||
- **ReactiveTracker** — отслеживает зависимости element → variable, dirty_set
|
||||
- **RheiContext** — компилирует и выполняет Rhai-скрипты, кэширует AST
|
||||
- **ComputedStyle** — результат применения CSS-правил: ~40 полей (color, padding, font-size, etc.)
|
||||
|
||||
## Фазы оптимизации
|
||||
|
||||
См. [ARCHITECTURE-IMPROVEMENT-PLAN.md](../ARCHITECTURE-IMPROVEMENT-PLAN.md).
|
||||
|
||||
**Реализовано:** фазы 3–8, 10, 9.1–9.2 (content_hash, стабильные Iced widget ID).
|
||||
|
||||
**Осталось:**
|
||||
| Фаза | Описание | Оценка ускорения |
|
||||
|------|----------|-----------------|
|
||||
| 0 | Профилирование (bench, flamegraph) | — |
|
||||
| 1 | Типизированные Value в стилях вместо HashMap<String, String> | 2–3× |
|
||||
| 2 | String interning для горячих путей | 1.5–2× |
|
||||
| 9.3 | Кэш Iced-виджетов по content_hash | 1.5–2× (render) |
|
||||
303
docs/ru/architecture/01-data-flow.md
Normal file
303
docs/ru/architecture/01-data-flow.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Поток данных в Glint Runtime
|
||||
|
||||
Как исходный код превращается в пиксели на экране.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "Компиляция"
|
||||
A1[".gltm markup"] --> P["Parser: glt crate"]
|
||||
A2[".glts style"] --> P
|
||||
P --> M["ModuleSoA — плоские массивы"]
|
||||
M --> AST
|
||||
AST --> C["Compiler: glt crate"]
|
||||
C --> BC[".glbc bytecode"]
|
||||
end
|
||||
|
||||
subgraph "Загрузка"
|
||||
BC --> IR["Interpreter::run"]
|
||||
IR --> R["Reader: парсит бинарник"]
|
||||
R --> DOC["Document: дерево + стили + переменные"]
|
||||
end
|
||||
|
||||
subgraph "Инициализация"
|
||||
DOC --> BOOT["iced::application boot"]
|
||||
BOOT --> RC["RheiContext: компилирует Rhai-скрипты"]
|
||||
BOOT --> EV0["evaluate_vdom: собирает VDOM целиком"]
|
||||
EV0 --> APP["GlintApp: готов к работе"]
|
||||
end
|
||||
|
||||
subgraph "Круг жизни: каждый кадр"
|
||||
APP --> LOOP{"iced event loop"}
|
||||
|
||||
LOOP -->|пришло событие| MSG[Message]
|
||||
MSG --> UPD["GlintApp::update"]
|
||||
UPD --> SET["меняет переменную"]
|
||||
SET --> TV["tracker помечает зависимые элементы как dirty"]
|
||||
TV --> DIRTY["забрать dirty_set"]
|
||||
DIRTY --> VDOM["evaluate_vdom_incr: пересчитать только dirty"]
|
||||
VDOM --> STYLE["StyleSheet: применить стили (с кэшем)"]
|
||||
STYLE --> NEW_VDOM["новый VDOM"]
|
||||
|
||||
LOOP -->|по таймеру| VIEW["GlintApp::view"]
|
||||
VIEW --> REND["render_element: Element → Iced-виджет"]
|
||||
REND --> ICED["iced::Element дерево"]
|
||||
ICED --> DIFF["Iced: сравнивает с предыдущим кадром"]
|
||||
DIFF --> LAYOUT[Layout]
|
||||
LAYOUT --> DRAW["GPU рисует"]
|
||||
end
|
||||
|
||||
subgraph "Стили — отдельно"
|
||||
STYLE --> SI["StyleIndex: ищет правила за O(1)\u2013O(K)"]
|
||||
SI --> SC["StyleCache: не парсит одно и то же дважды"]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Этап 1: Компиляция — из текста в байткод
|
||||
|
||||
Всё начинается с двух типов файлов:
|
||||
|
||||
- **`.gltm`** — разметка: кнопки, панели, тексты, слайдеры и так далее.
|
||||
- **`.glts`** — стили: CSS-подобные правила, селекторы, цвета, отступы.
|
||||
|
||||
Их компилирует внешний crate **`glt`** (не часть этого репозитория). Он делает три вещи:
|
||||
|
||||
### 1.1 Парсинг
|
||||
|
||||
`Parser` читает `.gltm` и `.glts` и складывает всё в **`ModuleSoA`**.
|
||||
|
||||
**Что такое ModuleSoA?** SoA = Structure of Arrays (структура массивов). Вместо того чтобы хранить элементы как список структур:
|
||||
|
||||
```text
|
||||
// Array of Structures (AoS) — как мы привыкли
|
||||
Element { name: "Button", props: [...], children: [...] }
|
||||
Element { name: "Text", props: [...], children: [...] }
|
||||
```
|
||||
|
||||
компилятор хранит их как структуру с параллельными массивами:
|
||||
|
||||
```text
|
||||
// Structure of Arrays (SoA) — эффективнее для компилятора
|
||||
ModuleSoA {
|
||||
type_names: ["Button", "Text", ...],
|
||||
properties_vec: [ [...], [...], ...],
|
||||
hierarchy: [parent_id, parent_id, ...],
|
||||
}
|
||||
```
|
||||
|
||||
Так компилятор проходит по всем именам разом (кэш процессора не простаивает),
|
||||
быстрее ищет родительские связи и легче применяет оптимизации.
|
||||
|
||||
### 1.2 Построение AST
|
||||
|
||||
Из `ModuleSoA` строится AST-дерево. Здесь раскрываются компоненты, if/each ветки,
|
||||
подставляются параметры.
|
||||
|
||||
### 1.3 Генерация байткода
|
||||
|
||||
`Compiler` обходит AST и превращает его в бинарный формат **`.glbc`**:
|
||||
- заголовок с magic-байтами (`"glBc"`)
|
||||
- пул строк (все имена, классы, тексты — одним блоком)
|
||||
- байт-кодированные опкоды (см. `opcodes.rs`: `OP_ELEM_PUSH`, `OP_PROP`, `OP_IF`, `OP_EACH` и т.д.)
|
||||
|
||||
Результат — компактный бинарник, который можно быстро загрузить и скормить рантайму.
|
||||
|
||||
---
|
||||
|
||||
## Этап 2: Загрузка — из байткода в Document
|
||||
|
||||
Рантайм берёт `.glbc` и превращает его в структуры данных, с которыми можно работать.
|
||||
|
||||
### `Interpreter::run(bytecode) → Document`
|
||||
|
||||
Внутри `Reader` последовательно читает байткоп:
|
||||
1. Проверяет magic-байты (это точно `.glbc`?)
|
||||
2. Читает пул строк
|
||||
3. Исполняет опкоды, на лету собирая дерево `Element`
|
||||
|
||||
Параллельно происходят две важные вещи:
|
||||
|
||||
**Стили:** каждая встреченная стилевая директива парсится в `StyleRule`,
|
||||
потом из всех правил строится `StyleIndex` — каталог: «вот все правила для тэга Button,
|
||||
вот для класса primary, вот для элемента с id=submit». Так поиск стиля
|
||||
потом будет занимать не O(все правила), а O(пара штук).
|
||||
|
||||
**Зависимости:** каждое свойство вида `"text": "Hello $name"` — это подсказка:
|
||||
элемент зависит от переменной `name`. `ReactiveTracker` сканирует все свойства,
|
||||
находит `$var` и запоминает: «элемент ElementId(5) зависит от переменной "name"».
|
||||
|
||||
В итоге получается **`Document`**:
|
||||
```rust
|
||||
Document {
|
||||
roots: Vec<Element>, // корневые элементы
|
||||
components: HashMap<String, ComponentDef>, // компоненты
|
||||
variables: HashMap<String, Value>, // начальные значения
|
||||
stylesheet: StyleSheet, // таблица стилей
|
||||
rhei_scripts: Vec<String>, // init-скрипты
|
||||
tracker: ReactiveTracker, // кто от чего зависит
|
||||
interner: Interner, // пул уникальных строк
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Этап 3: Инициализация — подготовка к жизни
|
||||
|
||||
`Document` готов, но его надо «завести». Это делает boot-функция Iced.
|
||||
|
||||
### 3.1 Клонирование
|
||||
|
||||
`doc.clone()` — все строки внутри Element имеют тип `&'a str` с исходным
|
||||
временем жизни. После клонирования они становятся `&'static str` (рантайм
|
||||
делает `Box::leak`, чтобы строки жили вечно — приложение работает до закрытия окна).
|
||||
|
||||
### 3.2 Компиляция Rhai
|
||||
|
||||
`RheiContext::new(scripts)`:
|
||||
- Создаёт Rhai-движок (`Engine`)
|
||||
- Компилирует все init-скрипты в AST и сохраняет их
|
||||
- Собирает все функции из скриптов в глобальный модуль
|
||||
- Потом `precompile_all_from_doc()` проходит по всему дереву Element и компилирует
|
||||
каждый `__on:click { ... }` и каждое `!rhei:expr` в кэш.
|
||||
**Теперь при клике не надо компилировать заново** — достаточно взять AST из кэша.
|
||||
|
||||
### 3.3 Запуск init-скриптов
|
||||
|
||||
`initialize()`: синхронизирует переменные с Rhai-скопом, выполняет init-скрипты,
|
||||
забирает из скопа всё, что изменилось.
|
||||
|
||||
### 3.4 Первый VDOM
|
||||
|
||||
`evaluate_vdom()` — полный проход по дереву:
|
||||
- Подставляет переменные в строки (`$name` → реальное значение)
|
||||
- Вычисляет условия `@if`
|
||||
- Раскрывает `@each` в реальное количество элементов
|
||||
- Для каждого элемента находит подходящие стили и вычисляет `ComputedStyle`
|
||||
- Присваивает `content_hash`
|
||||
|
||||
Результат: `GlintApp { doc, rhei, vdom_roots }`. Первый кадр готов к показу.
|
||||
|
||||
---
|
||||
|
||||
## Этап 4: Круг жизни — каждый кадр
|
||||
|
||||
Iced работает в цикле: событие → `update()` → `view()` → отрисовка.
|
||||
|
||||
### 4.1 Пришло событие: update()
|
||||
|
||||
Пользователь нажал кнопку, подвигал слайдер, ввёл текст — Iced присылает `Message`.
|
||||
|
||||
```rust
|
||||
enum Message {
|
||||
SliderChanged(Option<String>, f64), // слайдер: (привязанная переменная, новое значение)
|
||||
InputChanged(Option<String>, String), // текстовое поле
|
||||
ToggleChanged(Option<String>, bool), // чекбокс
|
||||
EventTriggered(String), // клик по кнопке: запустить Rhai-скрипт
|
||||
WindowScrolled(f32), // скролл окна
|
||||
ScrollableScrolled(u64, f32), // скролл внутри контейнера
|
||||
}
|
||||
```
|
||||
|
||||
**GlintApp::update()** делает так:
|
||||
|
||||
1. **Меняет переменную.** Например, `SliderChanged("volume", 75)` → `variables["volume"] = 75.0`.
|
||||
2. **Сообщает трекеру:** `tracker.on_variable_changed("volume")`. Трекер смотрит:
|
||||
«от этой переменной зависят элементы с ID = 5, 12, 18». Он помечает их как dirty.
|
||||
3. **Забирает dirty_set:** `tracker.take_dirty_set()`.
|
||||
4. **Пересчитывает VDOM:** `evaluate_vdom_incr(roots, &dirty_set)`. Она проходит по дереву.
|
||||
Если элемент в dirty_set — пересчитывает его (подстановка переменных, вычисление стилей).
|
||||
Если нет — оставляет как есть. **Дети dirty-элемента тоже пересчитываются** (каскад).
|
||||
|
||||
### 4.2 По таймеру: view()
|
||||
|
||||
Даже если ничего не произошло, Iced вызывает `view()` каждый кадр (60 раз в секунду).
|
||||
Нужно вернуть Iced-виджеты, которые он нарисует.
|
||||
|
||||
**render_element()** — рекурсивная функция, которая превращает Element в Iced-виджет:
|
||||
|
||||
- `Button` → `iced::button(...).on_press(...)`
|
||||
- `Text` → `iced::text("...").size(16).color(...)`
|
||||
- `Panel` → `iced::column[...].spacing(10)`, обёрнутый в контейнер с фоном и рамкой
|
||||
- `Input` → `iced::text_input("placeholder", "value").on_input(...)`
|
||||
- `Image` → `iced::image(path)` или `iced::svg(path)`
|
||||
- Неизвестный тип → просто колонка с детьми
|
||||
|
||||
Каждый виджет оборачивается в **`apply_universal_box_model`**:
|
||||
```text
|
||||
контейнер [margin]
|
||||
контейнер [padding, border, background]
|
||||
scrollable (если overflow: scroll/auto)
|
||||
контейнер [padding]
|
||||
сам виджет
|
||||
```
|
||||
|
||||
**Проблема:** `render_element` создаёт **все** виджеты с нуля каждый кадр, даже если
|
||||
Element не изменился. Iced потом диффит новое дерево со старым — но само построение
|
||||
дерева стоит ~7ms. Это главный резерв оптимизации.
|
||||
|
||||
### 4.3 Iced делает своё дело
|
||||
|
||||
Iced получает дерево `iced::Element`, сравнивает с предыдущим (diff), вычисляет
|
||||
раскладку (layout) и рисует через GPU (wgpu). Всё это без участия нашего кода.
|
||||
|
||||
---
|
||||
|
||||
## Анатомия Element
|
||||
|
||||
```rust
|
||||
Element {
|
||||
type_name: "Button", // что это за элемент
|
||||
properties: [("label", "Click"), ("color", "red"), ...], // его свойства
|
||||
computed_style: ComputedStyle { color: Some(Red), padding: Some(8px), ... }, // вычисленный стиль
|
||||
element_id: ElementId(42), // уникальный ID в дереве
|
||||
content_hash: 0xABCD1234, // хэш содержимого (для кэша виджетов)
|
||||
children: [Element, ...], // дочерние элементы
|
||||
}
|
||||
```
|
||||
|
||||
## Анатомия стилей
|
||||
|
||||
Стили хранятся в `StyleSheet` и работают в три этапа:
|
||||
|
||||
**1. Индекс (`StyleIndex`):** при загрузке все CSS-правила раскладываются по полочкам:
|
||||
```text
|
||||
Правило: "Button.primary#submit { color: red; padding: 10px }"
|
||||
→ by_tag["Button"] = { RuleId(1) }
|
||||
→ by_class["primary"] = { RuleId(1) }
|
||||
→ by_id["submit"] = { RuleId(1) }
|
||||
```
|
||||
|
||||
**2. Поиск:** когда нужно найти стили для элемента `Button.primary#submit`,
|
||||
мы берём пересечение множеств из всех трёх полок. Вместо проверки 500 правил — 3 lookup'а.
|
||||
|
||||
**3. Кэш:** даже если стили найдены, `ComputedStyle::compute()` парсит все свойства
|
||||
(цвет, отступы, шрифты — около 40 полей). Это дорого. `StyleCache` запоминает
|
||||
результат: `hash(type_name, properties, эпоха) → ComputedStyle`. Если элемент
|
||||
не менялся — берём готовый стиль из кэша, не парсим.
|
||||
|
||||
---
|
||||
|
||||
## Событийный цикл на примере слайдера
|
||||
|
||||
```
|
||||
1. Пользователь двигает слайдер громкости
|
||||
2. Iced: SliderChanged(Some("volume"), 75.0)
|
||||
3. GlintApp::update:
|
||||
a. variables["volume"] = Float(75.0)
|
||||
b. tracker.on_variable_changed("volume")
|
||||
→ грязные: ElementId(5) — текст с "$volume", ElementId(12) — ширина от "$volume"
|
||||
c. evaluate_vdom_incr(roots, &{5, 12})
|
||||
→ Element 5: пересчитать текст (новая громкость)
|
||||
→ Element 12: пересчитать ширину
|
||||
→ остальные 48 элементов: не трогать
|
||||
4. GlintApp::view:
|
||||
→ render_element для всех 50 root-элементов
|
||||
→ рекурсивно для всех детей (даже для тех 48, что не менялись)
|
||||
→ Iced получает полностью новое дерево из 200+ виджетов
|
||||
5. Iced: диффит → находит 2 изменения → перерисовывает 2 области
|
||||
```
|
||||
|
||||
**Узкое место:** шаг 4. VDOM пересчитал только 2 элемента из 50 (спасибо ReactiveTracker).
|
||||
Но render_element создаёт виджеты для всех 200+ узлов. Iced потом всё равно диффит
|
||||
и ничего не делает с 198 из них, но время на их создание уже потрачено.
|
||||
68
docs/ru/architecture/02-performance.md
Normal file
68
docs/ru/architecture/02-performance.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Анализ производительности
|
||||
|
||||
Измерения с флагом `--perf` на `desktop.glbc`.
|
||||
|
||||
## Результаты замеров
|
||||
|
||||
**Steady state** (нет событий, idle):
|
||||
|
||||
```
|
||||
VDOM: 6.6ms | Style: 0.3ms | Render: 7.2ms | Total: 14.1ms
|
||||
```
|
||||
|
||||
**При событиях** (перетаскивание слайдера, пиковые значения):
|
||||
|
||||
```
|
||||
VDOM: 17.4ms | Style: 0.8ms | Render: 15.2ms | Total: 33.5ms ⚠️
|
||||
```
|
||||
|
||||
**60 FPS frame budget: 16ms.** В покое укладываемся (14ms), при событиях — нет (до 33ms).
|
||||
|
||||
## Анализ bottleneck'ов
|
||||
|
||||
### Style matching — НЕ bottleneck (0.3-0.8ms)
|
||||
|
||||
Style matching занимает менее 1ms даже на пике. Это результат работы:
|
||||
- **Phase 4** (StyleIndex) — O(K) вместо O(N×M)
|
||||
- **Phase 8** (StyleCache) — мемоизация computed style
|
||||
|
||||
### VDOM eval — основной потребитель (6-17ms)
|
||||
|
||||
В покое ~6.6ms — это полный проход по дереву Element'ов. При событиях до 17ms:
|
||||
- `evaluate_vdom_incr` пересчитывает dirty-элементы (Phase 3)
|
||||
- Каждое событие может делать dirty целые поддеревья
|
||||
- Внутри: `resolve_string`, `resolve_prop`, `compute_cached`, рекурсивный проход
|
||||
|
||||
### Render — второй потребитель (7-15ms)
|
||||
|
||||
**Здесь главный резерв оптимизации.** `render_element` создаёт ВСЕ Iced-виджеты
|
||||
каждый кадр с нуля, даже если Element не изменился. Iced затем диффит новое дерево
|
||||
со старым — но само построение дерева стоит ~7ms.
|
||||
|
||||
### Сценарий: слайдер
|
||||
|
||||
1. `SliderChanged` → `age` и `volume_level` меняются
|
||||
2. `tracker.on_variable_changed` → dirty_set для зависимых элементов
|
||||
3. `evaluate_vdom_incr` пересчитывает dirty-элементы и их детей
|
||||
4. `view()` → `render_element` для ВСЕХ элементов (полный перерендер)
|
||||
5. Итог: VDOM 13ms + Render 14ms = 27ms — пропуск кадра
|
||||
|
||||
Первые несколько кадров после события — самые тяжёлые (VDOM ~13ms), затем
|
||||
стабилизируются (~7ms), так как dirty_set постепенно очищается.
|
||||
|
||||
## Рекомендации
|
||||
|
||||
1. **Phase 9.3 — кэш виджетов** — сократит Render с 7ms до ~0ms для неизменившихся
|
||||
элементов. Если изменился 1 элемент из 50, перерендеривать нужно только его.
|
||||
Это снизит общее время с 14ms до ~7ms в покое.
|
||||
|
||||
2. **Phase 1 — Value enum в стилях** — `ComputedStyle::compute()` и `parse_*()`
|
||||
принимают `&str` и парсят каждое свойство. Если передавать `&Value` — парсинг
|
||||
не нужен. Потенциально ускорит и style matching, и VDOM eval.
|
||||
|
||||
3. **Phase 2 — InternedStr** — сравнения строк (`type_name == "Button"`,
|
||||
`key == "padding-top"`) происходят тысячами за кадр. Замена на сравнение u32
|
||||
даст 1.5-2× в VDOM и render путях.
|
||||
|
||||
4. **Phase 0.3 — flamegraph** — подтвердить гипотезы замерами профилировщика
|
||||
(`perf record`), прежде чем вкладываться в оптимизацию.
|
||||
182
docs/ru/modules/01-types.md
Normal file
182
docs/ru/modules/01-types.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Модуль типов: `src/interpreter/types.rs`
|
||||
|
||||
Базовые типы данных рантайма: интернирование строк, Value, DOM-элементы, плоский VDOM и Document.
|
||||
|
||||
---
|
||||
|
||||
## `InternedStr(u32)`
|
||||
|
||||
Новыйтип-обёртка над `u32`. Компактный идентификатор строки для быстрых сравнений.
|
||||
|
||||
```rust
|
||||
pub struct InternedStr(pub u32);
|
||||
```
|
||||
|
||||
**Методы:**
|
||||
- `from_raw(id: u32) -> Self` — константный конструктор
|
||||
- `raw(&self) -> u32` — сырое значение
|
||||
- `eq_str(&self, other: &str) -> bool` — сравнение со строкой через `Interner::lookup`
|
||||
|
||||
**Используется:** определён, но в горячих путях (Element, style matching, properties) не применяется. Фаза 2 не завершена.
|
||||
|
||||
---
|
||||
|
||||
## `Interner`
|
||||
|
||||
Пул строк с выделением уникальных ID. Каждая строка интернируется один раз.
|
||||
|
||||
```rust
|
||||
pub struct Interner {
|
||||
strings: Vec<String>,
|
||||
map: HashMap<String, u32>,
|
||||
next_id: u32,
|
||||
}
|
||||
```
|
||||
|
||||
- `new()` — пустой интернер
|
||||
- `intern(&mut self, s: &str) -> InternedStr` — получить/создать ID
|
||||
- `lookup(&self, id: InternedStr) -> &str` — получить строку по ID
|
||||
- `intern_or_none(&mut self, s: Option<&str>) -> Option<InternedStr>` — опциональное интернирование
|
||||
|
||||
Хранится в `Document::interner`. Доступен через `with_interner()` (thread_local).
|
||||
|
||||
---
|
||||
|
||||
## `Value`
|
||||
|
||||
Типизированное значение переменной. Замена сырым строкам для устранения parse/format round-trip.
|
||||
|
||||
```rust
|
||||
pub enum Value {
|
||||
Str(CompactString),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
Array(Vec<Value>),
|
||||
None,
|
||||
}
|
||||
```
|
||||
|
||||
**Методы:**
|
||||
- `as_str(&self) -> Option<&str>` — заимствование строки
|
||||
- `to_owned_string(&self) -> CompactString` — форматирование в строку (используется в renderer)
|
||||
|
||||
**Реализовано:** `From<&str>`, `From<String>`, `From<i64>`, `From<f64>`, `From<bool>`, `From<Vec<T>>`.
|
||||
`PartialEq` — Float сравнивается с `f64::EPSILON`.
|
||||
|
||||
**Используется:** `Document::variables`, конверсии `Value↔Dynamic` в rhei.rs.
|
||||
**НЕ используется:** в стилях (`ComputedStyle::compute` всё ещё принимает `&str`, matched_sheets — `HashMap<String, String>`).
|
||||
|
||||
---
|
||||
|
||||
## `Element<'a>`
|
||||
|
||||
Узел VDOM-дерева. Центральный тип — из него строится UI.
|
||||
|
||||
```rust
|
||||
pub struct Element<'a> {
|
||||
pub type_name: &'a str, // "Button", "Panel", "Text", etc.
|
||||
pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>, // (key, value)
|
||||
pub children: Vec<Element<'a>>,
|
||||
pub computed_style: ComputedStyle,
|
||||
pub element_id: ElementId,
|
||||
pub content_hash: u64, // Phase 9.1: хэш для кэша виджетов
|
||||
}
|
||||
```
|
||||
|
||||
**Методы (в types.rs):**
|
||||
- `id(&self) -> Option<&str>` — значение HTML-атрибута `id`
|
||||
- `new(type_name)` — свежий элемент с `element_id: u32::MAX`
|
||||
- `new_with_id(type_name, id)` — с заданным ElementId
|
||||
|
||||
**Методы (в renderer.rs):**
|
||||
- `get_prop(&self, key: &str) -> Option<&str>` — значение свойства по ключу
|
||||
- `push_prop(key, val)` — добавить свойство
|
||||
- `set_prop(key, val)` — установить/перезаписать свойство
|
||||
|
||||
---
|
||||
|
||||
## `ComponentDef<'a>`
|
||||
|
||||
Определение компонента из шаблона.
|
||||
|
||||
```rust
|
||||
pub struct ComponentDef<'a> {
|
||||
pub name: String,
|
||||
pub params: Vec<(String, String)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
}
|
||||
```
|
||||
|
||||
Хранится в `Document::components`. Используется в `evaluate_vdom` при разворачивании
|
||||
компонентов: параметры передаются через переменные, тело компонента вставляется как children.
|
||||
|
||||
---
|
||||
|
||||
## `Document<'a>`
|
||||
|
||||
Полное состояние приложения после загрузки байткода.
|
||||
|
||||
```rust
|
||||
pub struct Document<'a> {
|
||||
pub roots: Vec<Element<'a>>,
|
||||
pub components: HashMap<String, ComponentDef<'a>>,
|
||||
pub variables: HashMap<String, Value>,
|
||||
pub rhei_scripts: Vec<String>,
|
||||
pub stylesheet: StyleSheet,
|
||||
pub interner: Interner,
|
||||
pub tracker: ReactiveTracker,
|
||||
}
|
||||
```
|
||||
|
||||
Создаётся в `Interpreter::run()`. Клонируется при старте Iced-приложения (boot function).
|
||||
Содержит всё необходимое для интерпретации: дерево, стили, переменные, реактивность.
|
||||
|
||||
---
|
||||
|
||||
## `VNode<'a>` и `FlatVDom<'a>`
|
||||
|
||||
Плоское представление VDOM для эффективной сериализации/десериализации.
|
||||
|
||||
```rust
|
||||
pub struct VNode<'a> {
|
||||
pub id: NodeId,
|
||||
pub type_name: &'a str,
|
||||
pub properties: Range<PropertyIdx>,
|
||||
pub children_range: Range<NodeIdx>,
|
||||
pub computed_style: ComputedStyle,
|
||||
pub element_id: ElementId,
|
||||
}
|
||||
|
||||
pub struct FlatVDom<'a> {
|
||||
pub nodes: Vec<VNode<'a>>,
|
||||
pub properties: Vec<(String, String)>,
|
||||
root_indices: Vec<NodeIdx>,
|
||||
}
|
||||
```
|
||||
|
||||
**Методы:**
|
||||
- `new()` — пустой
|
||||
- `from_elements(elements)` — рекурсивно уплощает дерево Element'ов в VNode'ы
|
||||
- `into_elements(self) -> Vec<Element>` — обратная сборка
|
||||
- `get_node(idx)`, `node_count()`, `root_count()`, `root_indices()`
|
||||
|
||||
**Используется:** в тестах (`test_flat_vdom_roundtrip`, `test_flat_vdom_nested`, `test_flat_vdom_empty`).
|
||||
В горячем пути не участвует — VDOM передаётся как `Vec<Element>`.
|
||||
|
||||
---
|
||||
|
||||
## `InterpError`
|
||||
|
||||
Ошибки загрузки байткода.
|
||||
|
||||
```rust
|
||||
pub enum InterpError {
|
||||
BadMagic,
|
||||
UnexpectedEof,
|
||||
InvalidUtf8,
|
||||
UnexpectedPop,
|
||||
}
|
||||
```
|
||||
|
||||
Реализует `Display` и `std::error::Error`. Возвращается из `Interpreter::run()`.
|
||||
482
docs/ru/modules/02-style.md
Normal file
482
docs/ru/modules/02-style.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# Модуль стилей: `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.
|
||||
92
docs/ru/modules/03-reactive.md
Normal file
92
docs/ru/modules/03-reactive.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Модуль реактивности: `src/interpreter/reactive.rs`
|
||||
|
||||
Система отслеживания зависимостей между переменными и элементами VDOM.
|
||||
Позволяет пересчитывать только изменившиеся элементы (Phase 3).
|
||||
|
||||
---
|
||||
|
||||
## `ElementId(u32)`
|
||||
|
||||
Уникальный идентификатор элемента в VDOM-дереве. Раздаётся `ReactiveTracker::alloc_id()`
|
||||
во время загрузки шаблона.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ElementId(pub u32);
|
||||
```
|
||||
|
||||
**Используется как:**
|
||||
- Ключ в `dirty_set: HashSet<ElementId>`
|
||||
- Ключ в `dependencies: HashMap<ElementId, HashSet<String>>`
|
||||
- Поле в `Element::element_id` (связь VDOM → tracker)
|
||||
- Источник для `iced::widget::Id` (Phase 9.2: `format!("ti:{}", id.0)`)
|
||||
- Источник для scrollable_id (`el.element_id.0 as u64`)
|
||||
|
||||
---
|
||||
|
||||
## `ReactiveTracker`
|
||||
|
||||
Граф зависимостей: переменная → список элементов, которые её используют.
|
||||
|
||||
```rust
|
||||
pub struct ReactiveTracker {
|
||||
subscribers: HashMap<String, HashSet<ElementId>>,
|
||||
dependencies: HashMap<ElementId, HashSet<String>>,
|
||||
dirty_set: HashSet<ElementId>,
|
||||
next_id: u32,
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `subscribers` — для каждой переменной: какие ElementId от неё зависят
|
||||
- `dependencies` — для каждого элемента: от каких переменных он зависит (обратная связь)
|
||||
- `dirty_set` — элементы, которые нужно пересчитать в следующем фрейме
|
||||
- `next_id` — счётчик для `alloc_id()`
|
||||
|
||||
### Методы
|
||||
|
||||
| Метод | Описание |
|
||||
|-------|----------|
|
||||
| `new()` | Пустой tracker |
|
||||
| `alloc_id() -> ElementId` | Выделить новый ID, увеличить счётчик |
|
||||
| `add_dependency(element, var_name)` | Зарегистрировать зависимость |
|
||||
| `scan_value(element, value)` | Сканировать строку на `$var` и добавить зависимости |
|
||||
| `on_variable_changed(name)` | Пометить все зависимые элементы как dirty |
|
||||
| `take_dirty_set() -> HashSet<ElementId>` | Забрать dirty_set и очистить |
|
||||
| `is_dirty(id) -> bool` | Проверить, помечен ли элемент |
|
||||
| `reset()` | Очистить все данные |
|
||||
|
||||
### scan_value()
|
||||
|
||||
Парсит строку в поиске `$var_name` паттернов:
|
||||
|
||||
```rust
|
||||
"Hello $name, you are $age years old"
|
||||
// → add_dependency(element, "name")
|
||||
// → add_dependency(element, "age")
|
||||
```
|
||||
|
||||
Используется во время загрузки шаблона для каждой properties строки,
|
||||
содержащей `$`. В результате каждый элемент знает, от каких переменных зависит.
|
||||
|
||||
### Цикл обновления
|
||||
|
||||
```
|
||||
Событие → update() → on_variable_changed("var")
|
||||
→ dirty_set = {element_A, element_B, ...}
|
||||
→ take_dirty_set() → evaluate_vdom_incr(roots, &dirty_set)
|
||||
→ для элементов в dirty_set: пересчитать
|
||||
→ для остальных: вернуть как есть
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Тесты
|
||||
|
||||
| Тест | Что проверяет |
|
||||
|------|--------------|
|
||||
| `test_basic_dependency_tracking` | Два элемента, две переменные, правильность dirty_set |
|
||||
| `test_scan_value` | Парсинг `$var` из строки |
|
||||
| `test_scan_no_vars` | Строка без `$` не создаёт зависимостей |
|
||||
| `test_take_dirty_set` | `take_dirty_set()` очищает внутренний set |
|
||||
| `test_reset` | `reset()` очищает всё |
|
||||
200
docs/ru/modules/04-rhei.md
Normal file
200
docs/ru/modules/04-rhei.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Модуль Rhai: `src/interpreter/rhei.rs`
|
||||
|
||||
Интеграция скриптового движка [Rhai](https://rhai.rs/) — компиляция, кеширование AST и выполнение выражений/скриптов.
|
||||
|
||||
---
|
||||
|
||||
## `RHEI_PREFIX` — префикс Rhai-выражений
|
||||
|
||||
```rust
|
||||
pub const RHEI_PREFIX: &str = "__rhei:";
|
||||
```
|
||||
|
||||
Константа-маркер для свойств элементов, содержимое которых должно интерпретироваться как Rhai-выражение. Используется в `collect_and_precompile()` для выборки свойств вида `!rhei:...`.
|
||||
|
||||
---
|
||||
|
||||
## `RheiContext` — контекст выполнения Rhai
|
||||
|
||||
```rust
|
||||
pub struct RheiContext {
|
||||
engine: Engine,
|
||||
init_ast: AST,
|
||||
scope: RefCell<Scope<'static>>,
|
||||
action_cache: RefCell<HashMap<String, AST>>,
|
||||
expr_cache: RefCell<HashMap<String, AST>>,
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Назначение |
|
||||
|---|---|
|
||||
| `engine` | Настроенный экземпляр `rhai::Engine` |
|
||||
| `init_ast` | Объединённое AST всех скриптов инициализации (включая определения функций) |
|
||||
| `scope` | Общая область видимости (`Scope`), разделяемая между вызовами; обёрнута в `RefCell` для interior mutability |
|
||||
| `action_cache` | Кеш скомпилированных скриптов (действий), ключ — исходный код |
|
||||
| `expr_cache` | Кеш скомпилированных выражений, ключ — исходный код |
|
||||
|
||||
---
|
||||
|
||||
## Конструкторы
|
||||
|
||||
### `new(scripts)`
|
||||
|
||||
```rust
|
||||
pub fn new(scripts: &[String]) -> Self
|
||||
```
|
||||
|
||||
Создаёт контекст через `new_empty()` и сразу прекомпилирует все переданные скрипты вызовом `precompile_scripts()`.
|
||||
|
||||
### `new_empty(scripts)`
|
||||
|
||||
```rust
|
||||
fn new_empty(scripts: &[String]) -> Self
|
||||
```
|
||||
|
||||
1. Создаёт `Engine::new()`.
|
||||
2. Настраивает обработчики `on_print` (вывод в stdout с префиксом `[rhei]`) и `on_debug` (вывод в stderr).
|
||||
3. Компилирует все скрипты и сливает их AST в единое дерево через `merge()`. Ошибки компиляции отдельных блоков логируются, но не прерывают процесс.
|
||||
4. Из объединённого AST создаёт модуль (`Module::eval_ast_as_new`) с пустым скопом — в нём регистрируются глобальные функции, определённые в скриптах. Модуль регистрируется в движке как глобальный (`register_global_module`).
|
||||
5. Инициализирует пустой `Scope`, пустые кеши `action_cache` и `expr_cache`.
|
||||
|
||||
---
|
||||
|
||||
## `sync_scope()` — синхронизация переменных
|
||||
|
||||
```rust
|
||||
pub fn sync_scope(&self, variables: &HashMap<String, Value>)
|
||||
```
|
||||
|
||||
Синхронизирует значения из внешнего `HashMap` в Rhai `Scope`:
|
||||
- Если переменная уже есть в скопе и её значение не изменилось — пропускает.
|
||||
- Если переменная есть — обновляет через `set_value()`.
|
||||
- Если переменной нет — добавляет через `push_dynamic()`.
|
||||
|
||||
Преобразование `Value → Dynamic` выполняется через `value_to_dynamic()`.
|
||||
|
||||
---
|
||||
|
||||
## `initialize()` — инициализация
|
||||
|
||||
```rust
|
||||
pub fn initialize(&self, variables: &mut HashMap<String, Value>)
|
||||
```
|
||||
|
||||
1. Синхронизирует переменные через `sync_scope()`.
|
||||
2. Запускает `init_ast` (объединённое AST всех скриптов) через `run_ast_with_scope()`.
|
||||
3. Обходит все переменные скопа через `iter_raw()` и записывает обратно в `HashMap` те, чьи значения изменились.
|
||||
|
||||
---
|
||||
|
||||
## `eval_expr()` — вычисление выражения
|
||||
|
||||
```rust
|
||||
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, Value>) -> Value
|
||||
```
|
||||
|
||||
1. Синхронизирует переменные.
|
||||
2. Получает (компилирует или берёт из кеша) AST выражения через `get_or_compile_expr()`.
|
||||
3. Выполняет через `eval_ast_with_scope::<Dynamic>()`.
|
||||
4. Преобразует результат `Dynamic → Value` через `dynamic_to_value()`.
|
||||
5. При ошибке возвращает `Value::None`.
|
||||
|
||||
---
|
||||
|
||||
## `eval_condition()` — вычисление условия
|
||||
|
||||
```rust
|
||||
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, Value>) -> bool
|
||||
```
|
||||
|
||||
Аналогичен `eval_expr()`, но типизирован как `bool`. При ошибке возвращает `false`.
|
||||
|
||||
---
|
||||
|
||||
## `execute_action()` — выполнение скрипта
|
||||
|
||||
```rust
|
||||
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, Value>)
|
||||
```
|
||||
|
||||
1. Синхронизирует переменные.
|
||||
2. Получает AST скрипта через `get_or_compile_action()`.
|
||||
3. Выполняет через `run_ast_with_scope()`.
|
||||
4. После выполнения обходит скоп и записывает обратно в `HashMap` изменившиеся переменные.
|
||||
|
||||
---
|
||||
|
||||
## Кеширование AST
|
||||
|
||||
### `get_or_compile_action(script)`
|
||||
|
||||
```rust
|
||||
fn get_or_compile_action(&self, script: &str) -> Option<AST>
|
||||
```
|
||||
|
||||
Проверяет `action_cache`. При промахе компилирует через `engine.compile()`, сохраняет в кеш.
|
||||
|
||||
### `get_or_compile_expr(expr)`
|
||||
|
||||
```rust
|
||||
fn get_or_compile_expr(&self, expr: &str) -> Option<AST>
|
||||
```
|
||||
|
||||
Проверяет `expr_cache`. При промахе компилирует через `engine.compile_expression()`, сохраняет в кеш.
|
||||
|
||||
Оба метода при ошибке компиляции логируют её и возвращают `None`.
|
||||
|
||||
---
|
||||
|
||||
## Пакетная прекомпиляция
|
||||
|
||||
```rust
|
||||
pub fn precompile_scripts(&self, scripts: &[String])
|
||||
pub fn precompile_actions(&self, actions: &[String])
|
||||
pub fn precompile_exprs(&self, exprs: &[String])
|
||||
pub fn precompile_all_from_doc(&self, doc: &super::Document)
|
||||
```
|
||||
|
||||
| Метод | Действие |
|
||||
|---|---|
|
||||
| `precompile_scripts` | Компилирует каждый скрипт как действие |
|
||||
| `precompile_actions` | То же, что `precompile_scripts` (алиас) |
|
||||
| `precompile_exprs` | Компилирует каждое выражение |
|
||||
| `precompile_all_from_doc` | Компилирует все `doc.rhei_scripts` и рекурсивно обходит дерево элементов |
|
||||
|
||||
### `collect_and_precompile()`
|
||||
|
||||
```rust
|
||||
fn collect_and_precompile(el: &super::Element, ctx: &RheiContext)
|
||||
```
|
||||
|
||||
Рекурсивно обходит дерево `Element`:
|
||||
- Для свойств, начинающихся с `__on:*` и непустых — компилирует как действие.
|
||||
- Для свойств, начинающихся с `RHEI_PREFIX` (`!rhei:`) — компилирует оставшуюся часть как выражение.
|
||||
|
||||
---
|
||||
|
||||
## Конвертация типов
|
||||
|
||||
### `value_to_dynamic(v: &Value) -> Dynamic`
|
||||
|
||||
```rust
|
||||
Value::Int(i) → Dynamic::from(*i)
|
||||
Value::Float(f) → Dynamic::from(*f)
|
||||
Value::Bool(b) → Dynamic::from(*b)
|
||||
Value::Str(s) → str_to_dynamic(s)
|
||||
Value::None → Dynamic::UNIT
|
||||
Value::Array(a) → Dynamic::from_iter(value_to_dynamic каждого элемента)
|
||||
```
|
||||
|
||||
### `dynamic_to_value(d: &Dynamic) -> Value`
|
||||
|
||||
Проверяет тип через `is_string()`, `is_int()`, `is_float()`, `is_bool()`, `is_array()` в порядке приоритета. Если тип не распознан — возвращает `Value::None`.
|
||||
|
||||
### `str_to_dynamic(s: &str) -> Dynamic`
|
||||
|
||||
Эвристический парсер строки, пробует последовательно:
|
||||
1. `s.parse::<i64>()` — целое число
|
||||
2. `s.parse::<f64>()` — дробное число
|
||||
3. `s.parse::<bool>()` — булево значение
|
||||
4. Иначе — `Dynamic::from(s)` как строка
|
||||
537
docs/ru/modules/05-renderer.md
Normal file
537
docs/ru/modules/05-renderer.md
Normal file
@@ -0,0 +1,537 @@
|
||||
# Модуль рендерера: `src/renderer.rs`
|
||||
|
||||
Преобразует дерево `Element` в виджеты Iced. Отвечает за построение иерархии виджетов, применение боксовой модели, обработку псевдоклассов `:hover`/`:active`, позиционирование (fixed, absolute, sticky) и рендеринг всех встроенных типов элементов.
|
||||
|
||||
---
|
||||
|
||||
## Импорты
|
||||
|
||||
```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` и `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
|
||||
```
|
||||
|
||||
`thread_local`-кеш для `iced::widget::Id`. Ключ — кортеж `(element_id, префикс)`. Строка формируется как `"{prefix}:{key}"` и «протекает» через `Box::leak`, чтобы получить `&'static str`. Используется для `scrollable::id` (префикс `"sc"`) и `text_input::id` (префикс `"ti"`).
|
||||
|
||||
---
|
||||
|
||||
## Методы `Element`
|
||||
|
||||
### `get_prop()`
|
||||
```rust
|
||||
impl<'a> Element<'a> {
|
||||
#[inline]
|
||||
pub fn get_prop(&self, key: &str) -> Option<&str>
|
||||
}
|
||||
```
|
||||
Ищет свойство по ключу в `self.properties`. Возвращает значение или `None`.
|
||||
|
||||
### `push_prop()`
|
||||
```rust
|
||||
pub fn push_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V)
|
||||
```
|
||||
Добавляет пару `(key, val)` в `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)
|
||||
```
|
||||
Устанавливает свойство: если ключ уже существует — заменяет значение, иначе — добавляет новую пару.
|
||||
|
||||
---
|
||||
|
||||
## `extract_var_binding()`
|
||||
|
||||
```rust
|
||||
fn extract_var_binding(el: &Element, prop: &str) -> Option<String>
|
||||
```
|
||||
|
||||
Ищет свойство вида `__bind:<prop>` и возвращает его значение. Используется для реактивной привязки переменных: `__bind:value` для `Input`, `Toggle`, `Slider`.
|
||||
|
||||
---
|
||||
|
||||
## `collect_hover_active()`
|
||||
|
||||
```rust
|
||||
pub fn collect_hover_active<'a>(
|
||||
el: &'a Element,
|
||||
stylesheet: &StyleSheet,
|
||||
) -> (HashMap<String, String>, HashMap<String, String>)
|
||||
```
|
||||
|
||||
Собирает CSS-свойства для псевдоклассов `:hover` и `:active` для элемента. Вызывает `stylesheet.matching_pseudo_rules()` дважды — для `"hover"` и `"active"`. Возвращает кортеж `(hover_props, active_props)`. Используется в `render_element()` и `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>
|
||||
```
|
||||
|
||||
Оборачивает произвольный виджет в `button` для поддержки `:hover`/`:active` стилей. Условия срабатывания:
|
||||
- Есть хотя бы один `hover` или `active` стиль;
|
||||
- У элемента есть обработчик `__on:click`.
|
||||
|
||||
Кнопке назначается `on_press(Message::EventTriggered(...))`. В замыкании `style()` подставляются `apply_overrides` в зависимости от `button::Status`:
|
||||
- `Hovered` → `hover_props`;
|
||||
- `Pressed` → `active_props`, если их нет — `hover_props`.
|
||||
|
||||
Применяется **только для не-Button и не-Input** элементов (строка 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>>
|
||||
```
|
||||
|
||||
**Главная публичная функция рендеринга.** Возвращает `None` если `display: none`.
|
||||
|
||||
### Общая логика для всех элементов
|
||||
|
||||
1. Собираются `hover_props`, `active_props` через `collect_hover_active()`.
|
||||
2. Определяется тип позиционирования: `is_fixed`, `is_absolute`, `is_sticky`.
|
||||
3. Если заданы `left` + `right` без `width` — `width = Fill`. Если `top` + `bottom` без `height` — `height = Fill`.
|
||||
4. `flex-grow` преобразуется в `FillPortion(grow_value)` по оси родителя.
|
||||
5. Наследуются `current_color` и `current_font_size`.
|
||||
|
||||
### Ветка `"Window"`
|
||||
|
||||
```rust
|
||||
if el.type_name == "Window"
|
||||
```
|
||||
|
||||
- Создаётся `column` со `spacing` (по умолчанию 12px).
|
||||
- Рендерятся дети через `render_children()`.
|
||||
- Применяется `apply_universal_box_model(is_window = true, scrollable_id = window_id)` — окно всегда скроллируемое.
|
||||
- Формируется `iced::widget::stack`:
|
||||
1. Основной поток (main_flow)
|
||||
2. `abs_layers`
|
||||
3. `sticky_layers`
|
||||
4. `fixed_layers`
|
||||
|
||||
Итоговая структура:
|
||||
```
|
||||
stack[
|
||||
container[ scrollable[ container[ column[...] ] ] ]
|
||||
...abs-слои
|
||||
...sticky-слои
|
||||
...fixed-слои
|
||||
]
|
||||
```
|
||||
|
||||
### Ветка `"Panel"`
|
||||
|
||||
Делегирует `render_panel()`. Если есть `abs_layers` — оборачивает в `stack`.
|
||||
|
||||
### Ветка `"Button"`
|
||||
|
||||
Делегирует `render_button()`. Если есть `abs_layers` или `sticky_layers` — оборачивает в `stack`.
|
||||
|
||||
### Ветка `"Input"`
|
||||
|
||||
Делегирует `render_input()` с передачей `hover_props` и `active_props`.
|
||||
|
||||
### Текстовые виджеты: `"Title"`, `"Header"`, `"Text"`, `"Label"`, `"#text"`
|
||||
|
||||
```rust
|
||||
"Title" | "Header" => ...
|
||||
"Text" | "Label" | "#text" => ...
|
||||
```
|
||||
|
||||
- Читают свойство `text` (или пустая строка).
|
||||
- Создают `iced::widget::text` с размером шрифта (24 для Title/Header, 16 для Text).
|
||||
- Применяют `color`, `font_weight` (Light ≤399, Normal 400–599, Bold 600–799, ExtraBold ≥800), `text_align`, `line_height` (с `Wrapping::Word`).
|
||||
- Для Title/Header размер по умолчанию 24px, для Text — 16px.
|
||||
|
||||
### `"Image"`
|
||||
|
||||
- Читает `src`. Если путь начинается с `fs:` — обрезает префикс.
|
||||
- `.svg` → `svg::Handle`, иначе `image::Viewer`.
|
||||
- Размер изображения вычисляется с вычетом padding и border-width.
|
||||
- Для растровых изображений применяется `border_radius`.
|
||||
|
||||
### `"Icon"`
|
||||
|
||||
Выводит символ `🔹` как текст размера 18px (или `current_font_size`). Заглушка.
|
||||
|
||||
### `"Toggle"`
|
||||
|
||||
Делегирует `render_toggle()`.
|
||||
|
||||
### `"Slider"`
|
||||
|
||||
Делегирует `render_slider()`.
|
||||
|
||||
### `"ProgressBar"`
|
||||
|
||||
```rust
|
||||
progress_bar(0.0..=100.0, value)
|
||||
```
|
||||
Свойство `value` парсится как `f32`.
|
||||
|
||||
### `"Divider"`, `"Separator"`
|
||||
|
||||
```rust
|
||||
iced::widget::rule::horizontal(1)
|
||||
```
|
||||
Горизонтальная линия толщиной 1px.
|
||||
|
||||
### Ветка по умолчанию (неизвестный тип)
|
||||
|
||||
- Создаётся `column` со `spacing` (10px).
|
||||
- Рендерятся дети.
|
||||
- Если есть `abs_layers` — оборачиваются в `stack`.
|
||||
- Применяется `apply_universal_box_model()`.
|
||||
- Если есть `sticky_layers` — накладываются поверх через `stack`.
|
||||
- Возвращается `None` (элемент уже записан в `final_widget_opt`).
|
||||
|
||||
### Постобработка для не-Button и не-Input
|
||||
|
||||
```rust
|
||||
if el.type_name != "Button" && el.type_name != "Input" {
|
||||
final_widget_opt = make_hoverable(...);
|
||||
}
|
||||
```
|
||||
|
||||
### Обработка позиционирования
|
||||
|
||||
После получения `final_widget`:
|
||||
|
||||
- **Fixed** → `wrap_fixed_position()` → кладётся в `fixed_layers`, возвращается `None`.
|
||||
- **Absolute** → `wrap_fixed_position()` → кладётся в `abs_layers`, возвращается `None`.
|
||||
- **Sticky** → если `scroll_y > threshold`, элемент перекладывается в `sticky_layers`, а на его место вставляется пустой `spacer` высотой `estimate_element_height()`. Иначе элемент остаётся на месте.
|
||||
|
||||
---
|
||||
|
||||
## `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>>
|
||||
```
|
||||
|
||||
Рекурсивно вызывает `render_element()` для каждого ребёнка. Фильтрует `None` (display: none). Возвращает вектор отрендеренных элементов.
|
||||
|
||||
---
|
||||
|
||||
## `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>
|
||||
```
|
||||
|
||||
Рендерит `Panel` — контейнер с тремя режимами раскладки:
|
||||
|
||||
### Row
|
||||
|
||||
```rust
|
||||
LayoutDirection::Row
|
||||
```
|
||||
`iced::widget::row` с `spacing` (10px). `align_y` из `cs.align_items` или `Alignment::Center` по умолчанию.
|
||||
|
||||
### Column
|
||||
|
||||
```rust
|
||||
LayoutDirection::Column
|
||||
```
|
||||
`iced::widget::column` с `spacing`. `align_x` из `cs.align_items`.
|
||||
|
||||
### Grid
|
||||
|
||||
```rust
|
||||
LayoutDirection::Grid
|
||||
```
|
||||
Столбцы (`column`), внутри каждого — строка (`row`). Количество колонок из свойства `columns` (по умолчанию 3). Каждый ряд — чанк по `cols` детей.
|
||||
|
||||
Во всех режимах:
|
||||
- Если есть `abs_layers` — они оборачиваются в `stack` внутри контента.
|
||||
- После контента применяется `apply_universal_box_model()`.
|
||||
- `sticky_layers` накладываются поверх через `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>
|
||||
```
|
||||
|
||||
- Если нет детей — читает `label` (по умолчанию `"Button"`) и создаёт `text`.
|
||||
- Если есть дети — рендерит их в `row` со `spacing = 8`.
|
||||
- Padding: по умолчанию 8px по вертикали, 16px по горизонтали. С авто-сжатием если `padding + border > width/height`.
|
||||
- `on_press` из `__on:click`.
|
||||
- Стиль: через `get_button_style()` с динамическими `hover`/`active` переопределениями из `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>
|
||||
```
|
||||
|
||||
- Читает `label` (опционально) и `value` (парсится как `bool`, по умолчанию `false`).
|
||||
- Извлекает `__bind:value` для реактивного биндинга.
|
||||
- Создаёт `checkbox`, на `on_toggle` отправляет `Message::ToggleChanged`.
|
||||
- Если есть label — оборачивает `row![checkbox, label]` с `spacing=8` и `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>
|
||||
```
|
||||
|
||||
- Читает `placeholder` (по умолчанию `"Type here…"`) и `value`.
|
||||
- Извлекает `__bind:value`.
|
||||
- Создаёт `text_input` с `padding` из `get_padding(cs, 10.0)`.
|
||||
- Назначает `id` через `get_or_create_widget_id(el.element_id.0, "ti")`.
|
||||
- Если есть hover/active стили — применяет динамический `style()` с `apply_overrides`.
|
||||
- Иначе — статический стиль через `get_text_input_style()`.
|
||||
|
||||
---
|
||||
|
||||
## `render_slider()`
|
||||
|
||||
```rust
|
||||
fn render_slider<'a>(
|
||||
el: &'a Element,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer>
|
||||
```
|
||||
|
||||
- Читает `value` (парсится как `f32`, по умолчанию 0.0).
|
||||
- Извлекает `__bind:value`.
|
||||
- Создаёт `slider` в диапазоне `0.0..=100.0`.
|
||||
- На изменение отправляет `Message::SliderChanged`.
|
||||
|
||||
---
|
||||
|
||||
## Вспомогательные функции
|
||||
|
||||
### `get_padding()`
|
||||
|
||||
```rust
|
||||
fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding
|
||||
```
|
||||
|
||||
Собирает `padding` из `ComputedStyle` с учётом `padding-top/right/bottom/left`. Если `padding + border * 2` превышает фиксированные `width`/`height` — масштабирует padding пропорционально (авто-сжатие).
|
||||
|
||||
### `get_margin()`
|
||||
|
||||
```rust
|
||||
fn get_margin(cs: &ComputedStyle) -> iced::Padding
|
||||
```
|
||||
|
||||
Собирает `margin` из `ComputedStyle` с учётом `margin-top/right/bottom/left`. База — `cs.margin` (по умолчанию 0).
|
||||
|
||||
### `get_button_style()`
|
||||
|
||||
```rust
|
||||
fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style
|
||||
```
|
||||
|
||||
Формирует `button::Style` из `ComputedStyle`. В зависимости от `status`:
|
||||
- `Hovered` — фон светлее на 15% (`* 1.15`);
|
||||
- `Pressed` — фон темнее на 15% (`* 0.85`);
|
||||
- `Active` — без изменений.
|
||||
|
||||
### `get_text_input_style()`
|
||||
|
||||
```rust
|
||||
fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style
|
||||
```
|
||||
|
||||
Формирует `text_input::Style` из `ComputedStyle`: `background`, `border`, `icon`, `placeholder`, `value`, `selection`. Значения по умолчанию — тёмная тема.
|
||||
|
||||
### `estimate_element_height()`
|
||||
|
||||
```rust
|
||||
fn estimate_element_height(cs: &ComputedStyle) -> f32
|
||||
```
|
||||
|
||||
Приблизительно вычисляет высоту элемента для sticky-спейсера: `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>
|
||||
```
|
||||
|
||||
Оборачивает виджет в `container` с `padding-top` из `cs.top`. Ширина `Fill`. Используется для sticky-элементов, когда `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>
|
||||
```
|
||||
|
||||
Оборачивает виджет в `container` с `width = Fill`, `height = Fill` и выравниванием (`align_x`, `align_y`) на основе установленных `top`/`bottom`/`left`/`right`. Padding выставляется соответственно. Используется для `fixed` и `absolute` позиционирования.
|
||||
|
||||
### `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>
|
||||
```
|
||||
|
||||
Применяет боксовую модель к любому виджету. **Порядок обёртки:**
|
||||
|
||||
```
|
||||
outer_container (margin) ← если есть margin и !is_window
|
||||
inner_container (bg, border, clip)
|
||||
scrollable ← если overflow_x/overflow_y = Scroll/Auto
|
||||
container (padding)
|
||||
widget
|
||||
```
|
||||
|
||||
#### Этапы
|
||||
|
||||
1. **Padding**: `container(widget).padding(get_padding(cs, default_padding))`.
|
||||
2. **Overflow**: если `overflow-y` = Scroll/Auto — добавляется `scrollable` с направлением `Vertical` (или `Both` если `overflow-x` тоже Scroll/Auto). Для Window `overflow-y` по умолчанию `Auto`. Скроллу назначается `id` и `on_scroll`.
|
||||
3. **Clip**: если `overflow = Hidden` — `container.clip(true)`.
|
||||
4. **Фон, рамка, скругление**: `container.style(...)` с `Background`, `Border`.
|
||||
5. **Ширина/высота**: для Window — `Fill`/`Fill`; иначе — из `cs.width`, `cs.height`, `cs.max_width`, `cs.max_height`.
|
||||
6. **Выравнивание контента**: `align_x` из `cs.content_align`.
|
||||
7. **Margin**: если есть margin — внешний `container` с `padding = margin`.
|
||||
|
||||
---
|
||||
|
||||
## Структура дерева виджетов (Widget Tree)
|
||||
|
||||
Для типичного окна (`Window`) дерево выглядит так:
|
||||
|
||||
```
|
||||
stack[
|
||||
container (Window) [Fill, Fill]
|
||||
scrollable [id="sc:window_id"]
|
||||
container [bg, border, padding]
|
||||
column [spacing]
|
||||
...дочерние элементы...
|
||||
container (fixed) ← слой fixed
|
||||
container (absolute) ← слой absolute
|
||||
container (sticky) ← слой sticky
|
||||
]
|
||||
```
|
||||
|
||||
Для `Panel`:
|
||||
|
||||
```
|
||||
stack[
|
||||
container [bg, border, margin]
|
||||
scrollable (если overflow)
|
||||
container [padding]
|
||||
row | column | grid
|
||||
...дочерние элементы...
|
||||
container (sticky) ← слой sticky, поверх boxed-контента
|
||||
]
|
||||
```
|
||||
|
||||
Для неизвестных элементов — аналогично Panel, но abs-слои внутри скролла, sticky — поверх.
|
||||
|
||||
Для простых элементов (Text, Image, Toggle, Slider, ProgressBar, Divider):
|
||||
|
||||
```
|
||||
container [bg, border, margin, padding]
|
||||
scrollable (если overflow)
|
||||
container [padding]
|
||||
text | image | checkbox | slider | progress_bar | rule
|
||||
```
|
||||
305
docs/ru/modules/06-mod.md
Normal file
305
docs/ru/modules/06-mod.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# Модуль `interpreter` — ядро интерпретатора байткода
|
||||
|
||||
## Структура модуля
|
||||
|
||||
```
|
||||
interpreter/
|
||||
├── mod.rs — Интерпретатор: парсинг байткода, VDOM, стили
|
||||
├── opcodes.rs — Определения opcode (OP_ELEM_PUSH, OP_IF, OP_EACH, …)
|
||||
├── reactive.rs — ReactiveTracker, ElementId — отслеживание грязных узлов
|
||||
├── reader.rs — Reader — чтение байткода (str_ref, i64, f64, value, …)
|
||||
├── rhei.rs — RheiContext — выполнение Rhai-скриптов и выражений
|
||||
├── style.rs — StyleSheet, AncestorInfo, ComputedStyle, StructuralContext
|
||||
└── types.rs — Element, Document, ComponentDef, Value, FlatVDom, InterpError
|
||||
```
|
||||
|
||||
## `Interpreter` (пустая структура)
|
||||
|
||||
```rust
|
||||
pub struct Interpreter;
|
||||
```
|
||||
|
||||
Структура не содержит полей — все методы статические. Служит пространством имён для функций интерпретации.
|
||||
|
||||
## `Interpreter::run()` — первичный парсинг байткода
|
||||
|
||||
```rust
|
||||
pub fn run<'a>(bytecode: &'a [u8]) -> Result<Document<'a>, InterpError>
|
||||
```
|
||||
|
||||
1. Проверяет магическое число (`bytecode[..4] == MAGIC`).
|
||||
2. Создаёт `Reader`, `HashMap` для `variables` и `components`, `Vec` для `rhei_scripts`, пустой `StyleSheet` и `ReactiveTracker`.
|
||||
3. Вызывает `parse_block_elements()` для корневого уровня, который читает поток опкодов и строит дерево `Element`.
|
||||
4. После парсинга вызывает `stylesheet.build_index()`.
|
||||
5. Возвращает `Document { roots, components, variables, rhei_scripts, stylesheet, interner, tracker }`.
|
||||
|
||||
## `parse_block_elements()` — рекурсивный парсинг блока
|
||||
|
||||
```rust
|
||||
fn parse_block_elements<'a>(
|
||||
r: &mut Reader<'a>,
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &mut HashMap<String, ComponentDef<'a>>,
|
||||
rhei_scripts: &mut Vec<String>,
|
||||
stylesheet: &mut SS,
|
||||
tracker: &mut ReactiveTracker,
|
||||
is_root: bool,
|
||||
) -> Result<Vec<Element<'a>>, InterpError>
|
||||
```
|
||||
|
||||
Читает опкоды в цикле, используя стек для построения вложенности (`stack: Vec<Element>`). На `OP_END_BLOCK` завершает текущий блок (если `!is_root`).
|
||||
|
||||
### Обрабатываемые опкоды
|
||||
|
||||
| Опкод | Действие |
|
||||
|---|---|
|
||||
| `OP_ELEM_PUSH` | Создаёт `Element` с `tracker.alloc_id()`, кладёт на стек |
|
||||
| `OP_ELEM_POP` | Снимает элемент со стека, вызывает `attach()` |
|
||||
| `OP_GLOBAL` / `OP_LET` | Читает имя и значение, вставляет в `variables` |
|
||||
| `OP_SINGLETON` | Пропускает данные синглтона |
|
||||
| `OP_CONTENT` | Текстовое содержимое — свойство `text`. Если `OP_PROP_RHEI` — префикс `!rhei:` |
|
||||
| `OP_PROP_STR`, `OP_PROP_VAR`, `OP_PROP_INT`, `OP_PROP_FLOAT`, `OP_PROP_BOOL`, `OP_PROP_RHEI`, `OP_PROP_UNIT`, `OP_PROP_CALL`, `OP_PROP_IDENT`, `OP_PROP_FSPATH`, `OP_PROP_COLOR` | Читает ключ и значение, вызывает `el.push_prop()` |
|
||||
| `OP_RHEI_BLK` | На корневом уровне без родителя — скрипт; иначе — элемент `#text` с `!rhei:` |
|
||||
| `OP_COMPONENT` | Читает имя, параметры, рекурсивно парсит дочерний блок; сохраняет `ComponentDef` |
|
||||
| `OP_IF` | Читает условие, парсит true-блок, проверяет `has_else`, парсит false-блок. Создаёт `@if` с `@else` как последним child |
|
||||
| `OP_EACH` | Читает имя переменной, источник (массив или `$var` или Rhai), парсит блок-шаблон. Создаёт `@each` |
|
||||
| `OP_ON` | Читает имя события, аргументы, ищет `OP_RHEI_BLK` — скрипт обработчика; устанавливает `__on:{event}` |
|
||||
| `OP_STYLE_RULE` | Читает селектор и свойства, вызывает `stylesheet.add_rule()` |
|
||||
|
||||
### `attach()`
|
||||
|
||||
```rust
|
||||
fn attach<'a>(stack: &mut Vec<Element<'a>>, roots: &mut Vec<Element<'a>>, el: Element<'a>)
|
||||
```
|
||||
|
||||
Если стек не пуст — добавляет в `parent.children`, иначе в `roots`.
|
||||
|
||||
---
|
||||
|
||||
## `evaluate_vdom()` — сборка VDOM (точка входа)
|
||||
|
||||
```rust
|
||||
pub fn evaluate_vdom<'a>(
|
||||
templates: &[Element<'a>],
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
) -> Vec<Element<'a>>
|
||||
```
|
||||
|
||||
Делегирует `evaluate_vdom_incr()` с пустым `dirty_set` — полный пересчёт.
|
||||
|
||||
### `evaluate_vdom_flat()`
|
||||
|
||||
```rust
|
||||
pub fn evaluate_vdom_flat<'a>(...) -> FlatVDom<'a>
|
||||
```
|
||||
|
||||
Оборачивает `evaluate_vdom()` и конвертирует результат через `FlatVDom::from_elements()`.
|
||||
|
||||
---
|
||||
|
||||
## `evaluate_vdom_incr()` — инкрементальная сборка VDOM
|
||||
|
||||
```rust
|
||||
pub fn evaluate_vdom_incr<'a>(
|
||||
templates: &[&Element<'a>],
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
dirty_set: &HashSet<ElementId>,
|
||||
) -> Vec<Element<'a>>
|
||||
```
|
||||
|
||||
### Предварительная обработка
|
||||
|
||||
1. **`sibling_infos`** — для каждого элемента из `templates` создаётся `AncestorInfo` (type_name, id, classes). Нужен для структурных псевдоклассов (CSS `:nth-child`, `:first-of-type` и т.д.).
|
||||
|
||||
2. **`type_counts`** / **`type_seen`** — подсчёт общего числа элементов каждого типа и счётчик для `StructuralContext`.
|
||||
|
||||
### Основной цикл по `templates`
|
||||
|
||||
Для каждого элемента вычисляется `StructuralContext`:
|
||||
|
||||
```rust
|
||||
let structural = StructuralContext {
|
||||
sibling_index: i,
|
||||
sibling_total: templates.len(),
|
||||
type_index: *type_idx,
|
||||
type_total,
|
||||
has_children: ...,
|
||||
is_root: ancestors.is_empty(),
|
||||
};
|
||||
```
|
||||
|
||||
#### `@if`
|
||||
|
||||
- Читает `condition`, вызывает `evaluate_condition()`.
|
||||
- Проходит по `child` элементам: если `@else` — активна когда условие ложно; иначе активна когда истинно.
|
||||
- Рекурсивно вызывает `evaluate_vdom_incr()` для активной ветки.
|
||||
|
||||
#### `@each`
|
||||
|
||||
- Получает `var_name` и `source`.
|
||||
- Разрешает источник: если с префиксом `!rhei:` — вызывает `normalize_rhai_array()`, иначе — `resolve_string()`.
|
||||
- Разбивает результат по `,`, для каждого элемента:
|
||||
- Вставляет `var_name` в `variables`, рекурсивно обходит шаблон, восстанавливает предыдущее значение переменной.
|
||||
|
||||
#### Обычный элемент / Компонент
|
||||
|
||||
- Если `el.type_name` найден в `components`:
|
||||
1. Собирает аргументы из параметров компонента через `resolve_prop()`.
|
||||
2. Сохраняет старые значения переменных, вставляет новые.
|
||||
3. Создаёт `vcomp`, копирует свойства, разрешая их через `resolve_prop()` и `resolve_string()`.
|
||||
4. Вычисляет стили: `collect_matching_styles()` → `stylesheet.compute_cached()`.
|
||||
5. Строит chain предков: `build_ancestor_chain()`.
|
||||
6. Рекурсивно обходит `comp.children`.
|
||||
7. Вычисляет `content_hash`.
|
||||
8. Восстанавливает переменные.
|
||||
- Иначе (обычный элемент):
|
||||
1. Создаёт `vnode`.
|
||||
2. Разрешает свойства: `__on:` → `resolve_string()`, `$var` → `__bind:{key}`, остальные → `resolve_prop()`.
|
||||
3. Вычисляет стили через `collect_matching_styles()` + `compute_cached()`.
|
||||
4. Строит chain предков, рекурсивно обходит `el.children`.
|
||||
5. Вычисляет `content_hash`.
|
||||
|
||||
---
|
||||
|
||||
## `compute_content_hash()`
|
||||
|
||||
```rust
|
||||
fn compute_content_hash(el: &Element) -> u64
|
||||
```
|
||||
|
||||
Хеш элемента для кэширования. Учитывает:
|
||||
- `type_name`
|
||||
- Все пары ключ-значение из `properties`
|
||||
- Рекурсивно `content_hash` дочерних элементов (`children`)
|
||||
|
||||
Использует `DefaultHasher`.
|
||||
|
||||
---
|
||||
|
||||
## `build_ancestor_chain()`
|
||||
|
||||
```rust
|
||||
fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec<AncestorInfo>
|
||||
```
|
||||
|
||||
Копирует текущий `ancestors`, добавляет `AncestorInfo` для текущего элемента (type_name, id, classes). Возвращает расширенную цепочку для передачи при рекурсивном обходе детей.
|
||||
|
||||
---
|
||||
|
||||
## `collect_matching_styles()`
|
||||
|
||||
```rust
|
||||
fn collect_matching_styles<'a>(
|
||||
el: &Element,
|
||||
stylesheet: &'a SS,
|
||||
active_pseudo: &[&str],
|
||||
structural: &StructuralContext,
|
||||
ancestors: &[AncestorInfo],
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
) -> Vec<&'a HashMap<String, String>>
|
||||
```
|
||||
|
||||
Извлекает `id`, `classes`, все атрибуты элемента, делегирует `stylesheet.matching_rules()` с полным контекстом для CSS-селекторов.
|
||||
|
||||
---
|
||||
|
||||
## `resolve_string()` — подстановка `$var`
|
||||
|
||||
```rust
|
||||
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str>
|
||||
```
|
||||
|
||||
- Если в строке нет `$` — возвращает `Cow::Borrowed(val)` (без аллокаций).
|
||||
- Иначе обходит строку посимвольно. После `$` собирает имя переменной (буквы, цифры, `_`), ищет в `scope`, подставляет значение. Если переменная не найдена — оставляет `$var` как есть.
|
||||
|
||||
## `resolve_prop()` — разрешение значения свойства
|
||||
|
||||
```rust
|
||||
fn resolve_prop(v: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> String
|
||||
```
|
||||
|
||||
- Если начинается с `!rhei:` — вызывает `rhei.eval_expr()`.
|
||||
- Иначе — `resolve_string()`.
|
||||
|
||||
## `evaluate_condition()` — вычисление условия `@if`
|
||||
|
||||
```rust
|
||||
fn evaluate_condition(cond: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> bool
|
||||
```
|
||||
|
||||
- Если `!rhei:` — `rhei.eval_condition()`.
|
||||
- Иначе:
|
||||
1. Удаляет фигурные скобки `{...}`.
|
||||
2. Выполняет `resolve_string()`.
|
||||
3. Проверяет `is_truthy_str()`, затем `false`/`0`/пусто.
|
||||
4. Пытается разобрать числовые операторы (`>=`, `<=`, `>`, `<`, `==`, `!=`).
|
||||
|
||||
## `is_truthy()` / `is_truthy_str()` — приведение к bool
|
||||
|
||||
```rust
|
||||
fn is_truthy(v: &Value) -> bool
|
||||
fn is_truthy_str(s: &str) -> bool
|
||||
```
|
||||
|
||||
- `Value::Bool` — по значению.
|
||||
- `Value::Int` — ненулевой.
|
||||
- `Value::Float` — ненулевой.
|
||||
- `Value::Str` — делегирует `is_truthy_str()`.
|
||||
- `Value::None` — `false`.
|
||||
- `Value::Array` — не пустой.
|
||||
- Строка: `""`, `"false"`, `"0"`, `"null"` → `false`; `"true"`, `"1"` → `true`; иначе парсит как `f64`.
|
||||
|
||||
## `normalize_rhai_array()` — нормализация Rhai-массива
|
||||
|
||||
```rust
|
||||
fn normalize_rhai_array(s: &str) -> String
|
||||
```
|
||||
|
||||
Обрезает `[...]`, разбивает по `,`, обрезает пробелы, соединяет через `,`. Используется в `@each` для приведения Rhai-массива к формату, ожидаемому циклом.
|
||||
|
||||
---
|
||||
|
||||
## `Document`
|
||||
|
||||
```rust
|
||||
pub struct Document<'a> {
|
||||
pub roots: Vec<Element<'a>>,
|
||||
pub components: HashMap<String, ComponentDef<'a>>,
|
||||
pub variables: HashMap<String, Value>,
|
||||
pub rhei_scripts: Vec<String>,
|
||||
pub stylesheet: StyleSheet,
|
||||
pub interner: Interner,
|
||||
pub tracker: ReactiveTracker,
|
||||
}
|
||||
```
|
||||
|
||||
## `Element`
|
||||
|
||||
```rust
|
||||
pub struct Element<'a> {
|
||||
pub type_name: &'a str,
|
||||
pub properties: Vec<(String, String)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
pub element_id: ElementId,
|
||||
pub computed_style: Option<ComputedStyle>,
|
||||
pub content_hash: u64,
|
||||
}
|
||||
```
|
||||
|
||||
## `ComponentDef`
|
||||
|
||||
```rust
|
||||
pub struct ComponentDef<'a> {
|
||||
pub name: String,
|
||||
pub params: Vec<(String, String)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
}
|
||||
```
|
||||
109
docs/ru/modules/07-app-perf.md
Normal file
109
docs/ru/modules/07-app-perf.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# App и Perf модули
|
||||
|
||||
## `src/app.rs` — GlintApp
|
||||
|
||||
Точка входа Iced-приложения. Держит состояние и реализует `update`/`view`.
|
||||
|
||||
### `GlintApp`
|
||||
|
||||
```rust
|
||||
pub struct GlintApp {
|
||||
pub doc: Document<'static>,
|
||||
pub vdom_roots: Vec<Element<'static>>,
|
||||
pub rhei: RheiContext,
|
||||
}
|
||||
```
|
||||
|
||||
- `doc` — оригинальный Document (стили, переменные, tracker)
|
||||
- `vdom_roots` — результат последнего evaluate_vdom (_incr)
|
||||
- `rhei` — Rhai-движок с кэшированными AST
|
||||
|
||||
### `update(&mut self, message: Message) -> iced::Task<Message>`
|
||||
|
||||
Обработка сообщений от Iced:
|
||||
|
||||
| Message | Действие |
|
||||
|---------|----------|
|
||||
| `WindowScrolled(y)` | Установить `__scroll_y`, вызвать `on_variable_changed` |
|
||||
| `ScrollableScrolled(id, y)` | Установить `__scroll_{id}`, вызвать `on_variable_changed` |
|
||||
| `EventTriggered(script)` | `execute_action()`, затем diff переменных для поиска изменившихся |
|
||||
| `InputChanged(var, val)` | Установить переменную, вызвать `on_variable_changed` |
|
||||
| `ToggleChanged(var, val)` | Установить `Bool`, вызвать `on_variable_changed` |
|
||||
| `SliderChanged(var, val)` | Установить `Float`, вызвать `on_variable_changed` |
|
||||
|
||||
После обработки сообщения:
|
||||
1. `tracker.take_dirty_set()` — получить dirty элементы
|
||||
2. `evaluate_vdom_incr()` — пересчитать VDOM
|
||||
3. Сохранить в `self.vdom_roots`
|
||||
|
||||
**Perf:** VDOM фаза замеряется `PerfScope::new("vdom")`.
|
||||
|
||||
### `view(&self) -> iced::Element`
|
||||
|
||||
Построить Iced-виджеты из `self.vdom_roots`:
|
||||
|
||||
1. Собрать scroll_positions из `__scroll_*` переменных
|
||||
2. Для каждого root: `render_element()` → push в column
|
||||
3. Наложить global fixed/absolute/sticky слои
|
||||
4. Вернуть `container(layout).into()`
|
||||
|
||||
**Perf:** Render фаза замеряется `PerfScope::new("render")`.
|
||||
В конце `perf::print_frame()` — вывод в stderr.
|
||||
|
||||
---
|
||||
|
||||
## `src/perf.rs` — Performance Monitoring
|
||||
|
||||
Система замера времени по фазам (VDOM, Style, Render). Включение: флаг `--perf`.
|
||||
|
||||
### `PerfReport`
|
||||
|
||||
```rust
|
||||
pub struct PerfReport {
|
||||
pub vdom_eval: f64, // ms
|
||||
pub style: f64, // ms
|
||||
pub render: f64, // ms
|
||||
pub total: f64, // ms
|
||||
}
|
||||
```
|
||||
|
||||
Хранится в `thread_local!` `RefCell<PerfReport>`.
|
||||
|
||||
### `PerfScope`
|
||||
|
||||
Drop-guard: замеряет время от создания до разрушения.
|
||||
|
||||
```rust
|
||||
pub struct PerfScope {
|
||||
name: &'static str,
|
||||
start: Instant,
|
||||
}
|
||||
```
|
||||
|
||||
При `drop()`: добавляет elapsed ms к соответствующему полю `PerfReport`.
|
||||
Имена: `"vdom"`, `"style"`, `"render"`.
|
||||
|
||||
### Функции
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `set_enabled(bool)` | Включить/выключить замеры |
|
||||
| `is_enabled() -> bool` | Проверить состояние |
|
||||
| `report_and_reset() -> Option<String>` | Собрать отчёт, проверить >16ms threshold |
|
||||
| `reset()` | Обнулить PerfReport |
|
||||
| `print_frame()` | Вызвать report_and_reset + сброс, напечатать в stderr |
|
||||
|
||||
### Пример вывода
|
||||
|
||||
```
|
||||
[perf] VDOM: 6.63ms | Style: 0.26ms | Render: 7.19ms | Total: 14.08ms
|
||||
[perf] VDOM: 13.26ms | Style: 0.52ms | Render: 7.18ms | Total: 20.95ms ⚠️ Frame budget exceeded! 20.95ms > 16ms
|
||||
```
|
||||
|
||||
### Точки инструментирования
|
||||
|
||||
| Файл | Фаза | Место |
|
||||
|------|------|-------|
|
||||
| `app.rs:update` | vdom | Вокруг `evaluate_vdom_incr()` |
|
||||
| `app.rs:view` | render | Вся функция `view()` |
|
||||
| `style.rs:compute_cached` | style | Внутри `StyleSheet::compute_cached()` |
|
||||
266
examples/demo/app.gltm
Normal file
266
examples/demo/app.gltm
Normal file
@@ -0,0 +1,266 @@
|
||||
// ============================================================================
|
||||
// Glint — Full Feature Demonstration
|
||||
// ============================================================================
|
||||
// Showcases: @global, @singleton, !rhei:, @component, @if/@else, @each,
|
||||
// @on click, variable binding, positioning (fixed, absolute),
|
||||
// typography, box model, opacity, overflow, grid layout, style inheritance.
|
||||
// Note: position:sticky approximated via in-flow layout + overlay
|
||||
// ============================================================================
|
||||
|
||||
@version 1
|
||||
@style "styles.glts"
|
||||
|
||||
// ── 1. State ─────────────────────────────────────────────────────────────────
|
||||
@global $title = "My Dashboard"
|
||||
@global $user = "guest"
|
||||
@global $cpu = 23.0
|
||||
@global $memory = 47.0
|
||||
@global $online = true
|
||||
@global $alert = 70.0
|
||||
@global $nodes = ["Alpha", "Bravo", "Charlie", "Delta"]
|
||||
@global $notif = true
|
||||
@global $show = true
|
||||
|
||||
@singleton AppInfo {
|
||||
build = "2026-07-22",
|
||||
mode = "release"
|
||||
}
|
||||
|
||||
!rhei: {
|
||||
fn pct(v) { v + "%" }
|
||||
fn label(v) {
|
||||
if v >= 80.0 { "High" }
|
||||
else if v >= 50.0 { "Medium" }
|
||||
else { "Low" }
|
||||
}
|
||||
print("[App] " + nodes.len() + " nodes ready.");
|
||||
}
|
||||
|
||||
// ── 2. Components ────────────────────────────────────────────────────────────
|
||||
@component Gauge(name: String, val: Float) {
|
||||
Panel(class="gauge") {
|
||||
Panel(class="gauge-row") {
|
||||
Text(text=$name)
|
||||
Text !rhei: { pct(val) }
|
||||
}
|
||||
ProgressBar(value=$val)
|
||||
Text(class="gauge-label") !rhei: { label(val) }
|
||||
}
|
||||
}
|
||||
|
||||
@component NodeTile(tag: String, active: Bool) {
|
||||
Panel(class="tile") {
|
||||
Panel(class="tile-row") {
|
||||
Icon "◇"
|
||||
Text(text=$tag)
|
||||
}
|
||||
@if $active {
|
||||
Text(class="tag-on") "● Live"
|
||||
} @else {
|
||||
Text(class="tag-off") "○ Off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Layout ────────────────────────────────────────────────────────────────
|
||||
Window(title=$title) {
|
||||
|
||||
// ── Header (sticky — stays at top while body scrolls beneath) ─
|
||||
Panel(class="header") {
|
||||
Panel(class="header-inner") {
|
||||
Icon "◇"
|
||||
Header "Glint"
|
||||
Panel(width="fill")
|
||||
Text !rhei: { "👤 " + user }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main body ────────────────────────────────────────────────────────────
|
||||
Panel(class="body") {
|
||||
|
||||
// ── LEFT COLUMN ──────────────────────────────────────────────────────
|
||||
Panel(class="col-left") {
|
||||
|
||||
// Panel: Controls
|
||||
Panel(class="card") {
|
||||
Header "Controls"
|
||||
Toggle(label="Online", value=$online)
|
||||
@if $online {
|
||||
Text(class="ok") "✓ Running"
|
||||
} @else {
|
||||
Text(class="err") "✗ Stopped"
|
||||
}
|
||||
Input(placeholder="Username", value=$user)
|
||||
}
|
||||
|
||||
// Panel: Metrics
|
||||
Panel(class="card") {
|
||||
Header "Metrics"
|
||||
Gauge(name="CPU", val=$cpu)
|
||||
Gauge(name="Memory", val=$memory)
|
||||
}
|
||||
|
||||
// Panel: Threshold
|
||||
Panel(class="card") {
|
||||
Header "Alert Threshold"
|
||||
Slider(value=$alert)
|
||||
Text !rhei: { "Alert when > " + alert + "%" }
|
||||
}
|
||||
|
||||
// Panel: Actions
|
||||
Panel(class="card") {
|
||||
Header "Actions"
|
||||
Panel(class="btn-row") {
|
||||
Button(label="Reset CPU") {
|
||||
@on click { !rhei: { cpu = 5.0; } }
|
||||
}
|
||||
Button(label="Max CPU") {
|
||||
@on click { !rhei: { cpu = 92.0; } }
|
||||
}
|
||||
}
|
||||
Button(label="Toggle Notification", class="btn-outline") {
|
||||
@on click { !rhei: { notif = !notif; } }
|
||||
}
|
||||
}
|
||||
|
||||
// Panel: Nodes
|
||||
Panel(class="card") {
|
||||
Header "Nodes"
|
||||
Panel(direction="grid", columns=2, gap=8) {
|
||||
@each $n in $nodes {
|
||||
NodeTile(tag=$n, active=$online)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── RIGHT COLUMN ─────────────────────────────────────────────────────
|
||||
Panel(class="col-right") {
|
||||
|
||||
// Panel: Typography
|
||||
Panel(class="card") {
|
||||
Header "Typography"
|
||||
Panel(class="demo-row") {
|
||||
Text(class="fw-300") "Light"
|
||||
Text(class="fw-400") "Normal"
|
||||
Text(class="fw-700") "Bold"
|
||||
Text(class="fw-900") "Black"
|
||||
}
|
||||
Panel(class="demo-col") {
|
||||
Panel(class="ta-box") { Text(class="ta-l") "Left" }
|
||||
Panel(class="ta-box") { Text(class="ta-c") "Center" }
|
||||
Panel(class="ta-box") { Text(class="ta-r") "Right" }
|
||||
}
|
||||
Panel(class="lh-box") {
|
||||
Text(class="lh-08") "Tight 0.8"
|
||||
Text(class="lh-12") "Normal 1.2"
|
||||
Text(class="lh-20") "Loose 2.0"
|
||||
}
|
||||
}
|
||||
|
||||
// Panel: Box Model
|
||||
Panel(class="card") {
|
||||
Header "Box Model"
|
||||
Panel(class="box-pad") {
|
||||
Text "padding: 24px top, 12px sides"
|
||||
}
|
||||
Panel(class="box-margin") {
|
||||
Text "margin-top: 16px"
|
||||
}
|
||||
Panel(class="box-combo") {
|
||||
Text "border: 2px, radius: 10px"
|
||||
}
|
||||
}
|
||||
|
||||
// Panel: Absolute Position
|
||||
Panel(class="card") {
|
||||
Header "Position: Absolute"
|
||||
Panel(class="abs-stage") {
|
||||
Text "Stage with overlay badge:"
|
||||
Panel(class="abs-badge") {
|
||||
Panel(class="badge-inner") {
|
||||
Text "★ HOT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Panel: Opacity
|
||||
Panel(class="card") {
|
||||
Header "Opacity"
|
||||
Panel(class="op-row") {
|
||||
Panel(class="op-30") { Text "30%" }
|
||||
Panel(class="op-60") { Text "60%" }
|
||||
Panel(class="op-85") { Text "85%" }
|
||||
Panel(class="op-100") { Text "100%" }
|
||||
}
|
||||
Text(class="gauge-label") "* Opacity depends on Iced widget support — works on containers, not on text"
|
||||
}
|
||||
|
||||
// Panel: Scrollable Log with sticky header
|
||||
Panel(class="card") {
|
||||
Header "Scrollable Log"
|
||||
Panel(class="log") {
|
||||
Panel(class="log-header") {
|
||||
Text "Event Log (sticky header — scroll below)"
|
||||
}
|
||||
Text "Init: system boot"
|
||||
Text "Info: all modules OK"
|
||||
Text "Warn: CPU at $cpu%"
|
||||
Text "Info: mem at $memory%"
|
||||
Text "Debug: threshold $alert"
|
||||
Text "Info: user $user"
|
||||
Text "Warn: node check"
|
||||
Text "Info: heartbeat OK"
|
||||
Text "Debug: idle"
|
||||
}
|
||||
}
|
||||
|
||||
// Panel: Inheritance
|
||||
Panel(class="card") {
|
||||
Header "Style Inheritance"
|
||||
Panel(class="inherit") {
|
||||
Text "Warm gold from parent"
|
||||
Text(class="override") "Terracotta — override"
|
||||
}
|
||||
}
|
||||
|
||||
// Panel: Display None (interactive toggle via @if)
|
||||
Panel(class="card") {
|
||||
Header "Display: None"
|
||||
Panel(class="demo-row") {
|
||||
Panel(class="vis") { Text "Always" }
|
||||
@if $show {
|
||||
Panel(class="vis") { Text "Toggled" }
|
||||
}
|
||||
Panel(class="vis") { Text "Always" }
|
||||
}
|
||||
Button(label="Toggle Center Element", class="btn-outline") {
|
||||
@on click { !rhei: { show = !show; } }
|
||||
}
|
||||
}
|
||||
|
||||
// Panel: Flex Grow
|
||||
Panel(class="card") {
|
||||
Header "Flex Grow"
|
||||
Panel(class="grow-bar") {
|
||||
Panel(class="grow-1") { Text "1" }
|
||||
Panel(class="grow-2") { Text "2" }
|
||||
Panel(class="grow-1") { Text "1" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fixed Toast ──────────────────────────────────────────────────────────
|
||||
@if $notif {
|
||||
Panel(class="toast") {
|
||||
Panel(class="toast-row") {
|
||||
Text "✓ App ready"
|
||||
Button(label="×", class="toast-x") {
|
||||
@on click { !rhei: { notif = false; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
466
examples/demo/styles.glts
Normal file
466
examples/demo/styles.glts
Normal file
@@ -0,0 +1,466 @@
|
||||
// ============================================================================
|
||||
// Glint Design — Warm Neutral Theme
|
||||
// ============================================================================
|
||||
// Warm off-white background, soft earth accents, no blue/purple.
|
||||
// ============================================================================
|
||||
|
||||
// ── Design Tokens ────────────────────────────────────────────────────────────
|
||||
$bg = #f5f2ed
|
||||
$surface = #ede8e0
|
||||
$card = #ffffff
|
||||
$accent = #d4785f
|
||||
$green = #7a9e7e
|
||||
$red = #c0574a
|
||||
$yellow = #d9a84d
|
||||
$text = #2e2b28
|
||||
$muted = #8a8580
|
||||
$border = #dcd5cc
|
||||
|
||||
// ── Mixins ───────────────────────────────────────────────────────────────────
|
||||
@mixin card-base {
|
||||
background: $card
|
||||
border-color: $border
|
||||
border-width: 1px
|
||||
border-radius: 10px
|
||||
padding: 12px
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
@mixin text-body {
|
||||
color: $text
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
// ── Globals ──────────────────────────────────────────────────────────────────
|
||||
Window {
|
||||
background: $bg
|
||||
height: 100vh
|
||||
}
|
||||
|
||||
Panel {
|
||||
background: transparent
|
||||
border-width: 0px
|
||||
padding: 0px
|
||||
gap: 12px
|
||||
direction: vertical
|
||||
}
|
||||
|
||||
// ── Header (sticky — sticks at top of Window when scrolled) ──────────────────
|
||||
.header {
|
||||
position: sticky
|
||||
top: 0
|
||||
background: $card
|
||||
padding: 8px 20px
|
||||
border-bottom: 1px solid $border
|
||||
width: fill
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
direction: horizontal
|
||||
align-items: center
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
// ── Layout ───────────────────────────────────────────────────────────────────
|
||||
.body {
|
||||
direction: horizontal
|
||||
padding: 12px
|
||||
gap: 12px
|
||||
width: fill
|
||||
}
|
||||
|
||||
.col-left {
|
||||
direction: vertical
|
||||
gap: 10px
|
||||
flex-grow: 2
|
||||
}
|
||||
|
||||
.col-right {
|
||||
direction: vertical
|
||||
gap: 10px
|
||||
flex-grow: 3
|
||||
}
|
||||
|
||||
// ── Card ─────────────────────────────────────────────────────────────────────
|
||||
.card {
|
||||
@use card-base
|
||||
}
|
||||
|
||||
// ── Header Component ─────────────────────────────────────────────────────────
|
||||
Header {
|
||||
color: $accent
|
||||
font-size: 18px
|
||||
padding: 2px 0
|
||||
}
|
||||
|
||||
Text {
|
||||
@use text-body
|
||||
}
|
||||
|
||||
Label {
|
||||
color: $muted
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────────────────
|
||||
Input {
|
||||
background-color: #f0ece6
|
||||
color: $text
|
||||
font-size: 14px
|
||||
padding: 10px
|
||||
border-radius: 8px
|
||||
border-width: 1px
|
||||
border-color: $border
|
||||
width: fill
|
||||
}
|
||||
|
||||
// ── Toggle ───────────────────────────────────────────────────────────────────
|
||||
Toggle {
|
||||
color: $text
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
// ── Button ───────────────────────────────────────────────────────────────────
|
||||
Button {
|
||||
border-radius: 8px
|
||||
border-width: 1px
|
||||
border-color: $border
|
||||
padding: 8px 16px
|
||||
background: $surface
|
||||
color: $text
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
background: $accent
|
||||
color: #fff
|
||||
border-color: $accent
|
||||
}
|
||||
|
||||
Button:active {
|
||||
background: #b8654e
|
||||
color: #fff
|
||||
border-color: #b8654e
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent
|
||||
border-color: $muted
|
||||
color: $muted
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: transparent
|
||||
border-color: $text
|
||||
color: $text
|
||||
}
|
||||
|
||||
// ── ProgressBar ──────────────────────────────────────────────────────────────
|
||||
ProgressBar {
|
||||
width: fill
|
||||
}
|
||||
|
||||
// ── Slider ───────────────────────────────────────────────────────────────────
|
||||
Slider {
|
||||
width: fill
|
||||
}
|
||||
|
||||
// ── Gauge Component ──────────────────────────────────────────────────────────
|
||||
.gauge {
|
||||
direction: vertical
|
||||
gap: 6px
|
||||
}
|
||||
|
||||
.gauge-row {
|
||||
direction: horizontal
|
||||
gap: 8px
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.gauge-label {
|
||||
font-size: 12px
|
||||
color: $muted
|
||||
}
|
||||
|
||||
// ── Tile Component ───────────────────────────────────────────────────────────
|
||||
.tile {
|
||||
@use card-base
|
||||
padding: 10px
|
||||
gap: 6px
|
||||
align-items: center
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.tile-row {
|
||||
direction: horizontal
|
||||
gap: 6px
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.tag-on {
|
||||
color: $green
|
||||
font-size: 12px
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.tag-off {
|
||||
color: $muted
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
// ── Status ───────────────────────────────────────────────────────────────────
|
||||
.ok {
|
||||
color: $green
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.err {
|
||||
color: $red
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
// ── Button Row ───────────────────────────────────────────────────────────────
|
||||
.btn-row {
|
||||
direction: horizontal
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
// ── Demo Rows ────────────────────────────────────────────────────────────────
|
||||
.demo-row {
|
||||
direction: horizontal
|
||||
gap: 12px
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.demo-col {
|
||||
direction: vertical
|
||||
gap: 6px
|
||||
}
|
||||
|
||||
// ── Font Weight ──────────────────────────────────────────────────────────────
|
||||
.fw-300 { font-weight: 300 }
|
||||
.fw-400 { font-weight: 400 }
|
||||
.fw-700 { font-weight: 700 }
|
||||
.fw-900 { font-weight: 900 }
|
||||
|
||||
// ── Text Align ───────────────────────────────────────────────────────────────
|
||||
.ta-box {
|
||||
background: #f0ece6
|
||||
padding: 6px 10px
|
||||
border-radius: 6px
|
||||
width: fill
|
||||
}
|
||||
|
||||
.ta-l { text-align: left }
|
||||
.ta-c { text-align: center }
|
||||
.ta-r { text-align: right }
|
||||
|
||||
// ── Line Height ──────────────────────────────────────────────────────────────
|
||||
.lh-box {
|
||||
direction: vertical
|
||||
gap: 4px
|
||||
background: #f0ece6
|
||||
padding: 10px
|
||||
border-radius: 6px
|
||||
}
|
||||
|
||||
.lh-08 { line-height: 0.8 }
|
||||
.lh-12 { line-height: 1.2 }
|
||||
.lh-20 { line-height: 2.0 }
|
||||
|
||||
// ── Box Model ────────────────────────────────────────────────────────────────
|
||||
.box-pad {
|
||||
background: #f0ece6
|
||||
padding-top: 24px
|
||||
padding-bottom: 8px
|
||||
padding-left: 12px
|
||||
padding-right: 12px
|
||||
border-radius: 6px
|
||||
}
|
||||
|
||||
.box-margin {
|
||||
background: #f0ece6
|
||||
margin-top: 16px
|
||||
padding: 8px
|
||||
border-radius: 6px
|
||||
}
|
||||
|
||||
.box-combo {
|
||||
background: #f0ece6
|
||||
padding: 12px
|
||||
border-width: 2px
|
||||
border-color: $accent
|
||||
border-radius: 10px
|
||||
}
|
||||
|
||||
// ── Absolute Positioning ─────────────────────────────────────────────────────
|
||||
.abs-stage {
|
||||
position: relative
|
||||
background: #f0ece6
|
||||
padding: 16px
|
||||
border-radius: 8px
|
||||
width: fill
|
||||
min-height: 50px
|
||||
gap: 6px
|
||||
}
|
||||
|
||||
.abs-badge {
|
||||
position: absolute
|
||||
top: -8px
|
||||
right: 8px
|
||||
}
|
||||
|
||||
.badge-inner {
|
||||
background: $accent
|
||||
padding: 3px 10px
|
||||
border-radius: 4px
|
||||
color: #fff
|
||||
font-weight: 700
|
||||
font-size: 10px
|
||||
}
|
||||
|
||||
// ── Opacity ──────────────────────────────────────────────────────────────────
|
||||
.op-row {
|
||||
direction: horizontal
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.op-30 {
|
||||
background: $accent
|
||||
padding: 8px 12px
|
||||
border-radius: 6px
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
.op-60 {
|
||||
background: $accent
|
||||
padding: 8px 12px
|
||||
border-radius: 6px
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
.op-85 {
|
||||
background: $accent
|
||||
padding: 8px 12px
|
||||
border-radius: 6px
|
||||
opacity: 0.85
|
||||
}
|
||||
|
||||
.op-100 {
|
||||
background: $accent
|
||||
padding: 8px 12px
|
||||
border-radius: 6px
|
||||
opacity: 1.0
|
||||
}
|
||||
|
||||
// ── Log / Scroll ─────────────────────────────────────────────────────────────
|
||||
.log {
|
||||
background: #f0ece6
|
||||
border-width: 1px
|
||||
border-color: $border
|
||||
border-radius: 10px
|
||||
height: 180px
|
||||
overflow-y: scroll
|
||||
padding: 4px
|
||||
gap: 2px
|
||||
}
|
||||
|
||||
.log-header {
|
||||
position: sticky
|
||||
top: 0
|
||||
background: $surface
|
||||
padding: 6px 10px
|
||||
border-radius: 4px
|
||||
font-weight: 600
|
||||
font-size: 12px
|
||||
color: $accent
|
||||
width: fill
|
||||
}
|
||||
|
||||
// Note: position:sticky approximated via in-flow spacer + overlay (no scroll-tracking)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── Style Inheritance ────────────────────────────────────────────────────────
|
||||
.inherit {
|
||||
background: #f0ece6
|
||||
padding: 12px
|
||||
border-radius: 8px
|
||||
color: $yellow
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.override {
|
||||
color: $red
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
// ── Display: None ────────────────────────────────────────────────────────────
|
||||
.vis {
|
||||
background: #ede8e0
|
||||
padding: 8px 14px
|
||||
border-radius: 6px
|
||||
}
|
||||
|
||||
.hid {
|
||||
background: $red
|
||||
padding: 8px 14px
|
||||
border-radius: 6px
|
||||
display: none
|
||||
}
|
||||
|
||||
// ── Flex Grow ────────────────────────────────────────────────────────────────
|
||||
.grow-bar {
|
||||
direction: horizontal
|
||||
gap: 6px
|
||||
width: fill
|
||||
}
|
||||
|
||||
.grow-1 {
|
||||
background: $accent
|
||||
padding: 8px
|
||||
border-radius: 4px
|
||||
text-align: center
|
||||
color: #fff
|
||||
flex-grow: 1
|
||||
}
|
||||
|
||||
.grow-2 {
|
||||
background: $green
|
||||
padding: 8px
|
||||
border-radius: 4px
|
||||
text-align: center
|
||||
color: #fff
|
||||
flex-grow: 2
|
||||
}
|
||||
|
||||
// ── Toast ────────────────────────────────────────────────────────────────────
|
||||
.toast {
|
||||
position: fixed
|
||||
top: 16px
|
||||
right: 16px
|
||||
background: $green
|
||||
padding: 10px 16px
|
||||
border-radius: 8px
|
||||
color: #fff
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.toast-row {
|
||||
direction: horizontal
|
||||
gap: 10px
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.toast-x {
|
||||
background: transparent
|
||||
border-width: 0px
|
||||
padding: 0px
|
||||
color: #fff
|
||||
font-size: 18px
|
||||
}
|
||||
|
||||
// ── Comma-separated selectors ────────────────────────────────────────────────
|
||||
.warn-text, .info-text {
|
||||
font-weight: 600
|
||||
padding: 6px 10px
|
||||
border-radius: 4px
|
||||
}
|
||||
17
examples/dock/dock.gltm
Normal file
17
examples/dock/dock.gltm
Normal file
@@ -0,0 +1,17 @@
|
||||
@version 1
|
||||
@style "styles.glts"
|
||||
|
||||
Window(title="Dock") {
|
||||
Panel(class="wallpaper")
|
||||
|
||||
Panel(class="dock-outer") {
|
||||
Panel(class="dock") {
|
||||
Button(label=" Finder ", class="dock-icon")
|
||||
Button(label=" Safari ", class="dock-icon")
|
||||
Button(label=" Mail ", class="dock-icon")
|
||||
Button(label=" Music ", class="dock-icon")
|
||||
Button(label=" Photos ", class="dock-icon")
|
||||
Button(label=" Trash ", class="dock-icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
56
examples/dock/styles.glts
Normal file
56
examples/dock/styles.glts
Normal file
@@ -0,0 +1,56 @@
|
||||
$dock-bg = #2c2c2e
|
||||
$icon-bg = #3a3a3c
|
||||
$text = #ffffff
|
||||
$accent = #0a84ff
|
||||
|
||||
Window {
|
||||
background: #1c1c1e
|
||||
height: 100vh
|
||||
}
|
||||
|
||||
Panel {
|
||||
background: transparent
|
||||
border-width: 0px
|
||||
padding: 0px
|
||||
gap: 0px
|
||||
direction: vertical
|
||||
}
|
||||
|
||||
.wallpaper {
|
||||
width: fill
|
||||
height: fill
|
||||
}
|
||||
|
||||
.dock-outer {
|
||||
position: fixed
|
||||
bottom: 12px
|
||||
width: 50%
|
||||
direction: horizontal
|
||||
content-align: center
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.dock {
|
||||
direction: horizontal
|
||||
gap: 6px
|
||||
background: $dock-bg
|
||||
padding: 8px 14px
|
||||
border-radius: 16px
|
||||
border-width: 1px
|
||||
border-color: #444
|
||||
}
|
||||
|
||||
.dock-icon {
|
||||
background: $icon-bg
|
||||
color: $text
|
||||
font-size: 13px
|
||||
font-weight: 600
|
||||
padding: 6px 10px
|
||||
border-width: 0px
|
||||
border-radius: 8px
|
||||
}
|
||||
|
||||
.dock-icon:hover {
|
||||
background: $accent
|
||||
color: #fff
|
||||
}
|
||||
432
examples/glt/main.glts
Normal file
432
examples/glt/main.glts
Normal file
@@ -0,0 +1,432 @@
|
||||
// =============================================================================
|
||||
// Glint Design System — Core Fixed Stylesheet (main.glts)
|
||||
// =============================================================================
|
||||
|
||||
$bg-app = #11111b
|
||||
$bg-panel = #1e1e2e
|
||||
$bg-surface = #313244
|
||||
$bg-input = #181825
|
||||
$accent = #b4befe
|
||||
$text-main = #cdd6f4
|
||||
$text-muted = #9399b2
|
||||
$border-glow = #45475a
|
||||
|
||||
@mixin text-body {
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
@mixin standard-card {
|
||||
background: $bg-panel
|
||||
border-color: $border-glow
|
||||
border-width: 1px
|
||||
border-radius: 12px
|
||||
}
|
||||
|
||||
Window {
|
||||
background: $bg-app
|
||||
}
|
||||
|
||||
Panel {
|
||||
background: transparent
|
||||
border-width: 0px
|
||||
padding: 0px
|
||||
gap: 12px
|
||||
direction: vertical
|
||||
}
|
||||
|
||||
.ui-card {
|
||||
@use standard-card
|
||||
padding: 14px
|
||||
gap: 12px
|
||||
align-items: center
|
||||
content-align: center
|
||||
}
|
||||
|
||||
// ── Служебные Layout-классы ──────────────────────────────────────────────────
|
||||
|
||||
.layout-viewport {
|
||||
width: fill
|
||||
padding: 16px
|
||||
gap: 16px
|
||||
}
|
||||
|
||||
.layout-row {
|
||||
direction: horizontal
|
||||
gap: 16px
|
||||
}
|
||||
|
||||
.layout-col {
|
||||
direction: vertical
|
||||
gap: 16px
|
||||
}
|
||||
|
||||
.layout-clean {
|
||||
width: fill
|
||||
}
|
||||
|
||||
// ── Стили атомарных компонентов ──────────────────────────────────────────────
|
||||
|
||||
Header {
|
||||
color: $accent
|
||||
font-size: 20px
|
||||
padding: 2px
|
||||
}
|
||||
|
||||
Text {
|
||||
@use text-body
|
||||
}
|
||||
|
||||
Label {
|
||||
color: $text-muted
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
Input {
|
||||
background-color: $bg-input
|
||||
color: $text-main
|
||||
font-size: 14px
|
||||
padding: 10px
|
||||
border-radius: 8px
|
||||
border-width: 1px
|
||||
border-color: $border-glow
|
||||
width: 240px
|
||||
}
|
||||
|
||||
Toggle {
|
||||
color: $text-main
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
Divider {
|
||||
background: $border-glow
|
||||
border-width: 1px
|
||||
}
|
||||
|
||||
Button {
|
||||
border-radius: 6px
|
||||
border-width: 1px
|
||||
padding: 8px
|
||||
}
|
||||
|
||||
.btn-custom {
|
||||
background: #000000
|
||||
}
|
||||
|
||||
Image {
|
||||
border-radius: 8px
|
||||
border-width: 1px
|
||||
}
|
||||
|
||||
// ── Секция тестирования продвинутой геометрии Box-Model ──────────────────────
|
||||
|
||||
.geometry-box {
|
||||
@use standard-card
|
||||
background: #24253a
|
||||
border-color: #f38ba8
|
||||
border-width: 2px
|
||||
padding-top: 24px
|
||||
padding-bottom: 12px
|
||||
padding-left: 16px
|
||||
padding-right: 40px
|
||||
width: 320px
|
||||
}
|
||||
|
||||
.margin-test-item {
|
||||
background: #a6e3a1
|
||||
padding: 8px
|
||||
margin-top: 12px
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
// ── Секция тестирования каскадного наследования (Inheritance) ────────────────
|
||||
|
||||
.inheritance-box {
|
||||
@use standard-card
|
||||
background: #181825
|
||||
padding: 12px
|
||||
color: #f9e2af
|
||||
font-size: 18px
|
||||
}
|
||||
|
||||
.override-style {
|
||||
color: #f38ba8
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
// ── Секция распределения весов Flex-Grow (Fill portions) ────────────────────
|
||||
|
||||
.flex-row-bar {
|
||||
background: $bg-surface
|
||||
direction: horizontal
|
||||
padding: 8px
|
||||
gap: 8px
|
||||
width: fill
|
||||
}
|
||||
|
||||
.fill-10 {
|
||||
background: #f38ba8
|
||||
width: 10%
|
||||
}
|
||||
|
||||
.fixed-120 {
|
||||
background: #f9e2af
|
||||
width: 50px
|
||||
color: #11111b
|
||||
}
|
||||
|
||||
.fill-20 {
|
||||
background: #a6e3a1
|
||||
width: 20%
|
||||
color: #11111b
|
||||
}
|
||||
|
||||
// ── СТИЛИ ДЛЯ ПРОВЕРКИ POSITION: FIXED ───────────────────────────────────────
|
||||
|
||||
.fixed-toast {
|
||||
position: fixed
|
||||
top: 20px
|
||||
right: 20px
|
||||
background: #a6e3a1
|
||||
border-radius: 8px
|
||||
padding: 12px
|
||||
border-width: 1px
|
||||
border-color: #a6e3a1
|
||||
color: #000
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: transparen
|
||||
border-width: 0px
|
||||
padding: 2px
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.scroll-test-box {
|
||||
@use standard-card
|
||||
background: #313244
|
||||
height: 150px
|
||||
width: 250px
|
||||
overflow-y: scroll
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
|
||||
// ── Стили для тестирования Sticky Эффекта ────────────────────
|
||||
|
||||
.sticky-container {
|
||||
@use standard-card
|
||||
background: #181825
|
||||
padding: 12px
|
||||
gap: 8px
|
||||
width: fill
|
||||
height: 200px
|
||||
overflow-y: scroll
|
||||
}
|
||||
|
||||
.sticky-header {
|
||||
position: sticky
|
||||
top: 0px
|
||||
background: #fab387
|
||||
padding: 10px 14px
|
||||
border-radius: 6px
|
||||
color: #11111b
|
||||
font-weight: bold
|
||||
font-size: 14px
|
||||
width: fill
|
||||
}
|
||||
|
||||
.scroll-spacer {
|
||||
gap: 100px
|
||||
padding: 10px
|
||||
width: fill
|
||||
}
|
||||
|
||||
// ── Стили для тестирования Opacity ────────────────────────────
|
||||
|
||||
.opacity-fade {
|
||||
background: #f38ba8
|
||||
padding: 12px
|
||||
border-radius: 8px
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
.opacity-mid {
|
||||
background: #a6e3a1
|
||||
padding: 12px
|
||||
border-radius: 8px
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
.opacity-subtle {
|
||||
background: #89b4fa
|
||||
padding: 12px
|
||||
border-radius: 8px
|
||||
opacity: 0.85
|
||||
}
|
||||
|
||||
// ── Стили для тестирования Font-Weight ─────────────────────────
|
||||
|
||||
.fw-light {
|
||||
font-weight: 300
|
||||
color: $text-main
|
||||
}
|
||||
|
||||
.fw-normal {
|
||||
font-weight: 400
|
||||
color: $text-main
|
||||
}
|
||||
|
||||
.fw-bold {
|
||||
font-weight: 700
|
||||
color: $text-main
|
||||
}
|
||||
|
||||
.fw-black {
|
||||
font-weight: 900
|
||||
color: $text-main
|
||||
}
|
||||
|
||||
// ── Стили для тестирования Text-Align ──────────────────────────
|
||||
|
||||
.ta-box {
|
||||
background: #313244
|
||||
padding: 8px
|
||||
border-radius: 6px
|
||||
width: fill
|
||||
}
|
||||
|
||||
.ta-left {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.ta-center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.ta-right {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
// ── Стили для тестирования Line-Height ─────────────────────────
|
||||
|
||||
.lh-box {
|
||||
background: #313244
|
||||
padding: 8px
|
||||
border-radius: 6px
|
||||
width: 300px
|
||||
}
|
||||
|
||||
.lh-tight {
|
||||
line-height: 0.8
|
||||
}
|
||||
|
||||
.lh-normal {
|
||||
line-height: 1.2
|
||||
}
|
||||
|
||||
.lh-loose {
|
||||
line-height: 2.0
|
||||
}
|
||||
|
||||
// ── Стили для тестирования селекторов через запятую ────────────
|
||||
|
||||
.warning-text, .error-text {
|
||||
font-weight: bold
|
||||
padding: 8px
|
||||
border-radius: 6px
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: #f9e2af
|
||||
background: #3d3520
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #f38ba8
|
||||
background: #3d2020
|
||||
}
|
||||
|
||||
// ── Стили для тестирования Display: None ───────────────────────
|
||||
|
||||
.hidden-box {
|
||||
background: #f38ba8
|
||||
padding: 12px
|
||||
border-radius: 8px
|
||||
display: none
|
||||
}
|
||||
|
||||
.test-section-header {
|
||||
color: $accent
|
||||
font-size: 16px
|
||||
padding: 4px 0
|
||||
margin-top: 8px
|
||||
}
|
||||
|
||||
.test-row {
|
||||
direction: horizontal
|
||||
gap: 12px
|
||||
align-items: center
|
||||
}
|
||||
|
||||
// ── Стили для тестирования POSITION: ABSOLUTE ────────────────
|
||||
|
||||
.abs-demo-container {
|
||||
position: relative
|
||||
background: #313244
|
||||
padding: 16px
|
||||
margin-top: 8px
|
||||
border-radius: 8px
|
||||
width: fill
|
||||
gap: 8px
|
||||
min-height: 60px
|
||||
}
|
||||
|
||||
.abs-demo-badge {
|
||||
position: absolute
|
||||
top: 10px
|
||||
right: 4px
|
||||
background: #f38ba8
|
||||
color: #11111b
|
||||
font-weight: bold
|
||||
font-size: 10px
|
||||
padding: 4px 8px
|
||||
border-radius: 4px
|
||||
}
|
||||
|
||||
// ── Полноценная демка absolute-позиционирования ──────────────
|
||||
|
||||
.absolute-demo-section {
|
||||
@use standard-card
|
||||
background: #181825
|
||||
padding: 12px
|
||||
gap: 8px
|
||||
width: fill
|
||||
}
|
||||
|
||||
.abs-stage {
|
||||
position: relative
|
||||
background: #24253a
|
||||
border-color: $accent
|
||||
border-width: 1px
|
||||
border-radius: 8px
|
||||
padding: 16px
|
||||
gap: 6px
|
||||
width: fill
|
||||
min-height: 80px
|
||||
}
|
||||
|
||||
.abs-overlay {
|
||||
position: absolute
|
||||
top: 10px
|
||||
right: 10px
|
||||
background: #f38ba8
|
||||
border-radius: 6px
|
||||
padding: 8px 12px
|
||||
color: #11111b
|
||||
font-weight: bold
|
||||
font-size: 12px
|
||||
gap: 4px
|
||||
}
|
||||
63
src/app.rs
63
src/app.rs
@@ -1,7 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iced::widget::{column, container};
|
||||
use iced::{Length, Theme};
|
||||
|
||||
use crate::interpreter::{Document, Element, Interpreter, RheiContext};
|
||||
use crate::interpreter::{Document, Element, Interpreter, RheiContext, Value};
|
||||
use crate::perf;
|
||||
use crate::renderer::render_element;
|
||||
use crate::Message;
|
||||
|
||||
@@ -19,55 +22,84 @@ impl GlintApp {
|
||||
pub fn update(&mut self, message: Message) -> iced::Task<Message> {
|
||||
match message {
|
||||
Message::WindowScrolled(y) => {
|
||||
self.doc.variables.insert("__scroll_y".to_string(), y.to_string());
|
||||
let var = "__scroll_y".to_string();
|
||||
self.doc.variables.insert(var.clone(), Value::Float(y as f64));
|
||||
self.doc.tracker.on_variable_changed(&var);
|
||||
}
|
||||
Message::ScrollableScrolled(id, y) => {
|
||||
let var = format!("__scroll_{}", id);
|
||||
self.doc.variables.insert(var.clone(), Value::Float(y as f64));
|
||||
self.doc.tracker.on_variable_changed(&var);
|
||||
}
|
||||
Message::EventTriggered(script) => {
|
||||
if !script.is_empty() {
|
||||
let old_vars: Vec<String> = self.doc.variables.keys().cloned().collect();
|
||||
self.rhei.execute_action(&script, &mut self.doc.variables);
|
||||
let new_vars: Vec<String> = self.doc.variables.keys().cloned().collect();
|
||||
for v in &new_vars {
|
||||
if !old_vars.contains(v) || self.doc.variables.get(v) != old_vars.iter().find_map(|k| if k == v { self.doc.variables.get(k) } else { None }) {
|
||||
self.doc.tracker.on_variable_changed(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::InputChanged(Some(var), val) => {
|
||||
self.doc.variables.insert(var, val);
|
||||
self.doc.variables.insert(var.clone(), Value::from(val));
|
||||
self.doc.tracker.on_variable_changed(&var);
|
||||
}
|
||||
Message::ToggleChanged(Some(var), val) => {
|
||||
self.doc.variables.insert(var, val.to_string());
|
||||
self.doc.variables.insert(var.clone(), Value::Bool(val));
|
||||
self.doc.tracker.on_variable_changed(&var);
|
||||
}
|
||||
Message::SliderChanged(Some(var), val) => {
|
||||
self.doc.variables.insert(var.clone(), format!("{:.1}", val));
|
||||
self.doc.variables.insert(var.clone(), Value::Float(val));
|
||||
self.doc.tracker.on_variable_changed(&var);
|
||||
if var == "volume_level" {
|
||||
self.doc.variables.insert("age".to_string(), val.to_string());
|
||||
self.doc.variables.insert("age".to_string(), Value::Float(val));
|
||||
self.doc.tracker.on_variable_changed("age");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.vdom_roots = Interpreter::evaluate_vdom(
|
||||
&self.doc.roots,
|
||||
let dirty_set = self.doc.tracker.take_dirty_set();
|
||||
let root_refs: Vec<&Element<'static>> = self.doc.roots.iter().collect();
|
||||
let _vdom_scope = perf::PerfScope::new("vdom");
|
||||
self.vdom_roots = Interpreter::evaluate_vdom_incr(
|
||||
&root_refs,
|
||||
&mut self.doc.variables,
|
||||
&self.doc.components,
|
||||
&self.rhei,
|
||||
&self.doc.stylesheet,
|
||||
&[],
|
||||
&dirty_set,
|
||||
);
|
||||
drop(_vdom_scope);
|
||||
|
||||
iced::Task::none()
|
||||
}
|
||||
|
||||
pub fn view(&self) -> iced::Element<'_, Message, Theme, iced::Renderer> {
|
||||
let _render_scope = perf::PerfScope::new("render");
|
||||
let mut content = column![]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
let mut scroll_positions: HashMap<u64, f32> = HashMap::new();
|
||||
for (key, val) in &self.doc.variables {
|
||||
if let Some(id_str) = key.strip_prefix("__scroll_") {
|
||||
if let (Ok(id), Value::Float(y)) = (id_str.parse::<u64>(), val) {
|
||||
scroll_positions.insert(id, *y as f32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut global_fixed_layers = Vec::new();
|
||||
let mut global_abs_layers = Vec::new();
|
||||
let mut global_sticky_layers = Vec::new();
|
||||
|
||||
let _scroll_y = self.doc.variables.get("__scroll_y")
|
||||
.and_then(|v| v.parse::<f32>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
for root in &self.vdom_roots {
|
||||
if let Some(el) = render_element(root, None, None, None, &mut global_fixed_layers, &mut global_abs_layers, &mut global_sticky_layers, &self.doc.stylesheet) {
|
||||
if let Some(el) = render_element(root, None, None, None, &mut global_fixed_layers, &mut global_abs_layers, &mut global_sticky_layers, &scroll_positions, 0, &self.doc.stylesheet) {
|
||||
content = content.push(el);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +119,7 @@ impl GlintApp {
|
||||
let has_sticky = !global_sticky_layers.is_empty();
|
||||
let has_fixed = !global_fixed_layers.is_empty();
|
||||
|
||||
let render_result = {
|
||||
if !has_abs && !has_sticky && !has_fixed {
|
||||
main_flow.into()
|
||||
} else {
|
||||
@@ -102,5 +135,9 @@ impl GlintApp {
|
||||
}
|
||||
stack_widget.into()
|
||||
}
|
||||
};
|
||||
drop(_render_scope);
|
||||
perf::print_frame();
|
||||
render_result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ pub fn run_file(path: &str) -> ! {
|
||||
move || {
|
||||
let mut local_doc = doc.clone();
|
||||
let rhei = RheiContext::new(&local_doc.rhei_scripts);
|
||||
rhei.precompile_all_from_doc(&local_doc);
|
||||
rhei.initialize(&mut local_doc.variables);
|
||||
|
||||
let vdom_roots = Interpreter::evaluate_vdom(
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
|
||||
pub mod opcodes;
|
||||
pub mod reactive;
|
||||
pub mod reader;
|
||||
pub mod rhei;
|
||||
pub mod style;
|
||||
pub mod types;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::hash::{Hash, Hasher};
|
||||
pub use rhei::RheiContext;
|
||||
use style::StyleSheet as SS;
|
||||
pub use types::{ComponentDef, Document, Element, InterpError};
|
||||
pub use types::{ComponentDef, Document, Element, Interner, InterpError};
|
||||
pub use reactive::{ElementId, ReactiveTracker};
|
||||
|
||||
use compact_str::CompactString;
|
||||
use opcodes::*;
|
||||
use reader::Reader;
|
||||
use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX};
|
||||
use rhei::RHEI_PREFIX;
|
||||
use style::{AncestorInfo, ComputedStyle, StructuralContext};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static RE_VAR: OnceLock<Regex> = OnceLock::new();
|
||||
pub use types::Value;
|
||||
use types::FlatVDom;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub struct Interpreter;
|
||||
|
||||
@@ -33,6 +35,7 @@ impl Interpreter {
|
||||
let mut components = HashMap::new();
|
||||
let mut rhei_scripts: Vec<String> = Vec::new();
|
||||
let mut stylesheet = SS::new();
|
||||
let mut tracker = ReactiveTracker::new();
|
||||
|
||||
let roots = Self::parse_block_elements(
|
||||
&mut r,
|
||||
@@ -40,21 +43,25 @@ impl Interpreter {
|
||||
&mut components,
|
||||
&mut rhei_scripts,
|
||||
&mut stylesheet,
|
||||
&mut tracker,
|
||||
true,
|
||||
)?;
|
||||
|
||||
//let rhei_ctx = RheiContext::new(&rhei_scripts);
|
||||
//rhei_ctx.initialize(&mut variables);
|
||||
|
||||
Ok(Document { roots, components, variables, rhei_scripts, stylesheet })
|
||||
stylesheet.build_index();
|
||||
|
||||
Ok(Document { roots, components, variables, rhei_scripts, stylesheet, interner: Interner::new(), tracker })
|
||||
}
|
||||
|
||||
fn parse_block_elements<'a>(
|
||||
r: &mut Reader<'a>,
|
||||
variables: &mut HashMap<String, String>,
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &mut HashMap<String, ComponentDef<'a>>,
|
||||
rhei_scripts: &mut Vec<String>,
|
||||
stylesheet: &mut SS,
|
||||
tracker: &mut ReactiveTracker,
|
||||
is_root: bool,
|
||||
) -> Result<Vec<Element<'a>>, InterpError> {
|
||||
let mut roots: Vec<Element> = Vec::new();
|
||||
@@ -65,8 +72,12 @@ impl Interpreter {
|
||||
if !is_root && op == OP_END_BLOCK { break; }
|
||||
|
||||
match op {
|
||||
OP_ELEM_PUSH => stack.push(Element::new(r.read_str_ref()?)),
|
||||
|
||||
OP_ELEM_PUSH => {
|
||||
let elem_id = tracker.alloc_id();
|
||||
let mut el = Element::new(r.read_str_ref()?);
|
||||
el.element_id = elem_id;
|
||||
stack.push(el);
|
||||
}
|
||||
OP_ELEM_POP => {
|
||||
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
|
||||
Self::attach(&mut stack, &mut roots, finished);
|
||||
@@ -75,7 +86,7 @@ impl Interpreter {
|
||||
OP_GLOBAL | OP_LET => {
|
||||
let name = r.read_string()?;
|
||||
let vop = r.read_byte()?;
|
||||
if let Some(value) = r.read_value_as_string(vop)? {
|
||||
if let Some(value) = r.read_value(vop)? {
|
||||
variables.insert(name, value);
|
||||
}
|
||||
}
|
||||
@@ -98,10 +109,12 @@ impl Interpreter {
|
||||
val.push_str(RHEI_PREFIX);
|
||||
val.push_str(expr_ref);
|
||||
if let Some(el) = stack.last_mut() {
|
||||
tracker.scan_value(el.element_id, &val);
|
||||
el.push_prop("text".to_string(), val);
|
||||
}
|
||||
} else if let Some(value) = r.read_value_as_string(vop)? {
|
||||
if let Some(el) = stack.last_mut() {
|
||||
tracker.scan_value(el.element_id, &value);
|
||||
el.push_prop("text".to_string(), value);
|
||||
}
|
||||
}
|
||||
@@ -110,7 +123,10 @@ impl Interpreter {
|
||||
OP_PROP_STR => {
|
||||
let key = r.read_str_ref()?;
|
||||
let val = r.read_str_ref()?;
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
if let Some(el) = stack.last_mut() {
|
||||
tracker.scan_value(el.element_id, val);
|
||||
el.push_prop(key, val);
|
||||
}
|
||||
}
|
||||
OP_PROP_VAR => {
|
||||
let key = r.read_str_ref()?;
|
||||
@@ -118,22 +134,31 @@ impl Interpreter {
|
||||
let mut val = String::with_capacity(val_ref.len() + 1);
|
||||
val.push('$');
|
||||
val.push_str(val_ref);
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
if let Some(el) = stack.last_mut() {
|
||||
tracker.scan_value(el.element_id, &val);
|
||||
el.push_prop(key, val);
|
||||
}
|
||||
}
|
||||
OP_PROP_INT => {
|
||||
let key = r.read_str_ref()?;
|
||||
let val = r.read_i64()?.to_string();
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.push_prop(key, val);
|
||||
}
|
||||
}
|
||||
OP_PROP_FLOAT => {
|
||||
let key = r.read_str_ref()?;
|
||||
let val = r.read_f64()?.to_string();
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.push_prop(key, val);
|
||||
}
|
||||
}
|
||||
OP_PROP_BOOL => {
|
||||
let key = r.read_str_ref()?;
|
||||
let val = (r.read_byte()? != 0).to_string();
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.push_prop(key, val);
|
||||
}
|
||||
}
|
||||
OP_PROP_RHEI => {
|
||||
let key = r.read_str_ref()?;
|
||||
@@ -142,13 +167,16 @@ impl Interpreter {
|
||||
val.push_str(RHEI_PREFIX);
|
||||
val.push_str(expr_ref);
|
||||
if let Some(el) = stack.last_mut() {
|
||||
tracker.scan_value(el.element_id, &val);
|
||||
el.push_prop(key, val);
|
||||
}
|
||||
}
|
||||
OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
|
||||
let key = r.read_str_ref()?;
|
||||
if let Some(val) = r.read_value_as_string(op)? {
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.push_prop(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +186,10 @@ impl Interpreter {
|
||||
rhei_scripts.push(script);
|
||||
} else {
|
||||
let mut text_el = Element::new("#text");
|
||||
text_el.push_prop(
|
||||
"text".to_string(),
|
||||
format!("{RHEI_PREFIX}{script}"),
|
||||
);
|
||||
text_el.element_id = tracker.alloc_id();
|
||||
let full_val = format!("{RHEI_PREFIX}{script}");
|
||||
tracker.scan_value(text_el.element_id, &full_val);
|
||||
text_el.push_prop("text".to_string(), full_val);
|
||||
Self::attach(&mut stack, &mut roots, text_el);
|
||||
}
|
||||
}
|
||||
@@ -173,7 +201,7 @@ impl Interpreter {
|
||||
.map(|_| Ok((r.read_string()?, r.read_string()?)))
|
||||
.collect::<Result<Vec<_>, InterpError>>()?;
|
||||
let children = Self::parse_block_elements(
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
r, variables, components, rhei_scripts, stylesheet, tracker, false,
|
||||
)?;
|
||||
components.insert(name.clone(), ComponentDef { name, params, children });
|
||||
}
|
||||
@@ -188,23 +216,26 @@ impl Interpreter {
|
||||
};
|
||||
|
||||
let true_children = Self::parse_block_elements(
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
r, variables, components, rhei_scripts, stylesheet, tracker, false,
|
||||
)?;
|
||||
let has_else = r.read_byte()? == 1;
|
||||
let false_children = if has_else {
|
||||
Self::parse_block_elements(
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
r, variables, components, rhei_scripts, stylesheet, tracker, false,
|
||||
)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut if_el = Element::new("@if");
|
||||
if_el.element_id = tracker.alloc_id();
|
||||
tracker.scan_value(if_el.element_id, &cond_val);
|
||||
if_el.push_prop("condition", cond_val);
|
||||
if_el.children = true_children;
|
||||
|
||||
if !false_children.is_empty() {
|
||||
let mut else_el = Element::new("@else");
|
||||
else_el.element_id = tracker.alloc_id();
|
||||
else_el.children = false_children;
|
||||
if_el.children.push(else_el);
|
||||
}
|
||||
@@ -223,10 +254,12 @@ impl Interpreter {
|
||||
};
|
||||
|
||||
let block_children = Self::parse_block_elements(
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
r, variables, components, rhei_scripts, stylesheet, tracker, false,
|
||||
)?;
|
||||
|
||||
let mut each_el = Element::new("@each");
|
||||
each_el.element_id = tracker.alloc_id();
|
||||
tracker.scan_value(each_el.element_id, &source_val);
|
||||
each_el.push_prop("var_name".to_string(), var_name);
|
||||
each_el.push_prop("source".to_string(), source_val);
|
||||
each_el.children = block_children;
|
||||
@@ -292,11 +325,36 @@ impl Interpreter {
|
||||
|
||||
pub fn evaluate_vdom<'a>(
|
||||
templates: &[Element<'a>],
|
||||
variables: &mut HashMap<String, String>,
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
) -> Vec<Element<'a>> {
|
||||
let refs: Vec<&Element<'a>> = templates.iter().collect();
|
||||
Self::evaluate_vdom_incr(&refs, variables, components, rhei, stylesheet, ancestors, &HashSet::new())
|
||||
}
|
||||
|
||||
pub fn evaluate_vdom_flat<'a>(
|
||||
templates: &[Element<'a>],
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
) -> FlatVDom<'a> {
|
||||
let elements = Self::evaluate_vdom(templates, variables, components, rhei, stylesheet, ancestors);
|
||||
FlatVDom::from_elements(&elements)
|
||||
}
|
||||
|
||||
pub fn evaluate_vdom_incr<'a>(
|
||||
templates: &[&Element<'a>],
|
||||
variables: &mut HashMap<String, Value>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
dirty_set: &HashSet<ElementId>,
|
||||
) -> Vec<Element<'a>> {
|
||||
let mut output = Vec::with_capacity(templates.len());
|
||||
|
||||
@@ -339,16 +397,16 @@ impl Interpreter {
|
||||
let cond = el.get_prop("condition").unwrap_or_default();
|
||||
let is_true = Self::evaluate_condition(cond, variables, rhei);
|
||||
|
||||
let mut active_branch = Vec::new();
|
||||
for child in &el.children {
|
||||
let mut active_branch: Vec<&Element<'a>> = Vec::new();
|
||||
for child in el.children.iter() {
|
||||
if child.type_name == "@else" {
|
||||
if !is_true { active_branch.extend(child.children.clone()); }
|
||||
if !is_true { active_branch.extend(child.children.iter()); }
|
||||
} else if is_true {
|
||||
active_branch.push(child.clone());
|
||||
active_branch.push(child);
|
||||
}
|
||||
}
|
||||
output.extend(Self::evaluate_vdom(
|
||||
&active_branch, variables, components, rhei, stylesheet, ancestors,
|
||||
output.extend(Self::evaluate_vdom_incr(
|
||||
&active_branch, variables, components, rhei, stylesheet, ancestors, dirty_set,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -357,7 +415,7 @@ impl Interpreter {
|
||||
let source_expr = el.get_prop("source").unwrap_or_default();
|
||||
|
||||
let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) {
|
||||
Self::normalize_rhai_array(&rhei.eval_expr(expr, variables))
|
||||
Self::normalize_rhai_array(&rhei.eval_expr(expr, variables).to_owned_string())
|
||||
} else {
|
||||
Self::resolve_string(source_expr, variables).into_owned()
|
||||
};
|
||||
@@ -368,11 +426,13 @@ impl Interpreter {
|
||||
resolved_source.split(',').map(str::trim).map(str::to_string).collect()
|
||||
};
|
||||
|
||||
for item in items {
|
||||
let old_val = variables.insert(var_name.to_string(), item);
|
||||
let child_refs: Vec<&Element<'a>> = el.children.iter().collect();
|
||||
|
||||
output.extend(Self::evaluate_vdom(
|
||||
&el.children, variables, components, rhei, stylesheet, ancestors,
|
||||
for item in items {
|
||||
let old_val = variables.insert(var_name.to_string(), Value::Str(CompactString::new(item)));
|
||||
|
||||
output.extend(Self::evaluate_vdom_incr(
|
||||
&child_refs, variables, components, rhei, stylesheet, ancestors, dirty_set,
|
||||
));
|
||||
|
||||
if let Some(old) = old_val {
|
||||
@@ -397,7 +457,7 @@ impl Interpreter {
|
||||
|
||||
let mut old_vals = Vec::with_capacity(new_args.len());
|
||||
for (k, v) in new_args {
|
||||
old_vals.push((k.clone(), variables.insert(k, v)));
|
||||
old_vals.push((k.clone(), variables.insert(k, Value::from(v))));
|
||||
}
|
||||
|
||||
let mut vcomp = Element::new(el.type_name);
|
||||
@@ -410,12 +470,14 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
let matched_sheets = Self::collect_matching_styles(&vcomp, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]);
|
||||
vcomp.computed_style = ComputedStyle::compute(&vcomp.properties, &matched_sheets);
|
||||
vcomp.computed_style = stylesheet.compute_cached(vcomp.type_name, &vcomp.properties, &matched_sheets);
|
||||
|
||||
let child_ancestors = Self::build_ancestor_chain(ancestors, &vcomp);
|
||||
vcomp.children = Self::evaluate_vdom(
|
||||
&comp.children, variables, components, rhei, stylesheet, &child_ancestors,
|
||||
let comp_child_refs: Vec<&Element<'a>> = comp.children.iter().collect();
|
||||
vcomp.children = Self::evaluate_vdom_incr(
|
||||
&comp_child_refs, variables, components, rhei, stylesheet, &child_ancestors, dirty_set,
|
||||
);
|
||||
vcomp.content_hash = Self::compute_content_hash(&vcomp);
|
||||
output.push(vcomp);
|
||||
|
||||
for (k, old) in old_vals.into_iter().rev() {
|
||||
@@ -442,12 +504,14 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
let matched_sheets = Self::collect_matching_styles(&vnode, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]);
|
||||
vnode.computed_style = ComputedStyle::compute(&vnode.properties, &matched_sheets);
|
||||
vnode.computed_style = stylesheet.compute_cached(vnode.type_name, &vnode.properties, &matched_sheets);
|
||||
|
||||
let child_ancestors = Self::build_ancestor_chain(ancestors, &vnode);
|
||||
vnode.children = Self::evaluate_vdom(
|
||||
&el.children, variables, components, rhei, stylesheet, &child_ancestors,
|
||||
let child_refs: Vec<&Element<'a>> = el.children.iter().collect();
|
||||
vnode.children = Self::evaluate_vdom_incr(
|
||||
&child_refs, variables, components, rhei, stylesheet, &child_ancestors, dirty_set,
|
||||
);
|
||||
vnode.content_hash = Self::compute_content_hash(&vnode);
|
||||
output.push(vnode);
|
||||
}
|
||||
}
|
||||
@@ -457,6 +521,20 @@ impl Interpreter {
|
||||
output
|
||||
}
|
||||
|
||||
fn compute_content_hash(el: &Element) -> u64 {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
let mut h = DefaultHasher::new();
|
||||
el.type_name.hash(&mut h);
|
||||
for (k, v) in &el.properties {
|
||||
k.hash(&mut h);
|
||||
v.hash(&mut h);
|
||||
}
|
||||
for child in &el.children {
|
||||
child.content_hash.hash(&mut h);
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec<AncestorInfo> {
|
||||
let mut chain = ancestors.to_vec();
|
||||
let id = el.id().map(String::from);
|
||||
@@ -489,9 +567,9 @@ impl Interpreter {
|
||||
stylesheet.matching_rules(el.type_name, el_id, &classes, active_pseudo, structural, ancestors, preceding_siblings, &el_attributes)
|
||||
}
|
||||
|
||||
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String {
|
||||
fn resolve_prop(v: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> String {
|
||||
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
||||
rhei.eval_expr(expr, variables)
|
||||
rhei.eval_expr(expr, variables).to_owned_string().into()
|
||||
} else {
|
||||
Self::resolve_string(v, variables).into_owned()
|
||||
}
|
||||
@@ -499,7 +577,7 @@ impl Interpreter {
|
||||
|
||||
fn evaluate_condition(
|
||||
cond: &str,
|
||||
variables: &HashMap<String, String>,
|
||||
variables: &HashMap<String, Value>,
|
||||
rhei: &RheiContext,
|
||||
) -> bool {
|
||||
if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) {
|
||||
@@ -515,7 +593,7 @@ impl Interpreter {
|
||||
|
||||
let resolved = Self::resolve_string(&clean, variables).trim().to_string();
|
||||
|
||||
if Self::is_truthy(&resolved) { return true; }
|
||||
if Self::is_truthy_str(&resolved) { return true; }
|
||||
if resolved == "false" || resolved == "0" || resolved.is_empty() { return false; }
|
||||
|
||||
let operators: [(&str, fn(f64, f64) -> bool); 6] = [
|
||||
@@ -542,7 +620,19 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_truthy(s: &str) -> bool {
|
||||
fn is_truthy(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Bool(b) => *b,
|
||||
Value::Int(i) => *i != 0,
|
||||
Value::Float(f) => *f != 0.0,
|
||||
Value::Str(s) => Self::is_truthy_str(s),
|
||||
Value::None => false,
|
||||
Value::Array(a) => !a.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_truthy_str(s: &str) -> bool {
|
||||
match s.trim() {
|
||||
"" | "false" | "0" | "null" => false,
|
||||
"true" | "1" => true,
|
||||
@@ -563,7 +653,7 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, String>) -> Cow<'a, str> {
|
||||
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str> {
|
||||
if !val.contains('$') {
|
||||
return Cow::Borrowed(val);
|
||||
}
|
||||
@@ -582,7 +672,8 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
if let Some(resolved) = scope.get(&var_name) {
|
||||
result.push_str(resolved);
|
||||
let formatted = resolved.to_owned_string();
|
||||
result.push_str(&formatted);
|
||||
} else {
|
||||
result.push('$');
|
||||
result.push_str(&var_name);
|
||||
@@ -601,3 +692,69 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_flat_vdom_roundtrip() {
|
||||
let mut root = Element::new("Panel");
|
||||
root.element_id = ElementId(1);
|
||||
root.push_prop("class", "container");
|
||||
root.push_prop("color", "red");
|
||||
|
||||
let mut child1 = Element::new("Button");
|
||||
child1.element_id = ElementId(2);
|
||||
child1.push_prop("label", "Click");
|
||||
|
||||
let mut child2 = Element::new("Text");
|
||||
child2.element_id = ElementId(3);
|
||||
child2.push_prop("text", "Hello");
|
||||
|
||||
root.children.push(child1);
|
||||
root.children.push(child2);
|
||||
|
||||
let roots = vec![root];
|
||||
|
||||
let fv = FlatVDom::from_elements(&roots);
|
||||
assert_eq!(fv.node_count(), 3, "FlatVDom should have 3 nodes");
|
||||
|
||||
let roundtrip = fv.into_elements();
|
||||
assert_eq!(roundtrip.len(), 1, "Should have 1 root");
|
||||
assert_eq!(roundtrip[0].type_name, "Panel");
|
||||
assert_eq!(roundtrip[0].children.len(), 2);
|
||||
assert_eq!(roundtrip[0].children[0].type_name, "Button");
|
||||
assert_eq!(roundtrip[0].children[0].get_prop("label"), Some("Click"));
|
||||
assert_eq!(roundtrip[0].children[1].type_name, "Text");
|
||||
assert_eq!(roundtrip[0].children[1].get_prop("text"), Some("Hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flat_vdom_empty() {
|
||||
let fv = FlatVDom::from_elements(&[]);
|
||||
assert_eq!(fv.node_count(), 0);
|
||||
let elements = fv.into_elements();
|
||||
assert!(elements.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flat_vdom_nested() {
|
||||
let mut outer = Element::new("Window");
|
||||
outer.element_id = ElementId(1);
|
||||
let mut inner = Element::new("Panel");
|
||||
inner.element_id = ElementId(2);
|
||||
let mut btn = Element::new("Button");
|
||||
btn.element_id = ElementId(3);
|
||||
btn.push_prop("label", "Nested");
|
||||
inner.children.push(btn);
|
||||
outer.children.push(inner);
|
||||
|
||||
let roots = vec![outer];
|
||||
let fv = FlatVDom::from_elements(&roots);
|
||||
assert_eq!(fv.node_count(), 3);
|
||||
|
||||
let elements = fv.into_elements();
|
||||
assert_eq!(elements[0].children[0].children[0].get_prop("label"), Some("Nested"));
|
||||
}
|
||||
}
|
||||
|
||||
176
src/interpreter/reactive.rs
Normal file
176
src/interpreter/reactive.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Unique identifier for an element in the VDOM tree.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ElementId(pub u32);
|
||||
|
||||
/// Tracks which variables affect which elements for incremental VDOM updates.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReactiveTracker {
|
||||
/// For each variable name, which elements depend on it
|
||||
subscribers: HashMap<String, HashSet<ElementId>>,
|
||||
/// For each element, which variables it depends on
|
||||
dependencies: HashMap<ElementId, HashSet<String>>,
|
||||
/// Elements that need to be re-evaluated
|
||||
dirty_set: HashSet<ElementId>,
|
||||
next_id: u32,
|
||||
}
|
||||
|
||||
impl ReactiveTracker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: HashMap::new(),
|
||||
dependencies: HashMap::new(),
|
||||
dirty_set: HashSet::new(),
|
||||
next_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_id(&mut self) -> ElementId {
|
||||
let id = ElementId(self.next_id);
|
||||
self.next_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
/// Register that `element` depends on `var_name`.
|
||||
/// Call this during template parsing for each `$var` or `!rhee:{expr}` reference.
|
||||
pub fn add_dependency(&mut self, element: ElementId, var_name: &str) {
|
||||
self.subscribers.entry(var_name.to_string())
|
||||
.or_default()
|
||||
.insert(element);
|
||||
self.dependencies.entry(element)
|
||||
.or_default()
|
||||
.insert(var_name.to_string());
|
||||
}
|
||||
|
||||
/// Register multiple dependencies for an element from a property value.
|
||||
pub fn scan_value(&mut self, element: ElementId, value: &str) {
|
||||
// Scan for `$var` patterns
|
||||
let mut chars = value.char_indices().peekable();
|
||||
while let Some((_, c)) = chars.next() {
|
||||
if c == '$' {
|
||||
let mut var_name = String::new();
|
||||
while let Some(&(_, next_c)) = chars.peek() {
|
||||
if next_c.is_ascii_alphanumeric() || next_c == '_' {
|
||||
var_name.push(chars.next().unwrap().1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !var_name.is_empty() {
|
||||
self.add_dependency(element, &var_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when a variable changes. Marks affected elements as dirty.
|
||||
pub fn on_variable_changed(&mut self, name: &str) -> &HashSet<ElementId> {
|
||||
if let Some(affected) = self.subscribers.get(name) {
|
||||
self.dirty_set.extend(affected.iter());
|
||||
}
|
||||
&self.dirty_set
|
||||
}
|
||||
|
||||
/// Returns the current dirty set and clears it.
|
||||
pub fn take_dirty_set(&mut self) -> HashSet<ElementId> {
|
||||
std::mem::take(&mut self.dirty_set)
|
||||
}
|
||||
|
||||
/// Returns true if the element needs re-evaluation.
|
||||
pub fn is_dirty(&self, id: ElementId) -> bool {
|
||||
self.dirty_set.contains(&id)
|
||||
}
|
||||
|
||||
/// Clear all tracking data (e.g., on template reload).
|
||||
pub fn reset(&mut self) {
|
||||
self.subscribers.clear();
|
||||
self.dependencies.clear();
|
||||
self.dirty_set.clear();
|
||||
self.next_id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReactiveTracker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basic_dependency_tracking() {
|
||||
let mut tracker = ReactiveTracker::new();
|
||||
let e1 = tracker.alloc_id();
|
||||
let e2 = tracker.alloc_id();
|
||||
|
||||
tracker.add_dependency(e1, "volume");
|
||||
tracker.add_dependency(e2, "volume");
|
||||
tracker.add_dependency(e2, "brightness");
|
||||
|
||||
tracker.on_variable_changed("volume");
|
||||
let dirty = tracker.take_dirty_set();
|
||||
assert!(dirty.contains(&e1));
|
||||
assert!(dirty.contains(&e2));
|
||||
|
||||
tracker.on_variable_changed("brightness");
|
||||
let dirty = tracker.take_dirty_set();
|
||||
assert!(!dirty.contains(&e1));
|
||||
assert!(dirty.contains(&e2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scan_value() {
|
||||
let mut tracker = ReactiveTracker::new();
|
||||
let e1 = tracker.alloc_id();
|
||||
|
||||
tracker.scan_value(e1, "Hello $name, you are $age years old");
|
||||
|
||||
tracker.on_variable_changed("name");
|
||||
let dirty = tracker.take_dirty_set();
|
||||
assert!(dirty.contains(&e1));
|
||||
|
||||
tracker.on_variable_changed("age");
|
||||
let dirty = tracker.take_dirty_set();
|
||||
assert!(dirty.contains(&e1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scan_no_vars() {
|
||||
let mut tracker = ReactiveTracker::new();
|
||||
let e1 = tracker.alloc_id();
|
||||
|
||||
tracker.scan_value(e1, "Hello world");
|
||||
tracker.on_variable_changed("name");
|
||||
let dirty = tracker.take_dirty_set();
|
||||
assert!(!dirty.contains(&e1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_take_dirty_set() {
|
||||
let mut tracker = ReactiveTracker::new();
|
||||
let e1 = tracker.alloc_id();
|
||||
|
||||
tracker.add_dependency(e1, "x");
|
||||
tracker.on_variable_changed("x");
|
||||
|
||||
let dirty = tracker.take_dirty_set();
|
||||
assert_eq!(dirty.len(), 1);
|
||||
assert!(tracker.dirty_set.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset() {
|
||||
let mut tracker = ReactiveTracker::new();
|
||||
let e1 = tracker.alloc_id();
|
||||
tracker.add_dependency(e1, "x");
|
||||
tracker.reset();
|
||||
|
||||
assert!(tracker.subscribers.is_empty());
|
||||
assert!(tracker.dependencies.is_empty());
|
||||
assert!(tracker.dirty_set.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::opcodes::*;
|
||||
use super::types::InterpError;
|
||||
use super::types::{InterpError, Value};
|
||||
use compact_str::CompactString;
|
||||
|
||||
pub struct Reader<'a> {
|
||||
pub data: &'a [u8],
|
||||
@@ -138,6 +139,77 @@ impl<'a> Reader<'a> {
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn read_value(&mut self, type_op: u8) -> Result<Option<Value>, InterpError> {
|
||||
let val = match type_op {
|
||||
OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_IDENT => {
|
||||
Some(Value::Str(CompactString::new(self.read_string()?)))
|
||||
}
|
||||
OP_PROP_RHEI => {
|
||||
let raw = self.read_string()?;
|
||||
let mut s = String::with_capacity(super::rhei::RHEI_PREFIX.len() + raw.len());
|
||||
s.push_str(super::rhei::RHEI_PREFIX);
|
||||
s.push_str(&raw);
|
||||
Some(Value::Str(CompactString::new(s)))
|
||||
}
|
||||
OP_PROP_VAR => {
|
||||
let name = self.read_str_ref()?;
|
||||
let mut s = String::with_capacity(name.len() + 1);
|
||||
s.push('$');
|
||||
s.push_str(name);
|
||||
Some(Value::Str(CompactString::new(s)))
|
||||
}
|
||||
OP_PROP_INT => Some(Value::Int(self.read_i64()?)),
|
||||
OP_PROP_FLOAT => Some(Value::Float(self.read_f64()?)),
|
||||
OP_PROP_BOOL => Some(Value::Bool(self.read_byte()? != 0)),
|
||||
OP_PROP_NULL => None,
|
||||
OP_PROP_ARRAY => {
|
||||
let items = self.read_array_as_values()?;
|
||||
Some(Value::Array(items))
|
||||
}
|
||||
OP_PROP_UNIT => {
|
||||
let num = self.read_f64()?;
|
||||
let unit = self.read_str_ref()?;
|
||||
let s = if num.fract() == 0.0 {
|
||||
format!("{}{}", num as i64, unit)
|
||||
} else {
|
||||
format!("{}{}", num, unit)
|
||||
};
|
||||
Some(Value::Str(CompactString::new(s)))
|
||||
}
|
||||
OP_PROP_CALL => {
|
||||
let name = self.read_string()?;
|
||||
let arg_count = self.read_u32()? as usize;
|
||||
let mut args = Vec::with_capacity(arg_count);
|
||||
for _ in 0..arg_count {
|
||||
let op = self.read_byte()?;
|
||||
let val = self.read_value(op)?.unwrap_or(Value::None);
|
||||
args.push(match val {
|
||||
Value::Str(s) => s.to_string(),
|
||||
Value::Int(i) => i.to_string(),
|
||||
Value::Float(f) => f.to_string(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
_ => String::new(),
|
||||
});
|
||||
}
|
||||
Some(Value::Str(CompactString::new(format!("{}({})", name, args.join(",")))))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
pub fn read_array_as_values(&mut self) -> Result<Vec<Value>, InterpError> {
|
||||
let count = self.read_u32()? as usize;
|
||||
let mut items = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let elem_op = self.read_byte()?;
|
||||
if let Some(v) = self.read_value(elem_op)? {
|
||||
items.push(v);
|
||||
}
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub fn read_array_as_strings(&mut self) -> Result<Vec<String>, InterpError> {
|
||||
let count = self.read_u32()? as usize;
|
||||
let mut items = Vec::with_capacity(count);
|
||||
|
||||
@@ -2,16 +2,27 @@ use rhai::{Dynamic, Engine, Scope, AST, Module};
|
||||
use std::collections::HashMap;
|
||||
use std::cell::RefCell;
|
||||
|
||||
use super::types::Value;
|
||||
use compact_str::CompactString;
|
||||
|
||||
pub const RHEI_PREFIX: &str = "__rhei:";
|
||||
|
||||
pub struct RheiContext {
|
||||
engine: Engine,
|
||||
init_ast: AST,
|
||||
scope: RefCell<Scope<'static>>,
|
||||
action_cache: RefCell<HashMap<String, AST>>,
|
||||
expr_cache: RefCell<HashMap<String, AST>>,
|
||||
}
|
||||
|
||||
impl RheiContext {
|
||||
pub fn new(scripts: &[String]) -> Self {
|
||||
let ctx = Self::new_empty(scripts);
|
||||
ctx.precompile_scripts(scripts);
|
||||
ctx
|
||||
}
|
||||
|
||||
fn new_empty(scripts: &[String]) -> Self {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine.on_print(|s| println!("[rhei] {s}"));
|
||||
@@ -43,27 +54,29 @@ impl RheiContext {
|
||||
engine,
|
||||
init_ast: combined,
|
||||
scope: RefCell::new(Scope::new()),
|
||||
action_cache: RefCell::new(HashMap::new()),
|
||||
expr_cache: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync_scope(&self, variables: &HashMap<String, String>) {
|
||||
pub fn sync_scope(&self, variables: &HashMap<String, Value>) {
|
||||
let mut scope = self.scope.borrow_mut();
|
||||
|
||||
for (k, v) in variables.iter() {
|
||||
if scope.contains(k) {
|
||||
if let Some(old_val) = scope.get_value::<Dynamic>(k) {
|
||||
if dyn_to_str(&old_val) == *v {
|
||||
if dynamic_to_value(&old_val) == *v {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
scope.set_value(k, str_to_dyn(v));
|
||||
scope.set_value(k, value_to_dynamic(v));
|
||||
} else {
|
||||
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||
scope.push_dynamic(k.clone(), value_to_dynamic(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&self, variables: &mut HashMap<String, String>) {
|
||||
pub fn initialize(&self, variables: &mut HashMap<String, Value>) {
|
||||
self.sync_scope(variables);
|
||||
|
||||
let mut scope = self.scope.borrow_mut();
|
||||
@@ -72,31 +85,40 @@ impl RheiContext {
|
||||
}
|
||||
|
||||
for (name, _, val) in scope.iter_raw() {
|
||||
let s_val = dyn_to_str(&val);
|
||||
let s_val = dynamic_to_value(&val);
|
||||
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
|
||||
variables.insert(name.to_string(), s_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> String {
|
||||
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, Value>) -> Value {
|
||||
self.sync_scope(variables);
|
||||
let mut scope = self.scope.borrow_mut();
|
||||
|
||||
match self.engine.eval_expression_with_scope::<Dynamic>(&mut *scope, expr) {
|
||||
Ok(val) => dyn_to_str(&val),
|
||||
let ast = self.get_or_compile_expr(expr);
|
||||
match ast {
|
||||
Some(ast) => {
|
||||
match self.engine.eval_ast_with_scope::<Dynamic>(&mut *scope, &ast) {
|
||||
Ok(val) => dynamic_to_value(&val),
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei eval_expr error: {e}");
|
||||
String::new()
|
||||
Value::None
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Value::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool {
|
||||
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, Value>) -> bool {
|
||||
self.sync_scope(variables);
|
||||
let mut scope = self.scope.borrow_mut();
|
||||
|
||||
match self.engine.eval_expression_with_scope::<bool>(&mut *scope, expr) {
|
||||
let ast = self.get_or_compile_expr(expr);
|
||||
match ast {
|
||||
Some(ast) => {
|
||||
match self.engine.eval_ast_with_scope::<bool>(&mut *scope, &ast) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei eval_condition error: {e}");
|
||||
@@ -104,29 +126,103 @@ impl RheiContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) {
|
||||
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, Value>) {
|
||||
self.sync_scope(variables);
|
||||
let mut scope = self.scope.borrow_mut();
|
||||
|
||||
match self.engine.compile(script) {
|
||||
Ok(action_ast) => {
|
||||
let action_ast = self.get_or_compile_action(script);
|
||||
if let Some(action_ast) = action_ast {
|
||||
if let Err(e) = self.engine.run_ast_with_scope(&mut *scope, &action_ast) {
|
||||
eprintln!("⚠️ Rhei action execution error: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei action compilation error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
for (name, _, val) in scope.iter_raw() {
|
||||
let s_val = dyn_to_str(&val);
|
||||
let s_val = dynamic_to_value(&val);
|
||||
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
|
||||
variables.insert(name.to_string(), s_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_compile_action(&self, script: &str) -> Option<AST> {
|
||||
let mut cache = self.action_cache.borrow_mut();
|
||||
if let Some(ast) = cache.get(script) {
|
||||
return Some(ast.clone());
|
||||
}
|
||||
match self.engine.compile(script) {
|
||||
Ok(ast) => {
|
||||
cache.insert(script.to_string(), ast.clone());
|
||||
Some(ast)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei action compilation error: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_compile_expr(&self, expr: &str) -> Option<AST> {
|
||||
let mut cache = self.expr_cache.borrow_mut();
|
||||
if let Some(ast) = cache.get(expr) {
|
||||
return Some(ast.clone());
|
||||
}
|
||||
match self.engine.compile_expression(expr) {
|
||||
Ok(ast) => {
|
||||
cache.insert(expr.to_string(), ast.clone());
|
||||
Some(ast)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei expression compilation error: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn precompile_scripts(&self, scripts: &[String]) {
|
||||
for script in scripts {
|
||||
self.get_or_compile_action(script);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn precompile_actions(&self, actions: &[String]) {
|
||||
for action in actions {
|
||||
self.get_or_compile_action(action);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn precompile_exprs(&self, exprs: &[String]) {
|
||||
for expr in exprs {
|
||||
self.get_or_compile_expr(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn precompile_all_from_doc(&self, doc: &super::Document) {
|
||||
for script in &doc.rhei_scripts {
|
||||
self.get_or_compile_action(script);
|
||||
}
|
||||
for root in &doc.roots {
|
||||
Self::collect_and_precompile(root, self);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_and_precompile(el: &super::Element, ctx: &RheiContext) {
|
||||
for (k, v) in &el.properties {
|
||||
if k.starts_with("__on:") && !v.is_empty() {
|
||||
ctx.get_or_compile_action(v);
|
||||
}
|
||||
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
||||
ctx.get_or_compile_expr(expr);
|
||||
}
|
||||
}
|
||||
for child in &el.children {
|
||||
Self::collect_and_precompile(child, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RheiContext {
|
||||
@@ -135,16 +231,43 @@ impl Default for RheiContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn str_to_dyn(s: &str) -> Dynamic {
|
||||
pub fn value_to_dynamic(v: &Value) -> Dynamic {
|
||||
match v {
|
||||
Value::Int(i) => Dynamic::from(*i),
|
||||
Value::Float(f) => Dynamic::from(*f),
|
||||
Value::Bool(b) => Dynamic::from(*b),
|
||||
Value::Str(s) => str_to_dynamic(s),
|
||||
Value::None => Dynamic::UNIT,
|
||||
Value::Array(arr) => {
|
||||
let d: rhai::Dynamic = arr.iter().map(value_to_dynamic).collect();
|
||||
d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn str_to_dynamic(s: &str) -> Dynamic {
|
||||
if let Ok(i) = s.parse::<i64>() { return Dynamic::from(i); }
|
||||
if let Ok(f) = s.parse::<f64>() { return Dynamic::from(f); }
|
||||
if let Ok(b) = s.parse::<bool>() { return Dynamic::from(b); }
|
||||
Dynamic::from(s.to_owned())
|
||||
}
|
||||
|
||||
pub fn dyn_to_str(d: &Dynamic) -> String {
|
||||
pub fn dynamic_to_value(d: &Dynamic) -> Value {
|
||||
if d.is_string() {
|
||||
return d.clone().into_string().unwrap_or_default();
|
||||
return Value::Str(CompactString::new(d.clone().into_string().unwrap_or_default()));
|
||||
}
|
||||
d.to_string()
|
||||
if d.is_int() {
|
||||
return Value::Int(d.as_int().unwrap_or(0));
|
||||
}
|
||||
if d.is_float() {
|
||||
return Value::Float(d.as_float().unwrap_or(0.0));
|
||||
}
|
||||
if d.is_bool() {
|
||||
return Value::Bool(d.as_bool().unwrap_or(false));
|
||||
}
|
||||
if d.is_array() {
|
||||
let arr = d.clone().into_array().unwrap_or_default();
|
||||
return Value::Array(arr.iter().map(dynamic_to_value).collect());
|
||||
}
|
||||
Value::None
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Mutex;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use super::reactive::ElementId;
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -457,9 +461,82 @@ impl StyleRule {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyleCache {
|
||||
entries: HashMap<u64, ComputedStyle>,
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl StyleCache {
|
||||
pub fn new(max_entries: usize) -> Self {
|
||||
Self { entries: HashMap::new(), max_entries }
|
||||
}
|
||||
|
||||
pub fn get_or_compute(
|
||||
&mut self,
|
||||
type_name: &str,
|
||||
props: &[(Cow<'_, str>, Cow<'_, str>)],
|
||||
epoch: u64,
|
||||
matched_sheets: &[&HashMap<String, String>],
|
||||
) -> ComputedStyle {
|
||||
let key = {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
type_name.hash(&mut hasher);
|
||||
for (k, v) in props {
|
||||
k.hash(&mut hasher);
|
||||
v.hash(&mut hasher);
|
||||
}
|
||||
epoch.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
if let Some(cached) = self.entries.get(&key) {
|
||||
return cached.clone();
|
||||
}
|
||||
|
||||
let style = ComputedStyle::compute(props, matched_sheets);
|
||||
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.entries.clear();
|
||||
}
|
||||
self.entries.insert(key, style.clone());
|
||||
|
||||
style
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StyleSheet {
|
||||
rules: Vec<StyleRule>,
|
||||
index: Option<StyleIndex>,
|
||||
epoch: u64,
|
||||
cache: Mutex<StyleCache>,
|
||||
}
|
||||
|
||||
impl Clone for StyleSheet {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
rules: self.rules.clone(),
|
||||
index: self.index.clone(),
|
||||
epoch: self.epoch,
|
||||
cache: Mutex::new(StyleCache::new(1024)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StyleSheet {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rules: Vec::new(),
|
||||
index: None,
|
||||
epoch: 0,
|
||||
cache: Mutex::new(StyleCache::new(1024)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleSheet {
|
||||
@@ -471,6 +548,123 @@ impl StyleSheet {
|
||||
for part in split_selectors(&selector) {
|
||||
self.rules.push(StyleRule::build(part, properties.clone()));
|
||||
}
|
||||
self.index = None;
|
||||
}
|
||||
|
||||
pub fn build_index(&mut self) {
|
||||
self.epoch += 1;
|
||||
self.cache.lock().unwrap().clear();
|
||||
let mut index = StyleIndex::new();
|
||||
index.epoch = self.epoch;
|
||||
|
||||
for (i, rule) in self.rules.iter().enumerate() {
|
||||
let specificity = rule.selector.specificity();
|
||||
index.rule_specificities.push(specificity);
|
||||
index.rules.push(rule.clone());
|
||||
|
||||
if rule.selector.compounds.len() == 1 {
|
||||
if let Some(compound) = rule.selector.compounds.first() {
|
||||
let tag_key = compound.tag.as_deref().unwrap_or("*");
|
||||
index.by_tag.entry(tag_key.to_string()).or_default().push(i);
|
||||
|
||||
if tag_key == "*" {
|
||||
index.universal_rules.push(i);
|
||||
}
|
||||
|
||||
for cls in &compound.classes {
|
||||
index.by_class.entry(cls.clone()).or_default().push(i);
|
||||
index.by_tag_class.entry((tag_key.to_string(), cls.clone())).or_default().push(i);
|
||||
}
|
||||
|
||||
if let Some(id) = &compound.id {
|
||||
index.by_id.entry(id.clone()).or_default().push(i);
|
||||
index.by_tag_id.entry((tag_key.to_string(), id.clone())).or_default().push(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(compound) = rule.selector.compounds.last() {
|
||||
let tag_key = compound.tag.as_deref().unwrap_or("*");
|
||||
index.by_tag.entry(tag_key.to_string()).or_default().push(i);
|
||||
|
||||
if tag_key == "*" {
|
||||
index.universal_rules.push(i);
|
||||
}
|
||||
|
||||
for cls in &compound.classes {
|
||||
index.by_class.entry(cls.clone()).or_default().push(i);
|
||||
index.by_tag_class.entry((tag_key.to_string(), cls.clone())).or_default().push(i);
|
||||
}
|
||||
|
||||
if let Some(id) = &compound.id {
|
||||
index.by_id.entry(id.clone()).or_default().push(i);
|
||||
index.by_tag_id.entry((tag_key.to_string(), id.clone())).or_default().push(i);
|
||||
}
|
||||
}
|
||||
index.complex_rules.push((i, i));
|
||||
}
|
||||
}
|
||||
|
||||
self.index = Some(index);
|
||||
}
|
||||
|
||||
pub fn has_index(&self) -> bool {
|
||||
self.index.is_some()
|
||||
}
|
||||
|
||||
pub fn query_index<'a>(
|
||||
&'a self,
|
||||
type_name: &str,
|
||||
el_id: Option<&str>,
|
||||
el_classes: &[&str],
|
||||
active_pseudo: &[&str],
|
||||
structural: &StructuralContext,
|
||||
ancestors: &[AncestorInfo],
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
el_attributes: &HashMap<String, String>,
|
||||
) -> Option<Vec<&'a HashMap<String, String>>> {
|
||||
let index = self.index.as_ref()?;
|
||||
|
||||
let mut candidates: HashSet<RuleId> = HashSet::with_capacity(32);
|
||||
|
||||
candidates.extend(&index.universal_rules);
|
||||
|
||||
if let Some(rules) = index.by_tag.get(type_name) {
|
||||
candidates.extend(rules);
|
||||
}
|
||||
|
||||
for cls in el_classes {
|
||||
if let Some(rules) = index.by_class.get(*cls) {
|
||||
candidates.extend(rules);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = el_id {
|
||||
if let Some(rules) = index.by_id.get(id) {
|
||||
candidates.extend(rules);
|
||||
}
|
||||
}
|
||||
|
||||
for (_, rule_id) in &index.complex_rules {
|
||||
candidates.insert(*rule_id);
|
||||
}
|
||||
|
||||
let mut matched: Vec<(RuleId, &StyleRule)> = candidates
|
||||
.iter()
|
||||
.filter(|&&rule_id| {
|
||||
let rule = &index.rules[rule_id];
|
||||
rule.selector
|
||||
.matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes)
|
||||
})
|
||||
.map(|&rule_id| (rule_id, &index.rules[rule_id]))
|
||||
.collect();
|
||||
|
||||
matched.sort_by(|(i, _a), (j, _b)| {
|
||||
index.rule_specificities[*i]
|
||||
.cmp(&index.rule_specificities[*j])
|
||||
.then_with(|| i.cmp(j))
|
||||
});
|
||||
|
||||
Some(matched.into_iter().map(|(_, rule)| &rule.properties).collect())
|
||||
}
|
||||
|
||||
pub fn matching_rules<'a>(
|
||||
@@ -484,6 +678,10 @@ impl StyleSheet {
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
el_attributes: &HashMap<String, String>,
|
||||
) -> Vec<&'a HashMap<String, String>> {
|
||||
if let Some(matched) = self.query_index(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) {
|
||||
return matched;
|
||||
}
|
||||
|
||||
let mut matched: Vec<(usize, &StyleRule)> = self
|
||||
.rules
|
||||
.iter()
|
||||
@@ -504,6 +702,89 @@ impl StyleSheet {
|
||||
matched.into_iter().map(|(_, rule)| &rule.properties).collect()
|
||||
}
|
||||
|
||||
pub fn query_index_for_pseudo(
|
||||
&self,
|
||||
pseudo: &str,
|
||||
type_name: &str,
|
||||
el_id: Option<&str>,
|
||||
el_classes: &[&str],
|
||||
structural: &StructuralContext,
|
||||
ancestors: &[AncestorInfo],
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
el_attributes: &HashMap<String, String>,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
let index = self.index.as_ref()?;
|
||||
|
||||
let mut candidates: HashSet<RuleId> = HashSet::with_capacity(16);
|
||||
|
||||
candidates.extend(&index.universal_rules);
|
||||
|
||||
if let Some(rules) = index.by_tag.get(type_name) {
|
||||
candidates.extend(rules);
|
||||
}
|
||||
|
||||
for cls in el_classes {
|
||||
if let Some(rules) = index.by_class.get(*cls) {
|
||||
candidates.extend(rules);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = el_id {
|
||||
if let Some(rules) = index.by_id.get(id) {
|
||||
candidates.extend(rules);
|
||||
}
|
||||
}
|
||||
|
||||
for (_, rule_id) in &index.complex_rules {
|
||||
candidates.insert(*rule_id);
|
||||
}
|
||||
|
||||
let mut props = HashMap::new();
|
||||
for &rule_id in &candidates {
|
||||
let rule = &index.rules[rule_id];
|
||||
if !rule.selector.has_pseudo_class(pseudo) {
|
||||
continue;
|
||||
}
|
||||
if !rule.selector.matches(type_name, el_id, el_classes, &[pseudo], structural, ancestors, preceding_siblings, el_attributes) {
|
||||
continue;
|
||||
}
|
||||
for (k, v) in &rule.properties {
|
||||
props.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
Some(props)
|
||||
}
|
||||
|
||||
#[cfg(feature = "parallel")]
|
||||
pub fn matching_rules_batch<'a>(
|
||||
&'a self,
|
||||
type_names: &[&str],
|
||||
el_ids: &[Option<&str>],
|
||||
el_classes_list: &[&[&str]],
|
||||
active_pseudo_list: &[&[&str]],
|
||||
structural_list: &[&StructuralContext],
|
||||
ancestors_list: &[&[AncestorInfo]],
|
||||
preceding_siblings_list: &[&[AncestorInfo]],
|
||||
el_attributes_list: &[&HashMap<String, String>],
|
||||
) -> Vec<Vec<&'a HashMap<String, String>>> {
|
||||
use rayon::prelude::*;
|
||||
(0..type_names.len())
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
self.matching_rules(
|
||||
type_names[i],
|
||||
el_ids[i],
|
||||
el_classes_list[i],
|
||||
active_pseudo_list[i],
|
||||
structural_list[i],
|
||||
ancestors_list[i],
|
||||
preceding_siblings_list[i],
|
||||
el_attributes_list[i],
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn matching_pseudo_rules(
|
||||
&self,
|
||||
pseudo: &str,
|
||||
@@ -515,6 +796,10 @@ impl StyleSheet {
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
el_attributes: &HashMap<String, String>,
|
||||
) -> HashMap<String, String> {
|
||||
if let Some(props) = self.query_index_for_pseudo(pseudo, type_name, el_id, el_classes, structural, ancestors, preceding_siblings, el_attributes) {
|
||||
return props;
|
||||
}
|
||||
|
||||
let mut props = HashMap::new();
|
||||
for rule in &self.rules {
|
||||
if !rule.selector.has_pseudo_class(pseudo) {
|
||||
@@ -530,6 +815,21 @@ impl StyleSheet {
|
||||
props
|
||||
}
|
||||
|
||||
pub fn compute_cached<'a>(
|
||||
&self,
|
||||
type_name: &str,
|
||||
props: &[(Cow<'_, str>, Cow<'_, str>)],
|
||||
matched_sheets: &[&'a HashMap<String, String>],
|
||||
) -> ComputedStyle {
|
||||
let _scope = crate::perf::PerfScope::new("style");
|
||||
let epoch = self.epoch;
|
||||
self.cache.lock().unwrap().get_or_compute(type_name, props, epoch, matched_sheets)
|
||||
}
|
||||
|
||||
pub fn clear_cache(&self) {
|
||||
self.cache.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rules.is_empty()
|
||||
}
|
||||
@@ -563,6 +863,39 @@ fn split_selectors(input: &str) -> Vec<String> {
|
||||
parts
|
||||
}
|
||||
|
||||
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 {
|
||||
Self {
|
||||
by_tag: HashMap::new(),
|
||||
by_class: HashMap::new(),
|
||||
by_id: HashMap::new(),
|
||||
by_tag_class: HashMap::new(),
|
||||
by_tag_id: HashMap::new(),
|
||||
complex_rules: Vec::new(),
|
||||
universal_rules: Vec::new(),
|
||||
rule_specificities: Vec::new(),
|
||||
rules: Vec::new(),
|
||||
epoch: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Overflow {
|
||||
#[default]
|
||||
@@ -593,35 +926,50 @@ pub enum Display {
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SizeValue {
|
||||
Px(f32),
|
||||
Percent(f32),
|
||||
}
|
||||
|
||||
impl SizeValue {
|
||||
pub fn resolve(self, relative_to: Option<f32>) -> f32 {
|
||||
match self {
|
||||
SizeValue::Px(v) => v,
|
||||
SizeValue::Percent(p) => relative_to.map(|base| base * p / 100.0).unwrap_or(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ComputedStyle {
|
||||
pub font_size: Option<f32>,
|
||||
pub font_size: Option<SizeValue>,
|
||||
pub color: Option<iced::Color>,
|
||||
pub padding: Option<f32>,
|
||||
pub padding_top: Option<f32>,
|
||||
pub padding_right: Option<f32>,
|
||||
pub padding_bottom:Option<f32>,
|
||||
pub padding_left: Option<f32>,
|
||||
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<f32>,
|
||||
pub margin_top: Option<f32>,
|
||||
pub margin_right: Option<f32>,
|
||||
pub margin_bottom: Option<f32>,
|
||||
pub margin_left: Option<f32>,
|
||||
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<f32>,
|
||||
pub border_radius: Option<f32>,
|
||||
pub border_width: Option<f32>,
|
||||
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<f32>,
|
||||
pub max_width: Option<f32>,
|
||||
pub min_height: Option<f32>,
|
||||
pub max_height: Option<f32>,
|
||||
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>,
|
||||
@@ -629,10 +977,10 @@ pub struct ComputedStyle {
|
||||
pub flex_grow: Option<u16>,
|
||||
|
||||
pub position: Option<Position>,
|
||||
pub top: Option<f32>,
|
||||
pub right: Option<f32>,
|
||||
pub bottom: Option<f32>,
|
||||
pub left: Option<f32>,
|
||||
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>,
|
||||
@@ -640,7 +988,7 @@ pub struct ComputedStyle {
|
||||
|
||||
pub opacity: Option<f32>,
|
||||
pub font_weight: Option<u16>,
|
||||
pub line_height: Option<f32>,
|
||||
pub line_height: Option<SizeValue>,
|
||||
pub text_align: Option<TextAlign>,
|
||||
}
|
||||
|
||||
@@ -714,6 +1062,14 @@ let overflow = lookup("overflow", inline, matched_sheets).and_then(parse_overflo
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "parallel")]
|
||||
pub fn compute_batch<'a>(
|
||||
pairs: &[(&[(Cow<'_, str>, Cow<'_, str>)], &[&'a HashMap<String, String>])],
|
||||
) -> Vec<ComputedStyle> {
|
||||
use rayon::prelude::*;
|
||||
pairs.par_iter().map(|(inline, sheets)| Self::compute(inline, sheets)).collect()
|
||||
}
|
||||
|
||||
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>) {
|
||||
struct Override<'a>(&'a HashMap<String, String>);
|
||||
impl<'a> Override<'a> {
|
||||
@@ -974,16 +1330,175 @@ pub fn parse_text_align(s: &str) -> Option<TextAlign> {
|
||||
else { None }
|
||||
}
|
||||
|
||||
pub fn parse_size(s: &str) -> Option<f32> {
|
||||
pub fn parse_size(s: &str) -> Option<SizeValue> {
|
||||
let s = s.trim();
|
||||
if s.ends_with('%') || s.eq_ignore_ascii_case("auto") ||
|
||||
if s.eq_ignore_ascii_case("auto") ||
|
||||
s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("stretch") {
|
||||
return None;
|
||||
}
|
||||
if s.ends_with('%') {
|
||||
let val = s.trim_end_matches(|c: char| c == '%' || c.is_alphabetic())
|
||||
.parse::<f32>().ok()?;
|
||||
return Some(SizeValue::Percent(val));
|
||||
}
|
||||
if s.eq_ignore_ascii_case("vw") || s.eq_ignore_ascii_case("vh") {
|
||||
return None;
|
||||
}
|
||||
s.trim_end_matches(|c: char| c.is_alphabetic())
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
let val = s.trim_end_matches(|c: char| c.is_alphabetic())
|
||||
.parse::<f32>().ok()?;
|
||||
Some(SizeValue::Px(val))
|
||||
}
|
||||
|
||||
pub fn resolve_size(v: Option<SizeValue>, relative_to: Option<f32>) -> Option<f32> {
|
||||
v.map(|sv| sv.resolve(relative_to))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_style_sheet(rules: &[(&str, &[(&str, &str)])]) -> StyleSheet {
|
||||
let mut ss = StyleSheet::new();
|
||||
for (selector, props) in rules {
|
||||
let mut map = HashMap::new();
|
||||
for (k, v) in *props {
|
||||
map.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
ss.add_rule(selector.to_string(), map);
|
||||
}
|
||||
ss.build_index();
|
||||
ss
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_style_index_basic() {
|
||||
let ss = make_style_sheet(&[
|
||||
("Button", &[("color", "red")]),
|
||||
("Button.primary", &[("color", "blue")]),
|
||||
("#submit", &[("font-size", "20")]),
|
||||
("Label", &[("color", "green")]),
|
||||
]);
|
||||
|
||||
let attrs = HashMap::new();
|
||||
let structural = StructuralContext::default();
|
||||
|
||||
// Button class="" should match "Button" rule
|
||||
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
|
||||
assert_eq!(matched.len(), 1, "Button should match 1 rule");
|
||||
assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("red"));
|
||||
|
||||
// Button class="primary" should match both "Button" and "Button.primary"
|
||||
let matched = ss.matching_rules("Button", None, &["primary"], &[], &structural, &[], &[], &attrs);
|
||||
assert_eq!(matched.len(), 2, "Button.primary should match 2 rules");
|
||||
// The more specific rule comes first (or last depending on order)
|
||||
let colors: Vec<&str> = matched.iter().filter_map(|m| m.get("color").map(|s| s.as_str())).collect();
|
||||
assert!(colors.contains(&"red"));
|
||||
assert!(colors.contains(&"blue"));
|
||||
|
||||
// Button id="submit" should match "Button" and "#submit"
|
||||
let matched = ss.matching_rules("Button", Some("submit"), &[], &[], &structural, &[], &[], &attrs);
|
||||
assert_eq!(matched.len(), 2, "Button#submit should match 2 rules");
|
||||
assert!(matched.iter().any(|m| m.contains_key("color")));
|
||||
assert!(matched.iter().any(|m| m.contains_key("font-size")));
|
||||
|
||||
// Label should match "Label"
|
||||
let matched = ss.matching_rules("Label", None, &[], &[], &structural, &[], &[], &attrs);
|
||||
assert_eq!(matched.len(), 1);
|
||||
assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("green"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_style_index_empty() {
|
||||
let ss = make_style_sheet(&[]);
|
||||
let attrs = HashMap::new();
|
||||
let structural = StructuralContext::default();
|
||||
|
||||
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
|
||||
assert!(matched.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_style_index_pseudo() {
|
||||
let ss = make_style_sheet(&[
|
||||
("Button:hover", &[("color", "red")]),
|
||||
("Button", &[("color", "blue")]),
|
||||
]);
|
||||
|
||||
let attrs = HashMap::new();
|
||||
let structural = StructuralContext::default();
|
||||
|
||||
// No pseudo
|
||||
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
|
||||
assert_eq!(matched.len(), 1);
|
||||
assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("blue"));
|
||||
|
||||
// With hover pseudo
|
||||
let matched = ss.matching_rules("Button", None, &[], &["hover"], &structural, &[], &[], &attrs);
|
||||
assert_eq!(matched.len(), 2, "Button:hover should match 2 rules with pseudo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_style_index_complex() {
|
||||
let ss = make_style_sheet(&[
|
||||
("Panel > Button", &[("color", "red")]),
|
||||
("Panel Button", &[("font-size", "16")]),
|
||||
]);
|
||||
|
||||
let attrs = HashMap::new();
|
||||
let structural = StructuralContext::default();
|
||||
|
||||
// Button with Panel ancestor
|
||||
let ancestors = [AncestorInfo::new("Panel", vec![])];
|
||||
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &ancestors, &[], &attrs);
|
||||
assert_eq!(matched.len(), 2, "Both complex rules should match");
|
||||
|
||||
// Button without Panel ancestor
|
||||
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
|
||||
assert_eq!(matched.len(), 0, "No matching without Panel ancestor");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_style_index_off_by_one_same_as_fallback() {
|
||||
// Verify index gives same results as fallback for various inputs
|
||||
let ss = make_style_sheet(&[
|
||||
("*", &[("margin", "0")]),
|
||||
("Button", &[("padding", "10")]),
|
||||
("Button.primary#submit", &[("color", "red")]),
|
||||
("Label:hover", &[("color", "blue")]),
|
||||
(".highlight", &[("background", "yellow")]),
|
||||
]);
|
||||
|
||||
let attrs: HashMap<String, String> = HashMap::new();
|
||||
let structural = StructuralContext::default();
|
||||
|
||||
// Verify index matches expected behavior
|
||||
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
|
||||
assert!(!matched.is_empty(), "Button should match universal rule");
|
||||
assert!(matched.iter().any(|m| m.get("margin").map(|s| s.as_str()) == Some("0")), "Should include universal margin");
|
||||
assert!(matched.iter().any(|m| m.get("padding").map(|s| s.as_str()) == Some("10")), "Should include Button padding");
|
||||
|
||||
let matched = ss.matching_rules("Button", Some("submit"), &["primary"], &[], &structural, &[], &[], &attrs);
|
||||
assert!(matched.iter().any(|m| m.contains_key("color")), "#submit.primary should match color rule");
|
||||
|
||||
let matched = ss.matching_rules("Label", None, &["highlight"], &[], &structural, &[], &[], &attrs);
|
||||
assert!(matched.iter().any(|m| m.contains_key("background")), "highlight class should match");
|
||||
|
||||
let matched = ss.matching_rules("Panel", None, &[], &[], &structural, &[], &[], &attrs);
|
||||
assert!(matched.iter().any(|m| m.contains_key("margin")), "Panel should match universal rule");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_style_index_epoch_increases() {
|
||||
let mut ss = make_style_sheet(&[("Button", &[("color", "red")])]);
|
||||
let epoch1 = ss.index.as_ref().unwrap().epoch;
|
||||
ss.add_rule("Label".to_string(), {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("color".to_string(), "blue".to_string());
|
||||
m
|
||||
});
|
||||
assert!(ss.index.is_none(), "add_rule should invalidate index");
|
||||
ss.build_index();
|
||||
let epoch2 = ss.index.as_ref().unwrap().epoch;
|
||||
assert!(epoch2 > epoch1, "build_index should increase epoch");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,164 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::ops::Range;
|
||||
|
||||
|
||||
use super::reactive::{ElementId, ReactiveTracker};
|
||||
use super::style::{ComputedStyle, StyleSheet};
|
||||
use compact_str::CompactString;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct InternedStr(u32);
|
||||
|
||||
impl InternedStr {
|
||||
pub const fn from_raw(id: u32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl InternedStr {
|
||||
pub fn eq_str(&self, other: &str) -> bool {
|
||||
with_interner(|interner| interner.lookup(*self) == other)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Interner {
|
||||
strings: Vec<String>,
|
||||
map: HashMap<String, u32>,
|
||||
next_id: u32,
|
||||
}
|
||||
|
||||
impl Interner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
strings: Vec::new(),
|
||||
map: HashMap::new(),
|
||||
next_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intern(&mut self, s: &str) -> InternedStr {
|
||||
if let Some(&id) = self.map.get(s) {
|
||||
return InternedStr(id);
|
||||
}
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.strings.push(s.to_string());
|
||||
self.map.insert(s.to_string(), id);
|
||||
InternedStr(id)
|
||||
}
|
||||
|
||||
pub fn lookup(&self, id: InternedStr) -> &str {
|
||||
&self.strings[id.0 as usize]
|
||||
}
|
||||
|
||||
pub fn intern_or_none(&mut self, s: Option<&str>) -> Option<InternedStr> {
|
||||
s.map(|s| self.intern(s))
|
||||
}
|
||||
}
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static GLOBAL_INTERNER: RefCell<Interner> = RefCell::new(Interner::new());
|
||||
}
|
||||
|
||||
pub fn with_interner<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Interner) -> R,
|
||||
{
|
||||
GLOBAL_INTERNER.with(|i| f(&mut *i.borrow_mut()))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Value {
|
||||
Str(CompactString),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
Array(Vec<Value>),
|
||||
None,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Value::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_owned_string(&self) -> CompactString {
|
||||
match self {
|
||||
Value::Str(s) => s.clone(),
|
||||
Value::Int(i) => CompactString::new(i.to_string()),
|
||||
Value::Float(f) => CompactString::new(if f.fract() == 0.0 {
|
||||
format!("{:.1}", f)
|
||||
} else {
|
||||
f.to_string()
|
||||
}),
|
||||
Value::Bool(b) => CompactString::new(b.to_string()),
|
||||
Value::Array(a) => CompactString::new(a.iter().map(|v| v.to_owned_string()).collect::<Vec<_>>().join(",")),
|
||||
Value::None => CompactString::new(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(s: &str) -> Self {
|
||||
Value::Str(CompactString::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(s: String) -> Self {
|
||||
Value::Str(CompactString::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(i: i64) -> Self {
|
||||
Value::Int(i)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Value {
|
||||
fn from(f: f64) -> Self {
|
||||
Value::Float(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(b: bool) -> Self {
|
||||
Value::Bool(b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Value>> From<Vec<T>> for Value {
|
||||
fn from(v: Vec<T>) -> Self {
|
||||
Value::Array(v.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Value::Str(a), Value::Str(b)) => a == b,
|
||||
(Value::Int(a), Value::Int(b)) => a == b,
|
||||
(Value::Float(a), Value::Float(b)) => (a - b).abs() < f64::EPSILON,
|
||||
(Value::Bool(a), Value::Bool(b)) => a == b,
|
||||
(Value::Array(a), Value::Array(b)) => a == b,
|
||||
(Value::None, Value::None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Element<'a> {
|
||||
@@ -10,6 +166,8 @@ pub struct Element<'a> {
|
||||
pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>,
|
||||
pub children: Vec<Element<'a>>,
|
||||
pub computed_style: ComputedStyle,
|
||||
pub element_id: ElementId,
|
||||
pub content_hash: u64,
|
||||
}
|
||||
|
||||
impl<'a> Element<'a> {
|
||||
@@ -25,6 +183,19 @@ impl<'a> Element<'a> {
|
||||
properties: Vec::with_capacity(8),
|
||||
children: Vec::with_capacity(4),
|
||||
computed_style: ComputedStyle::default(),
|
||||
element_id: ElementId(u32::MAX),
|
||||
content_hash: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_id(type_name: &'a str, id: ElementId) -> Self {
|
||||
Self {
|
||||
type_name,
|
||||
properties: Vec::with_capacity(8),
|
||||
children: Vec::with_capacity(4),
|
||||
computed_style: ComputedStyle::default(),
|
||||
element_id: id,
|
||||
content_hash: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,9 +211,117 @@ pub struct ComponentDef<'a> {
|
||||
pub struct Document<'a> {
|
||||
pub roots: Vec<Element<'a>>,
|
||||
pub components: HashMap<String, ComponentDef<'a>>,
|
||||
pub variables: HashMap<String, String>,
|
||||
pub variables: HashMap<String, Value>,
|
||||
pub rhei_scripts: Vec<String>,
|
||||
pub stylesheet: StyleSheet,
|
||||
pub interner: Interner,
|
||||
pub tracker: ReactiveTracker,
|
||||
}
|
||||
|
||||
pub type NodeId = u32;
|
||||
pub type PropertyIdx = usize;
|
||||
pub type NodeIdx = usize;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VNode<'a> {
|
||||
pub id: NodeId,
|
||||
pub type_name: &'a str,
|
||||
pub properties: Range<PropertyIdx>,
|
||||
pub children_range: Range<NodeIdx>,
|
||||
pub computed_style: ComputedStyle,
|
||||
pub element_id: ElementId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlatVDom<'a> {
|
||||
pub nodes: Vec<VNode<'a>>,
|
||||
pub properties: Vec<(String, String)>,
|
||||
root_indices: Vec<NodeIdx>,
|
||||
}
|
||||
|
||||
impl<'a> FlatVDom<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: Vec::new(),
|
||||
properties: Vec::new(),
|
||||
root_indices: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_elements(elements: &[Element<'a>]) -> Self {
|
||||
let mut fv = FlatVDom::new();
|
||||
fv.append_elements(elements, true);
|
||||
fv
|
||||
}
|
||||
|
||||
fn append_elements(&mut self, elements: &[Element<'a>], is_root: bool) {
|
||||
for el in elements {
|
||||
let node_idx = self.nodes.len();
|
||||
if is_root {
|
||||
self.root_indices.push(node_idx);
|
||||
}
|
||||
let prop_start = self.properties.len();
|
||||
for (k, v) in &el.properties {
|
||||
self.properties.push((k.clone().into_owned(), v.clone().into_owned()));
|
||||
}
|
||||
self.nodes.push(VNode {
|
||||
id: 0,
|
||||
type_name: el.type_name,
|
||||
properties: prop_start..self.properties.len(),
|
||||
children_range: 0..0,
|
||||
computed_style: el.computed_style,
|
||||
element_id: el.element_id,
|
||||
});
|
||||
let child_start = self.nodes.len();
|
||||
self.append_elements(&el.children, false);
|
||||
self.nodes[node_idx].children_range = child_start..self.nodes.len();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_elements(self) -> Vec<Element<'a>> {
|
||||
let mut result = Vec::with_capacity(self.root_indices.len());
|
||||
for &root_idx in &self.root_indices {
|
||||
if let Some(el) = Self::node_to_element(&self.nodes, &self.properties, root_idx) {
|
||||
result.push(el);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn node_to_element(nodes: &[VNode<'a>], props: &[(String, String)], idx: NodeIdx) -> Option<Element<'a>> {
|
||||
let vn = nodes.get(idx)?;
|
||||
let mut el = Element::new(vn.type_name);
|
||||
el.element_id = vn.element_id;
|
||||
el.computed_style = vn.computed_style;
|
||||
for i in vn.properties.clone() {
|
||||
if i < props.len() {
|
||||
let (k, v) = &props[i];
|
||||
el.push_prop(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
for child_idx in vn.children_range.clone() {
|
||||
if let Some(child) = Self::node_to_element(nodes, props, child_idx) {
|
||||
el.children.push(child);
|
||||
}
|
||||
}
|
||||
Some(el)
|
||||
}
|
||||
|
||||
pub fn get_node(&self, idx: NodeIdx) -> Option<&VNode<'a>> {
|
||||
self.nodes.get(idx)
|
||||
}
|
||||
|
||||
pub fn node_count(&self) -> usize {
|
||||
self.nodes.len()
|
||||
}
|
||||
|
||||
pub fn root_count(&self) -> usize {
|
||||
self.root_indices.len()
|
||||
}
|
||||
|
||||
pub fn root_indices(&self) -> &[NodeIdx] {
|
||||
&self.root_indices
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
21
src/lib.rs
Normal file
21
src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub mod app;
|
||||
pub mod cli;
|
||||
pub mod interpreter;
|
||||
pub mod perf;
|
||||
pub mod renderer;
|
||||
|
||||
pub use app::GlintApp;
|
||||
pub use interpreter::{Element, Interpreter, RheiContext, Value};
|
||||
pub use interpreter::style::{ComputedStyle, StyleSheet};
|
||||
pub use interpreter::types::{Document, Interner};
|
||||
pub use interpreter::reactive::{ElementId, ReactiveTracker};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
EventTriggered(String),
|
||||
InputChanged(Option<String>, String),
|
||||
ToggleChanged(Option<String>, bool),
|
||||
SliderChanged(Option<String>, f64),
|
||||
WindowScrolled(f32),
|
||||
ScrollableScrolled(u64, f32),
|
||||
}
|
||||
20
src/main.rs
20
src/main.rs
@@ -1,18 +1,11 @@
|
||||
mod app;
|
||||
mod cli;
|
||||
mod interpreter;
|
||||
mod perf;
|
||||
mod renderer;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
EventTriggered(String),
|
||||
InputChanged(Option<String>, String),
|
||||
ToggleChanged(Option<String>, bool),
|
||||
SliderChanged(Option<String>, f64),
|
||||
WindowScrolled(f32),
|
||||
}
|
||||
use glint_runtime::Message;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "glint-runtime")]
|
||||
@@ -38,6 +31,10 @@ enum Commands {
|
||||
Run {
|
||||
/// Bytecode file to execute
|
||||
file: String,
|
||||
|
||||
/// Print per-frame performance metrics (VDOM/Style/Render timing)
|
||||
#[arg(long)]
|
||||
perf: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -58,7 +55,10 @@ fn main() {
|
||||
});
|
||||
cli::compile_files(&gltm_files, &glts_files, &output_path);
|
||||
}
|
||||
Commands::Run { file } => {
|
||||
Commands::Run { file, perf } => {
|
||||
if perf {
|
||||
crate::perf::set_enabled(true);
|
||||
}
|
||||
cli::run_file(&file);
|
||||
}
|
||||
}
|
||||
|
||||
87
src/perf.rs
Normal file
87
src/perf.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PerfReport {
|
||||
pub vdom_eval: f64,
|
||||
pub style: f64,
|
||||
pub render: f64,
|
||||
pub total: f64,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static PERF_ENABLED: Cell<bool> = Cell::new(false);
|
||||
static PERF_REPORT: RefCell<PerfReport> = RefCell::new(PerfReport::default());
|
||||
}
|
||||
|
||||
pub fn set_enabled(enabled: bool) {
|
||||
PERF_ENABLED.with(|e| e.set(enabled));
|
||||
}
|
||||
|
||||
pub fn is_enabled() -> bool {
|
||||
PERF_ENABLED.with(|e| e.get())
|
||||
}
|
||||
|
||||
pub struct PerfScope {
|
||||
name: &'static str,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl PerfScope {
|
||||
pub fn new(name: &'static str) -> Self {
|
||||
Self {
|
||||
name,
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PerfScope {
|
||||
fn drop(&mut self) {
|
||||
if !is_enabled() {
|
||||
return;
|
||||
}
|
||||
let ms = self.start.elapsed().as_secs_f64() * 1000.0;
|
||||
PERF_REPORT.with(|r| {
|
||||
let mut r = r.borrow_mut();
|
||||
match self.name {
|
||||
"vdom" => r.vdom_eval += ms,
|
||||
"style" => r.style += ms,
|
||||
"render" => r.render += ms,
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_and_reset() -> Option<String> {
|
||||
if !is_enabled() {
|
||||
return None;
|
||||
}
|
||||
PERF_REPORT.with(|r| {
|
||||
let r = r.borrow();
|
||||
let total = r.vdom_eval + r.style + r.render;
|
||||
let mut out = format!(
|
||||
"VDOM: {:.2}ms | Style: {:.2}ms | Render: {:.2}ms | Total: {:.2}ms",
|
||||
r.vdom_eval, r.style, r.render, total
|
||||
);
|
||||
if total > 16.0 {
|
||||
out.push_str(&format!(
|
||||
" ⚠️ Frame budget exceeded! {:.2}ms > 16ms",
|
||||
total
|
||||
));
|
||||
}
|
||||
Some(out)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset() {
|
||||
PERF_REPORT.with(|r| *r.borrow_mut() = PerfReport::default());
|
||||
}
|
||||
|
||||
pub fn print_frame() {
|
||||
if let Some(report) = report_and_reset() {
|
||||
eprintln!("[perf] {}", report);
|
||||
}
|
||||
reset();
|
||||
}
|
||||
221
src/renderer.rs
221
src/renderer.rs
@@ -1,6 +1,6 @@
|
||||
use crate::Message;
|
||||
use crate::interpreter::Element;
|
||||
use crate::interpreter::style::{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign};
|
||||
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,
|
||||
@@ -9,8 +9,25 @@ use iced::widget::{
|
||||
use iced::{Alignment, Background, Border, Length, Theme};
|
||||
use iced::font::Weight;
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
thread_local! {
|
||||
static WIDGET_ID_CACHE: RefCell<HashMap<(u32, &'static str), iced::widget::Id>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
fn get_or_create_widget_id(key: u32, prefix: &'static str) -> iced::widget::Id {
|
||||
WIDGET_ID_CACHE.with(|cache| {
|
||||
cache.borrow_mut()
|
||||
.entry((key, prefix))
|
||||
.or_insert_with(|| {
|
||||
let s = Box::leak(format!("{prefix}:{key}").into_boxed_str());
|
||||
iced::widget::Id::new(s)
|
||||
})
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_var_binding(el: &Element, prop: &str) -> Option<String> {
|
||||
el.properties.iter()
|
||||
.find_map(|(k, v)| {
|
||||
@@ -82,17 +99,18 @@ fn make_hoverable<'a>(
|
||||
return widget;
|
||||
}
|
||||
|
||||
let click_script = el.get_prop("__on:click").map(String::from);
|
||||
|
||||
if click_script.is_none() {
|
||||
return widget;
|
||||
}
|
||||
|
||||
let hover = hover_props.clone();
|
||||
let active = active_props.clone();
|
||||
let cs = base_cs.clone();
|
||||
|
||||
let mut btn = button(widget).padding(0);
|
||||
|
||||
if let Some(script) = el.get_prop("__on:click") {
|
||||
btn = btn.on_press(crate::Message::EventTriggered(script.to_string()));
|
||||
} else if has_hover || has_active {
|
||||
btn = btn.on_press(crate::Message::EventTriggered(String::new()));
|
||||
}
|
||||
btn = btn.on_press(crate::Message::EventTriggered(click_script.unwrap()));
|
||||
|
||||
btn.style(move |_, status| {
|
||||
let mut style_cs = cs.clone();
|
||||
@@ -115,8 +133,8 @@ fn make_hoverable<'a>(
|
||||
background: style_cs.background.map(Background::Color),
|
||||
border: Border {
|
||||
color: style_cs.border_color.unwrap_or(iced::Color::TRANSPARENT),
|
||||
width: style_cs.border_width.unwrap_or(0.0),
|
||||
radius: style_cs.border_radius.unwrap_or(0.0).into(),
|
||||
width: resolve_size(style_cs.border_width, None).unwrap_or(0.0),
|
||||
radius: resolve_size(style_cs.border_radius, None).unwrap_or(0.0).into(),
|
||||
},
|
||||
text_color: style_cs.color.unwrap_or(iced::Color::WHITE),
|
||||
..Default::default()
|
||||
@@ -133,6 +151,8 @@ fn render_children<'a>(
|
||||
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>> {
|
||||
children
|
||||
@@ -146,6 +166,8 @@ fn render_children<'a>(
|
||||
fixed_layers,
|
||||
abs_layers,
|
||||
sticky_layers,
|
||||
scroll_positions,
|
||||
container_id,
|
||||
stylesheet,
|
||||
)
|
||||
})
|
||||
@@ -153,13 +175,13 @@ fn render_children<'a>(
|
||||
}
|
||||
|
||||
fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding {
|
||||
let base = cs.padding.unwrap_or(default_pad);
|
||||
let mut top = cs.padding_top.unwrap_or(base);
|
||||
let mut right = cs.padding_right.unwrap_or(base);
|
||||
let mut bottom = cs.padding_bottom.unwrap_or(base);
|
||||
let mut left = cs.padding_left.unwrap_or(base);
|
||||
let base = resolve_size(cs.padding, None).unwrap_or(default_pad);
|
||||
let mut top = resolve_size(cs.padding_top, None).unwrap_or(base);
|
||||
let mut right = resolve_size(cs.padding_right, None).unwrap_or(base);
|
||||
let mut bottom = resolve_size(cs.padding_bottom, None).unwrap_or(base);
|
||||
let mut left = resolve_size(cs.padding_left, None).unwrap_or(base);
|
||||
|
||||
let bwidth = cs.border_width.unwrap_or(0.0);
|
||||
let bwidth = resolve_size(cs.border_width, None).unwrap_or(0.0);
|
||||
|
||||
if let Some(iced::Length::Fixed(w)) = cs.width {
|
||||
let total_h = left + right + bwidth * 2.0;
|
||||
@@ -188,12 +210,12 @@ fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding {
|
||||
}
|
||||
|
||||
fn get_margin(cs: &ComputedStyle) -> iced::Padding {
|
||||
let base = cs.margin.unwrap_or(0.0);
|
||||
let base = resolve_size(cs.margin, None).unwrap_or(0.0);
|
||||
iced::Padding {
|
||||
top: cs.margin_top.unwrap_or(base),
|
||||
right: cs.margin_right.unwrap_or(base),
|
||||
bottom: cs.margin_bottom.unwrap_or(base),
|
||||
left: cs.margin_left.unwrap_or(base),
|
||||
top: resolve_size(cs.margin_top, None).unwrap_or(base),
|
||||
right: resolve_size(cs.margin_right, None).unwrap_or(base),
|
||||
bottom: resolve_size(cs.margin_bottom, None).unwrap_or(base),
|
||||
left: resolve_size(cs.margin_left, None).unwrap_or(base),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,8 +244,8 @@ fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style
|
||||
background: Some(Background::Color(bg_color)),
|
||||
border: Border {
|
||||
color: cs.border_color.unwrap_or(iced::Color::TRANSPARENT),
|
||||
width: cs.border_width.unwrap_or(0.0),
|
||||
radius: cs.border_radius.unwrap_or(0.0).into(),
|
||||
width: resolve_size(cs.border_width, None).unwrap_or(0.0),
|
||||
radius: resolve_size(cs.border_radius, None).unwrap_or(0.0).into(),
|
||||
},
|
||||
text_color: cs.color.unwrap_or(iced::Color::WHITE),
|
||||
..Default::default()
|
||||
@@ -240,8 +262,8 @@ fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style {
|
||||
color: cs
|
||||
.border_color
|
||||
.unwrap_or(iced::Color::from_rgb(0.25, 0.25, 0.3)),
|
||||
width: cs.border_width.unwrap_or(1.0),
|
||||
radius: cs.border_radius.unwrap_or(0.0).into(),
|
||||
width: resolve_size(cs.border_width, None).unwrap_or(1.0),
|
||||
radius: resolve_size(cs.border_radius, None).unwrap_or(0.0).into(),
|
||||
},
|
||||
icon: iced::Color::from_rgb(0.5, 0.5, 0.5),
|
||||
placeholder: iced::Color::from_rgb(0.4, 0.4, 0.45),
|
||||
@@ -250,16 +272,39 @@ fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style {
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_element_height(cs: &ComputedStyle) -> f32 {
|
||||
let pad_top = resolve_size(cs.padding_top.or(cs.padding), None).unwrap_or(0.0);
|
||||
let pad_bottom = resolve_size(cs.padding_bottom.or(cs.padding), None).unwrap_or(0.0);
|
||||
let bwidth = resolve_size(cs.border_width, None).unwrap_or(0.0);
|
||||
let font_size = resolve_size(cs.font_size, None).unwrap_or(16.0);
|
||||
let line_height = resolve_size(cs.line_height, None).unwrap_or(1.2);
|
||||
pad_top + pad_bottom + bwidth * 2.0 + font_size * line_height
|
||||
}
|
||||
|
||||
fn wrap_sticky_position<'a>(
|
||||
widget: iced::Element<'a, Message, Theme, iced::Renderer>,
|
||||
cs: &ComputedStyle,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let top = resolve_size(cs.top, None).unwrap_or(0.0);
|
||||
container(widget)
|
||||
.width(Length::Fill)
|
||||
.padding(iced::Padding {
|
||||
top,
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn wrap_fixed_position<'a>(
|
||||
widget: iced::Element<'a, Message, Theme, iced::Renderer>,
|
||||
cs: &ComputedStyle,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
use iced::alignment::{Horizontal, Vertical};
|
||||
|
||||
let top = cs.top.unwrap_or(0.0);
|
||||
let left = cs.left.unwrap_or(0.0);
|
||||
let right = cs.right.unwrap_or(0.0);
|
||||
let bottom = cs.bottom.unwrap_or(0.0);
|
||||
let top = resolve_size(cs.top, None).unwrap_or(0.0);
|
||||
let left = resolve_size(cs.left, None).unwrap_or(0.0);
|
||||
let right = resolve_size(cs.right, None).unwrap_or(0.0);
|
||||
let bottom = resolve_size(cs.bottom, None).unwrap_or(0.0);
|
||||
|
||||
let has_top = cs.top.is_some();
|
||||
let has_bottom = cs.bottom.is_some();
|
||||
@@ -299,10 +344,11 @@ fn apply_universal_box_model<'a>(
|
||||
cs: &ComputedStyle,
|
||||
is_window: bool,
|
||||
default_padding: f32,
|
||||
scrollable_id: Option<u64>,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let bg = cs.background;
|
||||
let radius = cs.border_radius.unwrap_or(0.0);
|
||||
let bwidth = cs.border_width.unwrap_or(0.0);
|
||||
let radius = resolve_size(cs.border_radius, None).unwrap_or(0.0);
|
||||
let bwidth = resolve_size(cs.border_width, None).unwrap_or(0.0);
|
||||
|
||||
let bcolor = cs.border_color.unwrap_or_else(|| {
|
||||
if bwidth > 0.0 {
|
||||
@@ -348,6 +394,11 @@ fn apply_universal_box_model<'a>(
|
||||
s = s.height(Length::Fill);
|
||||
}
|
||||
|
||||
if let Some(sid) = scrollable_id {
|
||||
s = s.id(get_or_create_widget_id(sid as u32, "sc"))
|
||||
.on_scroll(move |viewport| Message::ScrollableScrolled(sid, viewport.absolute_offset().y));
|
||||
}
|
||||
|
||||
s.into()
|
||||
} else {
|
||||
padded_content.into()
|
||||
@@ -385,10 +436,10 @@ fn apply_universal_box_model<'a>(
|
||||
if let Some(h) = cs.height {
|
||||
inner = inner.height(h);
|
||||
}
|
||||
if let Some(max_w) = cs.max_width {
|
||||
if let Some(max_w) = resolve_size(cs.max_width, None) {
|
||||
inner = inner.max_width(max_w);
|
||||
}
|
||||
if let Some(max_h) = cs.max_height {
|
||||
if let Some(max_h) = resolve_size(cs.max_height, None) {
|
||||
inner = inner.max_height(max_h);
|
||||
}
|
||||
|
||||
@@ -438,6 +489,8 @@ pub fn render_element<'a>(
|
||||
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>> {
|
||||
let mut cs = el.computed_style;
|
||||
@@ -471,12 +524,13 @@ pub fn render_element<'a>(
|
||||
}
|
||||
|
||||
let current_color = cs.color.or(parent_color);
|
||||
let current_font_size = cs.font_size.or(parent_font_size);
|
||||
let current_font_size = resolve_size(cs.font_size, parent_font_size).or(parent_font_size);
|
||||
|
||||
if el.type_name == "Window" {
|
||||
let spacing = cs.spacing.unwrap_or(12.0);
|
||||
let spacing = resolve_size(cs.spacing, None).unwrap_or(12.0);
|
||||
let mut col = column![].spacing(spacing).width(Length::Fill);
|
||||
|
||||
let window_id = el.element_id.0 as u64;
|
||||
let mut window_fixed_layers = Vec::new();
|
||||
let mut window_abs_layers = Vec::new();
|
||||
let mut window_sticky_layers = Vec::new();
|
||||
@@ -489,20 +543,14 @@ pub fn render_element<'a>(
|
||||
&mut window_fixed_layers,
|
||||
&mut window_abs_layers,
|
||||
&mut window_sticky_layers,
|
||||
scroll_positions,
|
||||
window_id,
|
||||
stylesheet,
|
||||
) {
|
||||
col = col.push(child);
|
||||
}
|
||||
|
||||
let main_flow = apply_universal_box_model(col, &cs, true, 0.0);
|
||||
|
||||
let has_abs = !window_abs_layers.is_empty();
|
||||
let has_fixed = !window_fixed_layers.is_empty();
|
||||
let has_sticky = !window_sticky_layers.is_empty();
|
||||
|
||||
if !has_abs && !has_fixed && !has_sticky {
|
||||
return Some(main_flow);
|
||||
}
|
||||
let main_flow = apply_universal_box_model(col, &cs, true, 0.0, Some(window_id));
|
||||
|
||||
let mut stack_widget = iced::widget::stack![main_flow];
|
||||
for layer in window_abs_layers {
|
||||
@@ -530,6 +578,8 @@ pub fn render_element<'a>(
|
||||
fixed_layers,
|
||||
&mut panel_abs,
|
||||
&mut panel_sticky,
|
||||
scroll_positions,
|
||||
container_id,
|
||||
stylesheet,
|
||||
);
|
||||
if panel_abs.is_empty() {
|
||||
@@ -552,6 +602,8 @@ pub fn render_element<'a>(
|
||||
fixed_layers,
|
||||
&mut btn_abs,
|
||||
&mut btn_sticky,
|
||||
scroll_positions,
|
||||
container_id,
|
||||
stylesheet,
|
||||
);
|
||||
if btn_abs.is_empty() && btn_sticky.is_empty() {
|
||||
@@ -590,7 +642,7 @@ let element_opt: Option<iced::Element<'a, Message, Theme, iced::Renderer>> =
|
||||
widget = widget.align_x(iced::alignment::Horizontal::from(ta));
|
||||
widget = widget.width(Length::Fill);
|
||||
}
|
||||
if let Some(lh) = cs.line_height {
|
||||
if let Some(lh) = resolve_size(cs.line_height, current_font_size) {
|
||||
widget = widget.line_height(lh);
|
||||
widget = widget.width(Length::Fill);
|
||||
widget = widget.wrapping(Wrapping::Word);
|
||||
@@ -615,7 +667,7 @@ let element_opt: Option<iced::Element<'a, Message, Theme, iced::Renderer>> =
|
||||
widget = widget.align_x(iced::alignment::Horizontal::from(ta));
|
||||
widget = widget.width(Length::Fill);
|
||||
}
|
||||
if let Some(lh) = cs.line_height {
|
||||
if let Some(lh) = resolve_size(cs.line_height, current_font_size) {
|
||||
widget = widget.line_height(lh);
|
||||
widget = widget.width(Length::Fill);
|
||||
widget = widget.wrapping(Wrapping::Word);
|
||||
@@ -626,9 +678,9 @@ let element_opt: Option<iced::Element<'a, Message, Theme, iced::Renderer>> =
|
||||
"Image" => {
|
||||
let src = el.get_prop("src").unwrap_or("");
|
||||
let path = src.strip_prefix("fs:").unwrap_or(src);
|
||||
let radius = cs.border_radius.unwrap_or(0.0);
|
||||
let radius = resolve_size(cs.border_radius, None).unwrap_or(0.0);
|
||||
let padding = get_padding(&cs, 0.0);
|
||||
let bwidth = cs.border_width.unwrap_or(0.0);
|
||||
let bwidth = resolve_size(cs.border_width, None).unwrap_or(0.0);
|
||||
|
||||
let content_w = cs.width.map(|len| {
|
||||
if let iced::Length::Fixed(w) = len {
|
||||
@@ -711,7 +763,7 @@ let element_opt: Option<iced::Element<'a, Message, Theme, iced::Renderer>> =
|
||||
if el.children.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let spacing = cs.spacing.unwrap_or(10.0);
|
||||
let spacing = resolve_size(cs.spacing, None).unwrap_or(10.0);
|
||||
let mut col = column![].spacing(spacing);
|
||||
let mut default_abs = Vec::new();
|
||||
let mut default_sticky = Vec::new();
|
||||
@@ -723,6 +775,8 @@ let element_opt: Option<iced::Element<'a, Message, Theme, iced::Renderer>> =
|
||||
fixed_layers,
|
||||
&mut default_abs,
|
||||
&mut default_sticky,
|
||||
scroll_positions,
|
||||
container_id,
|
||||
stylesheet,
|
||||
) {
|
||||
col = col.push(child);
|
||||
@@ -738,24 +792,23 @@ let element_opt: Option<iced::Element<'a, Message, Theme, iced::Renderer>> =
|
||||
}
|
||||
stack.into()
|
||||
};
|
||||
let boxed = apply_universal_box_model(content_widget, &cs, false, 0.0);
|
||||
// Sticky outside (after box model = outside scrollable)
|
||||
if default_sticky.is_empty() {
|
||||
final_widget_opt = Some(boxed);
|
||||
} else {
|
||||
let is_scrollable = cs.overflow_y.map(|o| o == Overflow::Scroll || o == Overflow::Auto).unwrap_or(false);
|
||||
let sc_id: Option<u64> = if is_scrollable { Some(el.element_id.0 as u64) } else { None };
|
||||
let boxed = apply_universal_box_model(content_widget, &cs, false, 0.0, sc_id);
|
||||
let mut stack = iced::widget::stack![boxed];
|
||||
for layer in default_sticky {
|
||||
stack = stack.push(layer);
|
||||
}
|
||||
final_widget_opt = Some(stack.into());
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(element) = element_opt {
|
||||
final_widget_opt = Some(apply_universal_box_model(element, &cs, false, 0.0));
|
||||
let is_scrollable = cs.overflow_y.map(|o| o == Overflow::Scroll || o == Overflow::Auto).unwrap_or(false);
|
||||
let sc_id: Option<u64> = if is_scrollable { Some(el.element_id.0 as u64) } else { None };
|
||||
final_widget_opt = Some(apply_universal_box_model(element, &cs, false, 0.0, sc_id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -776,9 +829,21 @@ let element_opt: Option<iced::Element<'a, Message, Theme, iced::Renderer>> =
|
||||
abs_layers.push(wrapped);
|
||||
None
|
||||
} else if is_sticky {
|
||||
let wrapped = wrap_fixed_position(final_widget, &cs);
|
||||
let threshold = resolve_size(cs.top, None).unwrap_or(0.0);
|
||||
let scroll_y = scroll_positions.get(&container_id).copied().unwrap_or(0.0);
|
||||
if scroll_y > threshold {
|
||||
let wrapped = wrap_sticky_position(final_widget, &cs);
|
||||
sticky_layers.push(wrapped);
|
||||
None
|
||||
let spacer_h = estimate_element_height(&cs);
|
||||
let spacer: iced::Element<'a, Message, Theme, iced::Renderer> =
|
||||
container(iced::widget::text(""))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(spacer_h))
|
||||
.into();
|
||||
Some(spacer)
|
||||
} else {
|
||||
Some(final_widget)
|
||||
}
|
||||
} else {
|
||||
Some(final_widget)
|
||||
}
|
||||
@@ -795,9 +860,11 @@ fn render_panel<'a>(
|
||||
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> {
|
||||
let spacing = cs.spacing.unwrap_or(10.0);
|
||||
let spacing = resolve_size(cs.spacing, None).unwrap_or(10.0);
|
||||
|
||||
let is_horizontal = el.children.iter().all(|c| {
|
||||
matches!(
|
||||
@@ -812,6 +879,10 @@ let is_horizontal = el.children.iter().all(|c| {
|
||||
LayoutDirection::Column
|
||||
});
|
||||
|
||||
let is_scrollable = cs.overflow_y.map(|o| o == Overflow::Scroll || o == Overflow::Auto).unwrap_or(false);
|
||||
let child_container: u64 = if is_scrollable { el.element_id.0 as u64 } else { container_id };
|
||||
let sc_id: Option<u64> = if is_scrollable { Some(el.element_id.0 as u64) } else { None };
|
||||
|
||||
let content: iced::Element<'a, Message, Theme, iced::Renderer> = match direction {
|
||||
LayoutDirection::Row => {
|
||||
let mut r = row![].spacing(spacing);
|
||||
@@ -828,6 +899,8 @@ let is_horizontal = el.children.iter().all(|c| {
|
||||
fixed_layers,
|
||||
abs_layers,
|
||||
sticky_layers,
|
||||
scroll_positions,
|
||||
child_container,
|
||||
stylesheet,
|
||||
) {
|
||||
r = r.push(child);
|
||||
@@ -856,6 +929,8 @@ let is_horizontal = el.children.iter().all(|c| {
|
||||
fixed_layers,
|
||||
abs_layers,
|
||||
sticky_layers,
|
||||
scroll_positions,
|
||||
child_container,
|
||||
stylesheet,
|
||||
) {
|
||||
c = c.push(child);
|
||||
@@ -895,6 +970,8 @@ let is_horizontal = el.children.iter().all(|c| {
|
||||
fixed_layers,
|
||||
&mut chunk_abs,
|
||||
&mut chunk_sticky,
|
||||
scroll_positions,
|
||||
child_container,
|
||||
stylesheet,
|
||||
) {
|
||||
r = r.push(child);
|
||||
@@ -916,11 +993,8 @@ let is_horizontal = el.children.iter().all(|c| {
|
||||
}
|
||||
};
|
||||
|
||||
let boxed = apply_universal_box_model(content, &cs, false, 5.0);
|
||||
let boxed = apply_universal_box_model(content, &cs, false, 5.0, sc_id);
|
||||
|
||||
if sticky_layers.is_empty() {
|
||||
boxed
|
||||
} else {
|
||||
let layers = std::mem::take(sticky_layers);
|
||||
let mut stack = iced::widget::stack![boxed];
|
||||
for layer in layers {
|
||||
@@ -928,7 +1002,6 @@ let is_horizontal = el.children.iter().all(|c| {
|
||||
}
|
||||
stack.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_button<'a>(
|
||||
el: &'a Element,
|
||||
@@ -938,6 +1011,8 @@ fn render_button<'a>(
|
||||
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> {
|
||||
let final_color = cs.color.or(parent_color);
|
||||
@@ -959,6 +1034,8 @@ fn render_button<'a>(
|
||||
fixed_layers,
|
||||
abs_layers,
|
||||
sticky_layers,
|
||||
scroll_positions,
|
||||
container_id,
|
||||
stylesheet,
|
||||
) {
|
||||
r = r.push(child);
|
||||
@@ -977,20 +1054,15 @@ fn render_button<'a>(
|
||||
}
|
||||
};
|
||||
|
||||
let base_px = resolve_size(cs.padding, None).unwrap_or(8.0);
|
||||
let mut padding = iced::Padding {
|
||||
top: cs.padding_top.or(cs.padding).unwrap_or(8.0),
|
||||
bottom: cs.padding_bottom.or(cs.padding).unwrap_or(8.0),
|
||||
left: cs
|
||||
.padding_left
|
||||
.or_else(|| cs.padding.map(|p| p * 2.0))
|
||||
.unwrap_or(16.0),
|
||||
right: cs
|
||||
.padding_right
|
||||
.or_else(|| cs.padding.map(|p| p * 2.0))
|
||||
.unwrap_or(16.0),
|
||||
top: resolve_size(cs.padding_top, None).unwrap_or(base_px),
|
||||
bottom: resolve_size(cs.padding_bottom, None).unwrap_or(base_px),
|
||||
left: resolve_size(cs.padding_left, None).unwrap_or(base_px * 2.0),
|
||||
right: resolve_size(cs.padding_right, None).unwrap_or(base_px * 2.0),
|
||||
};
|
||||
|
||||
let bwidth = cs.border_width.unwrap_or(0.0);
|
||||
let bwidth = resolve_size(cs.border_width, None).unwrap_or(0.0);
|
||||
if let Some(iced::Length::Fixed(w)) = cs.width {
|
||||
let total_h = padding.left + padding.right + bwidth * 2.0;
|
||||
if total_h > w && w > 0.0 {
|
||||
@@ -1113,6 +1185,7 @@ fn render_input<'a>(
|
||||
|
||||
let mut input = text_input(&placeholder, &value)
|
||||
.on_input(move |v| Message::InputChanged(var_name.clone(), v))
|
||||
.id(get_or_create_widget_id(el.element_id.0, "ti"))
|
||||
.padding(padding);
|
||||
|
||||
if let Some(w) = cs.width {
|
||||
|
||||
Reference in New Issue
Block a user