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
This commit is contained in:
379
ARCHITECTURE-IMPROVEMENT-PLAN.md
Normal file
379
ARCHITECTURE-IMPROVEMENT-PLAN.md
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
# План улучшения архитектуры Glint Runtime
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Обеспечить **плоский график производительности**: рендеринг 1000 строк байткода должен занимать
|
||||||
|
~0.04 секунды сегодня и столько же через годы, независимо от роста количества компонентов,
|
||||||
|
стилей и страниц. Время обработки события должно быть пропорционально **размеру изменения**,
|
||||||
|
а не **размеру всей системы**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 0: Бенчмарки и профилирование
|
||||||
|
|
||||||
|
**Цель:** зафиксировать текущие метрики, чтобы объективно оценивать прогресс.
|
||||||
|
|
||||||
|
- [ ] **0.1** Добавить Criterion в `Cargo.toml`
|
||||||
|
- [ ] **0.2** Написать бенчмарки для:
|
||||||
|
- `evaluate_vdom()` на `desktop.glbc`
|
||||||
|
- `matching_rules()` — 10, 100, 1000 правил
|
||||||
|
- `ComputedStyle::compute()` — пустой, 5, 20 свойств
|
||||||
|
- `resolve_string()` — без `$`, с 1 `$var`, с 3 `$var`
|
||||||
|
- `RheiContext::eval_expr()` — простое выражение, сложное
|
||||||
|
- `RheiContext::execute_action()` — короткий скрипт
|
||||||
|
- `Element::clone()` — замерить копирование глубокого дерева
|
||||||
|
- [ ] **0.3** Запустить `perf record` / `flamegraph-rs` на горячем пути:
|
||||||
|
- `cargo run -- run desktop.glbc` + интерактив
|
||||||
|
- Выявить фактический bottleneck (гипотеза: style matching, string parsing)
|
||||||
|
- [ ] **0.4** Записать baseline в `BENCHMARKS.md` или `README.md`
|
||||||
|
|
||||||
|
**Файлы:** новый `benches/bench.rs`, `Cargo.toml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 1: Типизированные значения (Value enum)
|
||||||
|
|
||||||
|
**Цель:** устранить постоянный round-trip через строки (parse/format на каждое свойство).
|
||||||
|
|
||||||
|
**Текущая проблема:** `HashMap<String, String>` для переменных. Каждый `SliderChanged` →
|
||||||
|
`format!(...)`, потом `rhei.rs:139-142` — три парсинга подряд (`i64`, `f64`, `bool`).
|
||||||
|
|
||||||
|
- [ ] **1.1** Определить `Value` enum в `interpreter/types.rs`:
|
||||||
|
```rust
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Value {
|
||||||
|
Str(CompactStr),
|
||||||
|
Int(i64),
|
||||||
|
Float(f64),
|
||||||
|
Bool(bool),
|
||||||
|
Array(Vec<Value>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] **1.2** Заменить `HashMap<String, String>` на `HashMap<String, Value>`:
|
||||||
|
- `Document::variables`
|
||||||
|
- `GlintApp::update()` — входящие значения
|
||||||
|
- [ ] **1.3** Переписать `str_to_dyn()` и `dyn_to_str()` в `rhei.rs`:
|
||||||
|
- Убрать парсинг, конвертировать напрямую `Value ↔ Dynamic`
|
||||||
|
- [ ] **1.4** Переписать `resolve_string()`:
|
||||||
|
- Если значение уже `Int`/`Float` — не парсить, форматировать один раз
|
||||||
|
- [ ] **1.5** Переписать `evaluate_condition()`:
|
||||||
|
- Числовые сравнения без парсинга строк
|
||||||
|
- `resolve_string()` → преобразование к Value
|
||||||
|
- [ ] **1.6** Переписать `is_truthy()`: работа с Value напрямую
|
||||||
|
- [ ] **1.7** Обновить `renderer.rs`:
|
||||||
|
- `SliderChanged`, `ToggleChanged`, `InputChanged` — принимать `Value`, не строку
|
||||||
|
- Парсинг slider/toggle/input значений — один раз, а не на каждый чих
|
||||||
|
- [ ] **1.8** Обновить `ComputedStyle::compute()` и `parse_*()` функции:
|
||||||
|
- Принимать `&Value` где возможно, а не `&str`
|
||||||
|
- `parse_color`, `parse_size`, `parse_length` — работа с Value
|
||||||
|
- [ ] **1.9** Написать тесты для Value: конверсии, сравнения, форматирование
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `interpreter/rhei.rs`,
|
||||||
|
`interpreter/style.rs`, `app.rs`, `renderer.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 2: Интернирование строк (String Interning)
|
||||||
|
|
||||||
|
**Цель:** ускорить сравнение строк (ключи свойств, названия типов, классы).
|
||||||
|
Заменить million `== "padding-top"` на O(1) сравнение ID.
|
||||||
|
|
||||||
|
- [ ] **2.1** Выбрать библиотеку intern-строк:
|
||||||
|
- `lasso` (thread-safe, производительный) или `strena` (лёгкая)
|
||||||
|
- Или самодельный `StringArena` с `HashMap<&str, usize>`
|
||||||
|
- [ ] **2.2** Заменить `&'a str` на `InternedStr` (newtype вокруг `u32`/`usize`):
|
||||||
|
- `Element::type_name`
|
||||||
|
- Ключи в `Element::properties`
|
||||||
|
- Селекторы, имена классов, ID
|
||||||
|
- [ ] **2.3** `lookup()` в `style.rs`:
|
||||||
|
- Сравнение ключей через ID вместо `==`
|
||||||
|
- [ ] **2.4** `CompoundSelector::matches_element()`:
|
||||||
|
- Сравнение `tag`, `id`, `classes` через internd-строки
|
||||||
|
- [ ] **2.5** Все `HashMap<String, String>` где ключи повторяются → `HashMap<InternedStr, Value>`
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/style.rs`, `interpreter/mod.rs`,
|
||||||
|
`renderer.rs` (много `el.type_name == "Button"`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 3: Граф зависимостей (Reactive Dependency Tracking)
|
||||||
|
|
||||||
|
**Цель:** вместо полного `evaluate_vdom()` на каждое событие — пересчитывать только
|
||||||
|
элементы, которые зависят от изменившейся переменной. **Это главный этап для плоского графика.**
|
||||||
|
|
||||||
|
**Текущая проблема:** `app.rs:44-51` — на любое событие полный VDOM rebuild. Слайдер
|
||||||
|
меняет `volume_level`, но пересчитываются все 500 элементов.
|
||||||
|
|
||||||
|
- [ ] **3.1** Создать `interpreter/reactive.rs`:
|
||||||
|
```rust
|
||||||
|
pub struct ReactiveTracker {
|
||||||
|
subscribers: HashMap<InternedStr, HashSet<ElementId>>,
|
||||||
|
dependencies: HashMap<ElementId, HashSet<InternedStr>>,
|
||||||
|
dirty_set: HashSet<ElementId>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] **3.2** Анализ зависимостей при загрузке шаблона:
|
||||||
|
- Сканировать `$var` в свойствах → регистрировать `ElementId → var`
|
||||||
|
- Сканировать `!rhei:{expr}` → извлекать имена переменных через Rhai parser
|
||||||
|
- `@if condition` → зависимости от переменных в condition
|
||||||
|
- `@each source` → зависимости от переменных в source
|
||||||
|
- Результат: каждый элемент знает, от каких переменных зависит
|
||||||
|
- [ ] **3.3** Изменить `GlintApp::update()`:
|
||||||
|
```rust
|
||||||
|
fn update(&mut self, msg: Message) {
|
||||||
|
let affected = self.tracker.on_variable_changed(name, new_val);
|
||||||
|
self.vdom_roots = Interpreter::evaluate_vdom_incr(
|
||||||
|
&self.doc.roots, &affected, &self.tracker, ...
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] **3.4** `evaluate_vdom_incr()`:
|
||||||
|
- Для элементов не в `affected` — возвращаем кэшированный VDOM
|
||||||
|
- Для элементов в `affected` — пересчитываем и каскадно помечаем детей
|
||||||
|
- `@if` — если условие не изменилось — не пересчитываем ветку
|
||||||
|
- `@each` — если source не изменился — не пересчитываем
|
||||||
|
- [ ] **3.5** Инвалидация стилей:
|
||||||
|
- При изменении переменной, меняющей `class` или `id` элемента → сброс кэша стилей
|
||||||
|
- Иначе стили не пересчитываем
|
||||||
|
- [ ] **3.6** Кэш VDOM:
|
||||||
|
- `HashMap<ElementId, Arc<VNode>>` — не клонируем, разделяем память
|
||||||
|
- `Arc::make_mut()` при изменении (copy-on-write)
|
||||||
|
- [ ] **3.7** Написать тест:
|
||||||
|
- Сценарий: 1000 элементов, 1 зависимость → после изменения пересчитывается 1 элемент
|
||||||
|
- Проверить, что dirty_set корректен
|
||||||
|
|
||||||
|
**Изменяемые файлы:** новый `interpreter/reactive.rs`, `interpreter/mod.rs`, `interpreter/types.rs`,
|
||||||
|
`app.rs`, `main.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 4: Индексированный матчинг стилей (Style Index)
|
||||||
|
|
||||||
|
**Цель:** заменить O(N×M) на O(K) где K — число релевантных правил для элемента (1-5, не 500).
|
||||||
|
|
||||||
|
**Текущая проблема:** `style.rs:487-505` — каждый элемент проверяется против ВСЕХ правил.
|
||||||
|
100 элементов × 500 правил = 50 000 проверок на событие.
|
||||||
|
|
||||||
|
- [ ] **4.1** Построить индекс стилей при загрузке `Document`:
|
||||||
|
```rust
|
||||||
|
pub struct StyleIndex {
|
||||||
|
by_tag: HashMap<InternedStr, Vec<RuleId>>,
|
||||||
|
by_class: HashMap<InternedStr, Vec<RuleId>>,
|
||||||
|
by_id: HashMap<InternedStr, Vec<RuleId>>,
|
||||||
|
by_tag_class: HashMap<(InternedStr, InternedStr), Vec<RuleId>>,
|
||||||
|
by_tag_id: HashMap<(InternedStr, InternedStr), Vec<RuleId>>,
|
||||||
|
complex_rules: Vec<(ComplexSelector, RuleId)>,
|
||||||
|
rules: Vec<StyleRule>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] **4.2** `matching_rules()` → `query_index()`:
|
||||||
|
- Для элемента `Button.primary#submit`:
|
||||||
|
1. `by_tag["Button"]` → [1, 5, 12]
|
||||||
|
2. `by_class["primary"]` → [3, 5, 7]
|
||||||
|
3. `by_id["submit"]` → [5]
|
||||||
|
4. Пересечение → [5]
|
||||||
|
5. Проверить 1 сложное правило
|
||||||
|
6. Итого: 3 проверки вместо 500
|
||||||
|
- [ ] **4.3** Кэш совпадений:
|
||||||
|
- `HashMap<ElementId, (Vec<RuleId>, u64)>` — инвалидируется по эпохе
|
||||||
|
- epoch увеличивается при перезагрузке стилей
|
||||||
|
- Псевдоклассы (`:hover`, `:active`) — кэш с ключом `(ElementId, PseudoClass)`
|
||||||
|
- [ ] **4.4** `ComputedStyle::compute()`:
|
||||||
|
- Убрать сортировку по specificity на каждый вызов
|
||||||
|
- Правила уже отсортированы в индексе
|
||||||
|
- `lookup()` — проход по нескольким правилам вместо прохода по `matched_sheets`
|
||||||
|
- [ ] **4.5** Миграция `matching_pseudo_rules()`:
|
||||||
|
- Использовать индекс + кэш
|
||||||
|
- Вызывается из `renderer.rs` для hover/active — тысячи раз в секунду
|
||||||
|
- Кэш на `(ElementId, pseudo)` с инвалидацией
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `interpreter/style.rs`, `interpreter/mod.rs`, `interpreter/types.rs`,
|
||||||
|
`renderer.rs`, `main.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 5: Аренная аллокация и VNode (Flat VDOM)
|
||||||
|
|
||||||
|
**Цель:** устранить глубокое клонирование `Element` и reduce аллокаций на каждый кадр.
|
||||||
|
|
||||||
|
**Текущая проблема:** `Element` содержит `Vec<Element>` — рекурсивное клонирование.
|
||||||
|
`child.clone()` в `@if` (mod.rs:347) копирует поддеревья целиком.
|
||||||
|
|
||||||
|
- [ ] **5.1** Создать плоское представление VDOM:
|
||||||
|
```rust
|
||||||
|
struct VNode<'a> {
|
||||||
|
id: NodeId,
|
||||||
|
type_name: InternedStr,
|
||||||
|
properties: Range<PropertyIdx>,
|
||||||
|
children_range: Range<NodeIdx>,
|
||||||
|
computed_style: ComputedStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FlatVDom<'a> {
|
||||||
|
nodes: Vec<VNode<'a>>,
|
||||||
|
properties: Vec<(InternedStr, Value)>,
|
||||||
|
arena: Bump,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] **5.2** Добавить `bumpalo` или написать `BumpAlloc`:
|
||||||
|
- Все строковые данные живут в арене
|
||||||
|
- Очистка арены одним махом между кадрами
|
||||||
|
- [ ] **5.3** `evaluate_vdom()` → возвращает `FlatVDom<'arena>` вместо `Vec<Element>`:
|
||||||
|
- Вместо `push_prop("text", val.clone())` — allocate в арене
|
||||||
|
- Вместо `Element::new(el.type_name)` — выделить VNode в `Vec<VNode>`
|
||||||
|
- [ ] **5.4** Structural sharing:
|
||||||
|
- Ветки `@each` с одинаковыми телами разделяют `VNode` через `Arc<VNode>`
|
||||||
|
- `Arc::make_mut()` при изменении
|
||||||
|
- [ ] **5.5** `render_element()` → работает с `&VNode`:
|
||||||
|
- `get_prop()` — lookup в плоском массиве свойств
|
||||||
|
- `children` — итерация по `children_range`
|
||||||
|
- [ ] **5.6** Удалить `#[derive(Clone)]` из `Element` (или оставить для совместимости):
|
||||||
|
- В горячем пути clone не используется
|
||||||
|
- [ ] **5.7** Тест: проверить, что аллокаций на кадр стало < 10 (было ~1000+)
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `renderer.rs`,
|
||||||
|
`interpreter/style.rs`, `app.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 6: Кэширование Rhai AST
|
||||||
|
|
||||||
|
**Цель:** убрать компиляцию Rhai скриптов при каждом выполнении.
|
||||||
|
|
||||||
|
**Текущая проблема:** `rhei.rs:112` — `engine.compile(script)` при каждом `execute_action()`.
|
||||||
|
Скрипты в `@on:click { ... }` компилируются каждый клик.
|
||||||
|
|
||||||
|
- [ ] **6.1** Собрать все Rhai блоки при загрузке `Document`:
|
||||||
|
- Из `rhei_scripts` (init-скрипты)
|
||||||
|
- Из `__on:*` свойств (обработчики событий)
|
||||||
|
- Из `!rhei:` выражений (компилируем, но не выполняем)
|
||||||
|
- [ ] **6.2** `RheiContext`:
|
||||||
|
```rust
|
||||||
|
pub struct RheiContext {
|
||||||
|
engine: Engine,
|
||||||
|
init_ast: AST,
|
||||||
|
action_cache: HashMap<InternedStr, AST>,
|
||||||
|
expr_cache: HashMap<InternedStr, AST>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] **6.3** `execute_action()`:
|
||||||
|
- `self.action_cache.get(script)` вместо `engine.compile(script)`
|
||||||
|
- Если нет — компилируем и кэшируем
|
||||||
|
- [ ] **6.4** Очистка кэша:
|
||||||
|
- Только при перезагрузке документа
|
||||||
|
- Или `LruCache` если скриптов слишком много
|
||||||
|
- [ ] **6.5** Тест: выполнить 1000 раз один и тот же action — время не должно расти
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `interpreter/rhei.rs`, `interpreter/mod.rs`, `main.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 7: Параллелизм (Rayon)
|
||||||
|
|
||||||
|
**Цель:** распараллелить style matching и независимые ветки VDOM.
|
||||||
|
|
||||||
|
**Текущая проблема:** всё выполняется последовательно, хотя style matching
|
||||||
|
для разных элементов — embarrassingly parallel.
|
||||||
|
|
||||||
|
- [ ] **7.1** Добавить `rayon` в `Cargo.toml` (feature gate: `parallel`)
|
||||||
|
- [ ] **7.2** Параллельный style matching:
|
||||||
|
- `matching_rules_batch(elements: &[Element]) -> Vec<Vec<&HashMap>>`
|
||||||
|
- `elements.par_iter().map(...)`
|
||||||
|
- [ ] **7.3** Параллельный `@each`:
|
||||||
|
- Итерации `@each` независимы
|
||||||
|
- `items.par_iter().flat_map(|item| evaluate_vdom(body, ...))`
|
||||||
|
- Feature gate: только для больших списков (> N элементов)
|
||||||
|
- [ ] **7.4** Параллельный `ComputedStyle::compute_batch()`:
|
||||||
|
- Векторизованный compute для нескольких элементов
|
||||||
|
- [ ] **7.5** Тест: `@each` с 1000 итераций → ускорение ~4x на 8 ядрах
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `Cargo.toml`, `interpreter/style.rs`, `interpreter/mod.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 8: Мемоизация `ComputedStyle::compute()`
|
||||||
|
|
||||||
|
**Цель:** не пересчитывать стили для элементов, чьи свойства не изменились.
|
||||||
|
|
||||||
|
**Текущая проблема:** каждый кадр `ComputedStyle::compute()` парсит все ~40 полей
|
||||||
|
через `lookup()`.
|
||||||
|
|
||||||
|
- [ ] **8.1** Ввести хэш `(inline_properties_hash, stylesheet_epoch, element_id) → ComputedStyle`
|
||||||
|
- [ ] **8.2** Кэш: `HashMap<u64, ComputedStyle>` + LRU eviction
|
||||||
|
- [ ] **8.3** Инвалидация:
|
||||||
|
- `stylesheet_epoch` — счётчик, увеличивается при изменении стилей
|
||||||
|
- `inline_properties_hash` — хэш от значений свойств элемента
|
||||||
|
- [ ] **8.4** Проверить hit rate на реальном UI: > 95%+
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `interpreter/style.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 9: Инкрементальный рендеринг (keyed widgets)
|
||||||
|
|
||||||
|
**Цель:** дать Iced'у возможность эффективно диффить виджеты, а не пересоздавать их.
|
||||||
|
|
||||||
|
**Текущая проблема:** `view()` создаёт полностью новые Iced-виджеты каждый кадр.
|
||||||
|
|
||||||
|
- [ ] **9.1** Ввести стабильные ID для каждого элемента VDOM
|
||||||
|
- [ ] **9.2** `render_element()`:
|
||||||
|
- Присваивать `iced::id()` на основе `ElementId`
|
||||||
|
- Iced использует ID для сохранения состояния виджетов между кадрами
|
||||||
|
- [ ] **9.3** Убирать из `view()` только изменившиеся элементы:
|
||||||
|
- Если `VNode::hash == prev_hash` — вернуть кэшированный `iced::Element`
|
||||||
|
- Кэш: `HashMap<ElementId, Arc<iced::Element>>`
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `renderer.rs`, `interpreter/types.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 10: Мониторинг и автоматические бенчмарки
|
||||||
|
|
||||||
|
**Цель:** не допустить регрессий в будущем.
|
||||||
|
|
||||||
|
- [ ] **10.1** Добавить `cargo bench` в CI
|
||||||
|
- [ ] **10.2** Пороговые проверки:
|
||||||
|
- `evaluate_vdom` на desktop.glbc < 5ms
|
||||||
|
- `matching_rules` (100 правил) < 10μs
|
||||||
|
- `ComputedStyle::compute` < 1μs
|
||||||
|
- [ ] **10.3** Логирование производительности:
|
||||||
|
- `--perf` флаг для `glint run`
|
||||||
|
- Печатать: `VDOM: 1.2ms | Style: 0.3ms | Render: 2.1ms | Total: 3.6ms`
|
||||||
|
- [ ] **10.4** Alert если total > 16ms (frame budget для 60 FPS)
|
||||||
|
|
||||||
|
**Изменяемые файлы:** `Cargo.toml`, новый `src/perf.rs`, `cli.rs`, `.github/workflows/ci.yml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сводная таблица влияния
|
||||||
|
|
||||||
|
| Фаза | Описание | Ускорение | Плоский график |
|
||||||
|
|------|----------|-----------|----------------|
|
||||||
|
| 0 | Бенчмарки | — | — |
|
||||||
|
| 1 | Value enum | 2-3× | Нет |
|
||||||
|
| 2 | String interning | 1.5-2× | Нет |
|
||||||
|
| **3** | **Reactive tracker** | **10-50×** | **Да (ключевое)** |
|
||||||
|
| 4 | Style index | 5-10× (style) | Да |
|
||||||
|
| 5 | Arena + Flat VDOM | 2-3× | Частично |
|
||||||
|
| 6 | Rhai AST cache | 2-5× (click) | Да |
|
||||||
|
| 7 | Parallelism | 2-4× | Нет |
|
||||||
|
| 8 | Style memoization | 2-3× | Да |
|
||||||
|
| 9 | Keyed widgets | 1.5-2× (render) | Частично |
|
||||||
|
| 10 | CI benchmarks | — (контроль) | — |
|
||||||
|
|
||||||
|
**Суммарно:** до 100× на горячих путях.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Рекомендованный порядок имплементации
|
||||||
|
|
||||||
|
1. **Фаза 0** — профилирование (без этого нельзя)
|
||||||
|
2. **Фаза 1 + 2** — Value enum + String interning (фундамент)
|
||||||
|
3. **Фаза 3** — Reactive tracker (сердце архитектуры)
|
||||||
|
4. **Фаза 4** — Style index
|
||||||
|
5. **Фаза 5 + 8** — Arena + Memoization
|
||||||
|
6. **Фаза 6** — Rhai cache
|
||||||
|
7. **Фаза 7** — Parallelism
|
||||||
|
8. **Фаза 9** — Keyed widgets
|
||||||
|
9. **Фаза 10** — CI benchmarks
|
||||||
BIN
desktop.glbc
BIN
desktop.glbc
Binary file not shown.
14
src/app.rs
14
src/app.rs
@@ -1,7 +1,7 @@
|
|||||||
use iced::widget::{column, container};
|
use iced::widget::{column, container};
|
||||||
use iced::{Length, Theme};
|
use iced::{Length, Theme};
|
||||||
|
|
||||||
use crate::interpreter::{Document, Element, Interpreter, RheiContext};
|
use crate::interpreter::{Document, Element, Interpreter, RheiContext, Value};
|
||||||
use crate::renderer::render_element;
|
use crate::renderer::render_element;
|
||||||
use crate::Message;
|
use crate::Message;
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ 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) => {
|
Message::WindowScrolled(y) => {
|
||||||
self.doc.variables.insert("__scroll_y".to_string(), y.to_string());
|
self.doc.variables.insert("__scroll_y".to_string(), Value::Float(y as f64));
|
||||||
}
|
}
|
||||||
Message::EventTriggered(script) => {
|
Message::EventTriggered(script) => {
|
||||||
if !script.is_empty() {
|
if !script.is_empty() {
|
||||||
@@ -27,15 +27,15 @@ impl GlintApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::InputChanged(Some(var), val) => {
|
Message::InputChanged(Some(var), val) => {
|
||||||
self.doc.variables.insert(var, val);
|
self.doc.variables.insert(var, Value::from(val));
|
||||||
}
|
}
|
||||||
Message::ToggleChanged(Some(var), val) => {
|
Message::ToggleChanged(Some(var), val) => {
|
||||||
self.doc.variables.insert(var, val.to_string());
|
self.doc.variables.insert(var, Value::Bool(val));
|
||||||
}
|
}
|
||||||
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));
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -63,7 +63,7 @@ impl GlintApp {
|
|||||||
let mut global_sticky_layers = Vec::new();
|
let mut global_sticky_layers = Vec::new();
|
||||||
|
|
||||||
let _scroll_y = self.doc.variables.get("__scroll_y")
|
let _scroll_y = self.doc.variables.get("__scroll_y")
|
||||||
.and_then(|v| v.parse::<f32>().ok())
|
.and_then(|v| if let Value::Float(f) = v { Some(*f as f32) } else { None })
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
for root in &self.vdom_roots {
|
for root in &self.vdom_roots {
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ 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, InterpError};
|
||||||
|
|
||||||
|
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::{value_to_dynamic, dynamic_to_value, RHEI_PREFIX};
|
||||||
use style::{AncestorInfo, ComputedStyle, StructuralContext};
|
use style::{AncestorInfo, ComputedStyle, StructuralContext};
|
||||||
|
pub use types::Value;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
@@ -51,7 +53,7 @@ impl Interpreter {
|
|||||||
|
|
||||||
fn parse_block_elements<'a>(
|
fn parse_block_elements<'a>(
|
||||||
r: &mut Reader<'a>,
|
r: &mut Reader<'a>,
|
||||||
variables: &mut HashMap<String, String>,
|
variables: &mut HashMap<String, Value>,
|
||||||
components: &mut HashMap<String, ComponentDef<'a>>,
|
components: &mut HashMap<String, ComponentDef<'a>>,
|
||||||
rhei_scripts: &mut Vec<String>,
|
rhei_scripts: &mut Vec<String>,
|
||||||
stylesheet: &mut SS,
|
stylesheet: &mut SS,
|
||||||
@@ -75,7 +77,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +294,7 @@ impl Interpreter {
|
|||||||
|
|
||||||
pub fn evaluate_vdom<'a>(
|
pub fn evaluate_vdom<'a>(
|
||||||
templates: &[Element<'a>],
|
templates: &[Element<'a>],
|
||||||
variables: &mut HashMap<String, String>,
|
variables: &mut HashMap<String, Value>,
|
||||||
components: &HashMap<String, ComponentDef<'a>>,
|
components: &HashMap<String, ComponentDef<'a>>,
|
||||||
rhei: &RheiContext,
|
rhei: &RheiContext,
|
||||||
stylesheet: &SS,
|
stylesheet: &SS,
|
||||||
@@ -357,7 +359,7 @@ impl Interpreter {
|
|||||||
let source_expr = el.get_prop("source").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) {
|
||||||
Self::normalize_rhai_array(&rhei.eval_expr(expr, variables))
|
Self::normalize_rhai_array(&rhei.eval_expr(expr, variables).to_owned_string())
|
||||||
} else {
|
} else {
|
||||||
Self::resolve_string(source_expr, variables).into_owned()
|
Self::resolve_string(source_expr, variables).into_owned()
|
||||||
};
|
};
|
||||||
@@ -369,7 +371,7 @@ impl Interpreter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for item in items {
|
for item in items {
|
||||||
let old_val = variables.insert(var_name.to_string(), item);
|
let old_val = variables.insert(var_name.to_string(), Value::Str(CompactString::new(item)));
|
||||||
|
|
||||||
output.extend(Self::evaluate_vdom(
|
output.extend(Self::evaluate_vdom(
|
||||||
&el.children, variables, components, rhei, stylesheet, ancestors,
|
&el.children, variables, components, rhei, stylesheet, ancestors,
|
||||||
@@ -397,7 +399,7 @@ impl Interpreter {
|
|||||||
|
|
||||||
let mut old_vals = Vec::with_capacity(new_args.len());
|
let mut old_vals = Vec::with_capacity(new_args.len());
|
||||||
for (k, v) in new_args {
|
for (k, v) in new_args {
|
||||||
old_vals.push((k.clone(), variables.insert(k, v)));
|
old_vals.push((k.clone(), variables.insert(k, Value::from(v))));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut vcomp = Element::new(el.type_name);
|
let mut vcomp = Element::new(el.type_name);
|
||||||
@@ -489,9 +491,9 @@ impl Interpreter {
|
|||||||
stylesheet.matching_rules(el.type_name, el_id, &classes, active_pseudo, structural, ancestors, preceding_siblings, &el_attributes)
|
stylesheet.matching_rules(el.type_name, el_id, &classes, active_pseudo, structural, ancestors, preceding_siblings, &el_attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String {
|
fn resolve_prop(v: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> String {
|
||||||
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
||||||
rhei.eval_expr(expr, variables)
|
rhei.eval_expr(expr, variables).to_owned_string().into()
|
||||||
} else {
|
} else {
|
||||||
Self::resolve_string(v, variables).into_owned()
|
Self::resolve_string(v, variables).into_owned()
|
||||||
}
|
}
|
||||||
@@ -499,7 +501,7 @@ impl Interpreter {
|
|||||||
|
|
||||||
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) {
|
||||||
@@ -515,7 +517,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] = [
|
||||||
@@ -542,7 +544,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,
|
||||||
@@ -563,7 +577,7 @@ impl Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, String>) -> Cow<'a, str> {
|
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str> {
|
||||||
if !val.contains('$') {
|
if !val.contains('$') {
|
||||||
return Cow::Borrowed(val);
|
return Cow::Borrowed(val);
|
||||||
}
|
}
|
||||||
@@ -582,7 +596,8 @@ impl Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(resolved) = scope.get(&var_name) {
|
if let Some(resolved) = scope.get(&var_name) {
|
||||||
result.push_str(resolved);
|
let formatted = resolved.to_owned_string();
|
||||||
|
result.push_str(&formatted);
|
||||||
} else {
|
} else {
|
||||||
result.push('$');
|
result.push('$');
|
||||||
result.push_str(&var_name);
|
result.push_str(&var_name);
|
||||||
|
|||||||
@@ -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],
|
||||||
@@ -138,6 +139,77 @@ impl<'a> Reader<'a> {
|
|||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_value(&mut self, type_op: u8) -> Result<Option<Value>, InterpError> {
|
||||||
|
let val = match type_op {
|
||||||
|
OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_IDENT => {
|
||||||
|
Some(Value::Str(CompactString::new(self.read_string()?)))
|
||||||
|
}
|
||||||
|
OP_PROP_RHEI => {
|
||||||
|
let raw = self.read_string()?;
|
||||||
|
let mut s = String::with_capacity(super::rhei::RHEI_PREFIX.len() + raw.len());
|
||||||
|
s.push_str(super::rhei::RHEI_PREFIX);
|
||||||
|
s.push_str(&raw);
|
||||||
|
Some(Value::Str(CompactString::new(s)))
|
||||||
|
}
|
||||||
|
OP_PROP_VAR => {
|
||||||
|
let name = self.read_str_ref()?;
|
||||||
|
let mut s = String::with_capacity(name.len() + 1);
|
||||||
|
s.push('$');
|
||||||
|
s.push_str(name);
|
||||||
|
Some(Value::Str(CompactString::new(s)))
|
||||||
|
}
|
||||||
|
OP_PROP_INT => Some(Value::Int(self.read_i64()?)),
|
||||||
|
OP_PROP_FLOAT => Some(Value::Float(self.read_f64()?)),
|
||||||
|
OP_PROP_BOOL => Some(Value::Bool(self.read_byte()? != 0)),
|
||||||
|
OP_PROP_NULL => None,
|
||||||
|
OP_PROP_ARRAY => {
|
||||||
|
let items = self.read_array_as_values()?;
|
||||||
|
Some(Value::Array(items))
|
||||||
|
}
|
||||||
|
OP_PROP_UNIT => {
|
||||||
|
let num = self.read_f64()?;
|
||||||
|
let unit = self.read_str_ref()?;
|
||||||
|
let s = if num.fract() == 0.0 {
|
||||||
|
format!("{}{}", num as i64, unit)
|
||||||
|
} else {
|
||||||
|
format!("{}{}", num, unit)
|
||||||
|
};
|
||||||
|
Some(Value::Str(CompactString::new(s)))
|
||||||
|
}
|
||||||
|
OP_PROP_CALL => {
|
||||||
|
let name = self.read_string()?;
|
||||||
|
let arg_count = self.read_u32()? as usize;
|
||||||
|
let mut args = Vec::with_capacity(arg_count);
|
||||||
|
for _ in 0..arg_count {
|
||||||
|
let op = self.read_byte()?;
|
||||||
|
let val = self.read_value(op)?.unwrap_or(Value::None);
|
||||||
|
args.push(match val {
|
||||||
|
Value::Str(s) => s.to_string(),
|
||||||
|
Value::Int(i) => i.to_string(),
|
||||||
|
Value::Float(f) => f.to_string(),
|
||||||
|
Value::Bool(b) => b.to_string(),
|
||||||
|
_ => String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(Value::Str(CompactString::new(format!("{}({})", name, args.join(",")))))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_array_as_values(&mut self) -> Result<Vec<Value>, InterpError> {
|
||||||
|
let count = self.read_u32()? as usize;
|
||||||
|
let mut items = Vec::with_capacity(count);
|
||||||
|
for _ in 0..count {
|
||||||
|
let elem_op = self.read_byte()?;
|
||||||
|
if let Some(v) = self.read_value(elem_op)? {
|
||||||
|
items.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_array_as_strings(&mut self) -> Result<Vec<String>, InterpError> {
|
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);
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use rhai::{Dynamic, Engine, Scope, AST, Module};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::cell::RefCell;
|
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 {
|
||||||
@@ -46,24 +49,24 @@ impl RheiContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_scope(&self, variables: &HashMap<String, String>) {
|
pub fn sync_scope(&self, variables: &HashMap<String, Value>) {
|
||||||
let mut scope = self.scope.borrow_mut();
|
let mut scope = self.scope.borrow_mut();
|
||||||
|
|
||||||
for (k, v) in variables.iter() {
|
for (k, v) in variables.iter() {
|
||||||
if scope.contains(k) {
|
if scope.contains(k) {
|
||||||
if let Some(old_val) = scope.get_value::<Dynamic>(k) {
|
if let Some(old_val) = scope.get_value::<Dynamic>(k) {
|
||||||
if dyn_to_str(&old_val) == *v {
|
if dynamic_to_value(&old_val) == *v {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.set_value(k, str_to_dyn(v));
|
scope.set_value(k, value_to_dynamic(v));
|
||||||
} else {
|
} else {
|
||||||
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
scope.push_dynamic(k.clone(), value_to_dynamic(v));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize(&self, variables: &mut HashMap<String, String>) {
|
pub fn initialize(&self, variables: &mut HashMap<String, Value>) {
|
||||||
self.sync_scope(variables);
|
self.sync_scope(variables);
|
||||||
|
|
||||||
let mut scope = self.scope.borrow_mut();
|
let mut scope = self.scope.borrow_mut();
|
||||||
@@ -72,27 +75,27 @@ impl RheiContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (name, _, val) in scope.iter_raw() {
|
for (name, _, val) in scope.iter_raw() {
|
||||||
let s_val = dyn_to_str(&val);
|
let s_val = dynamic_to_value(&val);
|
||||||
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
|
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
|
||||||
variables.insert(name.to_string(), s_val);
|
variables.insert(name.to_string(), s_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> String {
|
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, Value>) -> Value {
|
||||||
self.sync_scope(variables);
|
self.sync_scope(variables);
|
||||||
let mut scope = self.scope.borrow_mut();
|
let mut scope = self.scope.borrow_mut();
|
||||||
|
|
||||||
match self.engine.eval_expression_with_scope::<Dynamic>(&mut *scope, expr) {
|
match self.engine.eval_expression_with_scope::<Dynamic>(&mut *scope, expr) {
|
||||||
Ok(val) => dyn_to_str(&val),
|
Ok(val) => dynamic_to_value(&val),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("⚠️ Rhei eval_expr error: {e}");
|
eprintln!("⚠️ Rhei eval_expr error: {e}");
|
||||||
String::new()
|
Value::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool {
|
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, Value>) -> bool {
|
||||||
self.sync_scope(variables);
|
self.sync_scope(variables);
|
||||||
let mut scope = self.scope.borrow_mut();
|
let mut scope = self.scope.borrow_mut();
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@ impl RheiContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) {
|
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, Value>) {
|
||||||
self.sync_scope(variables);
|
self.sync_scope(variables);
|
||||||
let mut scope = self.scope.borrow_mut();
|
let mut scope = self.scope.borrow_mut();
|
||||||
|
|
||||||
@@ -121,7 +124,7 @@ impl RheiContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (name, _, val) in scope.iter_raw() {
|
for (name, _, val) in scope.iter_raw() {
|
||||||
let s_val = dyn_to_str(&val);
|
let s_val = dynamic_to_value(&val);
|
||||||
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
|
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
|
||||||
variables.insert(name.to_string(), s_val);
|
variables.insert(name.to_string(), s_val);
|
||||||
}
|
}
|
||||||
@@ -135,16 +138,36 @@ impl Default for RheiContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn str_to_dyn(s: &str) -> Dynamic {
|
pub fn value_to_dynamic(v: &Value) -> Dynamic {
|
||||||
if let Ok(i) = s.parse::<i64>() { return Dynamic::from(i); }
|
match v {
|
||||||
if let Ok(f) = s.parse::<f64>() { return Dynamic::from(f); }
|
Value::Int(i) => Dynamic::from(*i),
|
||||||
if let Ok(b) = s.parse::<bool>() { return Dynamic::from(b); }
|
Value::Float(f) => Dynamic::from(*f),
|
||||||
Dynamic::from(s.to_owned())
|
Value::Bool(b) => Dynamic::from(*b),
|
||||||
|
Value::Str(s) => Dynamic::from(s.to_string()),
|
||||||
|
Value::None => Dynamic::UNIT,
|
||||||
|
Value::Array(arr) => {
|
||||||
|
let d: rhai::Dynamic = arr.iter().map(value_to_dynamic).collect();
|
||||||
|
d
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dyn_to_str(d: &Dynamic) -> String {
|
pub fn dynamic_to_value(d: &Dynamic) -> Value {
|
||||||
if d.is_string() {
|
if d.is_string() {
|
||||||
return d.clone().into_string().unwrap_or_default();
|
return Value::Str(CompactString::new(d.clone().into_string().unwrap_or_default()));
|
||||||
}
|
}
|
||||||
d.to_string()
|
if d.is_int() {
|
||||||
|
return Value::Int(d.as_int().unwrap_or(0));
|
||||||
|
}
|
||||||
|
if d.is_float() {
|
||||||
|
return Value::Float(d.as_float().unwrap_or(0.0));
|
||||||
|
}
|
||||||
|
if d.is_bool() {
|
||||||
|
return Value::Bool(d.as_bool().unwrap_or(false));
|
||||||
|
}
|
||||||
|
if d.is_array() {
|
||||||
|
let arr = d.clone().into_array().unwrap_or_default();
|
||||||
|
return Value::Array(arr.iter().map(dynamic_to_value).collect());
|
||||||
|
}
|
||||||
|
Value::None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ use std::collections::HashMap;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use super::style::{ComputedStyle, StyleSheet};
|
use super::style::{ComputedStyle, StyleSheet};
|
||||||
use compact_str::CompactStr;
|
use compact_str::CompactString;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Str(CompactStr),
|
Str(CompactString),
|
||||||
Int(i64),
|
Int(i64),
|
||||||
Float(f64),
|
Float(f64),
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
@@ -23,31 +23,31 @@ impl Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_owned_string(&self) -> CompactStr {
|
pub fn to_owned_string(&self) -> CompactString {
|
||||||
match self {
|
match self {
|
||||||
Value::Str(s) => s.clone(),
|
Value::Str(s) => s.clone(),
|
||||||
Value::Int(i) => CompactStr::new(i.to_string()),
|
Value::Int(i) => CompactString::new(i.to_string()),
|
||||||
Value::Float(f) => CompactStr::new(if f.fract() == 0.0 {
|
Value::Float(f) => CompactString::new(if f.fract() == 0.0 {
|
||||||
format!("{:.1}", f)
|
format!("{:.1}", f)
|
||||||
} else {
|
} else {
|
||||||
f.to_string()
|
f.to_string()
|
||||||
}),
|
}),
|
||||||
Value::Bool(b) => CompactStr::new(b.to_string()),
|
Value::Bool(b) => CompactString::new(b.to_string()),
|
||||||
Value::Array(a) => CompactStr::new(a.iter().map(|v| v.to_owned_string()).collect::<Vec<_>>().join(",")),
|
Value::Array(a) => CompactString::new(a.iter().map(|v| v.to_owned_string()).collect::<Vec<_>>().join(",")),
|
||||||
Value::None => CompactStr::new_empty(),
|
Value::None => CompactString::new(""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for Value {
|
impl From<&str> for Value {
|
||||||
fn from(s: &str) -> Self {
|
fn from(s: &str) -> Self {
|
||||||
Value::Str(CompactStr::new(s))
|
Value::Str(CompactString::new(s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Value {
|
impl From<String> for Value {
|
||||||
fn from(s: String) -> Self {
|
fn from(s: String) -> Self {
|
||||||
Value::Str(CompactStr::new(s))
|
Value::Str(CompactString::new(s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ pub struct ComponentDef<'a> {
|
|||||||
pub struct Document<'a> {
|
pub struct Document<'a> {
|
||||||
pub roots: Vec<Element<'a>>,
|
pub roots: Vec<Element<'a>>,
|
||||||
pub components: HashMap<String, ComponentDef<'a>>,
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user