feat: new styles

This commit is contained in:
faynot
2026-07-08 23:45:37 +03:00
parent 9c279015dc
commit 75af23930f
18 changed files with 3556 additions and 816 deletions

33
Cargo.lock generated
View File

@@ -583,6 +583,15 @@ dependencies = [
"wayland-client", "wayland-client",
] ]
[[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"
@@ -742,6 +751,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"
@@ -1454,11 +1477,13 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"colored", "colored",
"compact_str",
"glt", "glt",
"iced", "iced",
"indicatif", "indicatif",
"regex", "regex",
"rhai", "rhai",
"rustc-hash 2.1.2",
] ]
[[package]] [[package]]
@@ -1475,7 +1500,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"
@@ -3488,6 +3513,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"

View File

@@ -7,8 +7,10 @@ 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"

View File

@@ -0,0 +1,570 @@
# План завершения стилевой системы Glint
## Цель
Достичь полной мощности HTML+CSS в языке Glint. Система разделена на 4 уровня:
- **Парсер** (glt/src/style_parser.rs) — чтение `.glts`
- **AST + Компилятор** (glt/src/ast.rs, compiler.rs) — представление и байткод
- **Интерпретатор** (runtime/src/interpreter/) — VDOM + вычисление стилей
- **Рендерер** (runtime/src/renderer.rs) — отрисовка через Iced
---
## 1. Селекторы (критично)
### 1.1 Структурированное представление селектора
**Сейчас:** Селектор — плоская строка `"Panel.desktop"`, matching по точному совпадению.
**Нужно:** Разобрать селектор в структуру:
```rust
enum Selector {
Simple(SimpleSelector),
Compound(CompoundSelector), // без комбинатора
Complex {
left: Box<Selector>,
combinator: Combinator,
right: Box<Selector>,
},
List(Vec<Selector>), // запятые: `Button, .btn, #id`
}
struct SimpleSelector {
tag: Option<String>, // `Button` | `*`
id: Option<String>, // `#myid`
classes: Vec<String>, // `.foo.bar`
pseudo_classes: Vec<PseudoClass>, // `:hover`
pseudo_element: Option<PseudoElement>, // `::before`
attribute: Vec<AttributeSelector>, // `[disabled]`, `[type="text"]`
}
enum Combinator {
Descendant, // пробел
Child, // >
NextSibling, // +
Subsequent, // ~
}
enum PseudoClass {
Hover, Active, Focus,
FirstChild, LastChild,
NthChild(i32, i32), // An+B
FirstOfType, LastOfType,
Disabled, Enabled, Checked,
Root, Empty,
Not(Box<Selector>),
// ...
}
struct AttributeSelector {
name: String,
op: AttrOp,
value: Option<String>,
}
enum AttrOp { Set, Eq, Contain, StartsWith, EndsWith, InList }
```
### 1.2 Парсинг селекторов (style_parser.rs)
- Заменить `parse_selector()` на полноценный парсер селекторов.
- Поддержка: `Button.active:hover`, `Panel > .btn`, `#header`, `[type="text"]`, `*`, `:nth-child(2n+1)`, запятые.
- Валидация и сообщения об ошибках.
### 1.3 Байткод для селекторов
- Новый опкод `OP_SELECTOR` или расширение `OP_STYLE_RULE`:
- Записать дерево селектора в байткод.
- На каждый тип узла — свой подопкод (0x300x3F).
- Компилятор: сериализация структуры селектора.
- Читатель (reader.rs): десериализация обратно в структуру.
### 1.4 Runtime matching (interpreter/mod.rs)
- Заменить `collect_matching_styles()` на алгоритм обхода дерева селекторов.
- **Специфичность:** inline > id > class/attr/pseudo > tag.
- При одинаковой специфичности — порядок объявления (последний побеждает).
- **Комбинаторы:** при обходе VDOM подниматься вверх по предкам/соседям.
- Кэширование результатов matching для производительности.
---
## 2. Псевдоклассы (высокий приоритет)
### 2.1 Интерактивные (`:hover`, `:active`, `:focus`)
**Сейчас:** Только у Button ховер/нажатие — хардкод в renderer.
**Нужно:**
- Хранить состояние интерактивности в VDOM/Element (`ElementState`).
- В renderer передавать статус (hovered/pressed/focused) в `collect_matching_styles`.
- `:hover`-стили переопределяют базовые.
- Плавный переход между состояниями (см. transitions).
**Изменения:**
- `types.rs`: `Element` может получить поле `state: ElementState`.
- `renderer.rs`: у `render_element` появляется доступ к состоянию мыши.
- `style.rs`: `ComputedStyle::compute()` принимает псевдоклассы.
### 2.2 Структурные (`:first-child`, `:nth-child`, `:last-child`, `:first-of-type`, `:last-of-type`, `:empty`, `:root`)
**Сейчас:** Не поддерживаются.
**Нужно:**
- При matching вычислять позицию элемента среди siblings.
- Для `:nth-child(An+B)` — парсить выражение и проверять `(index - B) % A == 0`.
- `:empty``el.children.is_empty()`.
- `:root` — элемент верхнего уровня в VDOM.
### 2.3 Состояния (`:disabled`, `:enabled`, `:checked`)
- `:disabled`/`:enabled` — по свойству `disabled`.
- `:checked` — по свойству `checked`.
---
## 3. Медиа-запросы (`@media`) (средний приоритет)
### 3.1 Парсер
```glts
@media (max-width: 600px) {
Panel { direction: vertical }
}
```
- Парсить `@media` в `Directive::MediaQuery { query, child_span }`.
- Поддержка: `width`, `height`, `min-width`, `max-width`, `orientation`, `prefers-color-scheme`.
- Логические операторы: `and`, `or`, `not`, `,` (or).
### 3.2 Runtime
- Собрать информацию об окне (размер, тема ОС).
- При изменении размера окна переоценивать media queries.
- Хранить в `Document::media_state: MediaState`.
- `collect_matching_styles` проверяет media query перед добавлением правил.
---
## 4. Анимации (`@anim`) (средний приоритет)
### 4.1 Парсинг — уже есть
`@anim name { from { ... } to { ... } }` — парсится и компилируется.
### 4.2 Runtime — НЕ реализован
**Сейчас:** `OP_STYLE_ANIM` пропускается.
**Нужно:**
1. Хранить `KeyframeAnimation` в `StyleSheet` (не только в байткоде, но и в runtime-структуре).
2. В `Element` поле `animation_state: HashMap<String, AnimationInstance>`.
3. Система тиков:
- Iced `subscription` на каждый кадр (`on_every_event``Event::Window(RedrawRequested)`).
- При каждом тике обновлять `elapsed` для активных анимаций.
- Интерполировать между keyframes.
4. Свойства `animation-name`, `animation-duration`, `animation-timing-function`, `animation-delay`, `animation-iteration-count`, `animation-fill-mode`.
5. Поддержка `@keyframes` с процентами: `0% { ... } 50% { ... } 100% { ... }`.
### 4.3 Timing functions
- `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out`.
- `cubic-bezier(p1x, p1y, p2x, p2y)`.
- `steps(n, direction)`.
---
## 5. Транзишены (`transition`) (средний приоритет)
### 5.1 Свойства
- `transition-property`, `transition-duration`, `transition-timing-function`, `transition-delay`.
- Шорткат `transition: all 0.3s ease`.
### 5.2 Runtime
- При изменении `ComputedStyle` (например, при ховере) не применять мгновенно, а запускать интерполяцию.
- Хранить `TransitionState` на элемент: `HashMap<String, (start_value, end_value, elapsed, duration, easing)>`.
- Каждый кадр обновлять transitioning-свойства.
---
## 6. Новые CSS-свойства в ComputedStyle
### 6.1 Типографика
| Свойство | Тип | Парсер |
|----------|-----|--------|
| `font-family` | `Option<String>` | список шрифтов |
| `font-weight` | `Option<u16>` | `normal(400)`, `bold(700)`, числовое |
| `font-style` | `Option<FontStyle>` | `normal`, `italic`, `oblique` |
| `line-height` | `Option<f32>` | числовое или `normal` |
| `letter-spacing` | `Option<f32>` | px |
| `text-align` | `Option<TextAlign>` | `left`, `center`, `right`, `justify` |
| `text-decoration` | `Option<TextDecoration>` | `none`, `underline`, `line-through` |
| `text-transform` | `Option<TextTransform>` | `none`, `uppercase`, `lowercase`, `capitalize` |
| `white-space` | `Option<WhiteSpace>` | `normal`, `nowrap`, `pre` |
| `word-break` | `Option<WordBreak>` | `normal`, `break-all`, `keep-all` |
| `text-overflow` | `Option<TextOverflow>` | `clip`, `ellipsis` |
### 6.2 Фон и границы
| Свойство | Тип | Парсер |
|----------|-----|--------|
| `opacity` | `Option<f32>` | 0.01.0 |
| `background-image` | `Option<String>` | `url(...)` |
| `background-repeat` | `Option<BgRepeat>` | `repeat`, `no-repeat` |
| `background-size` | `Option<BgSize>` | `cover`, `contain`, px, % |
| `background-position` | `Option<BgPos>` | `center`, `top left`, px |
| `linear-gradient(...)` | `Option<Gradient>` | парсить в `Value::Call` и обрабатывать |
| `box-shadow` | `Option<Vec<Shadow>>` | `offset-x offset-y blur spread color` |
| `text-shadow` | `Option<Vec<Shadow>>` | то же |
| `outline` | `Option<Outline>` | width, style, color |
| `border-style` | `Option<BorderStyle>` | `solid`, `dashed`, `dotted` |
| `border` (шорткат) | — | разворачивать в width/style/color |
### 6.3 Flexbox (расширение)
| Свойство | Тип |
|----------|-----|
| `justify-content` | `Option<JustifyContent>` | `start`, `center`, `end`, `space-between`, `space-around`, `space-evenly` |
| `flex-wrap` | `Option<FlexWrap>` | `nowrap`, `wrap`, `wrap-reverse` |
| `flex-direction` | `Option<FlexDirection>` | (расширение `LayoutDirection`) |
| `align-self` | `Option<Alignment>` | переопределяет `align-items` для конкретного элемента |
| `align-content` | `Option<AlignContent>` | multi-line выравнивание |
| `flex` (шорткат) | — | grow, shrink, basis |
| `flex-shrink` | `Option<f32>` | |
| `flex-basis` | `Option<Length>` | |
### 6.4 Grid Layout
| Свойство | Тип |
|----------|-----|
| `grid-template-columns` | `Option<Vec<GridTrack>>` | `1fr 1fr`, `repeat(3, 1fr)`, `auto` |
| `grid-template-rows` | `Option<Vec<GridTrack>>` | |
| `grid-gap` / `gap` | уже есть как `spacing` | |
| `grid-column` | `Option<(u32, u32)>` | start / end |
| `grid-row` | `Option<(u32, u32)>` | |
| `grid-auto-flow` | `Option<GridFlow>` | |
### 6.5 Позиционирование (расширение)
| Свойство | Тип | Статус |
|----------|-----|--------|
| `position: absolute` | — | сейчас мэппится в Static |
| `position: relative` | — | сейчас мэппится в Static |
| `position: sticky` | — | сейчас мэппится в Static |
| `z-index` | `Option<i32>` | |
| `display` | `Option<Display>` | `block`, `flex`, `grid`, `none`, `inline` |
### 6.6 Прочее
| Свойство | Тип |
|----------|-----|
| `visibility` | `Option<Visibility>` | `visible`, `hidden` |
| `cursor` | `Option<Cursor>` | `pointer`, `default`, `text` |
| `pointer-events` | `Option<PointerEvents>` | `auto`, `none` |
| `transform` | `Option<Vec<Transform>>` | `translate(x,y)`, `scale(s)`, `rotate(a)` |
| `transform-origin` | `Option<String>` | |
| `backdrop-filter` | `Option<Vec<Filter>>` | `blur(10px)`, `brightness(1.2)` |
| `filter` | `Option<Vec<Filter>>` | то же |
| `list-style` | `Option<ListStyle>` | |
| `isolation` | `Option<Isolation>` | `auto`, `isolate` |
| `mix-blend-mode` | `Option<BlendMode>` | `multiply`, `screen` |
### 6.7 Итого: ~4060 новых полей в ComputedStyle
Каждое поле: тип `Option<T>`, значение по умолчанию `None`.
Парсинг: новая функция `parse_<property>(s: &str) -> Option<T>` для каждого.
Lookup в `ComputedStyle::compute()`.
**Рендеринг в Iced:**
- Iced 0.14 поддерживает Background::Gradient.
- Box-shadow пока не поддерживается, но можно эмулировать через container style с offset-тенью.
- Transforms и filters — Iced пока не нативно; нужен кастомный widget или пропуск.
---
## 7. Система каскада и наследования
### 7.1 Наследование (сейчас: только `color`, `font-size`)
Добавить наследование для:
- `font-family`, `font-weight`, `font-style`, `line-height`, `letter-spacing`, `text-align`, `white-space`, `word-break`, `text-transform`, `visibility`, `cursor`, `pointer-events`.
Механизм: в `render_element()` передавать не только `parent_color` и `parent_font_size`, а `&ComputedStyle` родителя или отдельный `InheritedStyle`.
### 7.2 `inherit` / `initial` / `unset`
- Специальные значения для любого свойства.
### 7.3 `!important`
- Флаг важности в правиле. Переопределяет специфичность.
- Парсить `value!important`.
---
## 8. CSS-функции и значения
### 8.1 `calc()`
```rust
enum CalcValue {
Number(f32),
Percentage(f32),
Add(Box<CalcValue>, Box<CalcValue>),
Sub(Box<CalcValue>, Box<CalcValue>),
Mul(Box<CalcValue>, Box<CalcValue>),
Div(Box<CalcValue>, Box<CalcValue>),
Var(String),
}
```
- Парсить `calc(100% - 20px)`.
- Вычислять в runtime с учётом контекста (размер родителя).
### 8.2 `var()` — CSS custom properties
- `--my-var: value;` в правилах.
- `var(--my-var, fallback)` при использовании.
- Хранить custom properties в `ComputedStyle.custom_props: HashMap<String, String>`.
- Наследуются по умолчанию.
### 8.3 `min()`, `max()`, `clamp()`
- `min(100%, 500px)`, `max(200px, 50%)`, `clamp(200px, 50%, 500px)`.
### 8.4 `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `oklch()`, `color-mix()`
- Сейчас только `#hex` и 3 named colors.
- Парсить `rgb(255, 0, 0)`, `rgba(255, 0, 0, 0.5)`.
- Расширить `parse_color()`.
### 8.5 Named colors
- Расширить словарь до 140+ CSS named colors (AliceBlue, ...).
---
## 9. @-правила
### 9.1 `@import`
```glts
@import "theme.glts"
```
- В парсере: обработать `@import`, загрузить файл, влить переменные и миксины.
- Может быть сложным (циклы, кэширование). Опционально на раннем этапе.
### 9.2 `@font-face`
```glts
@font-face {
font-family: "MyFont"
src: fs:/fonts/myfont.ttf
}
```
- Регистрировать в Iced через `font::Family`.
- Потребует `iced::font::load()`.
### 9.3 `@scope` (CSS Cascading Layers)
- `@scope (.card) { ... }` — ограничение области действия правил.
---
## 10. Рендеринг (renderer.rs)
### 10.1 Псевдоклассы в рендере
- Для интерактивных элементов передавать `button::Status`.
- `:hover`/`:active`-стили должны пересчитываться при изменении статуса.
### 10.2 Flexbox (полноценный)
- `justify-content`: Iced `Row`/`Column` не имеют нативного justifyContent; эмулировать через наполнители (`Length::Fill`).
- `flex-wrap`: нужен `Flow` widget или эмуляция строками.
### 10.3 Grid
- Полноценный CSS Grid: имплементировать кастомный Iced widget или разбивать на строки/колонки вручную.
- `grid-template-columns: 1fr 1fr 1fr` → разбивка children на ряды.
- `grid-column`/`grid-row`: позиционирование ячеек.
### 10.4 `display: none`
- Пропускать элемент при рендеринге (уже частично — `None` возвращается).
### 10.5 `z-index`
- Сортировать fixed-слой по z-index внутри stack.
### 10.6 `position: absolute` / `relative`
- `relative`: смещение через padding контейнера.
- `absolute`: позиционирование относительно предка с `position: relative` (или окна). Эмулировать через stack слои, как `fixed`.
### 10.7 `position: sticky`
- Эмулировать через Iced scrollable + перехват событий скролла.
- Пока не поддерживается Iced нативно. Можно через subscription на scroll position + ручное позиционирование.
### 10.8 `opacity`
- Iced: `widget.opacity(f32)` (iced 0.14+).
### 10.9 `box-shadow`, `text-shadow`
- Эмуляция: container с подложкой (offset background) или кастомный шейдер.
- Iced 0.14 имеет `Border` только с `color`, `width`, `radius`. Shadow — нет.
### 10.10 `outline`
- Через border с `outline-offset` эмуляцией.
### 10.11 Трансформы (`transform`)
- Iced не поддерживает transforms нативно.
- Потребуется кастомный widget с `lyon` или `vello` для path-трансформаций.
- На раннем этапе — документировать как unsupported.
---
## 11. Инструментарий разработчика
### 11.1 Вывод вычисленных стилей
- Флаг `--debug-styles` в CLI: печатать `ComputedStyle` каждого элемента.
### 11.2 Инспектор элементов
- Iced debug overlay: `iced::widget::pane_grid` с инспектором.
- Показывать matched selectors, specificity, computed properties.
### 11.3 Hot-reload стилей
- Перекомпиляция `.glts` без перезапуска приложения.
- Обновление bytecode в runtime.
---
## 12. Порядок реализации (приоритеты)
### Фаза 1: Критическое (MVP+)
1. Структурированные селекторы + matching engine со специфичностью
2. Комбинаторы (особенно descendant)
3. Псевдоклассы `:hover`, `:active`, `:focus`
4. Селектор по ID (`#id`)
5. Множественные селекторы через запятую
6. Псевдоклассы `:first-child`, `:nth-child`, `:last-child`, `:empty`
### Фаза 2: Визуальное обогащение
7. Новые свойства: opacity, font-weight, text-align, justify-content, line-height
8. fill-шорткаты для flex (flex-grow/shrink/basis)
9. `position: absolute` / `relative` / `sticky`
10. `z-index`
11. `display: none`
12. `calc()`, `min()`, `max()`, `clamp()`
13. `rgb()`, `rgba()`, `hsl()` + расширенный словарь named colors
14. `!important`
### Фаза 3: Продвинутое
15. `@media` queries
16. Animations (playback)
17. Transitions
18. `box-shadow`, `text-shadow` (эмуляция)
19. CSS Grid
20. `flex-wrap` + `justify-content` полноценно
21. `var()` CSS custom properties
22. `@import`
23. `@font-face`
### Фаза 4: Экспертное
24. `transform`, `transform-origin`
25. `backdrop-filter`, `filter`
26. `gradient` (linear, radial)
27. `color-mix()`, `oklch()`
28. Hot-reload
29. DevTools инспектор
30. `@scope`
---
## 13. Архитектурные изменения
### 13.1 `ComputedStyle` → разбить на подс-труктуры
Вместо 40+ плоских полей:
```rust
pub struct ComputedStyle {
pub box_model: BoxModel,
pub typography: Typography,
pub background: BackgroundStyle,
pub border: BorderStyle,
pub flex: FlexStyle,
pub grid: GridStyle,
pub position: Positioning,
pub effects: Effects,
pub overflow: OverflowStyle,
pub animation: AnimationStyle,
pub custom: HashMap<String, String>,
pub display: Option<Display>,
pub visibility: Option<Visibility>,
}
```
### 13.2 Selector engine — отдельный модуль
```rust
mod selector {
pub struct Selector { ... }
pub fn parse(input: &str) -> Result<Selector, String>;
pub fn specificity(&self) -> (u32, u32, u32);
pub fn matches(&self, el: &Element, ctx: &MatchContext) -> bool;
}
```
### 13.3 StyleSheet — расширение
```rust
pub struct StyleRule {
pub selector: Selector,
pub properties: HashMap<String, String>,
pub source_order: u32,
pub media: Option<MediaQuery>,
pub important: HashSet<String>,
}
```
### 13.4 Система тиков для анимаций/транзишенов
- Новый модуль `animation` в runtime.
- `AnimationEngine` с `HashMap<AnimationId, AnimationInstance>`.
- Iced subscription: `time::every(Duration::from_millis(16))` (60fps).
---
## 14. Оценка сложности
| Компонент | Новые файлы | Изменённые файлы | Пример LOC |
|-----------|-------------|------------------|------------|
| Selector parser | `glt/src/selector.rs` | `style_parser.rs` | ~500 |
| Selector matching | `runtime/src/selector.rs` | `mod.rs` | ~400 |
| ComputedStyle расширение | — | `style.rs` | +400 |
| Медиа-запросы | — | `style_parser.rs`, `mod.rs` | ~300 |
| Анимации | `runtime/src/animation.rs` | `style.rs`, `mod.rs`, `app.rs` | ~500 |
| Транзишены | `runtime/src/transition.rs` | | ~400 |
| Псевдоклассы | — | `mod.rs`, `renderer.rs`, `types.rs` | ~300 |
| Новые парсеры свойств | — | `style.rs` | +600 |
| Рендеринг | — | `renderer.rs` | +500 |
| Flex/Grid | `runtime/src/layout.rs` | `renderer.rs` | ~400 |
| calc/var | `runtime/src/values.rs` | `style.rs` | ~300 |
| @import/@font-face | — | `style_parser.rs`, `renderer.rs` | ~200 |
| Инструменты | `runtime/src/inspector.rs` | `cli.rs`, `app.rs` | ~300 |
**Итого:** ~57 новых файлов, ~15 изменённых, ~50006000 строк нового кода.

