Compare commits

...

45 Commits

Author SHA1 Message Date
d42e7df8af Delete STYLE-SYSTEM-COMPLETION-PLAN.md 2026-07-24 22:11:51 +03:00
Glint Dev
e6523bd35b move example template files to examples/, clean up root 2026-07-24 22:04:41 +03:00
Glint Dev
35f92435a3 docs: reorganize into en/ru and add English translation 2026-07-24 22:04:29 +03:00
Glint Dev
938f05f59d Phase 10: Monitoring and automated benchmarks
10.1: Add Criterion dev-dependency and benchmarks for:
  - matching_rules (100 rules)
  - ComputedStyle::compute (empty, 5 props)
  - resolve_string (no vars, 1 var, 3 vars)

10.3: Add --perf CLI flag and src/perf.rs module:
  - PerfScope drop-guard timed scopes (vdom, style, render)
  - Instrumented: VDOM eval (app.rs update), render (app.rs view),
    style matching (style.rs compute_cached)
  - Per-frame report printed to stderr at end of view()

10.2+10.4: Threshold check in perf report:
  - Prints warning if total > 16ms (60 FPS frame budget)

Refactor: Move Message enum to lib.rs so benchmarks can import
the public API. Added lib.rs as library root.
2026-07-24 19:58:58 +03:00
Glint Dev
b6fa841d11 Phase 9: Keyed widgets (9.1 + 9.2)
9.1: Add content_hash field to Element, computed during VDOM evaluation
  from type_name + properties + children content_hashes via DefaultHasher.

9.2: Assign stable iced::widget::Id to text_input and scrollable widgets
  so Iced preserves widget state (cursor position, scroll offset) across
  frames. Ids are cached in a thread_local HashMap to avoid leaks.

9.3 skipped: iced::Element is not Clone and contains Box<dyn Widget>,
  making element-level caching impractical without unsafe lifetime hacks.
2026-07-24 19:53:02 +03:00
Glint Dev
28bd450787 Phase 8: StyleCache memoization
Add StyleCache struct with hash-keyed LRU-eviction strategy and
integrate into StyleSheet via Mutex<StyleCache> so no signature
changes are needed on evaluate_vdom_incr.

- StyleCache::get_or_compute hashes (type_name, props, epoch) -> ComputedStyle
- LRU: full clear on overflow (max 1024)
- RefCell avoided in favor of Mutex so StyleSheet remains Sync
  for parallel matching_rules_batch
- Manual Clone impl skips cache (fresh cache on clone)
- Call sites in evaluate_vdom_incr switched from ComputedStyle::compute
  to stylesheet.compute_cached
- Cache cleared in build_index after epoch increment (stale entries)
2026-07-24 18:47:41 +03:00
Glint Dev
906dde1569 phase 7: update Cargo.lock for rayon dependency 2026-07-24 16:56:05 +03:00
Glint Dev
144ac12107 phase 7: fix @each to stay sequential (RheiContext not Sync); keep compute_batch/matching_rules_batch in style.rs 2026-07-24 16:55:52 +03:00
Glint Dev
469e87eaf3 phase 7.3: parallel @each using rayon (feature gate, threshold >= 10 items) 2026-07-24 16:47:02 +03:00
Glint Dev
1ede7d0f7f phase 7.2/7.4: add compute_batch and matching_rules_batch for parallel style processing 2026-07-24 16:46:31 +03:00
Glint Dev
6f12c53b78 phase 7.1: add rayon dependency with parallel feature gate 2026-07-24 16:46:12 +03:00
Glint Dev
32beb81ac4 demo: layout fixes, sticky removal, style overrides, css-like rendering improvements 2026-07-24 16:46:00 +03:00
Glint Dev
415de4d6e6 demo: restructure log header outside scrollable (always visible), remove sticky references, add Iced limitation note 2026-07-22 14:48:53 +03:00
Glint Dev
075533db6a demo: header as always-visible top bar with scrollable body beneath (closest to sticky in Iced) 2026-07-22 14:44:28 +03:00
Glint Dev
37a0ea47be demo: no sticky (iced limitation), reduce spacing, Window height:100vh 2026-07-22 14:39:02 +03:00
Glint Dev
f51b32ecc3 fix: pass cs.height to Window column so height:100vh constrains layout 2026-07-22 14:38:54 +03:00
Glint Dev
e770d1a68a demo: fix header (no sticky), make Display None interactive, add opacity note 2026-07-22 14:31:39 +03:00
Glint Dev
bdb5df2bf2 fix: only wrap hover/active elements in button when __on:click exists (no more phantom pointer cursor) 2026-07-22 14:31:36 +03:00
Glint Dev
66b684e412 examples: warm neutral theme (no blue) + cleaner copy 2026-07-22 14:22:46 +03:00
Glint Dev
9ce650a803 chore: add *.glbc to gitignore 2026-07-22 14:21:24 +03:00
Glint Dev
ec73dcabb8 examples: add demo stylesheet with comprehensive style features 2026-07-22 14:21:12 +03:00
Glint Dev
1f5dc7f2ca examples: add demo app markup with full feature showcase 2026-07-22 14:21:09 +03:00
Glint Dev
8784c189f5 Phase 6: Pre-populate Rhai AST cache from Document at startup 2026-07-22 14:01:00 +03:00
Glint Dev
09e3f5e75d Phase 6: Add action_cache and expr_cache to RheiContext + cache-first eval/execute methods 2026-07-22 14:00:56 +03:00
Glint Dev
4eec003939 feat: add FlatVDom types and evaluate_vdom_flat helper 2026-07-22 13:40:04 +03:00
Glint Dev
a798e966f3 feat: add FlatVDom round-trip conversion tests 2026-07-22 13:38:03 +03:00
Glint Dev
0d89ff68be feat: define VNode, FlatVDom types with Element round-trip conversion 2026-07-22 13:36:25 +03:00
Glint Dev
daba2136c3 feat: add bumpalo dependency for arena allocation 2026-07-22 13:35:32 +03:00
Glint Dev
be6d5c36fc fix: eliminate deep Element cloning in @if by using &[&Element] references 2026-07-22 13:35:12 +03:00
Glint Dev
fd2863678b Phase 4.5: Add tests for StyleIndex 2026-07-22 13:10:09 +03:00
Glint Dev
06aa67b23f Phase 4.4: Build index at Document load time in interpeter::run() 2026-07-22 13:07:30 +03:00
Glint Dev
43c4291ac8 Phase 4.3: Use index in matching_pseudo_rules() 2026-07-22 13:07:02 +03:00
Glint Dev
cd2cf64e80 Phase 4.2: Add query_index() and use it in matching_rules() 2026-07-22 13:06:43 +03:00
Glint Dev
24fbd17f3e Phase 4.1: Add StyleIndex struct and build_index() to StyleSheet 2026-07-22 13:06:01 +03:00
Glint Dev
26ed1f900e Revert "Phase 4.1-4.2: StyleIndex with by_tag/by_class/by_id indexes, replace matching_rules with query_index, build_index after parsing"
This reverts commit 87feba394d.
2026-07-22 13:01:33 +03:00
Glint Dev
bc45e85adf Phase 4.3-4.4: Style match cache with epoch invalidation (RefCell), remove redundant re-sort in query(), merge-scan complex rules in order 2026-07-22 12:59:40 +03:00
Glint Dev
87feba394d Phase 4.1-4.2: StyleIndex with by_tag/by_class/by_id indexes, replace matching_rules with query_index, build_index after parsing 2026-07-22 12:56:36 +03:00
Glint Dev
4a6dea6dd8 Phase 3.4-3.7: evaluate_vdom_incr with dirty_set propagation, tests for ReactiveTracker 2026-07-21 20:40:23 +03:00
Glint Dev
d8d3f57a21 Phase 3.1-3.3: ReactiveTracker with ElementId assignment during parsing, dependency scanning, variable change notification in app.rs 2026-07-21 20:38:41 +03:00
Glint Dev
a46c9bc9e7 Phase 2: InternedStr and Interner types (infrastructure) 2026-07-21 20:35:56 +03:00
Glint Dev
f3ea96258c Phase 1: cleanup unused imports and static 2026-07-21 20:33:08 +03:00
Glint Dev
30cf2e5e26 Phase 1.2-1.6: replace variables HashMap<String,String> with HashMap<String,Value>, rewrite str_to_dyn/dyn_to_str -> value_to_dynamic/dynamic_to_value, update resolve_string/evaluate_condition/is_truthy for Value 2026-07-21 20:32:29 +03:00
Glint Dev
5597f07787 Phase 1.1: define Value enum in types.rs 2026-07-21 20:29:02 +03:00
faynot
06270a2126 Merge branch 'main' of https://git.inotfail.com/INotFail/Glint-Runtime 2026-07-08 23:46:25 +03:00
faynot
75af23930f feat: new styles 2026-07-08 23:45:37 +03:00
48 changed files with 10913 additions and 987 deletions

3
.gitignore vendored
View File

@@ -1 +1,4 @@
/target /target
*.glbc
STYLE-SYSTEM-COMPLETION-PLAN.md
ARCHITECTURE-IMPROVEMENT-PLAN.md

View 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) |

177
Cargo.lock generated
View File

@@ -108,6 +108,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "1.0.0" version = "1.0.0"
@@ -583,6 +589,21 @@ dependencies = [
"wayland-client", "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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.62"
@@ -620,6 +641,33 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "clap" name = "clap"
version = "4.6.1" version = "4.6.1"
@@ -742,6 +790,20 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "compact_str"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -887,6 +949,42 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.6" version = "0.8.6"
@@ -1451,14 +1549,19 @@ checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
name = "glint-runtime" name = "glint-runtime"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bumpalo",
"chrono", "chrono",
"clap", "clap",
"colored", "colored",
"compact_str",
"criterion",
"glt", "glt",
"iced", "iced",
"indicatif", "indicatif",
"rayon",
"regex", "regex",
"rhai", "rhai",
"rustc-hash 2.1.2",
] ]
[[package]] [[package]]
@@ -1475,7 +1578,7 @@ dependencies = [
[[package]] [[package]]
name = "glt" name = "glt"
version = "0.1.3" version = "0.1.4"
[[package]] [[package]]
name = "glutin_wgl_sys" name = "glutin_wgl_sys"
@@ -1922,12 +2025,32 @@ dependencies = [
"syn", "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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.2" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.14.0" version = "0.14.0"
@@ -2843,6 +2966,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]] [[package]]
name = "orbclient" name = "orbclient"
version = "0.3.54" version = "0.3.54"
@@ -2989,6 +3118,34 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 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]] [[package]]
name = "png" name = "png"
version = "0.17.16" version = "0.17.16"
@@ -3214,7 +3371,7 @@ dependencies = [
"built", "built",
"cfg-if", "cfg-if",
"interpolate_name", "interpolate_name",
"itertools", "itertools 0.14.0",
"libc", "libc",
"libfuzzer-sys", "libfuzzer-sys",
"log", "log",
@@ -3488,6 +3645,12 @@ dependencies = [
"unicode-script", "unicode-script",
] ]
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -4018,6 +4181,16 @@ dependencies = [
"tracing", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.11.0" version = "1.11.0"

View File

@@ -7,8 +7,23 @@ edition = "2024"
glt = { path = "../glt" } glt = { path = "../glt" }
regex = "1.10" regex = "1.10"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
iced = { version = "0.14.0", features = ["tokio", "image", "svg"] } iced = { version = "0.14.0", features = ["tokio", "image", "svg", "advanced"] }
colored = "2" colored = "2"
indicatif = "0.17" indicatif = "0.17"
rhai = "1.17.1" rhai = "1.17.1"
rustc-hash = "2"
chrono = "0.4.44" 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

99
benches/bench.rs Normal file
View 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);

