feat: new styles
This commit is contained in:
33
Cargo.lock
generated
33
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
570
STYLE-SYSTEM-COMPLETION-PLAN.md
Normal file
570
STYLE-SYSTEM-COMPLETION-PLAN.md
Normal 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`:
|
||||||
|
- Записать дерево селектора в байткод.
|
||||||
|
- На каждый тип узла — свой подопкод (0x30–0x3F).
|
||||||
|
- Компилятор: сериализация структуры селектора.
|
||||||
|
- Читатель (reader.rs): десериализация обратно в структуру.
|
||||||
|
|
||||||
|
### 1.4 Runtime matching (interpreter/mod.rs)
|
||||||
|
|
||||||
|
- Заменить `collect_matching_styles()` на алгоритм обхода дерева селекторов.
|
||||||
|
- **Специфичность:** inline > id > class/attr/pseudo > tag.
|
||||||
|
- При одинаковой специфичности — порядок объявления (последний побеждает).
|
||||||
|
- **Комбинаторы:** при обходе VDOM подниматься вверх по предкам/соседям.
|
||||||
|
- Кэширование результатов matching для производительности.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Псевдоклассы (высокий приоритет)
|
||||||
|
|
||||||
|
### 2.1 Интерактивные (`:hover`, `:active`, `:focus`)
|
||||||
|
|
||||||
|
**Сейчас:** Только у Button ховер/нажатие — хардкод в renderer.
|
||||||
|
|
||||||
|
**Нужно:**
|
||||||
|
- Хранить состояние интерактивности в VDOM/Element (`ElementState`).
|
||||||
|
- В renderer передавать статус (hovered/pressed/focused) в `collect_matching_styles`.
|
||||||
|
- `:hover`-стили переопределяют базовые.
|
||||||
|
- Плавный переход между состояниями (см. transitions).
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- `types.rs`: `Element` может получить поле `state: ElementState`.
|
||||||
|
- `renderer.rs`: у `render_element` появляется доступ к состоянию мыши.
|
||||||
|
- `style.rs`: `ComputedStyle::compute()` принимает псевдоклассы.
|
||||||
|
|
||||||
|
### 2.2 Структурные (`:first-child`, `:nth-child`, `:last-child`, `:first-of-type`, `:last-of-type`, `:empty`, `:root`)
|
||||||
|
|
||||||
|
**Сейчас:** Не поддерживаются.
|
||||||
|
|
||||||
|
**Нужно:**
|
||||||
|
- При matching вычислять позицию элемента среди siblings.
|
||||||
|
- Для `:nth-child(An+B)` — парсить выражение и проверять `(index - B) % A == 0`.
|
||||||
|
- `:empty` — `el.children.is_empty()`.
|
||||||
|
- `:root` — элемент верхнего уровня в VDOM.
|
||||||
|
|
||||||
|
### 2.3 Состояния (`:disabled`, `:enabled`, `:checked`)
|
||||||
|
|
||||||
|
- `:disabled`/`:enabled` — по свойству `disabled`.
|
||||||
|
- `:checked` — по свойству `checked`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Медиа-запросы (`@media`) (средний приоритет)
|
||||||
|
|
||||||
|
### 3.1 Парсер
|
||||||
|
|
||||||
|
```glts
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
Panel { direction: vertical }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Парсить `@media` в `Directive::MediaQuery { query, child_span }`.
|
||||||
|
- Поддержка: `width`, `height`, `min-width`, `max-width`, `orientation`, `prefers-color-scheme`.
|
||||||
|
- Логические операторы: `and`, `or`, `not`, `,` (or).
|
||||||
|
|
||||||
|
### 3.2 Runtime
|
||||||
|
|
||||||
|
- Собрать информацию об окне (размер, тема ОС).
|
||||||
|
- При изменении размера окна переоценивать media queries.
|
||||||
|
- Хранить в `Document::media_state: MediaState`.
|
||||||
|
- `collect_matching_styles` проверяет media query перед добавлением правил.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Анимации (`@anim`) (средний приоритет)
|
||||||
|
|
||||||
|
### 4.1 Парсинг — уже есть
|
||||||
|
|
||||||
|
`@anim name { from { ... } to { ... } }` — парсится и компилируется.
|
||||||
|
|
||||||
|
### 4.2 Runtime — НЕ реализован
|
||||||
|
|
||||||
|
**Сейчас:** `OP_STYLE_ANIM` пропускается.
|
||||||
|
|
||||||
|
**Нужно:**
|
||||||
|
1. Хранить `KeyframeAnimation` в `StyleSheet` (не только в байткоде, но и в runtime-структуре).
|
||||||
|
2. В `Element` поле `animation_state: HashMap<String, AnimationInstance>`.
|
||||||
|
3. Система тиков:
|
||||||
|
- Iced `subscription` на каждый кадр (`on_every_event` → `Event::Window(RedrawRequested)`).
|
||||||
|
- При каждом тике обновлять `elapsed` для активных анимаций.
|
||||||
|
- Интерполировать между keyframes.
|
||||||
|
4. Свойства `animation-name`, `animation-duration`, `animation-timing-function`, `animation-delay`, `animation-iteration-count`, `animation-fill-mode`.
|
||||||
|
5. Поддержка `@keyframes` с процентами: `0% { ... } 50% { ... } 100% { ... }`.
|
||||||
|
|
||||||
|
### 4.3 Timing functions
|
||||||
|
|
||||||
|
- `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out`.
|
||||||
|
- `cubic-bezier(p1x, p1y, p2x, p2y)`.
|
||||||
|
- `steps(n, direction)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Транзишены (`transition`) (средний приоритет)
|
||||||
|
|
||||||
|
### 5.1 Свойства
|
||||||
|
|
||||||
|
- `transition-property`, `transition-duration`, `transition-timing-function`, `transition-delay`.
|
||||||
|
- Шорткат `transition: all 0.3s ease`.
|
||||||
|
|
||||||
|
### 5.2 Runtime
|
||||||
|
|
||||||
|
- При изменении `ComputedStyle` (например, при ховере) не применять мгновенно, а запускать интерполяцию.
|
||||||
|
- Хранить `TransitionState` на элемент: `HashMap<String, (start_value, end_value, elapsed, duration, easing)>`.
|
||||||
|
- Каждый кадр обновлять transitioning-свойства.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Новые CSS-свойства в ComputedStyle
|
||||||
|
|
||||||
|
### 6.1 Типографика
|
||||||
|
|
||||||
|
| Свойство | Тип | Парсер |
|
||||||
|
|----------|-----|--------|
|
||||||
|
| `font-family` | `Option<String>` | список шрифтов |
|
||||||
|
| `font-weight` | `Option<u16>` | `normal(400)`, `bold(700)`, числовое |
|
||||||
|
| `font-style` | `Option<FontStyle>` | `normal`, `italic`, `oblique` |
|
||||||
|
| `line-height` | `Option<f32>` | числовое или `normal` |
|
||||||
|
| `letter-spacing` | `Option<f32>` | px |
|
||||||
|
| `text-align` | `Option<TextAlign>` | `left`, `center`, `right`, `justify` |
|
||||||
|
| `text-decoration` | `Option<TextDecoration>` | `none`, `underline`, `line-through` |
|
||||||
|
| `text-transform` | `Option<TextTransform>` | `none`, `uppercase`, `lowercase`, `capitalize` |
|
||||||
|
| `white-space` | `Option<WhiteSpace>` | `normal`, `nowrap`, `pre` |
|
||||||
|
| `word-break` | `Option<WordBreak>` | `normal`, `break-all`, `keep-all` |
|
||||||
|
| `text-overflow` | `Option<TextOverflow>` | `clip`, `ellipsis` |
|
||||||
|
|
||||||
|
### 6.2 Фон и границы
|
||||||
|
|
||||||
|
| Свойство | Тип | Парсер |
|
||||||
|
|----------|-----|--------|
|
||||||
|
| `opacity` | `Option<f32>` | 0.0–1.0 |
|
||||||
|
| `background-image` | `Option<String>` | `url(...)` |
|
||||||
|
| `background-repeat` | `Option<BgRepeat>` | `repeat`, `no-repeat` |
|
||||||
|
| `background-size` | `Option<BgSize>` | `cover`, `contain`, px, % |
|
||||||
|
| `background-position` | `Option<BgPos>` | `center`, `top left`, px |
|
||||||
|
| `linear-gradient(...)` | `Option<Gradient>` | парсить в `Value::Call` и обрабатывать |
|
||||||
|
| `box-shadow` | `Option<Vec<Shadow>>` | `offset-x offset-y blur spread color` |
|
||||||
|
| `text-shadow` | `Option<Vec<Shadow>>` | то же |
|
||||||
|
| `outline` | `Option<Outline>` | width, style, color |
|
||||||
|
| `border-style` | `Option<BorderStyle>` | `solid`, `dashed`, `dotted` |
|
||||||
|
| `border` (шорткат) | — | разворачивать в width/style/color |
|
||||||
|
|
||||||
|
### 6.3 Flexbox (расширение)
|
||||||
|
|
||||||
|
| Свойство | Тип |
|
||||||
|
|----------|-----|
|
||||||
|
| `justify-content` | `Option<JustifyContent>` | `start`, `center`, `end`, `space-between`, `space-around`, `space-evenly` |
|
||||||
|
| `flex-wrap` | `Option<FlexWrap>` | `nowrap`, `wrap`, `wrap-reverse` |
|
||||||
|
| `flex-direction` | `Option<FlexDirection>` | (расширение `LayoutDirection`) |
|
||||||
|
| `align-self` | `Option<Alignment>` | переопределяет `align-items` для конкретного элемента |
|
||||||
|
| `align-content` | `Option<AlignContent>` | multi-line выравнивание |
|
||||||
|
| `flex` (шорткат) | — | grow, shrink, basis |
|
||||||
|
| `flex-shrink` | `Option<f32>` | |
|
||||||
|
| `flex-basis` | `Option<Length>` | |
|
||||||
|
|
||||||
|
### 6.4 Grid Layout
|
||||||
|
|
||||||
|
| Свойство | Тип |
|
||||||
|
|----------|-----|
|
||||||
|
| `grid-template-columns` | `Option<Vec<GridTrack>>` | `1fr 1fr`, `repeat(3, 1fr)`, `auto` |
|
||||||
|
| `grid-template-rows` | `Option<Vec<GridTrack>>` | |
|
||||||
|
| `grid-gap` / `gap` | уже есть как `spacing` | |
|
||||||
|
| `grid-column` | `Option<(u32, u32)>` | start / end |
|
||||||
|
| `grid-row` | `Option<(u32, u32)>` | |
|
||||||
|
| `grid-auto-flow` | `Option<GridFlow>` | |
|
||||||
|
|
||||||
|
### 6.5 Позиционирование (расширение)
|
||||||
|
|
||||||
|
| Свойство | Тип | Статус |
|
||||||
|
|----------|-----|--------|
|
||||||
|
| `position: absolute` | — | сейчас мэппится в Static |
|
||||||
|
| `position: relative` | — | сейчас мэппится в Static |
|
||||||
|
| `position: sticky` | — | сейчас мэппится в Static |
|
||||||
|
| `z-index` | `Option<i32>` | |
|
||||||
|
| `display` | `Option<Display>` | `block`, `flex`, `grid`, `none`, `inline` |
|
||||||
|
|
||||||
|
### 6.6 Прочее
|
||||||
|
|
||||||
|
| Свойство | Тип |
|
||||||
|
|----------|-----|
|
||||||
|
| `visibility` | `Option<Visibility>` | `visible`, `hidden` |
|
||||||
|
| `cursor` | `Option<Cursor>` | `pointer`, `default`, `text` |
|
||||||
|
| `pointer-events` | `Option<PointerEvents>` | `auto`, `none` |
|
||||||
|
| `transform` | `Option<Vec<Transform>>` | `translate(x,y)`, `scale(s)`, `rotate(a)` |
|
||||||
|
| `transform-origin` | `Option<String>` | |
|
||||||
|
| `backdrop-filter` | `Option<Vec<Filter>>` | `blur(10px)`, `brightness(1.2)` |
|
||||||
|
| `filter` | `Option<Vec<Filter>>` | то же |
|
||||||
|
| `list-style` | `Option<ListStyle>` | |
|
||||||
|
| `isolation` | `Option<Isolation>` | `auto`, `isolate` |
|
||||||
|
| `mix-blend-mode` | `Option<BlendMode>` | `multiply`, `screen` |
|
||||||
|
|
||||||
|
### 6.7 Итого: ~40–60 новых полей в ComputedStyle
|
||||||
|
|
||||||
|
Каждое поле: тип `Option<T>`, значение по умолчанию `None`.
|
||||||
|
Парсинг: новая функция `parse_<property>(s: &str) -> Option<T>` для каждого.
|
||||||
|
Lookup в `ComputedStyle::compute()`.
|
||||||
|
|
||||||
|
**Рендеринг в Iced:**
|
||||||
|
- Iced 0.14 поддерживает Background::Gradient.
|
||||||
|
- Box-shadow пока не поддерживается, но можно эмулировать через container style с offset-тенью.
|
||||||
|
- Transforms и filters — Iced пока не нативно; нужен кастомный widget или пропуск.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Система каскада и наследования
|
||||||
|
|
||||||
|
### 7.1 Наследование (сейчас: только `color`, `font-size`)
|
||||||
|
|
||||||
|
Добавить наследование для:
|
||||||
|
- `font-family`, `font-weight`, `font-style`, `line-height`, `letter-spacing`, `text-align`, `white-space`, `word-break`, `text-transform`, `visibility`, `cursor`, `pointer-events`.
|
||||||
|
|
||||||
|
Механизм: в `render_element()` передавать не только `parent_color` и `parent_font_size`, а `&ComputedStyle` родителя или отдельный `InheritedStyle`.
|
||||||
|
|
||||||
|
### 7.2 `inherit` / `initial` / `unset`
|
||||||
|
|
||||||
|
- Специальные значения для любого свойства.
|
||||||
|
|
||||||
|
### 7.3 `!important`
|
||||||
|
|
||||||
|
- Флаг важности в правиле. Переопределяет специфичность.
|
||||||
|
- Парсить `value!important`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. CSS-функции и значения
|
||||||
|
|
||||||
|
### 8.1 `calc()`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
enum CalcValue {
|
||||||
|
Number(f32),
|
||||||
|
Percentage(f32),
|
||||||
|
Add(Box<CalcValue>, Box<CalcValue>),
|
||||||
|
Sub(Box<CalcValue>, Box<CalcValue>),
|
||||||
|
Mul(Box<CalcValue>, Box<CalcValue>),
|
||||||
|
Div(Box<CalcValue>, Box<CalcValue>),
|
||||||
|
Var(String),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Парсить `calc(100% - 20px)`.
|
||||||
|
- Вычислять в runtime с учётом контекста (размер родителя).
|
||||||
|
|
||||||
|
### 8.2 `var()` — CSS custom properties
|
||||||
|
|
||||||
|
- `--my-var: value;` в правилах.
|
||||||
|
- `var(--my-var, fallback)` при использовании.
|
||||||
|
- Хранить custom properties в `ComputedStyle.custom_props: HashMap<String, String>`.
|
||||||
|
- Наследуются по умолчанию.
|
||||||
|
|
||||||
|
### 8.3 `min()`, `max()`, `clamp()`
|
||||||
|
|
||||||
|
- `min(100%, 500px)`, `max(200px, 50%)`, `clamp(200px, 50%, 500px)`.
|
||||||
|
|
||||||
|
### 8.4 `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `oklch()`, `color-mix()`
|
||||||
|
|
||||||
|
- Сейчас только `#hex` и 3 named colors.
|
||||||
|
- Парсить `rgb(255, 0, 0)`, `rgba(255, 0, 0, 0.5)`.
|
||||||
|
- Расширить `parse_color()`.
|
||||||
|
|
||||||
|
### 8.5 Named colors
|
||||||
|
|
||||||
|
- Расширить словарь до 140+ CSS named colors (AliceBlue, ...).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. @-правила
|
||||||
|
|
||||||
|
### 9.1 `@import`
|
||||||
|
|
||||||
|
```glts
|
||||||
|
@import "theme.glts"
|
||||||
|
```
|
||||||
|
|
||||||
|
- В парсере: обработать `@import`, загрузить файл, влить переменные и миксины.
|
||||||
|
- Может быть сложным (циклы, кэширование). Опционально на раннем этапе.
|
||||||
|
|
||||||
|
### 9.2 `@font-face`
|
||||||
|
|
||||||
|
```glts
|
||||||
|
@font-face {
|
||||||
|
font-family: "MyFont"
|
||||||
|
src: fs:/fonts/myfont.ttf
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Регистрировать в Iced через `font::Family`.
|
||||||
|
- Потребует `iced::font::load()`.
|
||||||
|
|
||||||
|
### 9.3 `@scope` (CSS Cascading Layers)
|
||||||
|
|
||||||
|
- `@scope (.card) { ... }` — ограничение области действия правил.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Рендеринг (renderer.rs)
|
||||||
|
|
||||||
|
### 10.1 Псевдоклассы в рендере
|
||||||
|
|
||||||
|
- Для интерактивных элементов передавать `button::Status`.
|
||||||
|
- `:hover`/`:active`-стили должны пересчитываться при изменении статуса.
|
||||||
|
|
||||||
|
### 10.2 Flexbox (полноценный)
|
||||||
|
|
||||||
|
- `justify-content`: Iced `Row`/`Column` не имеют нативного justifyContent; эмулировать через наполнители (`Length::Fill`).
|
||||||
|
- `flex-wrap`: нужен `Flow` widget или эмуляция строками.
|
||||||
|
|
||||||
|
### 10.3 Grid
|
||||||
|
|
||||||
|
- Полноценный CSS Grid: имплементировать кастомный Iced widget или разбивать на строки/колонки вручную.
|
||||||
|
- `grid-template-columns: 1fr 1fr 1fr` → разбивка children на ряды.
|
||||||
|
- `grid-column`/`grid-row`: позиционирование ячеек.
|
||||||
|
|
||||||
|
### 10.4 `display: none`
|
||||||
|
|
||||||
|
- Пропускать элемент при рендеринге (уже частично — `None` возвращается).
|
||||||
|
|
||||||
|
### 10.5 `z-index`
|
||||||
|
|
||||||
|
- Сортировать fixed-слой по z-index внутри stack.
|
||||||
|
|
||||||
|
### 10.6 `position: absolute` / `relative`
|
||||||
|
|
||||||
|
- `relative`: смещение через padding контейнера.
|
||||||
|
- `absolute`: позиционирование относительно предка с `position: relative` (или окна). Эмулировать через stack слои, как `fixed`.
|
||||||
|
|
||||||
|
### 10.7 `position: sticky`
|
||||||
|
|
||||||
|
- Эмулировать через Iced scrollable + перехват событий скролла.
|
||||||
|
- Пока не поддерживается Iced нативно. Можно через subscription на scroll position + ручное позиционирование.
|
||||||
|
|
||||||
|
### 10.8 `opacity`
|
||||||
|
|
||||||
|
- Iced: `widget.opacity(f32)` (iced 0.14+).
|
||||||
|
|
||||||
|
### 10.9 `box-shadow`, `text-shadow`
|
||||||
|
|
||||||
|
- Эмуляция: container с подложкой (offset background) или кастомный шейдер.
|
||||||
|
- Iced 0.14 имеет `Border` только с `color`, `width`, `radius`. Shadow — нет.
|
||||||
|
|
||||||
|
### 10.10 `outline`
|
||||||
|
|
||||||
|
- Через border с `outline-offset` эмуляцией.
|
||||||
|
|
||||||
|
### 10.11 Трансформы (`transform`)
|
||||||
|
|
||||||
|
- Iced не поддерживает transforms нативно.
|
||||||
|
- Потребуется кастомный widget с `lyon` или `vello` для path-трансформаций.
|
||||||
|
- На раннем этапе — документировать как unsupported.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Инструментарий разработчика
|
||||||
|
|
||||||
|
### 11.1 Вывод вычисленных стилей
|
||||||
|
|
||||||
|
- Флаг `--debug-styles` в CLI: печатать `ComputedStyle` каждого элемента.
|
||||||
|
|
||||||
|
### 11.2 Инспектор элементов
|
||||||
|
|
||||||
|
- Iced debug overlay: `iced::widget::pane_grid` с инспектором.
|
||||||
|
- Показывать matched selectors, specificity, computed properties.
|
||||||
|
|
||||||
|
### 11.3 Hot-reload стилей
|
||||||
|
|
||||||
|
- Перекомпиляция `.glts` без перезапуска приложения.
|
||||||
|
- Обновление bytecode в runtime.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Порядок реализации (приоритеты)
|
||||||
|
|
||||||
|
### Фаза 1: Критическое (MVP+)
|
||||||
|
1. Структурированные селекторы + matching engine со специфичностью
|
||||||
|
2. Комбинаторы (особенно descendant)
|
||||||
|
3. Псевдоклассы `:hover`, `:active`, `:focus`
|
||||||
|
4. Селектор по ID (`#id`)
|
||||||
|
5. Множественные селекторы через запятую
|
||||||
|
6. Псевдоклассы `:first-child`, `:nth-child`, `:last-child`, `:empty`
|
||||||
|
|
||||||
|
### Фаза 2: Визуальное обогащение
|
||||||
|
7. Новые свойства: opacity, font-weight, text-align, justify-content, line-height
|
||||||
|
8. fill-шорткаты для flex (flex-grow/shrink/basis)
|
||||||
|
9. `position: absolute` / `relative` / `sticky`
|
||||||
|
10. `z-index`
|
||||||
|
11. `display: none`
|
||||||
|
12. `calc()`, `min()`, `max()`, `clamp()`
|
||||||
|
13. `rgb()`, `rgba()`, `hsl()` + расширенный словарь named colors
|
||||||
|
14. `!important`
|
||||||
|
|
||||||
|
### Фаза 3: Продвинутое
|
||||||
|
15. `@media` queries
|
||||||
|
16. Animations (playback)
|
||||||
|
17. Transitions
|
||||||
|
18. `box-shadow`, `text-shadow` (эмуляция)
|
||||||
|
19. CSS Grid
|
||||||
|
20. `flex-wrap` + `justify-content` полноценно
|
||||||
|
21. `var()` CSS custom properties
|
||||||
|
22. `@import`
|
||||||
|
23. `@font-face`
|
||||||
|
|
||||||
|
### Фаза 4: Экспертное
|
||||||
|
24. `transform`, `transform-origin`
|
||||||
|
25. `backdrop-filter`, `filter`
|
||||||
|
26. `gradient` (linear, radial)
|
||||||
|
27. `color-mix()`, `oklch()`
|
||||||
|
28. Hot-reload
|
||||||
|
29. DevTools инспектор
|
||||||
|
30. `@scope`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Архитектурные изменения
|
||||||
|
|
||||||
|
### 13.1 `ComputedStyle` → разбить на подс-труктуры
|
||||||
|
|
||||||
|
Вместо 40+ плоских полей:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ComputedStyle {
|
||||||
|
pub box_model: BoxModel,
|
||||||
|
pub typography: Typography,
|
||||||
|
pub background: BackgroundStyle,
|
||||||
|
pub border: BorderStyle,
|
||||||
|
pub flex: FlexStyle,
|
||||||
|
pub grid: GridStyle,
|
||||||
|
pub position: Positioning,
|
||||||
|
pub effects: Effects,
|
||||||
|
pub overflow: OverflowStyle,
|
||||||
|
pub animation: AnimationStyle,
|
||||||
|
pub custom: HashMap<String, String>,
|
||||||
|
pub display: Option<Display>,
|
||||||
|
pub visibility: Option<Visibility>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.2 Selector engine — отдельный модуль
|
||||||
|
|
||||||
|
```rust
|
||||||
|
mod selector {
|
||||||
|
pub struct Selector { ... }
|
||||||
|
pub fn parse(input: &str) -> Result<Selector, String>;
|
||||||
|
pub fn specificity(&self) -> (u32, u32, u32);
|
||||||
|
pub fn matches(&self, el: &Element, ctx: &MatchContext) -> bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.3 StyleSheet — расширение
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct StyleRule {
|
||||||
|
pub selector: Selector,
|
||||||
|
pub properties: HashMap<String, String>,
|
||||||
|
pub source_order: u32,
|
||||||
|
pub media: Option<MediaQuery>,
|
||||||
|
pub important: HashSet<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.4 Система тиков для анимаций/транзишенов
|
||||||
|
|
||||||
|
- Новый модуль `animation` в runtime.
|
||||||
|
- `AnimationEngine` с `HashMap<AnimationId, AnimationInstance>`.
|
||||||
|
- Iced subscription: `time::every(Duration::from_millis(16))` (60fps).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Оценка сложности
|
||||||
|
|
||||||
|
| Компонент | Новые файлы | Изменённые файлы | Пример LOC |
|
||||||
|
|-----------|-------------|------------------|------------|
|
||||||
|
| Selector parser | `glt/src/selector.rs` | `style_parser.rs` | ~500 |
|
||||||
|
| Selector matching | `runtime/src/selector.rs` | `mod.rs` | ~400 |
|
||||||
|
| ComputedStyle расширение | — | `style.rs` | +400 |
|
||||||
|
| Медиа-запросы | — | `style_parser.rs`, `mod.rs` | ~300 |
|
||||||
|
| Анимации | `runtime/src/animation.rs` | `style.rs`, `mod.rs`, `app.rs` | ~500 |
|
||||||
|
| Транзишены | `runtime/src/transition.rs` | | ~400 |
|
||||||
|
| Псевдоклассы | — | `mod.rs`, `renderer.rs`, `types.rs` | ~300 |
|
||||||
|
| Новые парсеры свойств | — | `style.rs` | +600 |
|
||||||
|
| Рендеринг | — | `renderer.rs` | +500 |
|
||||||
|
| Flex/Grid | `runtime/src/layout.rs` | `renderer.rs` | ~400 |
|
||||||
|
| calc/var | `runtime/src/values.rs` | `style.rs` | ~300 |
|
||||||
|
| @import/@font-face | — | `style_parser.rs`, `renderer.rs` | ~200 |
|
||||||
|
| Инструменты | `runtime/src/inspector.rs` | `cli.rs`, `app.rs` | ~300 |
|
||||||
|
|
||||||
|
**Итого:** ~5–7 новых файлов, ~15 изменённых, ~5000–6000 строк нового кода.
|
||||||
BIN
desktop.glbc
Normal file
BIN
desktop.glbc
Normal file
Binary file not shown.
316
desktop.gltm
316
desktop.gltm
@@ -1,207 +1,209 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 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",
|
||||||
refresh_rate = 60,
|
refresh_rate = 60,
|
||||||
debug_mode = true,
|
debug_mode = true,
|
||||||
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()
|
|
||||||
|
|
||||||
// ── Секция A: Control Panel (Переменные и биндинги) ──────────────────
|
|
||||||
Header "A. Control Panel & State Bindings"
|
|
||||||
|
|
||||||
Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {}
|
|
||||||
|
|
||||||
Panel {
|
|
||||||
Input(placeholder=$search_query, value=$search_query)
|
|
||||||
Toggle(label="Enable Live Metrics", value=$is_enabled)
|
|
||||||
Text !rhei: { "Live search query: " + search_query }
|
|
||||||
}
|
|
||||||
|
|
||||||
@if $is_enabled {
|
|
||||||
Text "✅ Features are ON. You can adjust the system."
|
|
||||||
} @else {
|
|
||||||
Text "⛔ Features are OFF."
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// ── Секция B: Arithmetic Conditions & Reactive Text ──────────────────
|
|
||||||
Header "B. Rhei Arithmetic Conditions"
|
|
||||||
|
|
||||||
Slider(value=$volume_level)
|
|
||||||
ProgressBar(value=$volume_level)
|
|
||||||
|
|
||||||
// Трехуровневое ветвление с использованием Rhai-условий
|
|
||||||
@if !rhei: { volume_level == 0.0 } {
|
|
||||||
Text "🔇 Muted"
|
|
||||||
} @else {
|
|
||||||
@if !rhei: { volume_level >= 75.0 } {
|
|
||||||
Text "🔊 High volume — protect your hearing!"
|
|
||||||
} @else {
|
|
||||||
// Инлайн-текст, вызывающий функцию из глобального скрипта
|
|
||||||
Text !rhei: { volume_icon(volume_level) + " Volume OK: " + volume_level + " / 100" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
// ── Секция C: Access Control & Component Instantiation ───────────────
|
// --- Главная рабочая область (Workspace Split) ---
|
||||||
Header "C. Multi-variable Logic & Components"
|
Panel(class="layout-row") {
|
||||||
|
|
||||||
Slider(value=$access_level)
|
// Левая колонка: Интерактивное состояние и биндинги
|
||||||
|
Panel(class="layout-col") {
|
||||||
|
Panel(class="ui-card") {
|
||||||
|
Header "State Bindings"
|
||||||
|
Input(placeholder="Change title...", value=$APP_TITLE)
|
||||||
|
Toggle(label="Live Engine", value=$is_enabled)
|
||||||
|
|
||||||
!rhei: { "Current slider level: " + access_level + " → " + level_label(access_level) }
|
@if $is_enabled {
|
||||||
|
Text "🟢 System: ONLINE"
|
||||||
|
} @else {
|
||||||
|
Text "🔴 System: OFFLINE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Panel {
|
Panel(class="ui-card") {
|
||||||
Button(label="Grant Admin (Level 9)") {
|
Header "Control Channels"
|
||||||
@on click {
|
Slider(value=$volume_level)
|
||||||
!rhei: { access_level = 9.0; }
|
ProgressBar(value=$volume_level)
|
||||||
|
Text !rhei: { "Metrics Level: " + volume_level + "%" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button(label="Reset (Level 1)") {
|
|
||||||
@on click {
|
// Правая колонка: Тестирование геометрии и каскада стилей
|
||||||
!rhei: { access_level = 1.0; }
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !rhei: {
|
Panel(class="inheritance-box") {
|
||||||
let threshold = 5.0;
|
Text "Inherited Yellow Color & 18px Font Size"
|
||||||
access_level >= threshold
|
Text(class="override-style") "Explicit Style Override (Pink, 12px)"
|
||||||
} {
|
}
|
||||||
Text "🛡️ Access granted — Privileged Zone"
|
|
||||||
ProfileCard(username=$username, access_level=$access_level)
|
|
||||||
} @else {
|
|
||||||
Text "🚫 Access denied — Raise your level to 5+"
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
// Управление уровнем доступа и изменение переменных по клику
|
||||||
|
Panel(class="ui-card") {
|
||||||
|
Header "Access Management"
|
||||||
|
Slider(value=$access_level)
|
||||||
|
|
||||||
// ── Секция D: Dynamic List Rendering (@each) ─────────────────────────
|
// ── ТЕСТ POSITION: ABSOLUTE ─────────────────────
|
||||||
Header "D. Team Management (Reactive Grid)"
|
Panel(class="abs-demo-container") {
|
||||||
|
Panel(class="abs-demo-badge") {
|
||||||
|
Text "ABSOLUTE!"
|
||||||
|
}
|
||||||
|
Text "(бадж спозиционирован абсолютно)"
|
||||||
|
Button(label="Press me") {}
|
||||||
|
}
|
||||||
|
|
||||||
// Передаем direction и columns как свойства компонента
|
Panel(class="layout-row") {
|
||||||
Panel(columns=2, direction="grid") {
|
Button(label="Set Admin (Lvl 9)", class="btn-custom") {
|
||||||
@each $name in $team_members {
|
@on click { !rhei: { access_level = 9.0; } }
|
||||||
Panel {
|
}
|
||||||
ProfileCard(username=$name, access_level=$access_level)
|
Button(label="Reset Access") {
|
||||||
|
@on click { !rhei: { access_level = 1.0; } }
|
||||||
Button(label="Promote System-wide") {
|
|
||||||
@on click {
|
|
||||||
!rhei: {
|
|
||||||
// VDOM автоматически заменит "$name" на "Alexander", "Beatrice" и т.д.
|
|
||||||
let target_name = "$name";
|
|
||||||
print("Promoting triggered by " + target_name);
|
|
||||||
|
|
||||||
if access_level < 10.0 {
|
|
||||||
access_level += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
// --- Динамический список узлов (@each Grid) ---
|
||||||
|
Panel(direction="grid", columns=3, gap=12, class="layout-clean") {
|
||||||
|
@each $node in $team_nodes {
|
||||||
|
NodeCard(nodename=$node, level=$access_level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Секция E: Footer Metadata ────────────────────────────────────────
|
// --- Нижняя панель распределения пространства ---
|
||||||
Panel {
|
Panel(class="flex-row-bar") {
|
||||||
Text "System Version: 1.0.5-stable"
|
Panel(class="fill-10") { Text "Grow 10%" }
|
||||||
|
Panel(class="fixed-120") { Text "Fixed 120px" }
|
||||||
|
Panel(class="fill-20") { Text "Grow 20%" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Многострочный скрипт прямо внутри текстового узла
|
// ── ТЕСТ ФИКСИРОВАННЫХ СЛОЕВ ───────────────────
|
||||||
!rhei: {
|
@if $show_toast {
|
||||||
let pct = (access_level * 10.0).to_string();
|
Panel(class="fixed-toast") {
|
||||||
"Access Power: " + pct + "% | Integrity: OK"
|
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: Уллаmко лаборис ниси ут."
|
||||||
|
Text "Item 9: Аликип экс иа коммодо."
|
||||||
|
Text "— Конец тестового контента —"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
display.glbc
Normal file
BIN
display.glbc
Normal file
Binary file not shown.
82
display.gtlm
Normal file
82
display.gtlm
Normal 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
440
main.glts
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/app.rs
89
src/app.rs
@@ -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![]
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
let mut global_fixed_layers = Vec::new();
|
||||||
|
let mut global_abs_layers = Vec::new();
|
||||||
|
let mut global_sticky_layers = Vec::new();
|
||||||
|
|
||||||
|
let _scroll_y = self.doc.variables.get("__scroll_y")
|
||||||
|
.and_then(|v| v.parse::<f32>().ok())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
for root in &self.vdom_roots {
|
for root in &self.vdom_roots {
|
||||||
if let Some(el) = render_element(root) {
|
if let Some(el) = render_element(root, None, None, None, &mut global_fixed_layers, &mut global_abs_layers, &mut global_sticky_layers, &self.doc.stylesheet) {
|
||||||
content = content.push(el);
|
content = content.push(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_bar = container(self.build_status_bar(&self.doc.variables))
|
|
||||||
.width(Length::Fill)
|
|
||||||
.padding(10);
|
|
||||||
|
|
||||||
let layout = column![
|
let layout = column![
|
||||||
scrollable(content).width(Length::Fill).height(Length::Fill),
|
content,
|
||||||
iced::widget::rule::horizontal(1),
|
iced::widget::rule::horizontal(1),
|
||||||
status_bar,
|
|
||||||
];
|
|
||||||
|
|
||||||
container(layout)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_status_bar<'a>(
|
|
||||||
&self,
|
|
||||||
scope: &std::collections::HashMap<String, String>,
|
|
||||||
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
|
||||||
let slider_val = scope.get("volume_level")
|
|
||||||
.and_then(|v| v.parse::<f64>().ok())
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
|
|
||||||
row![
|
|
||||||
text("🟢 Runtime Active (VDOM + Rhai)").size(14),
|
|
||||||
Space::new().width(Length::Fill),
|
|
||||||
text(format!("Шаблонных узлов: {}", self.doc.roots.len())).size(14),
|
|
||||||
Space::new().width(Length::Fixed(15.0)),
|
|
||||||
text(format!("Слайдер ($volume_level): {:.1}", slider_val)).size(14),
|
|
||||||
]
|
]
|
||||||
.align_y(Alignment::Center)
|
.width(Length::Fill)
|
||||||
.into()
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
let main_flow = container(layout)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
let has_abs = !global_abs_layers.is_empty();
|
||||||
|
let has_sticky = !global_sticky_layers.is_empty();
|
||||||
|
let has_fixed = !global_fixed_layers.is_empty();
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/cli.rs
20
src/cli.rs
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +23,8 @@ 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); }
|
||||||
|
|
||||||
let mut r = Reader::new(bytecode);
|
let mut r = Reader::new(bytecode);
|
||||||
@@ -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 {
|
||||||
|
variables.remove(&k);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut vnode = Element::new(el.type_name.clone());
|
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,14 +457,46 @@ impl Interpreter {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String {
|
fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec<AncestorInfo> {
|
||||||
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
let mut chain = ancestors.to_vec();
|
||||||
rhei.eval_expr(expr, variables).unwrap_or_default()
|
let id = el.id().map(String::from);
|
||||||
} else {
|
let classes: Vec<String> = el
|
||||||
Self::resolve_string(v, variables)
|
.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 {
|
||||||
|
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
||||||
|
rhei.eval_expr(expr, variables)
|
||||||
|
} else {
|
||||||
|
Self::resolve_string(v, variables).into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn evaluate_condition(
|
fn evaluate_condition(
|
||||||
cond: &str,
|
cond: &str,
|
||||||
variables: &HashMap<String, String>,
|
variables: &HashMap<String, String>,
|
||||||
@@ -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: ®ex::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),
|
||||||
|
|||||||
@@ -7,81 +7,115 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn read_str_ref(&mut self) -> Result<&'a str, InterpError> {
|
||||||
|
let len = self.read_u32()? as usize;
|
||||||
|
self.require(len)?;
|
||||||
|
|
||||||
|
let slice = &self.data[self.pos .. self.pos + len];
|
||||||
|
self.pos += len;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
Ok(std::str::from_utf8_unchecked(slice))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn read_string(&mut self) -> Result<String, InterpError> {
|
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;
|
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)?
|
|
||||||
.to_string();
|
|
||||||
self.pos += len;
|
self.pos += len;
|
||||||
Ok(s)
|
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()?;
|
||||||
Some(items.join(","))
|
Some(items.join(","))
|
||||||
}
|
}
|
||||||
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 {
|
||||||
@@ -89,13 +123,12 @@ impl<'a> Reader<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
OP_PROP_CALL => {
|
OP_PROP_CALL => {
|
||||||
let name = self.read_string()?;
|
let name = self.read_string()?;
|
||||||
let arg_count = self.read_u32()? as usize;
|
let arg_count = self.read_u32()? as usize;
|
||||||
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,21 +150,15 @@ 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()?; }
|
||||||
OP_PROP_BOOL => { self.read_byte()?; }
|
OP_PROP_BOOL => { self.read_byte()?; }
|
||||||
OP_PROP_NULL => {}
|
OP_PROP_NULL => {}
|
||||||
OP_PROP_ARRAY => {
|
OP_PROP_ARRAY => {
|
||||||
let count = self.read_u32()?;
|
let count = self.read_u32()?;
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 initialize(&self, variables: &mut HashMap<String, String>) {
|
pub fn sync_scope(&self, variables: &HashMap<String, String>) {
|
||||||
let mut scope = Scope::new();
|
let mut scope = self.scope.borrow_mut();
|
||||||
|
|
||||||
for (k, v) in variables.iter() {
|
for (k, v) in variables.iter() {
|
||||||
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
if scope.contains(k) {
|
||||||
}
|
if let Some(old_val) = scope.get_value::<Dynamic>(k) {
|
||||||
|
if dyn_to_str(&old_val) == *v {
|
||||||
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &self.init_ast) {
|
continue;
|
||||||
eprintln!("⚠️ Rhei init error: {e}");
|
}
|
||||||
}
|
}
|
||||||
|
scope.set_value(k, str_to_dyn(v));
|
||||||
let names: Vec<String> = scope.iter_raw()
|
} else {
|
||||||
.map(|(name, _, _)| name.to_string())
|
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||||
.collect();
|
|
||||||
for name in &names {
|
|
||||||
if let Some(val) = scope.get_value::<Dynamic>(name) {
|
|
||||||
variables.insert(name.clone(), dyn_to_str(&val));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> Option<String> {
|
pub fn initialize(&self, variables: &mut HashMap<String, String>) {
|
||||||
let mut scope = Scope::new();
|
self.sync_scope(variables);
|
||||||
for (k, v) in variables {
|
|
||||||
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
let mut scope = self.scope.borrow_mut();
|
||||||
|
if let Err(e) = self.engine.run_ast_with_scope(&mut *scope, &self.init_ast) {
|
||||||
|
eprintln!("⚠️ Rhei initialization error: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let expr_ast = self.engine
|
for (name, _, val) in scope.iter_raw() {
|
||||||
.compile_expression(expr)
|
let s_val = dyn_to_str(&val);
|
||||||
.or_else(|_| self.engine.compile(expr))
|
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
|
||||||
.ok()?;
|
variables.insert(name.to_string(), s_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let full = self.fn_ast.merge(&expr_ast);
|
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> String {
|
||||||
|
self.sync_scope(variables);
|
||||||
|
let mut scope = self.scope.borrow_mut();
|
||||||
|
|
||||||
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
|
match self.engine.eval_expression_with_scope::<Dynamic>(&mut *scope, expr) {
|
||||||
Ok(val) => Some(dyn_to_str(&val)),
|
Ok(val) => 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) => {
|
|
||||||
eprintln!("⚠️ Rhei condition compile `{expr}`: {e}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let full = self.fn_ast.merge(&ast);
|
|
||||||
|
|
||||||
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
|
|
||||||
Ok(val) => {
|
|
||||||
if val.is_bool() { return val.cast::<bool>(); }
|
|
||||||
if val.is_int() { return val.cast::<i64>() != 0; }
|
|
||||||
if val.is_float(){ return val.cast::<f64>() != 0.0; }
|
|
||||||
if val.is_string(){
|
|
||||||
let s = val.cast::<String>();
|
|
||||||
return !matches!(s.trim(), "" | "false" | "0" | "null");
|
|
||||||
}
|
|
||||||
!val.is_unit()
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("⚠️ Rhei condition eval `{expr}`: {e}");
|
eprintln!("⚠️ Rhei eval_condition error: {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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(¤t)),
|
||||||
|
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 {
|
||||||
return Some(v.as_str());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(v) = inline.get(&format!("style:{key}")) {
|
for sheet in matched_sheets.iter().rev() {
|
||||||
return Some(v.as_str());
|
if let Some(v) = sheet.get(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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
1160
src/renderer.rs
1160
src/renderer.rs
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user