BIN
desktop.glbc Normal file

Binary file not shown.

View File

@@ -1,22 +1,18 @@
// ============================================================================= // =============================================================================
// GLINT UI: Ultimate Reactive Showcase Architecture // GLINT UI: Compact Reactive Test Suite (Fixed Layout)
// ============================================================================= // =============================================================================
@version 1 @version 1
@style "main.glts" @style "main.glts"
// ── 1. Global Configuration & Constants ────────────────────────────────────── // ── 1. Global Application State ──────────────────────────────────────────────
@global $APP_TITLE = "Glint Core Console"
@global $APP_TITLE = "Glint Design System & Rhei Demo" @global $username = "Operator"
@global $IS_PRODUCTION = false @global $volume_level = 50.0
@global $is_enabled = true
// Глобальное состояние приложения @global $access_level = 5.0
@global $access_level = 3.0 @global $team_nodes = ["Alpha", "Bravo", "Charlie"]
@global $username = "Guest" @global $show_toast = true
@global $volume_level = 0.0
@global $is_enabled = false
@global $search_query = "Type to search..."
@global $team_members = ["Alexander", "Beatrice", "Cyrus", "Diana"]
@singleton SystemSettings { @singleton SystemSettings {
theme = "ocean-blue", theme = "ocean-blue",
@@ -25,183 +21,189 @@
storage_path = fs:/var/lib/glint/assets storage_path = fs:/var/lib/glint/assets
} }
// ── 2. Logic Layer (Document-level Rhai Script) ──────────────────────────────
!rhei: { !rhei: {
// Вспомогательные UI-функции fn load_status(val) {
fn level_label(lvl) { if val == 0.0 { "💤 Muted" }
if lvl >= 8.0 { "Admin" } else if val >= 80.0 { "🔥 Overload" }
else if lvl >= 5.0 { "Moderator" } else { "⚡ Active" }
else { "Guest" }
} }
fn volume_icon(vol) { let sum = 0;
if vol == 0.0 { "🔇" } for i in 1..=5 { sum = sum + i; }
else if vol < 40.0 { "🔈" } print("Rhai Engine online. Checksum: " + sum);
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 ───────────────────────────────────────────────────────── // ── 2. Reusable UI Components ────────────────────────────────────────────────
@component NodeCard(nodename: String, level: Float) {
@component ProfileCard(username: String, access_level: Float) { Panel(class="ui-card") {
Panel { Label(text=$nodename)
Header !rhei: { "User: " + username } @if !rhei: { level >= 5.0 } {
Label(text=$username) Text "🛡️ Secure Node"
// Сложное условие в Rhai: вычисляется при каждом обновлении VDOM
@if !rhei: { (access_level >= 5.0) && (username.len() > 0) } {
Text "✅ Administrator Privileges Active"
Text !rhei: { "Role: " + level_label(access_level) }
} @else { } @else {
Text "🔒 Restricted Access Mode" Text "🔓 Guest Node"
Text "Role: Guest"
} }
} }
} }
// ── 4. Main Application Tree ───────────────────────────────────────────────── // ── 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") {
Window( // --- Верхняя панель (Header Section) ---
title=$APP_TITLE, Panel(class="layout-row") {
width=1280, Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {}
height=800, Panel(class="layout-col") {
resizable=true Header "Glint Reactive Dashboard"
) { Text !rhei: { "User: " + username + " | Status: " + load_status(volume_level) }
// Локальное состояние окна }
@let $show_metrics = true }
Panel(id="main_viewport", padding=24) {
Header "Dashboard Overview"
Text "Welcome to the ultimate Glint component test suite."
Divider() Divider()
// ── Секция A: Control Panel (Переменные и биндинги) ────────────────── // --- Главная рабочая область (Workspace Split) ---
Header "A. Control Panel & State Bindings" Panel(class="layout-row") {
Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {} // Левая колонка: Интерактивное состояние и биндинги
Panel(class="layout-col") {
Panel { Panel(class="ui-card") {
Input(placeholder=$search_query, value=$search_query) Header "State Bindings"
Toggle(label="Enable Live Metrics", value=$is_enabled) Input(placeholder="Change title...", value=$APP_TITLE)
Text !rhei: { "Live search query: " + search_query } Toggle(label="Live Engine", value=$is_enabled)
}
@if $is_enabled { @if $is_enabled {
Text "✅ Features are ON. You can adjust the system." Text "🟢 System: ONLINE"
} @else { } @else {
Text "⛔ Features are OFF." Text "🔴 System: OFFLINE"
}
} }
Divider() Panel(class="ui-card") {
Header "Control Channels"
// ── Секция B: Arithmetic Conditions & Reactive Text ──────────────────
Header "B. Rhei Arithmetic Conditions"
Slider(value=$volume_level) Slider(value=$volume_level)
ProgressBar(value=$volume_level) ProgressBar(value=$volume_level)
Text !rhei: { "Metrics Level: " + 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() // Правая колонка: Тестирование геометрии и каскада стилей
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"
}
}
// ── Секция C: Access Control & Component Instantiation ─────────────── Panel(class="inheritance-box") {
Header "C. Multi-variable Logic & Components" 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) Slider(value=$access_level)
!rhei: { "Current slider level: " + access_level + " → " + level_label(access_level) } // ── ТЕСТ POSITION: ABSOLUTE ─────────────────────
Panel(class="abs-demo-container") {
Panel { Panel(class="abs-demo-badge") {
Button(label="Grant Admin (Level 9)") { Text "ABSOLUTE!"
@on click {
!rhei: { access_level = 9.0; }
}
}
Button(label="Reset (Level 1)") {
@on click {
!rhei: { access_level = 1.0; }
}
} }
Text "(бадж спозиционирован абсолютно)"
Button(label="Press me") {}
} }
@if !rhei: { Panel(class="layout-row") {
let threshold = 5.0; Button(label="Set Admin (Lvl 9)", class="btn-custom") {
access_level >= threshold @on click { !rhei: { access_level = 9.0; } }
} {
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;
}
} }
Button(label="Reset Access") {
@on click { !rhei: { access_level = 1.0; } }
} }
} }
} }
} }
} }
Divider() // --- Динамический список узлов (@each Grid) ---
Panel(direction="grid", columns=3, gap=12, class="layout-clean") {
// ── Секция E: Footer Metadata ──────────────────────────────────────── @each $node in $team_nodes {
Panel { NodeCard(nodename=$node, level=$access_level)
Text "System Version: 1.0.5-stable"
// Многострочный скрипт прямо внутри текстового узла
!rhei: {
let pct = (access_level * 10.0).to_string();
"Access Power: " + pct + "% | Integrity: OK"
} }
} }
// --- Нижняя панель распределения пространства ---
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 "— Конец тестового контента —"
}
} }
} }