View File

@@ -1,207 +0,0 @@
// =============================================================================
// GLINT UI: Ultimate Reactive Showcase Architecture
// =============================================================================
@version 1
@style "main.glts"
// ── 1. Global Configuration & Constants ──────────────────────────────────────
@global $APP_TITLE = "Glint Design System & Rhei Demo"
@global $IS_PRODUCTION = false
// Глобальное состояние приложения
@global $access_level = 3.0
@global $username = "Guest"
@global $volume_level = 0.0
@global $is_enabled = false
@global $search_query = "Type to search..."
@global $team_members = ["Alexander", "Beatrice", "Cyrus", "Diana"]
@singleton SystemSettings {
theme = "ocean-blue",
refresh_rate = 60,
debug_mode = true,
storage_path = fs:/var/lib/glint/assets
}
// ── 2. Logic Layer (Document-level Rhai Script) ──────────────────────────────
!rhei: {
// Вспомогательные UI-функции
fn level_label(lvl) {
if lvl >= 8.0 { "Admin" }
else if lvl >= 5.0 { "Moderator" }
else { "Guest" }
}
fn volume_icon(vol) {
if vol == 0.0 { "🔇" }
else if vol < 40.0 { "🔈" }
else if vol < 75.0 { "🔉" }
else { "🔊" }
}
// Демонстрация сложных вычислений при загрузке документа
const TARGET = 20;
fn fib(n) {
if n < 2 { return n; }
let a = 0; let b = 1;
for i in 2..=n {
let c = a + b;
a = b; b = c;
}
b
}
print(`Initializing Glint Engine...`);
let result = fib(TARGET);
print(`Fibonacci(${TARGET}) computed on load: ${result}`);
}
// ── 3. UI Components ─────────────────────────────────────────────────────────
@component ProfileCard(username: String, access_level: Float) {
Panel {
Header !rhei: { "User: " + username }
Label(text=$username)
// Сложное условие в Rhai: вычисляется при каждом обновлении VDOM
@if !rhei: { (access_level >= 5.0) && (username.len() > 0) } {
Text "✅ Administrator Privileges Active"
Text !rhei: { "Role: " + level_label(access_level) }
} @else {
Text "🔒 Restricted Access Mode"
Text "Role: Guest"
}
}
}
// ── 4. Main Application Tree ─────────────────────────────────────────────────
Window(
title=$APP_TITLE,
width=1280,
height=800,
resizable=true
) {
// Локальное состояние окна
@let $show_metrics = true
Panel(id="main_viewport", padding=24) {
Header "Dashboard Overview"
Text "Welcome to the ultimate Glint component test suite."
Divider()
// ── Секция A: Control Panel (Переменные и биндинги) ──────────────────
Header "A. Control Panel & State Bindings"
Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {}
Panel {
Input(placeholder=$search_query, value=$search_query)
Toggle(label="Enable Live Metrics", value=$is_enabled)
Text !rhei: { "Live search query: " + search_query }
}
@if $is_enabled {
Text "✅ Features are ON. You can adjust the system."
} @else {
Text "⛔ Features are OFF."
}
Divider()
// ── Секция B: Arithmetic Conditions & Reactive Text ──────────────────
Header "B. Rhei Arithmetic Conditions"
Slider(value=$volume_level)
ProgressBar(value=$volume_level)
// Трехуровневое ветвление с использованием Rhai-условий
@if !rhei: { volume_level == 0.0 } {
Text "🔇 Muted"
} @else {
@if !rhei: { volume_level >= 75.0 } {
Text "🔊 High volume — protect your hearing!"
} @else {
// Инлайн-текст, вызывающий функцию из глобального скрипта
Text !rhei: { volume_icon(volume_level) + " Volume OK: " + volume_level + " / 100" }
}
}
Divider()
// ── Секция C: Access Control & Component Instantiation ───────────────
Header "C. Multi-variable Logic & Components"
Slider(value=$access_level)
!rhei: { "Current slider level: " + access_level + " → " + level_label(access_level) }
Panel {
Button(label="Grant Admin (Level 9)") {
@on click {
!rhei: { access_level = 9.0; }
}
}
Button(label="Reset (Level 1)") {
@on click {
!rhei: { access_level = 1.0; }
}
}
}
@if !rhei: {
let threshold = 5.0;
access_level >= threshold
} {
Text "🛡️ Access granted — Privileged Zone"
ProfileCard(username=$username, access_level=$access_level)
} @else {
Text "🚫 Access denied — Raise your level to 5+"
}
Divider()
// ── Секция D: Dynamic List Rendering (@each) ─────────────────────────
Header "D. Team Management (Reactive Grid)"
// Передаем direction и columns как свойства компонента
Panel(columns=2, direction="grid") {
@each $name in $team_members {
Panel {
ProfileCard(username=$name, access_level=$access_level)
Button(label="Promote System-wide") {
@on click {
!rhei: {
// VDOM автоматически заменит "$name" на "Alexander", "Beatrice" и т.д.
let target_name = "$name";
print("Promoting triggered by " + target_name);
if access_level < 10.0 {
access_level += 1.0;
}
}
}
}
}
}
}
Divider()
// ── Секция E: Footer Metadata ────────────────────────────────────────
Panel {
Text "System Version: 1.0.5-stable"
// Многострочный скрипт прямо внутри текстового узла
!rhei: {
let pct = (access_level * 10.0).to_string();
"Access Power: " + pct + "% | Integrity: OK"
}
}
}
}

47
docs/en/README.md Normal file
View 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 38, 10, 9.19.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> | 23× |
| 2 | String interning for hot paths | 1.52× |
| 9.3 | Iced widget cache by content_hash | 1.52× (render) |

View 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.

View 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
View 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
View 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.01.0, clamped |
| `parse_font_weight` | `(s: &str) -> Option<u16>` | Names: `normal`→400, `bold`→700, `lighter`→300, `bolder`→900; numeric values |
| `parse_text_align` | `(s: &str) -> Option<TextAlign>` | `left`, `center`, `right` |
### `resolve_size`
```rust
pub fn resolve_size(v: Option<SizeValue>, relative_to: Option<f32>) -> Option<f32>
```
A convenience wrapper around `SizeValue::resolve`, returns `Option<f32>`.
---
## Usage in `renderer.rs`
`ComputedStyle` fields are actively used in `/home/faynot/software/glint-runtime/src/renderer.rs`:
| Field | Where used |
|---|---|
| `.color` | Text color in buttons, text fields, Label |
| `.background` | Background of containers, buttons, text fields |
| `.padding*` | `iced::Padding` for buttons, input fields, containers |
| `.margin*` | Spacing around elements |
| `.border_radius`, `.border_width`, `.border_color` | Borders of buttons, input fields, containers |
| `.width`, `.height` | Sizes of Scrollable, Column, Row, Image |
| `.min_width`, `.max_width`, `.min_height`, `.max_height` | Size constraints |
| `.direction` | Flex direction (Row / Column) |
| `.align_items` | Child element alignment |
| `.content_align` | Content alignment |
| `.flex_grow` | Flex-grow with `FillPortion` |
| `.spacing` | `iced::container::Style` spacing, gap in Row / Column |
| `.position` | Static / Fixed / Absolute / Sticky |
| `.top`, `.right`, `.bottom`, `.left` | Positioning |
| `.overflow_x`, `.overflow_y` | Scrolling (`Scrollable`) |
| `.display` | `Display::None` — element hiding |
| `.opacity` | Transparency |
| `.font_weight` | Font weight in Text |
| `.line_height` | Line spacing |
| `.text_align` | Horizontal text alignment |
| `.font_size` | Font size (inherited from parent) |
---
## `nth_matches(expr: &str, n: usize) -> bool`
Internal function for resolving `:nth-child(an+b)`, `:nth-child(odd)`, `:nth-child(even)`, and `:nth-child(<number>)`. Supports negative `a` and `b`.
---
## Relationships with other modules
- `types.rs` — each `DomNode` (both element and text node) contains `computed_style: ComputedStyle`.
- `renderer.rs` — imports `{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign, resolve_size}`.
- `mod.rs` — uses `AncestorInfo`, `ComputedStyle`, `StructuralContext` when traversing the DOM.

View 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
View 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

View 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 400599, Bold 600799, ExtraBold ≥800), `text_align`, `line_height` (with `Wrapping::Word`).
- For Title/Header the default size is 24px, for Text — 16px.
### `"Image"`
- Reads `src`. If the path starts with `fs:` — strips the prefix.
- `.svg``svg::Handle`, otherwise `image::Viewer`.
- Image size is calculated subtracting padding and border-width.
- For raster images `border_radius` is applied.
### `"Icon"`
Renders the character `🔹` as text of size 18px (or `current_font_size`). Placeholder.
### `"Toggle"`
Delegates to `render_toggle()`.
### `"Slider"`
Delegates to `render_slider()`.
### `"ProgressBar"`
```rust
progress_bar(0.0..=100.0, value)
```
The `value` property is parsed as `f32`.
### `"Divider"`, `"Separator"`
```rust
iced::widget::rule::horizontal(1)
```
Horizontal line with thickness 1px.
### Default branch (unknown type)
- Creates a `column` with `spacing` (10px).
- Renders children.
- If there are `abs_layers` — wraps in a `stack`.
- Applies `apply_universal_box_model()`.
- If there are `sticky_layers` — overlays them via `stack`.
- Returns `None` (the element is already written into `final_widget_opt`).
### Post-processing for non-Button and non-Input
```rust
if el.type_name != "Button" && el.type_name != "Input" {
final_widget_opt = make_hoverable(...);
}
```
### Positioning handling
After obtaining `final_widget`:
- **Fixed** → `wrap_fixed_position()` → placed into `fixed_layers`, returns `None`.
- **Absolute** → `wrap_fixed_position()` → placed into `abs_layers`, returns `None`.
- **Sticky** → if `scroll_y > threshold`, the element is moved to `sticky_layers`, and an empty `spacer` with height `estimate_element_height()` is inserted in its place. Otherwise the element stays in place.
---
## `render_children()`
```rust
fn render_children<'a>(
children: &'a [Element],
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
parent_direction: Option<LayoutDirection>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
scroll_positions: &HashMap<u64, f32>,
container_id: u64,
stylesheet: &StyleSheet,
) -> Vec<iced::Element<'a, Message, Theme, iced::Renderer>>
```
Recursively calls `render_element()` for each child. Filters out `None` (display: none). Returns a vector of rendered elements.
---
## `render_panel()`
```rust
fn render_panel<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
scroll_positions: &HashMap<u64, f32>,
container_id: u64,
stylesheet: &StyleSheet,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
Renders `Panel` — a container with three layout modes:
### Row
```rust
LayoutDirection::Row
```
`iced::widget::row` with `spacing` (10px). `align_y` from `cs.align_items` or `Alignment::Center` by default.
### Column
```rust
LayoutDirection::Column
```
`iced::widget::column` with `spacing`. `align_x` from `cs.align_items`.
### Grid
```rust
LayoutDirection::Grid
```
Columns (`column`), inside each — a row (`row`). Number of columns from the `columns` property (default 3). Each row is a chunk of `cols` children.
In all modes:
- If there are `abs_layers` — they are wrapped in a `stack` inside the content.
- After content, `apply_universal_box_model()` is applied.
- `sticky_layers` are overlaid on top via `stack`.
---
## `render_button()`
```rust
fn render_button<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
scroll_positions: &HashMap<u64, f32>,
container_id: u64,
stylesheet: &StyleSheet,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
- If there are no children — reads `label` (default `"Button"`) and creates `text`.
- If there are children — renders them in a `row` with `spacing = 8`.
- Padding: default 8px vertical, 16px horizontal. With auto-shrink if `padding + border > width/height`.
- `on_press` from `__on:click`.
- Style: via `get_button_style()` with dynamic `hover`/`active` overrides from `stylesheet.matching_rules()`.
---
## `render_toggle()`
```rust
fn render_toggle<'a>(
el: &'a Element,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
- Reads `label` (optional) and `value` (parsed as `bool`, default `false`).
- Extracts `__bind:value` for reactive binding.
- Creates a `checkbox`, on `on_toggle` sends `Message::ToggleChanged`.
- If there is a label — wraps in `row![checkbox, label]` with `spacing=8` and `align_y=Center`.
---
## `render_input()`
```rust
fn render_input<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
_parent_font_size: Option<f32>,
hover_props: &HashMap<String, String>,
active_props: &HashMap<String, String>,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
- Reads `placeholder` (default `"Type here…"`) and `value`.
- Extracts `__bind:value`.
- Creates `text_input` with `padding` from `get_padding(cs, 10.0)`.
- Assigns `id` via `get_or_create_widget_id(el.element_id.0, "ti")`.
- If there are hover/active styles — applies dynamic `style()` with `apply_overrides`.
- Otherwise — static style via `get_text_input_style()`.
---
## `render_slider()`
```rust
fn render_slider<'a>(
el: &'a Element,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
- Reads `value` (parsed as `f32`, default 0.0).
- Extracts `__bind:value`.
- Creates a `slider` in the range `0.0..=100.0`.
- On change sends `Message::SliderChanged`.
---
## Helper Functions
### `get_padding()`
```rust
fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding
```
Gathers `padding` from `ComputedStyle` considering `padding-top/right/bottom/left`. If `padding + border * 2` exceeds fixed `width`/`height` — scales padding proportionally (auto-shrink).
### `get_margin()`
```rust
fn get_margin(cs: &ComputedStyle) -> iced::Padding
```
Gathers `margin` from `ComputedStyle` considering `margin-top/right/bottom/left`. Base is `cs.margin` (default 0).
### `get_button_style()`
```rust
fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style
```
Builds `button::Style` from `ComputedStyle`. Depending on `status`:
- `Hovered` — background 15% lighter (`* 1.15`);
- `Pressed` — background 15% darker (`* 0.85`);
- `Active` — unchanged.
### `get_text_input_style()`
```rust
fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style
```
Builds `text_input::Style` from `ComputedStyle`: `background`, `border`, `icon`, `placeholder`, `value`, `selection`. Default values use a dark theme.
### `estimate_element_height()`
```rust
fn estimate_element_height(cs: &ComputedStyle) -> f32
```
Approximately calculates element height for sticky spacer: `padding_top + padding_bottom + border_width * 2 + font_size * line_height`.
### `wrap_sticky_position()`
```rust
fn wrap_sticky_position<'a>(
widget: iced::Element<'a, Message, Theme, iced::Renderer>,
cs: &ComputedStyle,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
Wraps a widget in a `container` with `padding-top` from `cs.top`. Width is `Fill`. Used for sticky elements when `scroll_y > threshold`.
### `wrap_fixed_position()`
```rust
fn wrap_fixed_position<'a>(
widget: iced::Element<'a, Message, Theme, iced::Renderer>,
cs: &ComputedStyle,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
Wraps a widget in a `container` with `width = Fill`, `height = Fill` and alignment (`align_x`, `align_y`) based on set `top`/`bottom`/`left`/`right`. Padding is set accordingly. Used for `fixed` and `absolute` positioning.
### `apply_universal_box_model()`
```rust
fn apply_universal_box_model<'a>(
widget: impl Into<iced::Element<'a, Message, Theme, iced::Renderer>>,
cs: &ComputedStyle,
is_window: bool,
default_padding: f32,
scrollable_id: Option<u64>,
) -> iced::Element<'a, Message, Theme, iced::Renderer>
```
Applies the box model to any widget. **Wrapping order:**
```
outer_container (margin) ← if margin exists and !is_window
inner_container (bg, border, clip)
scrollable ← if overflow_x/overflow_y = Scroll/Auto
container (padding)
widget
```
#### Stages
1. **Padding**: `container(widget).padding(get_padding(cs, default_padding))`.
2. **Overflow**: if `overflow-y` = Scroll/Auto — adds `scrollable` with direction `Vertical` (or `Both` if `overflow-x` is also Scroll/Auto). For Window `overflow-y` defaults to `Auto`. Scroll gets an `id` and `on_scroll`.
3. **Clip**: if `overflow = Hidden``container.clip(true)`.
4. **Background, border, rounding**: `container.style(...)` with `Background`, `Border`.
5. **Width/height**: for Window — `Fill`/`Fill`; otherwise — from `cs.width`, `cs.height`, `cs.max_width`, `cs.max_height`.
6. **Content alignment**: `align_x` from `cs.content_align`.
7. **Margin**: if margin exists — outer `container` with `padding = margin`.
---
## Widget Tree Structure
For a typical window (`Window`) the tree looks like:
```
stack[
container (Window) [Fill, Fill]
scrollable [id="sc:window_id"]
container [bg, border, padding]
column [spacing]
...child elements...
container (fixed) ← fixed layer
container (absolute) ← absolute layer
container (sticky) ← sticky layer
]
```
For `Panel`:
```
stack[
container [bg, border, margin]
scrollable (if overflow)
container [padding]
row | column | grid
...child elements...
container (sticky) ← sticky layer, on top of boxed content
]
```
For unknown elements — similar to Panel, but abs-layers inside scroll, sticky on top.
For simple elements (Text, Image, Toggle, Slider, ProgressBar, Divider):
```
container [bg, border, margin, padding]
scrollable (if overflow)
container [padding]
text | image | checkbox | slider | progress_bar | rule
```

305
docs/en/modules/06-mod.md Normal file
View 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>>,
}
```

View 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
View 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).
**Реализовано:** фазы 38, 10, 9.19.2 (content_hash, стабильные Iced widget ID).
**Осталось:**
| Фаза | Описание | Оценка ускорения |
|------|----------|-----------------|
| 0 | Профилирование (bench, flamegraph) | — |
| 1 | Типизированные Value в стилях вместо HashMap<String, String> | 23× |
| 2 | String interning для горячих путей | 1.52× |
| 9.3 | Кэш Iced-виджетов по content_hash | 1.52× (render) |

View 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 из них, но время на их создание уже потрачено.

View 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
View 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
View 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.01.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.

View 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
View 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)` как строка

View 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 400599, Bold 600799, 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
View 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>>,
}
```

View 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
View 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
View 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
}

View File

@@ -0,0 +1,82 @@
// =============================================================================
// GLINT UI: Style Properties Test Suite
// Tests: opacity, font-weight, text-align, line-height, display: none
// =============================================================================
@version 1
@style "main.glts"
@global $show_secret = true
Window(title="Style Test Suite", width=800, height=600) {
Panel(class="layout-viewport") {
Header "Style Properties Test Suite"
// ── 1. Opacity ───────────────────────────────────────────────
Text(class="test-section-header") "1. Opacity"
Panel(class="test-row") {
Panel(class="opacity-fade") {
Text "Fade (0.3)"
}
Panel(class="opacity-mid") {
Text "Mid (0.6)"
}
Panel(class="opacity-subtle") {
Text "Subtle (0.85)"
}
}
Divider()
// ── 2. Font Weight ────────────────────────────────────────────
Text(class="test-section-header") "2. Font Weight"
Panel(class="test-row") {
Text(class="fw-light") "Light 300"
Text(class="fw-normal") "Normal 400"
Text(class="fw-bold") "Bold 700"
Text(class="fw-black") "Black 900"
}
Divider()
// ── 3. Text Align ─────────────────────────────────────────────
Text(class="test-section-header") "3. Text Align"
Panel(class="ta-box") { Text(class="ta-left") "Left Aligned" }
Panel(class="ta-box") { Text(class="ta-center") "Center Aligned" }
Panel(class="ta-box") { Text(class="ta-right") "Right Aligned" }
Divider()
// ── 4. Comma-separated Selectors ─────────────────────────────
Text(class="test-section-header") "4. Comma Selectors"
Panel(class="layout-row") {
Text(class="warning-text") "⚠ Warning text"
Text(class="error-text") "✖ Error text"
}
Divider()
// ── 6. Line Height ────────────────────────────────────────────
Text(class="test-section-header") "4. Line Height"
Panel(class="lh-box") {
Text(class="lh-tight") "Line height 0.8 — плотный текст для проверки межстрочного интервала. Текст должен быть более сжатым по вертикали."
}
Panel(class="lh-box") {
Text(class="lh-normal") "Line height 1.2 — нормальное значение по умолчанию. Этот блок служит эталоном для сравнения."
}
Panel(class="lh-box") {
Text(class="lh-loose") "Line height 2.0 — разреженный текст для наглядного сравнения. Строки должны быть хорошо заметно разделены."
}
Divider()
// ── 7. Display: None ──────────────────────────────────────────
Text(class="test-section-header") "5. Display: None"
Panel(class="hidden-box") {
Text "Этого блока не должно быть видно!"
}
@if $show_secret {
Text "✅ Блок с display:none скрыт (выше не видно красную панель)."
}
}
}

432
examples/display/main.glts Normal file
View 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
}

17
examples/dock/dock.gltm Normal file
View 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
View 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
}

209
examples/glt/desktop.gltm Normal file
View File