BIN
display.glbc Normal file

Binary file not shown.

82
display.gtlm Normal file
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 скрыт (выше не видно красную панель)."
}
}
}

440
main.glts
View File

@@ -1,29 +1,18 @@
// ============================================================================= // =============================================================================
// Glint Design System — Master Stylesheet (main.glts) // Glint Design System — Core Fixed Stylesheet (main.glts)
// ============================================================================= // =============================================================================
// ── 1. Цветовая палитра и константы (Variables) ────────────────────────────── $bg-app = #11111b
$bg-app = #11111b // Глубокий темный фон для всего окна $bg-panel = #1e1e2e
$bg-panel = #1e1e2e // Базовый цвет для контейнеров и панелей $bg-surface = #313244
$bg-surface = #313244 // Цвет для карточек и выделенных блоков $bg-input = #181825
$bg-field = #181825 // Внутренний фон для полей ввода (Input) $accent = #b4befe
$text-main = #cdd6f4
$text-main = #cdd6f4 // Мягкий белый основной текст $text-muted = #9399b2
$text-muted = #9399b2 // Серый приглушенный текст для описаний $border-glow = #45475a
$accent = #b4befe // Лавандовый акцентный цвет для кнопок и заголовков
$border-glow = #45475a // Цвет аккуратных рамок и разделителей
// Сетка отступов (Размеры автоматически очищаются от "px" парсером Glint)
$pad-dense = 6px
$pad-normal = 12px
$pad-roomy = 20px
$pad-window = 28px
// ── 2. Шаблоны стилей (Mixins) ───────────────────────────────────────────────
@mixin text-body { @mixin text-body {
color: $text-main font-size: 14px
font-size: 15px
} }
@mixin standard-card { @mixin standard-card {
@@ -33,47 +22,94 @@ $pad-window = 28px
border-radius: 12px border-radius: 12px
} }
// ── 3. Правила для UI-компонентов (Element Rules) ────────────────────────────
// Главное окно приложения
Window { Window {
background: $bg-app background: $bg-app
padding: $pad-window
spacing: $pad-roomy
} }
// Универсальный контейнер ( viewport, карточки, обертки списков )
Panel { Panel {
@use standard-card background: transparent
padding: $pad-normal border-width: 0px
gap: 14px padding: 0px
gap: 12px
direction: vertical direction: vertical
}
.ui-card {
@use standard-card
padding: 14px
gap: 12px
align-items: center align-items: center
content-align: center content-align: center
} }
// Крупные заголовки секций // ── Служебные Layout-классы ──────────────────────────────────────────────────
Header {
color: $accent .layout-viewport {
font-size: 22px width: fill
padding: 4px 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 { Text {
@use text-body @use text-body
} }
// Подписи, второстепенные метаданные
Label { Label {
color: $text-muted color: $text-muted
font-size: 13px font-size: 12px
} }
// Интерактивные элементы управления Input {
Button { background-color: $bg-input
color: $text-main
font-size: 14px
padding: 10px
border-radius: 8px border-radius: 8px
border-width: 1px 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 { Image {
@@ -81,26 +117,316 @@ Image {
border-width: 1px border-width: 1px
} }
// Текстовые поля ввода // ── Секция тестирования продвинутой геометрии Box-Model ──────────────────────
Input {
background-color: $bg-field .geometry-box {
color: $text-main @use standard-card
font-size: 14px background: #24253a
padding: 12px border-color: #f38ba8
border-radius: 16px border-width: 2px
border-width: 1px padding-top: 24px
border-color: $border-glow padding-bottom: 12px
padding-left: 16px
padding-right: 40px
width: 320px width: 320px
} }
// Переключатели .margin-test-item {
Toggle { background: #a6e3a1
color: $text-main padding: 8px
font-size: 15px margin-top: 12px
margin-bottom: 4px
} }
// Разделительные линии (Divider / Separator) // ── Секция тестирования каскадного наследования (Inheritance) ────────────────
Divider {
background: $border-glow .inheritance-box {
border-width: 1px @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
} }

BIN
out.glbc

Binary file not shown.

View File

@@ -1,13 +1,13 @@
use iced::widget::{column, container, row, scrollable, text, Space}; use iced::widget::{column, container};
use iced::{Alignment, Length, Theme}; use iced::{Length, Theme};
use crate::interpreter::{Document, Element, Interpreter, RheiContext}; use crate::interpreter::{Document, Element, Interpreter, RheiContext};
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,6 +18,9 @@ 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) => {
self.doc.variables.insert("__scroll_y".to_string(), y.to_string());
}
Message::EventTriggered(script) => { Message::EventTriggered(script) => {
if !script.is_empty() { if !script.is_empty() {
self.rhei.execute_action(&script, &mut self.doc.variables); self.rhei.execute_action(&script, &mut self.doc.variables);
@@ -40,56 +43,64 @@ impl GlintApp {
self.vdom_roots = Interpreter::evaluate_vdom( self.vdom_roots = Interpreter::evaluate_vdom(
&self.doc.roots, &self.doc.roots,
&self.doc.variables, &mut self.doc.variables,
&self.doc.components, &self.doc.components,
&self.rhei, &self.rhei,
&self.doc.stylesheet, &self.doc.stylesheet,
&[],
); );
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 mut content = column![]
for root in &self.vdom_roots {
if let Some(el) = render_element(root) {
content = content.push(el);
}
}
let status_bar = container(self.build_status_bar(&self.doc.variables))
.width(Length::Fill) .width(Length::Fill)
.padding(10); .height(Length::Fill);
let layout = column![ let mut global_fixed_layers = Vec::new();
scrollable(content).width(Length::Fill).height(Length::Fill), let mut global_abs_layers = Vec::new();
iced::widget::rule::horizontal(1), let mut global_sticky_layers = Vec::new();
status_bar,
];
container(layout) let _scroll_y = self.doc.variables.get("__scroll_y")
.width(Length::Fill) .and_then(|v| v.parse::<f32>().ok())
.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); .unwrap_or(0.0);
row![ for root in &self.vdom_roots {
text("🟢 Runtime Active (VDOM + Rhai)").size(14), if let Some(el) = render_element(root, None, None, None, &mut global_fixed_layers, &mut global_abs_layers, &mut global_sticky_layers, &self.doc.stylesheet) {
Space::new().width(Length::Fill), content = content.push(el);
text(format!("Шаблонных узлов: {}", self.doc.roots.len())).size(14), }
Space::new().width(Length::Fixed(15.0)), }
text(format!("Слайдер ($volume_level): {:.1}", slider_val)).size(14),
let layout = column![
content,
iced::widget::rule::horizontal(1),
] ]
.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();
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()
}
} }
} }

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,21 @@ 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.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,9 +1,11 @@
pub mod opcodes; pub mod opcodes;
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;
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, InterpError};
@@ -11,7 +13,7 @@ pub use types::{ComponentDef, Document, Element, InterpError};
use opcodes::*; use opcodes::*;
use reader::Reader; use reader::Reader;
use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX}; use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX};
use style::ComputedStyle; use style::{AncestorInfo, ComputedStyle, StructuralContext};
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::OnceLock; use std::sync::OnceLock;
@@ -21,7 +23,7 @@ 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); }
@@ -41,20 +43,20 @@ impl Interpreter {
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 }) Ok(Document { roots, components, variables, rhei_scripts, stylesheet })
} }
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, String>,
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,
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,7 +65,7 @@ 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 => stack.push(Element::new(r.read_str_ref()?)),
OP_ELEM_POP => { OP_ELEM_POP => {
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?; let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
@@ -90,53 +92,63 @@ 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 { el.push_prop("text".to_string(), val);
format!("{RHEI_PREFIX}{value}") }
} else { } else if let Some(value) = r.read_value_as_string(vop)? {
value if let Some(el) = stack.last_mut() {
}; el.push_prop("text".to_string(), value);
el.properties.insert("text".to_string(), stored);
} }
} }
} }
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() { 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() { 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}")); 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,8 +157,8 @@ 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.push_prop(
"text".to_string(), "text".to_string(),
format!("{RHEI_PREFIX}{script}"), format!("{RHEI_PREFIX}{script}"),
); );
@@ -187,12 +199,12 @@ impl Interpreter {
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.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.children = false_children; else_el.children = false_children;
if_el.children.push(else_el); if_el.children.push(else_el);
} }
@@ -214,9 +226,9 @@ impl Interpreter {
r, variables, components, rhei_scripts, stylesheet, false, r, variables, components, rhei_scripts, stylesheet, 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.push_prop("var_name".to_string(), var_name);
each_el.properties.insert("source".to_string(), source_val); 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 +257,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,20 +290,54 @@ 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, String>,
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 mut output = Vec::with_capacity(templates.len()); let mut output = Vec::with_capacity(templates.len());
// Precompute sibling info for structural pseudo-classes and sibling combinators
let sibling_infos: Vec<AncestorInfo> = templates.iter().map(|el| {
AncestorInfo::new_with_id(
el.type_name,
el.id().map(String::from),
el.get_prop("class")
.map(|s| s.split_whitespace().map(String::from).collect())
.unwrap_or_default(),
)
}).collect();
// Precompute type totals for structural pseudo-class context
let mut type_counts: HashMap<&str, usize> = HashMap::new();
for el in templates { for el in templates {
match el.type_name.as_str() { *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" => { "@if" => {
let cond = el.properties.get("condition").cloned().unwrap_or_default(); let cond = el.get_prop("condition").unwrap_or_default();
let is_true = Self::evaluate_condition(&cond, variables, rhei); let is_true = Self::evaluate_condition(cond, variables, rhei);
let mut active_branch = Vec::new(); let mut active_branch = Vec::new();
for child in &el.children { for child in &el.children {
@@ -302,21 +348,19 @@ impl Interpreter {
} }
} }
output.extend(Self::evaluate_vdom( output.extend(Self::evaluate_vdom(
&active_branch, variables, components, rhei, stylesheet, &active_branch, variables, components, rhei, stylesheet, ancestors,
)); ));
} }
"@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))
.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![]
@@ -325,73 +369,84 @@ impl Interpreter {
}; };
for item in items { for item in items {
let mut local_vars = variables.clone(); let old_val = variables.insert(var_name.to_string(), item);
local_vars.insert(var_name.clone(), item);
output.extend(Self::evaluate_vdom( output.extend(Self::evaluate_vdom(
&el.children, &local_vars, components, rhei, stylesheet, &el.children, variables, components, rhei, stylesheet, ancestors,
)); ));
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, 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 = ComputedStyle::compute(&vcomp.properties, &matched_sheets);
stylesheet.resolve(&vcomp.type_name),
); let child_ancestors = Self::build_ancestor_chain(ancestors, &vcomp);
vcomp.children = Self::evaluate_vdom( vcomp.children = Self::evaluate_vdom(
&comp.children, &comp_scope, components, rhei, stylesheet, &comp.children, variables, components, rhei, stylesheet, &child_ancestors,
); );
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 = ComputedStyle::compute(&vnode.properties, &matched_sheets);
stylesheet.resolve(&el.type_name),
);
let child_ancestors = Self::build_ancestor_chain(ancestors, &vnode);
vnode.children = Self::evaluate_vdom( vnode.children = Self::evaluate_vdom(
&el.children, variables, components, rhei, stylesheet, &el.children, variables, components, rhei, stylesheet, &child_ancestors,
); );
output.push(vnode); output.push(vnode);
} }
@@ -402,13 +457,45 @@ impl Interpreter {
output output
} }
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, String>, rhei: &RheiContext) -> String { fn resolve_prop(v: &str, variables: &HashMap<String, String>, 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)
} else { } else {
Self::resolve_string(v, variables) Self::resolve_string(v, variables).into_owned()
}
} }
}
fn evaluate_condition( fn evaluate_condition(
cond: &str, cond: &str,
@@ -476,18 +563,38 @@ impl Interpreter {
} }
} }
pub fn resolve_string(val: &str, scope: &HashMap<String, String>) -> String { pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, String>) -> 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) {
result.push_str(resolved);
} 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),