@@ -0,0 +1,209 @@
// =============================================================================
// GLINT UI: Compact Reactive Test Suite (Fixed Layout)
// =============================================================================
@version 1
@style "main.glts"
// ── 1. Global Application State ──────────────────────────────────────────────
@global $APP_TITLE = "Glint Core Console"
@global $username = "Operator"
@global $volume_level = 50.0
@global $is_enabled = true
@global $access_level = 5.0
@global $team_nodes = ["Alpha", "Bravo", "Charlie"]
@global $show_toast = true
@singleton SystemSettings {
theme = "ocean-blue",
refresh_rate = 60,
debug_mode = true,
storage_path = fs:/var/lib/glint/assets
}
!rhei: {
fn load_status(val) {
if val == 0.0 { "💤 Muted" }
else if val >= 80.0 { "🔥 Overload" }
else { "⚡ Active" }
}
let sum = 0;
for i in 1..=5 { sum = sum + i; }
print("Rhai Engine online. Checksum: " + sum);
}
// ── 2. Reusable UI Components ────────────────────────────────────────────────
@component NodeCard(nodename: String, level: Float) {
Panel(class="ui-card") {
Label(text=$nodename)
@if !rhei: { level >= 5.0 } {
Text "🛡️ Secure Node"
} @else {
Text "🔓 Guest Node"
}
}
}
// ── 3. Application Layout Tree ───────────────────────────────────────────────
Window(title=$APP_TITLE, width=1024, height=720) {
Panel(class="sticky-header") {
Header "Glint Compiler/Runtime Demostration"
}
Panel(class="layout-viewport") {
// --- Верхняя панель (Header Section) ---
Panel(class="layout-row") {
Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {}
Panel(class="layout-col") {
Header "Glint Reactive Dashboard"
Text !rhei: { "User: " + username + " | Status: " + load_status(volume_level) }
}
}
Divider()
// --- Главная рабочая область (Workspace Split) ---
Panel(class="layout-row") {
// Левая колонка: Интерактивное состояние и биндинги
Panel(class="layout-col") {
Panel(class="ui-card") {
Header "State Bindings"
Input(placeholder="Change title...", value=$APP_TITLE)
Toggle(label="Live Engine", value=$is_enabled)
@if $is_enabled {
Text "🟢 System: ONLINE"
} @else {
Text "🔴 System: OFFLINE"
}
}
Panel(class="ui-card") {
Header "Control Channels"
Slider(value=$volume_level)
ProgressBar(value=$volume_level)
Text !rhei: { "Metrics Level: " + volume_level + "%" }
}
}
// Правая колонка: Тестирование геометрии и каскада стилей
Panel(class="layout-col") {
Panel(class="geometry-box") {
Header "Box Model Geometry"
Text "Asymmetric padding layout container."
Panel(class="margin-test-item") {
Text "Margin-Top Spacer Box"
}
}
Panel(class="inheritance-box") {
Text "Inherited Yellow Color & 18px Font Size"
Text(class="override-style") "Explicit Style Override (Pink, 12px)"
}
// Управление уровнем доступа и изменение переменных по клику
Panel(class="ui-card") {
Header "Access Management"
Slider(value=$access_level)
// ── ТЕСТ POSITION: ABSOLUTE ─────────────────────
Panel(class="abs-demo-container") {
Panel(class="abs-demo-badge") {
Text "ABSOLUTE!"
}
Text "(бадж спозиционирован абсолютно)"
Button(label="Press me") {}
}
Panel(class="layout-row") {
Button(label="Set Admin (Lvl 9)", class="btn-custom") {
@on click { !rhei: { access_level = 9.0; } }
}
Button(label="Reset Access") {
@on click { !rhei: { access_level = 1.0; } }
}
}
}
}
}
// --- Динамический список узлов (@each Grid) ---
Panel(direction="grid", columns=3, gap=12, class="layout-clean") {
@each $node in $team_nodes {
NodeCard(nodename=$node, level=$access_level)
}
}
// --- Нижняя панель распределения пространства ---
Panel(class="flex-row-bar") {
Panel(class="fill-10") { Text "Grow 10%" }
Panel(class="fixed-120") { Text "Fixed 120px" }
Panel(class="fill-20") { Text "Grow 20%" }
}
}
// ── ТЕСТ ФИКСИРОВАННЫХ СЛОЕВ ───────────────────
@if $show_toast {
Panel(class="fixed-toast") {
Panel(class="layout-row") {
Text "🔔 [FIXED LAYER TEST]: Runtime compiled successfully!"
Button(label="❌", class="btn-close") {
@on click { !rhei: { show_toast = false; } }
}
}
}
}
Panel(class="scroll-test-box") {
Header "Scroll Test"
Text "Line 1: Item A"
Text "Line 2: Item B"
Text "Line 3: Item C"
Text "Line 4: Item D"
Text "Line 5: Item E"
Text "Line 6: Item F"
Text "Line 7: Item G"
Text "Line 8: Item H"
Text "Line 9: Item I"
Text "Line 10: Item J"
}
// ── ДЕМО POSITION: ABSOLUTE (Overlay-карточка) ────────────────────
Panel(class="absolute-demo-section") {
Header "Position: Absolute Demo"
Panel(class="abs-stage") {
Panel(class="abs-overlay") {
Text "🎯 Абсолютно спозиционирована!"
Text "top: 10px | right: 10px"
}
Text "Этот текст — часть нормального потока."
Text "Оверлей лежит поверх, не влияя на раскладку."
Text "Работает как position: absolute в CSS."
}
}
Panel(class="sticky-container") {
Text "Scroll down to test sticky effect (внутренний скролл):"
// Стикер внутри скролл-контейнера
Panel(class="sticky-header") {
Text "📌 STICKY: я прилип к верху!"
}
// Большой блок текста снизу, чтобы гарантировать появление скролла
Panel(class="scroll-spacer") {
Text "Item 1: Лорем ипсум долор сит амет..."
Text "Item 2: Консектетур адипискинг элит."
Text "Item 3: Сед до эйусмод темпор."
Text "Item 4: Инсидидунт ут лаборе."
Text "Item 5: Долоре магна аликва."
Text "Item 6: Ут enim ад миним veniam."
Text "Item 7: Квис нострум экзерситатион."
Text "Item 8: Уллао лаборис ниси ут."
Text "Item 9: Аликип экс иа коммодо."
Text "— Конец тестового контента —"
}
}
}

432
examples/glt/main.glts Normal file
View 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
}

106
main.glts
View File

@@ -1,106 +0,0 @@
// =============================================================================
// Glint Design System — Master Stylesheet (main.glts)
// =============================================================================
// ── 1. Цветовая палитра и константы (Variables) ──────────────────────────────
$bg-app = #11111b // Глубокий темный фон для всего окна
$bg-panel = #1e1e2e // Базовый цвет для контейнеров и панелей
$bg-surface = #313244 // Цвет для карточек и выделенных блоков
$bg-field = #181825 // Внутренний фон для полей ввода (Input)
$text-main = #cdd6f4 // Мягкий белый основной текст
$text-muted = #9399b2 // Серый приглушенный текст для описаний
$accent = #b4befe // Лавандовый акцентный цвет для кнопок и заголовков
$border-glow = #45475a // Цвет аккуратных рамок и разделителей
// Сетка отступов (Размеры автоматически очищаются от "px" парсером Glint)
$pad-dense = 6px
$pad-normal = 12px
$pad-roomy = 20px
$pad-window = 28px
// ── 2. Шаблоны стилей (Mixins) ───────────────────────────────────────────────
@mixin text-body {
color: $text-main
font-size: 15px
}
@mixin standard-card {
background: $bg-panel
border-color: $border-glow
border-width: 1px
border-radius: 12px
}
// ── 3. Правила для UI-компонентов (Element Rules) ────────────────────────────
// Главное окно приложения
Window {
background: $bg-app
padding: $pad-window
spacing: $pad-roomy
}
// Универсальный контейнер ( viewport, карточки, обертки списков )
Panel {
@use standard-card
padding: $pad-normal
gap: 14px
direction: vertical
align-items: center
content-align: center
}
// Крупные заголовки секций
Header {
color: $accent
font-size: 22px
padding: 4px
}
// Обычный текст
Text {
@use text-body
}
// Подписи, второстепенные метаданные
Label {
color: $text-muted
font-size: 13px
}
// Интерактивные элементы управления
Button {
border-radius: 8px
border-width: 1px
}
Image {
border-radius: 8px
border-width: 1px
}
// Текстовые поля ввода
Input {
background-color: $bg-field
color: $text-main
font-size: 14px
padding: 12px
border-radius: 16px
border-width: 1px
border-color: $border-glow
width: 320px
}
// Переключатели
Toggle {
color: $text-main
font-size: 15px
}
// Разделительные линии (Divider / Separator)
Divider {
background: $border-glow
border-width: 1px
}

BIN
out.glbc

Binary file not shown.

View File

@@ -1,13 +1,16 @@
use iced::widget::{column, container, row, scrollable, text, Space}; use std::collections::HashMap;
use iced::{Alignment, Length, Theme};
use crate::interpreter::{Document, Element, Interpreter, RheiContext}; use iced::widget::{column, container};
use iced::{Length, Theme};
use crate::interpreter::{Document, Element, Interpreter, RheiContext, Value};
use crate::perf;
use crate::renderer::render_element; use crate::renderer::render_element;
use crate::Message; use crate::Message;
pub struct GlintApp { pub struct GlintApp {
pub doc: Document, pub doc: Document<'static>,
pub vdom_roots: Vec<Element>, pub vdom_roots: Vec<Element<'static>>,
pub rhei: RheiContext, pub rhei: RheiContext,
} }
@@ -18,78 +21,123 @@ impl GlintApp {
pub fn update(&mut self, message: Message) -> iced::Task<Message> { pub fn update(&mut self, message: Message) -> iced::Task<Message> {
match message { match message {
Message::WindowScrolled(y) => {
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) => { Message::EventTriggered(script) => {
if !script.is_empty() { if !script.is_empty() {
let old_vars: Vec<String> = self.doc.variables.keys().cloned().collect();
self.rhei.execute_action(&script, &mut self.doc.variables); 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) => { 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) => { 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) => { 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" { 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( let dirty_set = self.doc.tracker.take_dirty_set();
&self.doc.roots, let root_refs: Vec<&Element<'static>> = self.doc.roots.iter().collect();
&self.doc.variables, let _vdom_scope = perf::PerfScope::new("vdom");
self.vdom_roots = Interpreter::evaluate_vdom_incr(
&root_refs,
&mut self.doc.variables,
&self.doc.components, &self.doc.components,
&self.rhei, &self.rhei,
&self.doc.stylesheet, &self.doc.stylesheet,
&[],
&dirty_set,
); );
drop(_vdom_scope);
iced::Task::none() iced::Task::none()
} }
pub fn view(&self) -> iced::Element<'_, Message, Theme, iced::Renderer> { pub fn view(&self) -> iced::Element<'_, Message, Theme, iced::Renderer> {
let mut content = column![].spacing(15).padding(20); 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();
for root in &self.vdom_roots { for root in &self.vdom_roots {
if let Some(el) = render_element(root) { 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); content = content.push(el);
} }
} }
let status_bar = container(self.build_status_bar(&self.doc.variables))
.width(Length::Fill)
.padding(10);
let layout = column![ let layout = column![
scrollable(content).width(Length::Fill).height(Length::Fill), content,
iced::widget::rule::horizontal(1), iced::widget::rule::horizontal(1),
status_bar,
];
container(layout)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn build_status_bar<'a>(
&self,
scope: &std::collections::HashMap<String, String>,
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
let slider_val = scope.get("volume_level")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
row![
text("🟢 Runtime Active (VDOM + Rhai)").size(14),
Space::new().width(Length::Fill),
text(format!("Шаблонных узлов: {}", self.doc.roots.len())).size(14),
Space::new().width(Length::Fixed(15.0)),
text(format!("Слайдер ($volume_level): {:.1}", slider_val)).size(14),
] ]
.align_y(Alignment::Center) .width(Length::Fill)
.into() .height(Length::Fill);
let main_flow = container(layout)
.width(Length::Fill)
.height(Length::Fill);
let has_abs = !global_abs_layers.is_empty();
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 {
let mut stack_widget = iced::widget::stack![main_flow];
for layer in global_abs_layers {
stack_widget = stack_widget.push(layer);
}
for layer in global_sticky_layers {
stack_widget = stack_widget.push(layer);
}
for layer in global_fixed_layers {
stack_widget = stack_widget.push(layer);
}
stack_widget.into()
}
};
drop(_render_scope);
perf::print_frame();
render_result
} }
} }

View File