View File

@@ -7,73 +7,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 +115,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 +128,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 +138,6 @@ impl<'a> Reader<'a> {
Ok(s) Ok(s)
} }
/// Read `OP_PROP_ARRAY` (opcode already consumed) and return elements as strings.
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 +150,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 +167,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 +183,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 +225,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 +279,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,12 +1,13 @@
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;
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>>,
} }
impl RheiContext { impl RheiContext {
@@ -29,102 +30,88 @@ 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()),
}
}
pub fn sync_scope(&self, variables: &HashMap<String, String>) {
let mut scope = self.scope.borrow_mut();
for (k, v) in variables.iter() {
if scope.contains(k) {
if let Some(old_val) = scope.get_value::<Dynamic>(k) {
if dyn_to_str(&old_val) == *v {
continue;
}
}
scope.set_value(k, str_to_dyn(v));
} else {
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
} }
} }
pub fn initialize(&self, variables: &mut HashMap<String, String>) { pub fn initialize(&self, variables: &mut HashMap<String, String>) {
let mut scope = Scope::new(); self.sync_scope(variables);
for (k, v) in variables.iter() {
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}");
} }
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &self.init_ast) { for (name, _, val) in scope.iter_raw() {
eprintln!("⚠️ Rhei init error: {e}"); let s_val = dyn_to_str(&val);
} if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
variables.insert(name.to_string(), s_val);
let names: Vec<String> = scope.iter_raw()
.map(|(name, _, _)| name.to_string())
.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 eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> String {
let mut scope = Scope::new(); self.sync_scope(variables);
for (k, v) in variables { let mut scope = self.scope.borrow_mut();
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
let expr_ast = self.engine match self.engine.eval_expression_with_scope::<Dynamic>(&mut *scope, expr) {
.compile_expression(expr) Ok(val) => dyn_to_str(&val),
.or_else(|_| self.engine.compile(expr))
.ok()?;
let full = self.fn_ast.merge(&expr_ast);
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
Ok(val) => Some(dyn_to_str(&val)),
Err(e) => { Err(e) => {
eprintln!("⚠️ Rhei eval `{expr}`: {e}"); eprintln!("⚠️ Rhei eval_expr error: {e}");
None String::new()
} }
} }
} }
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool { pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool {
let mut scope = Scope::new(); self.sync_scope(variables);
for (k, v) in variables { let mut scope = self.scope.borrow_mut();
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
let ast = match self.engine.compile_expression(expr) { match self.engine.eval_expression_with_scope::<bool>(&mut *scope, expr) {
Ok(a) => a, Ok(b) => b,
Err(_) => match self.engine.compile(expr) {
Ok(a) => a,
Err(e) => { Err(e) => {
eprintln!("⚠️ Rhei condition compile `{expr}`: {e}"); eprintln!("⚠️ Rhei eval_condition error: {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) => {
eprintln!("⚠️ Rhei condition eval `{expr}`: {e}");
false false
} }
} }
} }
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) { pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) {
let mut scope = Scope::new(); self.sync_scope(variables);
for (k, v) in variables.iter() { let mut scope = self.scope.borrow_mut();
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
match self.engine.compile(script) { match self.engine.compile(script) {
Ok(action_ast) => { Ok(action_ast) => {
let full = self.fn_ast.merge(&action_ast); if let Err(e) = self.engine.run_ast_with_scope(&mut *scope, &action_ast) {
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &full) {
eprintln!("⚠️ Rhei action execution error: {e}"); eprintln!("⚠️ Rhei action execution error: {e}");
} }
} }
@@ -133,12 +120,10 @@ impl RheiContext {
} }
} }
let names: Vec<String> = scope.iter_raw() for (name, _, val) in scope.iter_raw() {
.map(|(name, _, _)| name.to_string()) let s_val = dyn_to_str(&val);
.collect(); if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
for name in &names { variables.insert(name.to_string(), s_val);
if let Some(val) = scope.get_value::<Dynamic>(name) {
variables.insert(name.clone(), dyn_to_str(&val));
} }
} }
} }
@@ -150,7 +135,6 @@ impl Default for RheiContext {
} }
} }
pub fn str_to_dyn(s: &str) -> Dynamic { pub fn str_to_dyn(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); }
@@ -158,12 +142,9 @@ pub fn str_to_dyn(s: &str) -> Dynamic {
Dynamic::from(s.to_owned()) Dynamic::from(s.to_owned())
} }
pub fn dyn_to_str(val: &Dynamic) -> String { pub fn dyn_to_str(d: &Dynamic) -> String {
if val.is_string() { if d.is_string() {
return val.clone().cast::<String>(); return d.clone().into_string().unwrap_or_default();
} }
if val.is_unit() { d.to_string()
return String::new();
}
val.to_string()
} }