@@ -136,11 +136,12 @@ pub fn compile_files(gltm_files: &[String], glts_files: &[String], output_path:
} }
pub fn run_file(path: &str) -> ! { pub fn run_file(path: &str) -> ! {
let bytecode = read_file_or_exit(path); let bytecode_vec = read_file_or_exit(path);
let bytecode: &'static [u8] = Box::leak(bytecode_vec.into_boxed_slice());
println!("{} {}", "▶️".green(), format!("Rendering: {}", path).bold()); println!("{} {}", "▶️".green(), format!("Rendering: {}", path).bold());
let start = Instant::now(); let start = Instant::now();
match Interpreter::run(&bytecode) { match Interpreter::run(bytecode) {
Ok(doc) => { Ok(doc) => {
println!( println!(
"{} {} ({:?})", "{} {} ({:?})",
@@ -157,18 +158,22 @@ pub fn run_file(path: &str) -> ! {
let result = iced::application( let result = iced::application(
move || { move || {
let rhei = RheiContext::new(&doc.rhei_scripts); 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( let vdom_roots = Interpreter::evaluate_vdom(
&doc.roots, &local_doc.roots,
&doc.variables, &mut local_doc.variables,
&doc.components, &local_doc.components,
&rhei, &rhei,
&doc.stylesheet, &local_doc.stylesheet,
&[],
); );
GlintApp { GlintApp {
doc: doc.clone(), doc: local_doc,
rhei, rhei,
vdom_roots, vdom_roots,
} }

View File

@@ -1,27 +1,31 @@
pub mod opcodes; pub mod opcodes;
pub mod reactive;
pub mod reader; pub mod reader;
pub mod rhei; pub mod rhei;
pub mod style; pub mod style;
pub mod types; pub mod types;
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
pub use rhei::RheiContext; pub use rhei::RheiContext;
use style::StyleSheet as SS; 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 opcodes::*;
use reader::Reader; use reader::Reader;
use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX}; use rhei::RHEI_PREFIX;
use style::ComputedStyle; use style::{AncestorInfo, ComputedStyle, StructuralContext};
use regex::Regex; pub use types::Value;
use std::collections::HashMap; use types::FlatVDom;
use std::sync::OnceLock; use std::collections::{HashMap, HashSet};
static RE_VAR: OnceLock<Regex> = OnceLock::new();
pub struct Interpreter; pub struct Interpreter;
impl Interpreter { impl Interpreter {
pub fn run(bytecode: &[u8]) -> Result<Document, InterpError> { pub fn run<'a>(bytecode: &'a [u8]) -> Result<Document<'a>, InterpError> {
if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); } if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); }
if &bytecode[..4] != MAGIC { return Err(InterpError::BadMagic); } if &bytecode[..4] != MAGIC { return Err(InterpError::BadMagic); }
@@ -31,6 +35,7 @@ impl Interpreter {
let mut components = HashMap::new(); let mut components = HashMap::new();
let mut rhei_scripts: Vec<String> = Vec::new(); let mut rhei_scripts: Vec<String> = Vec::new();
let mut stylesheet = SS::new(); let mut stylesheet = SS::new();
let mut tracker = ReactiveTracker::new();
let roots = Self::parse_block_elements( let roots = Self::parse_block_elements(
&mut r, &mut r,
@@ -38,23 +43,27 @@ impl Interpreter {
&mut components, &mut components,
&mut rhei_scripts, &mut rhei_scripts,
&mut stylesheet, &mut stylesheet,
&mut tracker,
true, true,
)?; )?;
let rhei_ctx = RheiContext::new(&rhei_scripts); //let rhei_ctx = RheiContext::new(&rhei_scripts);
rhei_ctx.initialize(&mut variables); //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( fn parse_block_elements<'a>(
r: &mut Reader, r: &mut Reader<'a>,
variables: &mut HashMap<String, String>, variables: &mut HashMap<String, Value>,
components: &mut HashMap<String, ComponentDef>, components: &mut HashMap<String, ComponentDef<'a>>,
rhei_scripts: &mut Vec<String>, rhei_scripts: &mut Vec<String>,
stylesheet: &mut SS, stylesheet: &mut SS,
tracker: &mut ReactiveTracker,
is_root: bool, is_root: bool,
) -> Result<Vec<Element>, InterpError> { ) -> Result<Vec<Element<'a>>, InterpError> {
let mut roots: Vec<Element> = Vec::new(); let mut roots: Vec<Element> = Vec::new();
let mut stack: Vec<Element> = Vec::new(); let mut stack: Vec<Element> = Vec::new();
@@ -63,8 +72,12 @@ impl Interpreter {
if !is_root && op == OP_END_BLOCK { break; } if !is_root && op == OP_END_BLOCK { break; }
match op { match op {
OP_ELEM_PUSH => stack.push(Element::new(r.read_string()?)), 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 => { OP_ELEM_POP => {
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?; let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
Self::attach(&mut stack, &mut roots, finished); Self::attach(&mut stack, &mut roots, finished);
@@ -73,7 +86,7 @@ impl Interpreter {
OP_GLOBAL | OP_LET => { OP_GLOBAL | OP_LET => {
let name = r.read_string()?; let name = r.read_string()?;
let vop = r.read_byte()?; 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); variables.insert(name, value);
} }
} }
@@ -90,53 +103,80 @@ impl Interpreter {
OP_CONTENT => { OP_CONTENT => {
let vop = r.read_byte()?; let vop = r.read_byte()?;
if let Some(value) = r.read_value_as_string(vop)? { if vop == OP_PROP_RHEI {
let expr_ref = r.read_str_ref()?;
let mut val = String::with_capacity(RHEI_PREFIX.len() + expr_ref.len());
val.push_str(RHEI_PREFIX);
val.push_str(expr_ref);
if let Some(el) = stack.last_mut() { if let Some(el) = stack.last_mut() {
let stored = if vop == OP_PROP_RHEI { tracker.scan_value(el.element_id, &val);
format!("{RHEI_PREFIX}{value}") el.push_prop("text".to_string(), val);
} else { }
value } else if let Some(value) = r.read_value_as_string(vop)? {
}; if let Some(el) = stack.last_mut() {
el.properties.insert("text".to_string(), stored); tracker.scan_value(el.element_id, &value);
el.push_prop("text".to_string(), value);
} }
} }
} }
OP_PROP_STR => { OP_PROP_STR => {
let (key, val) = (r.read_string()?, r.read_string()?); let key = r.read_str_ref()?;
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); } let val = r.read_str_ref()?;
if let Some(el) = stack.last_mut() {
tracker.scan_value(el.element_id, val);
el.push_prop(key, val);
}
} }
OP_PROP_VAR => { OP_PROP_VAR => {
let key = r.read_string()?; let key = r.read_str_ref()?;
let val = format!("${}", r.read_string()?); let val_ref = r.read_str_ref()?;
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); } let mut val = String::with_capacity(val_ref.len() + 1);
val.push('$');
val.push_str(val_ref);
if let Some(el) = stack.last_mut() {
tracker.scan_value(el.element_id, &val);
el.push_prop(key, val);
}
} }
OP_PROP_INT => { OP_PROP_INT => {
let key = r.read_string()?; let key = r.read_str_ref()?;
let val = r.read_i64()?.to_string(); let val = r.read_i64()?.to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); } if let Some(el) = stack.last_mut() {
el.push_prop(key, val);
}
} }
OP_PROP_FLOAT => { OP_PROP_FLOAT => {
let key = r.read_string()?; let key = r.read_str_ref()?;
let val = r.read_f64()?.to_string(); let val = r.read_f64()?.to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); } if let Some(el) = stack.last_mut() {
el.push_prop(key, val);
}
} }
OP_PROP_BOOL => { OP_PROP_BOOL => {
let key = r.read_string()?; let key = r.read_str_ref()?;
let val = (r.read_byte()? != 0).to_string(); let val = (r.read_byte()? != 0).to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); } if let Some(el) = stack.last_mut() {
el.push_prop(key, val);
}
} }
OP_PROP_RHEI => { OP_PROP_RHEI => {
let key = r.read_string()?; let key = r.read_str_ref()?;
let expr = r.read_string()?; let expr_ref = r.read_str_ref()?;
let mut val = String::with_capacity(RHEI_PREFIX.len() + expr_ref.len());
val.push_str(RHEI_PREFIX);
val.push_str(expr_ref);
if let Some(el) = stack.last_mut() { if let Some(el) = stack.last_mut() {
el.properties.insert(key, format!("{RHEI_PREFIX}{expr}")); 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 => { OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
let key = r.read_string()?; let key = r.read_str_ref()?;
if let Some(val) = r.read_value_as_string(op)? { if let Some(val) = r.read_value_as_string(op)? {
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); } if let Some(el) = stack.last_mut() {
el.push_prop(key, val);
}
} }
} }
@@ -145,11 +185,11 @@ impl Interpreter {
if is_root && stack.is_empty() { if is_root && stack.is_empty() {
rhei_scripts.push(script); rhei_scripts.push(script);
} else { } else {
let mut text_el = Element::new("#text".to_string()); let mut text_el = Element::new("#text");
text_el.properties.insert( text_el.element_id = tracker.alloc_id();
"text".to_string(), let full_val = format!("{RHEI_PREFIX}{script}");
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); Self::attach(&mut stack, &mut roots, text_el);
} }
} }
@@ -161,7 +201,7 @@ impl Interpreter {
.map(|_| Ok((r.read_string()?, r.read_string()?))) .map(|_| Ok((r.read_string()?, r.read_string()?)))
.collect::<Result<Vec<_>, InterpError>>()?; .collect::<Result<Vec<_>, InterpError>>()?;
let children = Self::parse_block_elements( 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 }); components.insert(name.clone(), ComponentDef { name, params, children });
} }
@@ -176,23 +216,26 @@ impl Interpreter {
}; };
let true_children = Self::parse_block_elements( 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 has_else = r.read_byte()? == 1;
let false_children = if has_else { let false_children = if has_else {
Self::parse_block_elements( Self::parse_block_elements(
r, variables, components, rhei_scripts, stylesheet, false, r, variables, components, rhei_scripts, stylesheet, tracker, false,
)? )?
} else { } else {
Vec::new() Vec::new()
}; };
let mut if_el = Element::new("@if".to_string()); let mut if_el = Element::new("@if");
if_el.properties.insert("condition".to_string(), cond_val); 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_el.children = true_children;
if !false_children.is_empty() { if !false_children.is_empty() {
let mut else_el = Element::new("@else".to_string()); let mut else_el = Element::new("@else");
else_el.element_id = tracker.alloc_id();
else_el.children = false_children; else_el.children = false_children;
if_el.children.push(else_el); if_el.children.push(else_el);
} }
@@ -211,12 +254,14 @@ impl Interpreter {
}; };
let block_children = Self::parse_block_elements( 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".to_string()); let mut each_el = Element::new("@each");
each_el.properties.insert("var_name".to_string(), var_name); each_el.element_id = tracker.alloc_id();
each_el.properties.insert("source".to_string(), source_val); 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; each_el.children = block_children;
Self::attach(&mut stack, &mut roots, each_el); Self::attach(&mut stack, &mut roots, each_el);
} }
@@ -245,7 +290,7 @@ impl Interpreter {
} }
if let Some(el) = stack.last_mut() { if let Some(el) = stack.last_mut() {
el.properties.insert( el.push_prop(
format!("__on:{event_name}"), format!("__on:{event_name}"),
handler_script, handler_script,
); );
@@ -278,45 +323,102 @@ impl Interpreter {
Ok(roots) Ok(roots)
} }
pub fn evaluate_vdom( pub fn evaluate_vdom<'a>(
templates: &[Element], templates: &[Element<'a>],
variables: &HashMap<String, String>, variables: &mut HashMap<String, Value>,
components: &HashMap<String, ComponentDef>, components: &HashMap<String, ComponentDef<'a>>,
rhei: &RheiContext, rhei: &RheiContext,
stylesheet: &SS, stylesheet: &SS,
) -> Vec<Element> { 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()); let mut output = Vec::with_capacity(templates.len());
for el in templates { // Precompute sibling info for structural pseudo-classes and sibling combinators
match el.type_name.as_str() { let sibling_infos: Vec<AncestorInfo> = templates.iter().map(|el| {
"@if" => { AncestorInfo::new_with_id(
let cond = el.properties.get("condition").cloned().unwrap_or_default(); el.type_name,
let is_true = Self::evaluate_condition(&cond, variables, rhei); el.id().map(String::from),
el.get_prop("class")
.map(|s| s.split_whitespace().map(String::from).collect())
.unwrap_or_default(),
)
}).collect();
let mut active_branch = Vec::new(); // Precompute type totals for structural pseudo-class context
for child in &el.children { let mut type_counts: HashMap<&str, usize> = HashMap::new();
for el in templates {
*type_counts.entry(el.type_name).or_insert(0) += 1;
}
let mut type_seen: HashMap<&str, usize> = HashMap::new();
for (i, el) in templates.iter().enumerate() {
let type_idx = type_seen.entry(el.type_name).or_insert(0);
let type_total = *type_counts.get(el.type_name).unwrap_or(&0);
let structural = StructuralContext {
sibling_index: i,
sibling_total: templates.len(),
type_index: *type_idx,
type_total,
has_children: !el.children.is_empty()
|| el.properties.iter().any(|(k, v)| k == "text" && !v.is_empty()),
is_root: ancestors.is_empty(),
};
*type_idx += 1;
match el.type_name {
"@if" => {
let cond = el.get_prop("condition").unwrap_or_default();
let is_true = Self::evaluate_condition(cond, variables, rhei);
let mut active_branch: Vec<&Element<'a>> = Vec::new();
for child in el.children.iter() {
if child.type_name == "@else" { 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 { } else if is_true {
active_branch.push(child.clone()); active_branch.push(child);
} }
} }
output.extend(Self::evaluate_vdom( output.extend(Self::evaluate_vdom_incr(
&active_branch, variables, components, rhei, stylesheet, &active_branch, variables, components, rhei, stylesheet, ancestors, dirty_set,
)); ));
} }
"@each" => { "@each" => {
let var_name = el.properties.get("var_name").cloned().unwrap_or_default(); let var_name = el.get_prop("var_name").unwrap_or_default();
let source_expr = el.properties.get("source").cloned().unwrap_or_default(); let source_expr = el.get_prop("source").unwrap_or_default();
let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) { let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) {
rhei.eval_expr(expr, variables) Self::normalize_rhai_array(&rhei.eval_expr(expr, variables).to_owned_string())
.map(|s| Self::normalize_rhai_array(&s)) } else {
.unwrap_or_default() Self::resolve_string(source_expr, variables).into_owned()
} else { };
Self::resolve_string(&source_expr, variables)
};
let items: Vec<String> = if resolved_source.is_empty() { let items: Vec<String> = if resolved_source.is_empty() {
vec![] vec![]
@@ -324,75 +426,92 @@ impl Interpreter {
resolved_source.split(',').map(str::trim).map(str::to_string).collect() resolved_source.split(',').map(str::trim).map(str::to_string).collect()
}; };
let child_refs: Vec<&Element<'a>> = el.children.iter().collect();
for item in items { for item in items {
let mut local_vars = variables.clone(); let old_val = variables.insert(var_name.to_string(), Value::Str(CompactString::new(item)));
local_vars.insert(var_name.clone(), item);
output.extend(Self::evaluate_vdom( output.extend(Self::evaluate_vdom_incr(
&el.children, &local_vars, components, rhei, stylesheet, &child_refs, variables, components, rhei, stylesheet, ancestors, dirty_set,
)); ));
if let Some(old) = old_val {
variables.insert(var_name.to_string(), old);
} else {
variables.remove(var_name);
}
} }
} }
_ => { _ => {
if let Some(comp) = components.get(&el.type_name) { if let Some(comp) = components.get(el.type_name) {
// Expand custom component let mut new_args = Vec::with_capacity(comp.params.len());
let mut comp_scope = variables.clone();
for (param, _) in &comp.params { for (param, _) in &comp.params {
if let Some(arg) = el.properties.get(param) { if let Some(arg) = el.get_prop(param) {
comp_scope.insert( new_args.push((
param.clone(), param.clone(),
Self::resolve_prop(arg, variables, rhei), Self::resolve_prop(arg, variables, rhei),
); ));
} }
} }
let mut vcomp = Element::new(el.type_name.clone()); let mut old_vals = Vec::with_capacity(new_args.len());
for (k, v) in new_args {
old_vals.push((k.clone(), variables.insert(k, Value::from(v))));
}
let mut vcomp = Element::new(el.type_name);
for (k, v) in &el.properties { for (k, v) in &el.properties {
if k.starts_with("__on:") { if k.starts_with("__on:") {
vcomp.properties.insert(k.clone(), Self::resolve_string(v, variables)); vcomp.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned());
} else { } else {
vcomp.properties.insert(k.clone(), Self::resolve_prop(v, variables, rhei)); vcomp.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei));
} }
} }
vcomp.computed_style = ComputedStyle::compute( let matched_sheets = Self::collect_matching_styles(&vcomp, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]);
&vcomp.properties, vcomp.computed_style = stylesheet.compute_cached(vcomp.type_name, &vcomp.properties, &matched_sheets);
stylesheet.resolve(&vcomp.type_name),
); let child_ancestors = Self::build_ancestor_chain(ancestors, &vcomp);
vcomp.children = Self::evaluate_vdom( let comp_child_refs: Vec<&Element<'a>> = comp.children.iter().collect();
&comp.children, &comp_scope, components, rhei, stylesheet, 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); output.push(vcomp);
for (k, old) in old_vals.into_iter().rev() {
if let Some(o) = old {
variables.insert(k, o);
} else { } else {
let mut vnode = Element::new(el.type_name.clone()); variables.remove(&k);
}
}
} else {
let mut vnode = Element::new(el.type_name);
for (k, v) in &el.properties { for (k, v) in &el.properties {
if k.starts_with("__on:") { if k.starts_with("__on:") {
vnode.properties.insert(k.clone(), Self::resolve_string(v, variables)); vnode.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned());
continue; continue;
} }
if v.starts_with('$') if v.starts_with('$')
&& !v[1..].contains(|c: char| !c.is_ascii_alphanumeric() && c != '_') && !v[1..].contains(|c: char| !c.is_ascii_alphanumeric() && c != '_')
{ {
vnode.properties.insert( vnode.set_prop(format!("__bind:{k}"), v[1..].to_string());
format!("__bind:{k}"),
v[1..].to_string(),
);
} }
vnode.properties.insert( vnode.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei));
k.clone(),
Self::resolve_prop(v, variables, rhei),
);
} }
vnode.computed_style = ComputedStyle::compute( let matched_sheets = Self::collect_matching_styles(&vnode, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]);
&vnode.properties, vnode.computed_style = stylesheet.compute_cached(vnode.type_name, &vnode.properties, &matched_sheets);
stylesheet.resolve(&el.type_name),
);
vnode.children = Self::evaluate_vdom( let child_ancestors = Self::build_ancestor_chain(ancestors, &vnode);
&el.children, variables, components, rhei, stylesheet, 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); output.push(vnode);
} }
} }
@@ -402,17 +521,63 @@ impl Interpreter {
output output
} }
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String { 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);
let classes: Vec<String> = el
.get_prop("class")
.map(|s| s.split_whitespace().map(String::from).collect())
.unwrap_or_default();
chain.push(AncestorInfo::new_with_id(el.type_name, id, classes));
chain
}
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>> {
let el_id = el.id();
let classes: Vec<&str> = el
.get_prop("class")
.map(|s| s.split_whitespace().collect())
.unwrap_or_default();
let el_attributes: HashMap<String, String> = el.properties.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
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, Value>, rhei: &RheiContext) -> String {
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) { if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
rhei.eval_expr(expr, variables).unwrap_or_default() rhei.eval_expr(expr, variables).to_owned_string().into()
} else { } else {
Self::resolve_string(v, variables) Self::resolve_string(v, variables).into_owned()
}
} }
}
fn evaluate_condition( fn evaluate_condition(
cond: &str, cond: &str,
variables: &HashMap<String, String>, variables: &HashMap<String, Value>,
rhei: &RheiContext, rhei: &RheiContext,
) -> bool { ) -> bool {
if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) { if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) {
@@ -428,7 +593,7 @@ impl Interpreter {
let resolved = Self::resolve_string(&clean, variables).trim().to_string(); 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; } if resolved == "false" || resolved == "0" || resolved.is_empty() { return false; }
let operators: [(&str, fn(f64, f64) -> bool); 6] = [ let operators: [(&str, fn(f64, f64) -> bool); 6] = [
@@ -455,7 +620,19 @@ impl Interpreter {
} }
#[inline] #[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() { match s.trim() {
"" | "false" | "0" | "null" => false, "" | "false" | "0" | "null" => false,
"true" | "1" => true, "true" | "1" => true,
@@ -476,21 +653,108 @@ impl Interpreter {
} }
} }
pub fn resolve_string(val: &str, scope: &HashMap<String, String>) -> String { pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str> {
let re = RE_VAR.get_or_init(|| Regex::new(r"\$([a-zA-Z0-9_]+)").unwrap()); if !val.contains('$') {
re.replace_all(val, |caps: &regex::Captures| { return Cow::Borrowed(val);
scope.get(&caps[1])
.map(|s| s.as_str())
.unwrap_or(&caps[0])
.to_string()
})
.to_string()
} }
fn attach(stack: &mut Vec<Element>, roots: &mut Vec<Element>, el: Element) { let mut result = String::with_capacity(val.len() + 16);
let mut chars = val.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 let Some(resolved) = scope.get(&var_name) {
let formatted = resolved.to_owned_string();
result.push_str(&formatted);
} else {
result.push('$');
result.push_str(&var_name);
}
} else {
result.push(c);
}
}
Cow::Owned(result)
}
fn attach<'a>(stack: &mut Vec<Element<'a>>, roots: &mut Vec<Element<'a>>, el: Element<'a>) {
match stack.last_mut() { match stack.last_mut() {
Some(parent) => parent.children.push(el), Some(parent) => parent.children.push(el),
None => roots.push(el), None => roots.push(el),
} }
} }
} }
#[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
View 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());
}
}

View File

@@ -1,5 +1,6 @@
use super::opcodes::*; use super::opcodes::*;
use super::types::InterpError; use super::types::{InterpError, Value};
use compact_str::CompactString;
pub struct Reader<'a> { pub struct Reader<'a> {
pub data: &'a [u8], pub data: &'a [u8],
@@ -7,73 +8,107 @@ pub struct Reader<'a> {
} }
impl<'a> Reader<'a> { impl<'a> Reader<'a> {
#[inline(always)]
pub fn new(data: &'a [u8]) -> Self { pub fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 } Self { data, pos: 0 }
} }
#[inline(always)]
pub fn remaining(&self) -> usize { pub fn remaining(&self) -> usize {
self.data.len() - self.pos self.data.len() - self.pos
} }
#[inline(always)]
fn require(&self, n: usize) -> Result<(), InterpError> {
if self.pos + n > self.data.len() {
Err(InterpError::UnexpectedEof)
} else {
Ok(())
}
}
#[inline(always)]
pub fn read_byte(&mut self) -> Result<u8, InterpError> { pub fn read_byte(&mut self) -> Result<u8, InterpError> {
self.require(1)?; self.require(1)?;
let b = self.data[self.pos]; let b = unsafe { *self.data.get_unchecked(self.pos) };
self.pos += 1; self.pos += 1;
Ok(b) Ok(b)
} }
#[inline(always)]
pub fn read_u32(&mut self) -> Result<u32, InterpError> { pub fn read_u32(&mut self) -> Result<u32, InterpError> {
self.require(4)?; self.require(4)?;
let v = u32::from_le_bytes(self.data[self.pos..self.pos + 4].try_into().unwrap()); let v = unsafe {
let ptr = self.data.as_ptr().add(self.pos) as *const [u8; 4];
u32::from_le_bytes(std::ptr::read_unaligned(ptr))
};
self.pos += 4; self.pos += 4;
Ok(v) Ok(v)
} }
#[inline(always)]
pub fn read_i64(&mut self) -> Result<i64, InterpError> { pub fn read_i64(&mut self) -> Result<i64, InterpError> {
self.require(8)?; self.require(8)?;
let v = i64::from_le_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap()); let v = unsafe {
let ptr = self.data.as_ptr().add(self.pos) as *const [u8; 8];
i64::from_le_bytes(std::ptr::read_unaligned(ptr))
};
self.pos += 8; self.pos += 8;
Ok(v) Ok(v)
} }
#[inline(always)]
pub fn read_f64(&mut self) -> Result<f64, InterpError> { pub fn read_f64(&mut self) -> Result<f64, InterpError> {
self.require(8)?; self.require(8)?;
let v = f64::from_le_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap()); let v = unsafe {
let ptr = self.data.as_ptr().add(self.pos) as *const [u8; 8];
f64::from_le_bytes(std::ptr::read_unaligned(ptr))
};
self.pos += 8; self.pos += 8;
Ok(v) Ok(v)
} }
pub fn read_string(&mut self) -> Result<String, InterpError> { #[inline(always)]
pub fn read_str_ref(&mut self) -> Result<&'a str, InterpError> {
let len = self.read_u32()? as usize; let len = self.read_u32()? as usize;
self.require(len)?; self.require(len)?;
let s = std::str::from_utf8(&self.data[self.pos..self.pos + len])
.map_err(|_| InterpError::InvalidUtf8)? let slice = &self.data[self.pos .. self.pos + len];
.to_string();
self.pos += len; self.pos += len;
Ok(s)
unsafe {
Ok(std::str::from_utf8_unchecked(slice))
}
}
#[inline(always)]
pub fn read_string(&mut self) -> Result<String, InterpError> {
self.read_str_ref().map(|s| s.to_owned())
}
#[inline(always)]
pub fn skip_string(&mut self) -> Result<(), InterpError> {
let len = self.read_u32()? as usize;
self.require(len)?;
self.pos += len;
Ok(())
} }
pub fn read_value_as_string(&mut self, type_op: u8) -> Result<Option<String>, InterpError> { pub fn read_value_as_string(&mut self, type_op: u8) -> Result<Option<String>, InterpError> {
let s = match type_op { let s = match type_op {
OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_RHEI => { OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_RHEI | OP_PROP_IDENT => {
Some(self.read_string()?)
}
OP_PROP_IDENT => {
Some(self.read_string()?) Some(self.read_string()?)
} }
OP_PROP_VAR => { OP_PROP_VAR => {
Some(format!("${}", self.read_string()?)) let name = self.read_str_ref()?;
} let mut s = String::with_capacity(name.len() + 1);
OP_PROP_INT => { s.push('$');
Some(self.read_i64()?.to_string()) s.push_str(name);
} Some(s)
OP_PROP_FLOAT => {
Some(self.read_f64()?.to_string())
}
OP_PROP_BOOL => {
Some((self.read_byte()? != 0).to_string())
} }
OP_PROP_INT => Some(self.read_i64()?.to_string()),
OP_PROP_FLOAT => Some(self.read_f64()?.to_string()),
OP_PROP_BOOL => Some((self.read_byte()? != 0).to_string()),
OP_PROP_NULL => None, OP_PROP_NULL => None,
OP_PROP_ARRAY => { OP_PROP_ARRAY => {
let items = self.read_array_as_strings()?; let items = self.read_array_as_strings()?;
@@ -81,7 +116,7 @@ impl<'a> Reader<'a> {
} }
OP_PROP_UNIT => { OP_PROP_UNIT => {
let num = self.read_f64()?; let num = self.read_f64()?;
let unit = self.read_string()?; let unit = self.read_str_ref()?;
if num.fract() == 0.0 { if num.fract() == 0.0 {
Some(format!("{}{}", num as i64, unit)) Some(format!("{}{}", num as i64, unit))
} else { } else {
@@ -94,8 +129,7 @@ impl<'a> Reader<'a> {
let mut args = Vec::with_capacity(arg_count); let mut args = Vec::with_capacity(arg_count);
for _ in 0..arg_count { for _ in 0..arg_count {
let op = self.read_byte()?; let op = self.read_byte()?;
let val = self.read_value_as_string(op)? let val = self.read_value_as_string(op)?.unwrap_or_default();
.unwrap_or_default();
args.push(val); args.push(val);
} }
Some(format!("{}({})", name, args.join(","))) Some(format!("{}({})", name, args.join(",")))
@@ -105,7 +139,77 @@ impl<'a> Reader<'a> {
Ok(s) Ok(s)
} }
/// Read `OP_PROP_ARRAY` (opcode already consumed) and return elements as strings. 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> { pub fn read_array_as_strings(&mut self) -> Result<Vec<String>, InterpError> {
let count = self.read_u32()? as usize; let count = self.read_u32()? as usize;
let mut items = Vec::with_capacity(count); let mut items = Vec::with_capacity(count);
@@ -118,16 +222,10 @@ impl<'a> Reader<'a> {
Ok(items) Ok(items)
} }
pub fn skip_value(&mut self, type_op: u8) -> Result<(), InterpError> { pub fn skip_value(&mut self, type_op: u8) -> Result<(), InterpError> {
match type_op { match type_op {
OP_PROP_STR OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_VAR | OP_PROP_RHEI | OP_PROP_IDENT => {
| OP_PROP_COLOR self.skip_string()?;
| OP_PROP_FSPATH
| OP_PROP_VAR
| OP_PROP_RHEI
| OP_PROP_IDENT => {
self.read_string()?;
} }
OP_PROP_INT => { self.read_i64()?; } OP_PROP_INT => { self.read_i64()?; }
OP_PROP_FLOAT => { self.read_f64()?; } OP_PROP_FLOAT => { self.read_f64()?; }
@@ -141,11 +239,11 @@ impl<'a> Reader<'a> {
} }
} }
OP_PROP_UNIT => { OP_PROP_UNIT => {
self.read_f64()?; // number self.read_f64()?;
self.read_string()?; // unit suffix self.skip_string()?;
} }
OP_PROP_CALL => { OP_PROP_CALL => {
self.read_string()?; // function name self.skip_string()?;
let arg_count = self.read_u32()?; let arg_count = self.read_u32()?;
for _ in 0..arg_count { for _ in 0..arg_count {
let op = self.read_byte()?; let op = self.read_byte()?;
@@ -157,49 +255,38 @@ impl<'a> Reader<'a> {
Ok(()) Ok(())
} }
/// Skip a full opcode + its payload without interpreting it.
pub fn skip_opcode(&mut self, op: u8) -> Result<(), InterpError> { pub fn skip_opcode(&mut self, op: u8) -> Result<(), InterpError> {
match op { match op {
OP_VERSION => { self.read_i64()?; } OP_VERSION => { self.read_i64()?; }
OP_STYLE | OP_RHEI_BLK => { self.read_string()?; } OP_STYLE | OP_RHEI_BLK => { self.skip_string()?; }
OP_PROP_STR OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_VAR | OP_PROP_RHEI |
| OP_PROP_COLOR OP_PROP_INT | OP_PROP_FLOAT | OP_PROP_BOOL | OP_PROP_NULL | OP_PROP_ARRAY |
| OP_PROP_FSPATH OP_PROP_CALL | OP_PROP_UNIT | OP_PROP_IDENT => {
| OP_PROP_VAR self.skip_string()?;
| OP_PROP_RHEI self.skip_value(op)?;
| OP_PROP_INT
| OP_PROP_FLOAT
| OP_PROP_BOOL
| OP_PROP_NULL
| OP_PROP_ARRAY
| OP_PROP_CALL
| OP_PROP_UNIT
| OP_PROP_IDENT => {
self.read_string()?; // key
self.skip_value(op)?; // value
} }
OP_GLOBAL | OP_LET => { OP_GLOBAL | OP_LET => {
self.read_string()?; self.skip_string()?;
let vop = self.read_byte()?; let vop = self.read_byte()?;
self.skip_value(vop)?; self.skip_value(vop)?;
} }
OP_SINGLETON => { OP_SINGLETON => {
self.read_string()?; self.skip_string()?;
let count = self.read_u32()?; let count = self.read_u32()?;
for _ in 0..count { for _ in 0..count {
self.read_string()?; self.skip_string()?;
let vop = self.read_byte()?; let vop = self.read_byte()?;
self.skip_value(vop)?; self.skip_value(vop)?;
} }
} }
OP_COMPONENT => { OP_COMPONENT => {
self.read_string()?; self.skip_string()?;
let params = self.read_u32()?; let params = self.read_u32()?;
for _ in 0..params { for _ in 0..params {
self.read_string()?; self.skip_string()?;
self.read_string()?; self.skip_string()?;
} }
self.skip_block()?; self.skip_block()?;
} }
@@ -210,50 +297,48 @@ impl<'a> Reader<'a> {
if self.read_byte()? == 1 { self.skip_block()?; } if self.read_byte()? == 1 { self.skip_block()?; }
} }
OP_EACH => { OP_EACH => {
self.read_string()?; self.skip_string()?;
let vop = self.read_byte()?; let vop = self.read_byte()?;
self.skip_value(vop)?; self.skip_value(vop)?;
self.skip_block()?; self.skip_block()?;
} }
OP_ON => { OP_ON => {
self.read_string()?; self.skip_string()?;
let args = self.read_u32()?; let args = self.read_u32()?;
for _ in 0..args { for _ in 0..args {
self.read_string()?; self.skip_string()?;
let vop = self.read_byte()?; let vop = self.read_byte()?;
self.skip_value(vop)?; self.skip_value(vop)?;
} }
self.skip_block()?; self.skip_block()?;
} }
OP_ELEM_PUSH => { self.read_string()?; } OP_ELEM_PUSH => { self.skip_string()?; }
OP_CONTENT => { OP_CONTENT => {
let vop = self.read_byte()?; let vop = self.read_byte()?;
self.skip_value(vop)?; self.skip_value(vop)?;
} }
OP_STYLE_RULE => { OP_STYLE_RULE => {
self.read_string()?; // selector self.skip_string()?;
let count = self.read_u32()?; let count = self.read_u32()?;
for _ in 0..count { for _ in 0..count {
self.read_string()?; // property key self.skip_string()?;
let vop = self.read_byte()?; let vop = self.read_byte()?;
self.skip_value(vop)?; // property value self.skip_value(vop)?;
} }
} }
OP_STYLE_ANIM => { OP_STYLE_ANIM => {
self.read_string()?; // animation name self.skip_string()?;
let frame_count = self.read_u32()?; let frame_count = self.read_u32()?;
for _ in 0..frame_count { for _ in 0..frame_count {
self.read_string()?; // step ("from", "to", "50%", …) self.skip_string()?;
let prop_count = self.read_u32()?; let prop_count = self.read_u32()?;
for _ in 0..prop_count { for _ in 0..prop_count {
self.read_string()?; self.skip_string()?;
let vop = self.read_byte()?; let vop = self.read_byte()?;
self.skip_value(vop)?; self.skip_value(vop)?;
} }
} }
} }
_ => {} _ => {}
} }
Ok(()) Ok(())
@@ -266,15 +351,4 @@ impl<'a> Reader<'a> {
self.skip_opcode(op)?; self.skip_opcode(op)?;
} }
} }
#[inline]
fn require(&self, n: usize) -> Result<(), InterpError> {
if self.remaining() < n {
Err(InterpError::UnexpectedEof)
} else {
Ok(())
}
}
} }

View File

@@ -1,16 +1,28 @@
use rhai::{Dynamic, Engine, Scope, AST}; use rhai::{Dynamic, Engine, Scope, AST, Module};
use std::collections::HashMap; use std::collections::HashMap;
use std::cell::RefCell;
use super::types::Value;
use compact_str::CompactString;
pub const RHEI_PREFIX: &str = "__rhei:"; pub const RHEI_PREFIX: &str = "__rhei:";
pub struct RheiContext { pub struct RheiContext {
engine: Engine, engine: Engine,
init_ast: AST, init_ast: AST,
fn_ast: AST, scope: RefCell<Scope<'static>>,
action_cache: RefCell<HashMap<String, AST>>,
expr_cache: RefCell<HashMap<String, AST>>,
} }
impl RheiContext { impl RheiContext {
pub fn new(scripts: &[String]) -> Self { 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(); let mut engine = Engine::new();
engine.on_print(|s| println!("[rhei] {s}")); engine.on_print(|s| println!("[rhei] {s}"));
@@ -29,118 +41,187 @@ impl RheiContext {
let mut fn_ast = combined.clone(); let mut fn_ast = combined.clone();
fn_ast.clear_statements(); fn_ast.clear_statements();
match Module::eval_ast_as_new(Scope::new(), &fn_ast, &engine) {
Ok(module) => {
engine.register_global_module(module.into());
}
Err(e) => {
eprintln!("⚠️ Rhei module creation error: {e}");
}
}
Self { Self {
engine, engine,
init_ast: combined, init_ast: combined,
fn_ast scope: RefCell::new(Scope::new()),
action_cache: RefCell::new(HashMap::new()),
expr_cache: RefCell::new(HashMap::new()),
} }
} }
pub fn initialize(&self, variables: &mut HashMap<String, String>) { pub fn sync_scope(&self, variables: &HashMap<String, Value>) {
let mut scope = Scope::new(); let mut scope = self.scope.borrow_mut();
for (k, v) in variables.iter() { for (k, v) in variables.iter() {
scope.push_dynamic(k.clone(), str_to_dyn(v)); if scope.contains(k) {
if let Some(old_val) = scope.get_value::<Dynamic>(k) {
if dynamic_to_value(&old_val) == *v {
continue;
} }
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &self.init_ast) {
eprintln!("⚠️ Rhei init error: {e}");
} }
scope.set_value(k, value_to_dynamic(v));
let names: Vec<String> = scope.iter_raw() } else {
.map(|(name, _, _)| name.to_string()) scope.push_dynamic(k.clone(), value_to_dynamic(v));
.collect();
for name in &names {
if let Some(val) = scope.get_value::<Dynamic>(name) {
variables.insert(name.clone(), dyn_to_str(&val));
} }
} }
} }
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> Option<String> { pub fn initialize(&self, variables: &mut HashMap<String, Value>) {
let mut scope = Scope::new(); self.sync_scope(variables);
for (k, v) in variables {
scope.push_dynamic(k.clone(), str_to_dyn(v)); let mut scope = self.scope.borrow_mut();
if let Err(e) = self.engine.run_ast_with_scope(&mut *scope, &self.init_ast) {
eprintln!("⚠️ Rhei initialization error: {e}");
} }
let expr_ast = self.engine for (name, _, val) in scope.iter_raw() {
.compile_expression(expr) let s_val = dynamic_to_value(&val);
.or_else(|_| self.engine.compile(expr)) if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
.ok()?; variables.insert(name.to_string(), s_val);
}
}
}
let full = self.fn_ast.merge(&expr_ast); 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_ast_with_scope::<Dynamic>(&mut scope, &full) { let ast = self.get_or_compile_expr(expr);
Ok(val) => Some(dyn_to_str(&val)), match ast {
Some(ast) => {
match self.engine.eval_ast_with_scope::<Dynamic>(&mut *scope, &ast) {
Ok(val) => dynamic_to_value(&val),
Err(e) => { Err(e) => {
eprintln!("⚠️ Rhei eval `{expr}`: {e}"); eprintln!("⚠️ Rhei eval_expr error: {e}");
Value::None
}
}
}
None => Value::None,
}
}
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, Value>) -> bool {
self.sync_scope(variables);
let mut scope = self.scope.borrow_mut();
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}");
false
}
}
}
None => false,
}
}
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, Value>) {
self.sync_scope(variables);
let mut scope = self.scope.borrow_mut();
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}");
}
}
for (name, _, val) in scope.iter_raw() {
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 None
} }
} }
} }
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool { fn get_or_compile_expr(&self, expr: &str) -> Option<AST> {
let mut scope = Scope::new(); let mut cache = self.expr_cache.borrow_mut();
for (k, v) in variables { if let Some(ast) = cache.get(expr) {
scope.push_dynamic(k.clone(), str_to_dyn(v)); return Some(ast.clone());
} }
match self.engine.compile_expression(expr) {
let ast = match self.engine.compile_expression(expr) { Ok(ast) => {
Ok(a) => a, cache.insert(expr.to_string(), ast.clone());
Err(_) => match self.engine.compile(expr) { Some(ast)
Ok(a) => a,
Err(e) => {
eprintln!("⚠️ Rhei condition compile `{expr}`: {e}");
return false;
}
},
};
let full = self.fn_ast.merge(&ast);
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
Ok(val) => {
if val.is_bool() { return val.cast::<bool>(); }
if val.is_int() { return val.cast::<i64>() != 0; }
if val.is_float(){ return val.cast::<f64>() != 0.0; }
if val.is_string(){
let s = val.cast::<String>();
return !matches!(s.trim(), "" | "false" | "0" | "null");
}
!val.is_unit()
} }
Err(e) => { Err(e) => {
eprintln!("⚠️ Rhei condition eval `{expr}`: {e}"); eprintln!("⚠️ Rhei expression compilation error: {e}");
false None
} }
} }
} }
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) { pub fn precompile_scripts(&self, scripts: &[String]) {
let mut scope = Scope::new(); for script in scripts {
for (k, v) in variables.iter() { self.get_or_compile_action(script);
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
match self.engine.compile(script) {
Ok(action_ast) => {
let full = self.fn_ast.merge(&action_ast);
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &full) {
eprintln!("⚠️ Rhei action execution error: {e}");
}
}
Err(e) => {
eprintln!("⚠️ Rhei action compilation error: {e}");
} }
} }
let names: Vec<String> = scope.iter_raw() pub fn precompile_actions(&self, actions: &[String]) {
.map(|(name, _, _)| name.to_string()) for action in actions {
.collect(); self.get_or_compile_action(action);
for name in &names {
if let Some(val) = scope.get_value::<Dynamic>(name) {
variables.insert(name.clone(), dyn_to_str(&val));
} }
} }
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);
}
} }
} }
@@ -150,20 +231,43 @@ impl Default for RheiContext {
} }
} }
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_dyn(s: &str) -> Dynamic { pub fn str_to_dynamic(s: &str) -> Dynamic {
if let Ok(i) = s.parse::<i64>() { return Dynamic::from(i); } 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(f) = s.parse::<f64>() { return Dynamic::from(f); }
if let Ok(b) = s.parse::<bool>() { return Dynamic::from(b); } if let Ok(b) = s.parse::<bool>() { return Dynamic::from(b); }
Dynamic::from(s.to_owned()) Dynamic::from(s.to_owned())
} }
pub fn dyn_to_str(val: &Dynamic) -> String { pub fn dynamic_to_value(d: &Dynamic) -> Value {
if val.is_string() { if d.is_string() {
return val.clone().cast::<String>(); return Value::Str(CompactString::new(d.clone().into_string().unwrap_or_default()));
} }
if val.is_unit() { if d.is_int() {
return String::new(); return Value::Int(d.as_int().unwrap_or(0));
} }
val.to_string() 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
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,327 @@
use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::ops::Range;
use super::reactive::{ElementId, ReactiveTracker};
use super::style::{ComputedStyle, StyleSheet}; use super::style::{ComputedStyle, StyleSheet};
use compact_str::CompactString;
#[derive(Debug, Clone)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Element { pub struct InternedStr(u32);
pub type_name: String,
pub properties: HashMap<String, String>, impl InternedStr {
pub children: Vec<Element>, pub const fn from_raw(id: u32) -> Self {
pub computed_style: ComputedStyle, Self(id)
}
pub fn raw(&self) -> u32 {
self.0
}
} }
impl Element { impl InternedStr {
pub fn new(type_name: String) -> Self { 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> {
pub type_name: &'a str,
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> {
pub fn id(&self) -> Option<&str> {
self.properties.iter().find_map(|(k, v)| {
if **k == *"id" { Some(v.as_ref()) } else { None }
})
}
pub fn new(type_name: &'a str) -> Self {
Self { Self {
type_name, type_name,
properties: HashMap::new(), properties: Vec::with_capacity(8),
children: Vec::new(), children: Vec::with_capacity(4),
computed_style: ComputedStyle::default(), 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,
} }
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ComponentDef { pub struct ComponentDef<'a> {
pub name: String, pub name: String,
pub params: Vec<(String, String)>, pub params: Vec<(String, String)>,
pub children: Vec<Element>, pub children: Vec<Element<'a>>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Document { pub struct Document<'a> {
pub roots: Vec<Element>, pub roots: Vec<Element<'a>>,
pub components: HashMap<String, ComponentDef>, pub components: HashMap<String, ComponentDef<'a>>,
pub variables: HashMap<String, String>, pub variables: HashMap<String, Value>,
pub rhei_scripts: Vec<String>, pub rhei_scripts: Vec<String>,
pub stylesheet: StyleSheet, 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)] #[derive(Debug)]

21
src/lib.rs Normal file
View 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),
}

View File

@@ -1,17 +1,11 @@
mod app; mod app;
mod cli; mod cli;
mod interpreter; mod interpreter;
mod perf;
mod renderer; mod renderer;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use glint_runtime::Message;
#[derive(Debug, Clone)]
pub enum Message {
EventTriggered(String),
InputChanged(Option<String>, String),
ToggleChanged(Option<String>, bool),
SliderChanged(Option<String>, f64),
}
#[derive(Parser)] #[derive(Parser)]
#[command(name = "glint-runtime")] #[command(name = "glint-runtime")]
@@ -37,6 +31,10 @@ enum Commands {
Run { Run {
/// Bytecode file to execute /// Bytecode file to execute
file: String, file: String,
/// Print per-frame performance metrics (VDOM/Style/Render timing)
#[arg(long)]
perf: bool,
}, },
} }
@@ -57,7 +55,10 @@ fn main() {
}); });
cli::compile_files(&gltm_files, &glts_files, &output_path); 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); cli::run_file(&file);
} }
} }

87
src/perf.rs Normal file
View 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();
}

File diff suppressed because it is too large Load Diff