View File

@@ -1,8 +1,465 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::borrow::Cow;
#[derive(Debug, Clone)]
pub enum AttributeSelector {
Exists(String),
Equals(String, String),
}
fn parse_attribute(input: &str) -> AttributeSelector {
let input = input.trim();
if let Some((name, val)) = input.split_once('=') {
let name = name.trim().to_string();
let val = val.trim().trim_matches('"').trim_matches('\'').to_string();
AttributeSelector::Equals(name, val)
} else {
AttributeSelector::Exists(input.to_string())
}
}
#[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>,
}
impl CompoundSelector {
fn parse(input: &str) -> Self {
let input = input.trim();
let mut tag = None;
let mut id = None;
let mut classes = Vec::new();
let mut pseudo_classes = Vec::new();
let mut attributes = Vec::new();
let mut current = String::new();
let mut delim: Option<char> = None;
for ch in input.chars() {
match ch {
'#' | '.' | ':' | '[' => {
if !current.is_empty() {
match (delim, ch) {
(Some('.'), _) => classes.push(std::mem::take(&mut current)),
(Some(':'), _) => pseudo_classes.push(std::mem::take(&mut current)),
(Some('#'), _) => id = Some(std::mem::take(&mut current)),
(Some('['), _) => {
attributes.push(parse_attribute(&std::mem::take(&mut current)));
}
_ if tag.is_none() && id.is_none()
&& classes.is_empty() && pseudo_classes.is_empty()
&& attributes.is_empty() =>
{
tag = Some(std::mem::take(&mut current));
}
_ => current.clear(),
}
}
delim = Some(ch);
}
']' => {
if delim == Some('[') && !current.is_empty() {
attributes.push(parse_attribute(&std::mem::take(&mut current)));
delim = None;
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
match delim {
Some('#') => id = Some(current),
Some('.') => classes.push(current),
Some(':') => pseudo_classes.push(current),
Some('[') => attributes.push(parse_attribute(&current)),
None => tag = Some(current),
_ => {}
}
}
Self { tag, id, classes, pseudo_classes, attributes }
}
fn specificity(&self) -> (u32, u32, u32) {
(
if self.id.is_some() { 1 } else { 0 },
self.classes.len() as u32 + self.attributes.len() as u32 + self.pseudo_classes.len() as u32,
if self.tag.is_some() { 1 } else { 0 },
)
}
pub fn matches_element(
&self,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
el_attributes: &HashMap<String, String>,
) -> bool {
if let Some(ref t) = self.tag {
if t != type_name && t != "*" {
return false;
}
}
if let Some(ref i) = self.id {
match el_id {
Some(eid) if eid == i => {}
_ => return false,
}
}
for cls in &self.classes {
if !el_classes.contains(&cls.as_str()) {
return false;
}
}
for attr in &self.attributes {
match attr {
AttributeSelector::Exists(name) => {
if !el_attributes.contains_key(name) {
return false;
}
}
AttributeSelector::Equals(name, val) => {
match el_attributes.get(name) {
Some(v) if v == val => {}
_ => return false,
}
}
}
}
for pc in &self.pseudo_classes {
match pc.as_str() {
"first-child" => {
if structural.sibling_index != 0 { return false; }
}
"last-child" => {
if structural.sibling_index + 1 != structural.sibling_total { return false; }
}
"first-of-type" => {
if structural.type_index != 0 { return false; }
}
"empty" => {
if structural.has_children { return false; }
}
"root" => {
if !structural.is_root { return false; }
}
s if s.starts_with("nth-child(") => {
let inner = &s[10..s.len().saturating_sub(1)];
if !nth_matches(inner, structural.sibling_index + 1) {
return false;
}
}
_ => {
if !active_pseudo.contains(&pc.as_str()) {
return false;
}
}
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Combinator {
Descendant, // Space
Child, // >
NextSibling, // +
Subsequent, // ~
}
#[derive(Debug, Clone)]
pub struct ComplexSelector {
pub compounds: Vec<CompoundSelector>,
pub combinators: Vec<Combinator>,
}
impl ComplexSelector {
pub fn parse(input: &str) -> Self {
let input = input.trim();
if input.is_empty() {
return Self { compounds: vec![], combinators: vec![] };
}
let mut parts: Vec<(String, Combinator)> = Vec::new();
let mut buf = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'>' | '+' | '~' => {
let right = buf.trim().to_string();
let combinator = match ch {
'>' => Combinator::Child,
'+' => Combinator::NextSibling,
'~' => Combinator::Subsequent,
_ => unreachable!(),
};
if !right.is_empty() {
parts.push((right, combinator));
}
buf.clear();
while let Some(&c) = chars.peek() {
if c.is_ascii_whitespace() { chars.next(); } else { break; }
}
}
'#' | '.' | ':' => {
buf.push(ch);
}
c if c.is_ascii_whitespace() => {
let candidate = buf.trim().to_string();
if !candidate.is_empty() {
let mut peek_pos = chars.clone();
let next_nonws = peek_pos.find(|c| !c.is_ascii_whitespace());
match next_nonws {
Some('>') | Some('+') | Some('~') => {
buf.push(' ');
}
_ => {
parts.push((candidate, Combinator::Descendant));
buf.clear();
}
}
}
}
_ => buf.push(ch),
}
}
let last = buf.trim().to_string();
if !last.is_empty() {
if parts.is_empty() {
parts.push((last, Combinator::Descendant));
} else {
parts.push((last, Combinator::Descendant));
}
}
let compounds: Vec<CompoundSelector> = parts.iter().map(|(s, _)| CompoundSelector::parse(s)).collect();
let combinators: Vec<Combinator> = parts.iter().rev().skip(1).rev().map(|(_, c)| *c).collect();
Self { compounds, combinators }
}
fn check_compound_against(&self, i: usize, info: &AncestorInfo) -> bool {
let compound = &self.compounds[i];
let tag_ok = compound.tag.as_ref().map_or(true, |t| {
t == &info.type_name || t == "*"
});
let id_ok = compound.id.as_ref().map_or(true, |id| {
info.id.as_ref().map_or(false, |sid| sid == id)
});
let classes_ok = compound.classes.iter()
.all(|c| info.classes.contains(c));
tag_ok && id_ok && classes_ok
}
pub fn matches(
&self,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> bool {
if self.compounds.is_empty() {
return false;
}
let target = self.compounds.last().unwrap();
if !target.matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) {
return false;
}
if self.compounds.len() == 1 {
return true;
}
let mut ai = 0;
let mut si = preceding_siblings.len();
for i in (0..self.compounds.len() - 1).rev() {
let combinator = self.combinators[i];
match combinator {
Combinator::Descendant => {
let mut found = false;
for a in &ancestors[ai..] {
if self.check_compound_against(i, a) {
found = true;
break;
}
}
if !found { return false; }
}
Combinator::Child => {
if ai >= ancestors.len() { return false; }
let a = &ancestors[ai];
if !self.check_compound_against(i, a) { return false; }
ai += 1;
}
Combinator::NextSibling => {
if si == 0 { return false; }
let sib = &preceding_siblings[si - 1];
if !self.check_compound_against(i, sib) { return false; }
si -= 1;
}
Combinator::Subsequent => {
let mut found = false;
for sib in preceding_siblings[..si].iter().rev() {
if self.check_compound_against(i, sib) {
found = true;
break;
}
}
if !found { return false; }
}
}
}
true
}
pub fn specificity(&self) -> (u32, u32, u32) {
let mut a = 0u32;
let mut b = 0u32;
let mut c = 0u32;
for compound in &self.compounds {
let (sa, sb, sc) = compound.specificity();
a += sa;
b += sb;
c += sc;
}
(a, b, c)
}
pub fn as_simple(&self) -> Option<&CompoundSelector> {
if self.compounds.len() == 1 {
self.compounds.first()
} else {
None
}
}
pub fn has_pseudo_class(&self, pc: &str) -> bool {
self.compounds.iter().any(|c| c.pseudo_classes.iter().any(|p| p == pc))
}
}
#[derive(Debug, Clone)]
pub struct AncestorInfo {
pub type_name: String,
pub id: Option<String>,
pub classes: Vec<String>,
}
impl AncestorInfo {
pub fn new(type_name: &str, classes: Vec<String>) -> Self {
Self { type_name: type_name.to_string(), id: None, classes }
}
pub fn new_with_id(type_name: &str, id: Option<String>, classes: Vec<String>) -> Self {
Self { type_name: type_name.to_string(), id, classes }
}
}
#[derive(Debug, Clone, Default)]
pub struct StructuralContext {
pub sibling_index: usize,
pub sibling_total: usize,
pub type_index: usize,
pub type_total: usize,
pub has_children: bool,
pub is_root: bool,
}
fn nth_matches(expr: &str, n: usize) -> bool {
let expr = expr.trim();
if expr.eq_ignore_ascii_case("odd") {
return n % 2 == 1;
}
if expr.eq_ignore_ascii_case("even") {
return n % 2 == 0;
}
if let Ok(num) = expr.parse::<i32>() {
return n == num as usize;
}
let expr_lower = expr.to_lowercase();
if let Some(n_pos) = expr_lower.find('n') {
let a_str = expr_lower[..n_pos].trim();
let b_str = expr_lower[n_pos + 1..].trim();
let a = if a_str.is_empty() || a_str == "+" {
1
} else if a_str == "-" {
-1
} else {
a_str.parse::<i32>().unwrap_or(0)
};
let b = if b_str.is_empty() {
0
} else if b_str.starts_with('+') {
b_str[1..].trim().parse::<i32>().unwrap_or(0)
} else if b_str.starts_with('-') {
b_str.parse::<i32>().unwrap_or(0)
} else {
b_str.parse::<i32>().unwrap_or(0)
};
if a == 0 {
return n == b as usize;
}
let n_i32 = n as i32;
if a > 0 {
let k = n_i32 - b;
k >= 0 && k % a == 0
} else {
let diff = b - n_i32;
diff >= 0 && diff % a.abs() == 0
}
} else {
false
}
}
#[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 {
Self {
selector: ComplexSelector::parse(&selector_str),
properties,
}
}
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct StyleSheet { pub struct StyleSheet {
index: HashMap<String, HashMap<String, String>>, rules: Vec<StyleRule>,
} }
impl StyleSheet { impl StyleSheet {
@@ -11,80 +468,374 @@ impl StyleSheet {
} }
pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>) { pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>) {
self.index for part in split_selectors(&selector) {
.entry(selector.trim().to_string()) self.rules.push(StyleRule::build(part, properties.clone()));
.or_default() }
.extend(properties);
} }
#[inline] pub fn matching_rules<'a>(
pub fn resolve(&self, element_type: &str) -> Option<&HashMap<String, String>> { &'a self,
self.index.get(element_type.trim()) type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> Vec<&'a HashMap<String, String>> {
let mut matched: Vec<(usize, &StyleRule)> = self
.rules
.iter()
.enumerate()
.filter(|(_, rule)| {
rule.selector
.matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes)
})
.collect();
matched.sort_by(|(i, a), (j, b)| {
a.selector
.specificity()
.cmp(&b.selector.specificity())
.then_with(|| i.cmp(j))
});
matched.into_iter().map(|(_, rule)| &rule.properties).collect()
}
pub fn matching_pseudo_rules(
&self,
pseudo: &str,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut props = HashMap::new();
for rule in &self.rules {
if !rule.selector.has_pseudo_class(pseudo) {
continue;
}
if !rule.selector.matches(type_name, el_id, el_classes, &[pseudo], structural, ancestors, preceding_siblings, el_attributes) {
continue;
}
for (k, v) in &rule.properties {
props.insert(k.clone(), v.clone());
}
}
props
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.index.is_empty() self.rules.is_empty()
} }
} }
fn split_selectors(input: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0u32;
let mut start = 0usize;
for (i, ch) in input.char_indices() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
let part = input[start..i].trim();
if !part.is_empty() {
parts.push(part.to_string());
}
start = i + 1;
}
_ => {}
}
}
let last = input[start..].trim();
if !last.is_empty() {
parts.push(last.to_string());
}
parts
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Overflow {
#[default]
Visible,
Hidden,
Scroll,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Position {
#[default]
Static,
Relative,
Absolute,
Sticky,
Fixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Display {
#[default]
Block,
Flex,
Grid,
Inline,
None,
}
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct ComputedStyle { pub struct ComputedStyle {
pub font_size: Option<f32>, pub font_size: Option<f32>,
pub color: Option<iced::Color>, pub color: Option<iced::Color>,
pub padding: Option<f32>, pub padding: Option<f32>,
pub padding_top: Option<f32>,
pub padding_right: Option<f32>,
pub padding_bottom:Option<f32>,
pub padding_left: Option<f32>,
pub margin: Option<f32>,
pub margin_top: Option<f32>,
pub margin_right: Option<f32>,
pub margin_bottom: Option<f32>,
pub margin_left: Option<f32>,
pub background: Option<iced::Color>, pub background: Option<iced::Color>,
pub spacing: Option<f32>, pub spacing: Option<f32>,
pub border_radius: Option<f32>, pub border_radius: Option<f32>,
pub border_width: Option<f32>, pub border_width: Option<f32>,
pub border_color: Option<iced::Color>, pub border_color: Option<iced::Color>,
pub width: Option<f32>,
pub width: Option<iced::Length>,
pub height: Option<iced::Length>,
pub min_width: Option<f32>,
pub max_width: Option<f32>,
pub min_height: Option<f32>,
pub max_height: Option<f32>,
pub direction: Option<LayoutDirection>, pub direction: Option<LayoutDirection>,
pub align_items: Option<iced::Alignment>, pub align_items: Option<iced::Alignment>,
pub content_align: Option<ContentAlign>, pub content_align: Option<ContentAlign>,
pub flex_grow: Option<u16>,
pub position: Option<Position>,
pub top: Option<f32>,
pub right: Option<f32>,
pub bottom: Option<f32>,
pub left: Option<f32>,
pub 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<f32>,
pub text_align: Option<TextAlign>,
} }
impl ComputedStyle { impl ComputedStyle {
pub fn compute( pub fn compute(
inline: &HashMap<String, String>, inline: &[(Cow<'_, str>, Cow<'_, str>)],
sheet: Option<&HashMap<String, String>>, matched_sheets: &[&HashMap<String, String>],
) -> Self { ) -> Self {
let overflow = lookup("overflow", inline, matched_sheets).and_then(parse_overflow);
Self { Self {
font_size: lookup("font-size", inline, sheet).and_then(parse_size), font_size: lookup("font-size", inline, matched_sheets).and_then(parse_size),
color: lookup("color", inline, sheet).and_then(parse_color), color: lookup("color", inline, matched_sheets).and_then(parse_color),
padding: lookup("padding", inline, sheet).and_then(parse_size), padding: lookup("padding", inline, matched_sheets).and_then(parse_size),
background: lookup("background", inline, sheet) padding_top: lookup("padding-top", inline, matched_sheets).and_then(parse_size),
.or_else(|| lookup("background-color", inline, sheet)) padding_right: lookup("padding-right", inline, matched_sheets).and_then(parse_size),
.and_then(parse_color), padding_bottom: lookup("padding-bottom", inline, matched_sheets).and_then(parse_size),
spacing: lookup("spacing", inline, sheet) padding_left: lookup("padding-left", inline, matched_sheets).and_then(parse_size),
.or_else(|| lookup("gap", inline, sheet))
.and_then(parse_size),
border_radius: lookup("border-radius", inline, sheet).and_then(parse_size),
border_width: lookup("border-width", inline, sheet).and_then(parse_size),
border_color: lookup("border-color", inline, sheet).and_then(parse_color),
width: lookup("width", inline, sheet).and_then(parse_size),
direction: lookup("direction", inline, sheet).and_then(parse_direction), margin: lookup("margin", inline, matched_sheets).and_then(parse_size),
align_items: lookup("align-items", inline, sheet).and_then(parse_alignment), margin_top: lookup("margin-top", inline, matched_sheets).and_then(parse_size),
content_align: lookup("content-align", inline, sheet).and_then(parse_content_align), margin_right: lookup("margin-right", inline, matched_sheets).and_then(parse_size),
margin_bottom: lookup("margin-bottom", inline, matched_sheets).and_then(parse_size),
margin_left: lookup("margin-left", inline, matched_sheets).and_then(parse_size),
background: lookup("background", inline, matched_sheets)
.or_else(|| lookup("background-color", inline, matched_sheets))
.and_then(parse_color),
spacing: lookup("spacing", inline, matched_sheets)
.or_else(|| lookup("gap", inline, matched_sheets))
.and_then(parse_size),
border_radius: lookup("border-radius", inline, matched_sheets).and_then(parse_size),
border_width: lookup("border-width", inline, matched_sheets).and_then(parse_size),
border_color: lookup("border-color", inline, matched_sheets).and_then(parse_color),
width: lookup("width", inline, matched_sheets).and_then(parse_length),
height: lookup("height", inline, matched_sheets).and_then(parse_length),
min_width: lookup("min-width", inline, matched_sheets).and_then(parse_size),
max_width: lookup("max-width", inline, matched_sheets).and_then(parse_size),
min_height: lookup("min-height", inline, matched_sheets).and_then(parse_size),
max_height: lookup("max-height", inline, matched_sheets).and_then(parse_size),
direction: lookup("direction", inline, matched_sheets).and_then(parse_direction),
align_items: lookup("align-items", inline, matched_sheets).and_then(parse_alignment),
content_align: lookup("content-align", inline, matched_sheets).and_then(parse_content_align),
flex_grow: lookup("flex-grow", inline, matched_sheets)
.and_then(|s| s.trim().parse::<f32>().ok())
.map(|v| v as u16),
position: lookup("position", inline, matched_sheets).and_then(parse_position),
top: lookup("top", inline, matched_sheets).and_then(parse_size),
right: lookup("right", inline, matched_sheets).and_then(parse_size),
bottom: lookup("bottom", inline, matched_sheets).and_then(parse_size),
left: lookup("left", inline, matched_sheets).and_then(parse_size),
overflow_x: lookup("overflow-x", inline, matched_sheets)
.and_then(parse_overflow)
.or(overflow),
overflow_y: lookup("overflow-y", inline, matched_sheets)
.and_then(parse_overflow)
.or(overflow),
display: lookup("display", inline, matched_sheets).and_then(parse_display),
opacity: lookup("opacity", inline, matched_sheets).and_then(parse_opacity),
font_weight: lookup("font-weight", inline, matched_sheets).and_then(parse_font_weight),
line_height: lookup("line-height", inline, matched_sheets).and_then(parse_size),
text_align: lookup("text-align", inline, matched_sheets).and_then(parse_text_align),
} }
} }
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>) {
struct Override<'a>(&'a HashMap<String, String>);
impl<'a> Override<'a> {
fn get(&self, key: &str) -> Option<&'a str> { self.0.get(key).map(|s| s.as_str()) }
}
let ov = Override(sheet);
if let Some(v) = ov.get("font-size") { self.font_size = parse_size(v); }
if let Some(v) = ov.get("color") { self.color = parse_color(v); }
if let Some(v) = ov.get("padding") { self.padding = parse_size(v); }
if let Some(v) = ov.get("padding-top") { self.padding_top = parse_size(v); }
if let Some(v) = ov.get("padding-right") { self.padding_right = parse_size(v); }
if let Some(v) = ov.get("padding-bottom") { self.padding_bottom = parse_size(v); }
if let Some(v) = ov.get("padding-left") { self.padding_left = parse_size(v); }
if let Some(v) = ov.get("margin") { self.margin = parse_size(v); }
if let Some(v) = ov.get("margin-top") { self.margin_top = parse_size(v); }
if let Some(v) = ov.get("margin-right") { self.margin_right = parse_size(v); }
if let Some(v) = ov.get("margin-bottom") { self.margin_bottom = parse_size(v); }
if let Some(v) = ov.get("margin-left") { self.margin_left = parse_size(v); }
if let Some(v) = ov.get("background").or_else(|| ov.get("background-color")) {
self.background = parse_color(v);
}
if let Some(v) = ov.get("spacing").or_else(|| ov.get("gap")) {
self.spacing = parse_size(v);
}
if let Some(v) = ov.get("border-radius") { self.border_radius = parse_size(v); }
if let Some(v) = ov.get("border-width") { self.border_width = parse_size(v); }
if let Some(v) = ov.get("border-color") { self.border_color = parse_color(v); }
if let Some(v) = ov.get("width") { self.width = parse_length(v); }
if let Some(v) = ov.get("height") { self.height = parse_length(v); }
if let Some(v) = ov.get("direction") { self.direction = parse_direction(v); }
if let Some(v) = ov.get("align-items") { self.align_items = parse_alignment(v); }
if let Some(v) = ov.get("content-align") { self.content_align = parse_content_align(v); }
if let Some(v) = ov.get("flex-grow") { self.flex_grow = v.trim().parse::<f32>().ok().map(|x| x as u16); }
if let Some(v) = ov.get("position") { self.position = parse_position(v); }
if let Some(v) = ov.get("top") { self.top = parse_size(v); }
if let Some(v) = ov.get("right") { self.right = parse_size(v); }
if let Some(v) = ov.get("bottom") { self.bottom = parse_size(v); }
if let Some(v) = ov.get("left") { self.left = parse_size(v); }
if let Some(v) = ov.get("overflow") { let o = parse_overflow(v);
if o.is_some() { self.overflow_x = o; self.overflow_y = o; } }
if let Some(v) = ov.get("overflow-x") { self.overflow_x = parse_overflow(v); }
if let Some(v) = ov.get("overflow-y") { self.overflow_y = parse_overflow(v); }
if let Some(v) = ov.get("display") { self.display = parse_display(v); }
if let Some(v) = ov.get("opacity") { self.opacity = parse_opacity(v); }
if let Some(v) = ov.get("font-weight") { self.font_weight = parse_font_weight(v); }
if let Some(v) = ov.get("line-height") { self.line_height = parse_size(v); }
if let Some(v) = ov.get("text-align") { self.text_align = parse_text_align(v); }
}
} }
#[inline] #[inline]
fn lookup<'a>( fn lookup<'a>(
key: &str, key: &str,
inline: &'a HashMap<String, String>, inline: &'a [(Cow<'_, str>, Cow<'_, str>)],
sheet: Option<&'a HashMap<String, String>>, matched_sheets: &[&'a HashMap<String, String>],
) -> Option<&'a str> { ) -> Option<&'a str> {
if let Some(v) = inline.get(key) { for (k, v) in inline {
if **k == *key { return Some(v.as_ref()); }
if k.len() == key.len() + 6
&& k.starts_with("style:")
&& &k[6..] == key
{
return Some(v.as_ref());
}
}
for sheet in matched_sheets.iter().rev() {
if let Some(v) = sheet.get(key) {
return Some(v.as_str()); return Some(v.as_str());
} }
if let Some(v) = inline.get(&format!("style:{key}")) {
return Some(v.as_str());
} }
sheet.and_then(|s| s.get(key)).map(String::as_str) None
} }
pub fn parse_overflow(s: &str) -> Option<Overflow> {
let s = s.trim();
if s.eq_ignore_ascii_case("visible") { Some(Overflow::Visible) }
else if s.eq_ignore_ascii_case("hidden") { Some(Overflow::Hidden) }
else if s.eq_ignore_ascii_case("scroll") { Some(Overflow::Scroll) }
else if s.eq_ignore_ascii_case("auto") { Some(Overflow::Auto) }
else { None }
}
pub fn parse_position(s: &str) -> Option<Position> {
let s = s.trim();
if s.eq_ignore_ascii_case("fixed") { Some(Position::Fixed) }
else if s.eq_ignore_ascii_case("sticky") { Some(Position::Sticky) }
else if s.eq_ignore_ascii_case("absolute") { Some(Position::Absolute) }
else if s.eq_ignore_ascii_case("relative") { Some(Position::Relative) }
else if s.eq_ignore_ascii_case("static") { Some(Position::Static) }
else { None }
}
pub fn parse_length(s: &str) -> Option<iced::Length> {
let s = s.trim();
if s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("100%") ||
s.eq_ignore_ascii_case("100vw") || s.eq_ignore_ascii_case("100vh") ||
s.eq_ignore_ascii_case("stretch") {
return Some(iced::Length::Fill);
}
if s.eq_ignore_ascii_case("shrink") || s.eq_ignore_ascii_case("fit-content") || s.eq_ignore_ascii_case("auto") {
return Some(iced::Length::Shrink);
}
let val = s.trim_end_matches(|c: char| c.is_alphabetic() || c == '%').parse::<f32>().ok()?;
if s.ends_with('%') {
if val >= 100.0 {
Some(iced::Length::Fill)
} else {
Some(iced::Length::FillPortion(val as u16))
}
} else {
Some(iced::Length::Fixed(val))
}
}
pub fn parse_color(s: &str) -> Option<iced::Color> { pub fn parse_color(s: &str) -> Option<iced::Color> {
let s = s.trim(); let s = s.trim();
@@ -148,36 +899,91 @@ pub enum ContentAlign {
End, End,
} }
pub fn parse_direction(s: &str) -> Option<LayoutDirection> { #[derive(Debug, Clone, Copy, PartialEq, Eq)]
match s.trim().to_lowercase().as_str() { pub enum TextAlign {
"horizontal" | "row" => Some(LayoutDirection::Row), Left,
"vertical" | "column" => Some(LayoutDirection::Column), Center,
"grid" => Some(LayoutDirection::Grid), Right,
_ => None, }
impl From<TextAlign> for iced::alignment::Horizontal {
fn from(ta: TextAlign) -> Self {
match ta {
TextAlign::Left => iced::alignment::Horizontal::Left,
TextAlign::Center => iced::alignment::Horizontal::Center,
TextAlign::Right => iced::alignment::Horizontal::Right,
} }
}
}
pub fn parse_direction(s: &str) -> Option<LayoutDirection> {
let s = s.trim();
if s.eq_ignore_ascii_case("horizontal") || s.eq_ignore_ascii_case("row") { Some(LayoutDirection::Row) }
else if s.eq_ignore_ascii_case("vertical") || s.eq_ignore_ascii_case("column") { Some(LayoutDirection::Column) }
else if s.eq_ignore_ascii_case("grid") { Some(LayoutDirection::Grid) }
else { None }
} }
pub fn parse_alignment(s: &str) -> Option<iced::Alignment> { pub fn parse_alignment(s: &str) -> Option<iced::Alignment> {
match s.trim().to_lowercase().as_str() { let s = s.trim();
"start" => Some(iced::Alignment::Start), if s.eq_ignore_ascii_case("start") { Some(iced::Alignment::Start) }
"center" => Some(iced::Alignment::Center), else if s.eq_ignore_ascii_case("center") { Some(iced::Alignment::Center) }
"end" => Some(iced::Alignment::End), else if s.eq_ignore_ascii_case("end") { Some(iced::Alignment::End) }
_ => None, else { None }
}
} }
pub fn parse_content_align(s: &str) -> Option<ContentAlign> { pub fn parse_content_align(s: &str) -> Option<ContentAlign> {
match s.trim().to_lowercase().as_str() { let s = s.trim();
"start" | "left" | "top" => Some(ContentAlign::Start), if s.eq_ignore_ascii_case("start") || s.eq_ignore_ascii_case("left") || s.eq_ignore_ascii_case("top") { Some(ContentAlign::Start) }
"center" => Some(ContentAlign::Center), else if s.eq_ignore_ascii_case("center") { Some(ContentAlign::Center) }
"end" | "right" | "bottom" => Some(ContentAlign::End), else if s.eq_ignore_ascii_case("end") || s.eq_ignore_ascii_case("right") || s.eq_ignore_ascii_case("bottom") { Some(ContentAlign::End) }
_ => None, else { None }
}
pub fn parse_display(s: &str) -> Option<Display> {
let s = s.trim();
if s.eq_ignore_ascii_case("none") { Some(Display::None) }
else if s.eq_ignore_ascii_case("block") { Some(Display::Block) }
else if s.eq_ignore_ascii_case("flex") { Some(Display::Flex) }
else if s.eq_ignore_ascii_case("grid") { Some(Display::Grid) }
else if s.eq_ignore_ascii_case("inline") { Some(Display::Inline) }
else { None }
}
pub fn parse_opacity(s: &str) -> Option<f32> {
let v = s.trim().parse::<f32>().ok()?;
Some(v.clamp(0.0, 1.0))
}
pub fn parse_font_weight(s: &str) -> Option<u16> {
let s = s.trim();
match s {
"normal" => Some(400),
"bold" => Some(700),
"lighter" => Some(300),
"bolder" => Some(900),
_ => s.parse::<f32>().ok().map(|v| v as u16),
} }
} }
pub fn parse_text_align(s: &str) -> Option<TextAlign> {
let s = s.trim();
if s.eq_ignore_ascii_case("left") { Some(TextAlign::Left) }
else if s.eq_ignore_ascii_case("center") { Some(TextAlign::Center) }
else if s.eq_ignore_ascii_case("right") { Some(TextAlign::Right) }
else { None }
}
pub fn parse_size(s: &str) -> Option<f32> { pub fn parse_size(s: &str) -> Option<f32> {
s.trim() let s = s.trim();
.trim_end_matches(|c: char| c.is_alphabetic() || c == '%') if s.ends_with('%') || s.eq_ignore_ascii_case("auto") ||
s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("stretch") {
return None;
}
if s.eq_ignore_ascii_case("vw") || s.eq_ignore_ascii_case("vh") {
return None;
}
s.trim_end_matches(|c: char| c.is_alphabetic())
.parse::<f32>() .parse::<f32>()
.ok() .ok()
} }

View File

@@ -1,38 +1,45 @@
use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use super::style::{ComputedStyle, StyleSheet}; use super::style::{ComputedStyle, StyleSheet};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Element { pub struct Element<'a> {
pub type_name: String, pub type_name: &'a str,
pub properties: HashMap<String, String>, pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>,
pub children: Vec<Element>, pub children: Vec<Element<'a>>,
pub computed_style: ComputedStyle, pub computed_style: ComputedStyle,
} }
impl Element { impl<'a> Element<'a> {
pub fn new(type_name: String) -> Self { 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(),
} }
} }
} }
#[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, String>,
pub rhei_scripts: Vec<String>, pub rhei_scripts: Vec<String>,
pub stylesheet: StyleSheet, pub stylesheet: StyleSheet,

View File

@@ -11,6 +11,7 @@ pub enum Message {
InputChanged(Option<String>, String), InputChanged(Option<String>, String),
ToggleChanged(Option<String>, bool), ToggleChanged(Option<String>, bool),
SliderChanged(Option<String>, f64), SliderChanged(Option<String>, f64),
WindowScrolled(f32),
} }
#[derive(Parser)] #[derive(Parser)]

File diff suppressed because it is too large Load Diff