From 75af23930f263f064e0e96527820cf274a95321d Mon Sep 17 00:00:00 2001 From: faynot Date: Wed, 8 Jul 2026 23:45:37 +0300 Subject: [PATCH] feat: new styles --- Cargo.lock | 33 +- Cargo.toml | 4 +- STYLE-SYSTEM-COMPLETION-PLAN.md | 570 +++++++++++++++ desktop.glbc | Bin 0 -> 5265 bytes desktop.gltm | 332 ++++----- display.glbc | Bin 0 -> 9258 bytes display.gtlm | 82 +++ main.glts | 440 ++++++++++-- out.glbc | Bin 6395 -> 12290 bytes src/app.rs | 89 ++- src/cli.rs | 26 +- src/interpreter/mod.rs | 333 ++++++--- src/interpreter/reader.rs | 186 ++--- src/interpreter/rhei.rs | 149 ++-- src/interpreter/style.rs | 916 ++++++++++++++++++++++-- src/interpreter/types.rs | 33 +- src/main.rs | 1 + src/renderer.rs | 1178 ++++++++++++++++++++++++++----- 18 files changed, 3556 insertions(+), 816 deletions(-) create mode 100644 STYLE-SYSTEM-COMPLETION-PLAN.md create mode 100644 desktop.glbc create mode 100644 display.glbc create mode 100644 display.gtlm diff --git a/Cargo.lock b/Cargo.lock index 524c934..4d247dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,6 +583,15 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.62" @@ -742,6 +751,20 @@ dependencies = [ "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]] name = "concurrent-queue" version = "2.5.0" @@ -1454,11 +1477,13 @@ dependencies = [ "chrono", "clap", "colored", + "compact_str", "glt", "iced", "indicatif", "regex", "rhai", + "rustc-hash 2.1.2", ] [[package]] @@ -1475,7 +1500,7 @@ dependencies = [ [[package]] name = "glt" -version = "0.1.3" +version = "0.1.4" [[package]] name = "glutin_wgl_sys" @@ -3488,6 +3513,12 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index f914329..d7d7f79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,10 @@ edition = "2024" glt = { path = "../glt" } regex = "1.10" 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" indicatif = "0.17" rhai = "1.17.1" +rustc-hash = "2" chrono = "0.4.44" +compact_str = "0.8" diff --git a/STYLE-SYSTEM-COMPLETION-PLAN.md b/STYLE-SYSTEM-COMPLETION-PLAN.md new file mode 100644 index 0000000..0c20937 --- /dev/null +++ b/STYLE-SYSTEM-COMPLETION-PLAN.md @@ -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, + combinator: Combinator, + right: Box, + }, + List(Vec), // запятые: `Button, .btn, #id` +} + +struct SimpleSelector { + tag: Option, // `Button` | `*` + id: Option, // `#myid` + classes: Vec, // `.foo.bar` + pseudo_classes: Vec, // `:hover` + pseudo_element: Option, // `::before` + attribute: Vec, // `[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), + // ... +} + +struct AttributeSelector { + name: String, + op: AttrOp, + value: Option, +} + +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`. +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`. +- Каждый кадр обновлять transitioning-свойства. + +--- + +## 6. Новые CSS-свойства в ComputedStyle + +### 6.1 Типографика + +| Свойство | Тип | Парсер | +|----------|-----|--------| +| `font-family` | `Option` | список шрифтов | +| `font-weight` | `Option` | `normal(400)`, `bold(700)`, числовое | +| `font-style` | `Option` | `normal`, `italic`, `oblique` | +| `line-height` | `Option` | числовое или `normal` | +| `letter-spacing` | `Option` | px | +| `text-align` | `Option` | `left`, `center`, `right`, `justify` | +| `text-decoration` | `Option` | `none`, `underline`, `line-through` | +| `text-transform` | `Option` | `none`, `uppercase`, `lowercase`, `capitalize` | +| `white-space` | `Option` | `normal`, `nowrap`, `pre` | +| `word-break` | `Option` | `normal`, `break-all`, `keep-all` | +| `text-overflow` | `Option` | `clip`, `ellipsis` | + +### 6.2 Фон и границы + +| Свойство | Тип | Парсер | +|----------|-----|--------| +| `opacity` | `Option` | 0.0–1.0 | +| `background-image` | `Option` | `url(...)` | +| `background-repeat` | `Option` | `repeat`, `no-repeat` | +| `background-size` | `Option` | `cover`, `contain`, px, % | +| `background-position` | `Option` | `center`, `top left`, px | +| `linear-gradient(...)` | `Option` | парсить в `Value::Call` и обрабатывать | +| `box-shadow` | `Option>` | `offset-x offset-y blur spread color` | +| `text-shadow` | `Option>` | то же | +| `outline` | `Option` | width, style, color | +| `border-style` | `Option` | `solid`, `dashed`, `dotted` | +| `border` (шорткат) | — | разворачивать в width/style/color | + +### 6.3 Flexbox (расширение) + +| Свойство | Тип | +|----------|-----| +| `justify-content` | `Option` | `start`, `center`, `end`, `space-between`, `space-around`, `space-evenly` | +| `flex-wrap` | `Option` | `nowrap`, `wrap`, `wrap-reverse` | +| `flex-direction` | `Option` | (расширение `LayoutDirection`) | +| `align-self` | `Option` | переопределяет `align-items` для конкретного элемента | +| `align-content` | `Option` | multi-line выравнивание | +| `flex` (шорткат) | — | grow, shrink, basis | +| `flex-shrink` | `Option` | | +| `flex-basis` | `Option` | | + +### 6.4 Grid Layout + +| Свойство | Тип | +|----------|-----| +| `grid-template-columns` | `Option>` | `1fr 1fr`, `repeat(3, 1fr)`, `auto` | +| `grid-template-rows` | `Option>` | | +| `grid-gap` / `gap` | уже есть как `spacing` | | +| `grid-column` | `Option<(u32, u32)>` | start / end | +| `grid-row` | `Option<(u32, u32)>` | | +| `grid-auto-flow` | `Option` | | + +### 6.5 Позиционирование (расширение) + +| Свойство | Тип | Статус | +|----------|-----|--------| +| `position: absolute` | — | сейчас мэппится в Static | +| `position: relative` | — | сейчас мэппится в Static | +| `position: sticky` | — | сейчас мэппится в Static | +| `z-index` | `Option` | | +| `display` | `Option` | `block`, `flex`, `grid`, `none`, `inline` | + +### 6.6 Прочее + +| Свойство | Тип | +|----------|-----| +| `visibility` | `Option` | `visible`, `hidden` | +| `cursor` | `Option` | `pointer`, `default`, `text` | +| `pointer-events` | `Option` | `auto`, `none` | +| `transform` | `Option>` | `translate(x,y)`, `scale(s)`, `rotate(a)` | +| `transform-origin` | `Option` | | +| `backdrop-filter` | `Option>` | `blur(10px)`, `brightness(1.2)` | +| `filter` | `Option>` | то же | +| `list-style` | `Option` | | +| `isolation` | `Option` | `auto`, `isolate` | +| `mix-blend-mode` | `Option` | `multiply`, `screen` | + +### 6.7 Итого: ~40–60 новых полей в ComputedStyle + +Каждое поле: тип `Option`, значение по умолчанию `None`. +Парсинг: новая функция `parse_(s: &str) -> Option` для каждого. +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, Box), + Sub(Box, Box), + Mul(Box, Box), + Div(Box, Box), + 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`. +- Наследуются по умолчанию. + +### 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, + pub display: Option, + pub visibility: Option, +} +``` + +### 13.2 Selector engine — отдельный модуль + +```rust +mod selector { + pub struct Selector { ... } + pub fn parse(input: &str) -> Result; + 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, + pub source_order: u32, + pub media: Option, + pub important: HashSet, +} +``` + +### 13.4 Система тиков для анимаций/транзишенов + +- Новый модуль `animation` в runtime. +- `AnimationEngine` с `HashMap`. +- 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 строк нового кода. diff --git a/desktop.glbc b/desktop.glbc new file mode 100644 index 0000000000000000000000000000000000000000..3bc0d39291c6e13dd7870072cccf274f96e875cc GIT binary patch literal 5265 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 8.0 { "Admin" } - else if lvl >= 5.0 { "Moderator" } - else { "Guest" } +!rhei: { + fn load_status(val) { + if val == 0.0 { "💤 Muted" } + else if val >= 80.0 { "🔥 Overload" } + else { "⚡ Active" } } - fn volume_icon(vol) { - if vol == 0.0 { "🔇" } - else if vol < 40.0 { "🔈" } - else if vol < 75.0 { "🔉" } - else { "🔊" } - } - - // Демонстрация сложных вычислений при загрузке документа - const TARGET = 20; - fn fib(n) { - if n < 2 { return n; } - let a = 0; let b = 1; - for i in 2..=n { - let c = a + b; - a = b; b = c; - } - b - } - - print(`Initializing Glint Engine...`); - let result = fib(TARGET); - print(`Fibonacci(${TARGET}) computed on load: ${result}`); + let sum = 0; + for i in 1..=5 { sum = sum + i; } + print("Rhai Engine online. Checksum: " + sum); } -// ── 3. UI Components ───────────────────────────────────────────────────────── - -@component ProfileCard(username: String, access_level: Float) { - Panel { - Header !rhei: { "User: " + username } - Label(text=$username) - - // Сложное условие в Rhai: вычисляется при каждом обновлении VDOM - @if !rhei: { (access_level >= 5.0) && (username.len() > 0) } { - Text "✅ Administrator Privileges Active" - Text !rhei: { "Role: " + level_label(access_level) } +// ── 2. Reusable UI Components ──────────────────────────────────────────────── +@component NodeCard(nodename: String, level: Float) { + Panel(class="ui-card") { + Label(text=$nodename) + @if !rhei: { level >= 5.0 } { + Text "🛡️ Secure Node" } @else { - Text "🔒 Restricted Access Mode" - Text "Role: Guest" + Text "🔓 Guest Node" } } } -// ── 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( - title=$APP_TITLE, - width=1280, - height=800, - resizable=true -) { - // Локальное состояние окна - @let $show_metrics = true - - Panel(id="main_viewport", padding=24) { - - Header "Dashboard Overview" - Text "Welcome to the ultimate Glint component test suite." - Divider() - - // ── Секция A: Control Panel (Переменные и биндинги) ────────────────── - Header "A. Control Panel & State Bindings" - - Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {} - - Panel { - Input(placeholder=$search_query, value=$search_query) - Toggle(label="Enable Live Metrics", value=$is_enabled) - Text !rhei: { "Live search query: " + search_query } - } - - @if $is_enabled { - Text "✅ Features are ON. You can adjust the system." - } @else { - Text "⛔ Features are OFF." - } - - Divider() - - // ── Секция B: Arithmetic Conditions & Reactive Text ────────────────── - Header "B. Rhei Arithmetic Conditions" - - Slider(value=$volume_level) - ProgressBar(value=$volume_level) - - // Трехуровневое ветвление с использованием Rhai-условий - @if !rhei: { volume_level == 0.0 } { - Text "🔇 Muted" - } @else { - @if !rhei: { volume_level >= 75.0 } { - Text "🔊 High volume — protect your hearing!" - } @else { - // Инлайн-текст, вызывающий функцию из глобального скрипта - Text !rhei: { volume_icon(volume_level) + " Volume OK: " + volume_level + " / 100" } + // --- Верхняя панель (Header Section) --- + Panel(class="layout-row") { + Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {} + Panel(class="layout-col") { + Header "Glint Reactive Dashboard" + Text !rhei: { "User: " + username + " | Status: " + load_status(volume_level) } } } - + Divider() - // ── Секция C: Access Control & Component Instantiation ─────────────── - Header "C. Multi-variable Logic & Components" - - Slider(value=$access_level) - - !rhei: { "Current slider level: " + access_level + " → " + level_label(access_level) } - - Panel { - Button(label="Grant Admin (Level 9)") { - @on click { - !rhei: { access_level = 9.0; } + // --- Главная рабочая область (Workspace Split) --- + Panel(class="layout-row") { + + // Левая колонка: Интерактивное состояние и биндинги + Panel(class="layout-col") { + Panel(class="ui-card") { + Header "State Bindings" + Input(placeholder="Change title...", value=$APP_TITLE) + Toggle(label="Live Engine", value=$is_enabled) + + @if $is_enabled { + Text "🟢 System: ONLINE" + } @else { + Text "🔴 System: OFFLINE" + } + } + + Panel(class="ui-card") { + Header "Control Channels" + Slider(value=$volume_level) + ProgressBar(value=$volume_level) + Text !rhei: { "Metrics Level: " + volume_level + "%" } } } - 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: { - let threshold = 5.0; - access_level >= threshold - } { - Text "🛡️ Access granted — Privileged Zone" - ProfileCard(username=$username, access_level=$access_level) - } @else { - Text "🚫 Access denied — Raise your level to 5+" - } - - Divider() + Panel(class="inheritance-box") { + Text "Inherited Yellow Color & 18px Font Size" + Text(class="override-style") "Explicit Style Override (Pink, 12px)" + } - // ── Секция D: Dynamic List Rendering (@each) ───────────────────────── - Header "D. Team Management (Reactive Grid)" - - // Передаем direction и columns как свойства компонента - Panel(columns=2, direction="grid") { - @each $name in $team_members { - Panel { - ProfileCard(username=$name, access_level=$access_level) - - Button(label="Promote System-wide") { - @on click { - !rhei: { - // VDOM автоматически заменит "$name" на "Alexander", "Beatrice" и т.д. - let target_name = "$name"; - print("Promoting triggered by " + target_name); - - if access_level < 10.0 { - access_level += 1.0; - } - } + // Управление уровнем доступа и изменение переменных по клику + Panel(class="ui-card") { + Header "Access Management" + Slider(value=$access_level) + + // ── ТЕСТ POSITION: ABSOLUTE ───────────────────── + Panel(class="abs-demo-container") { + Panel(class="abs-demo-badge") { + Text "ABSOLUTE!" + } + Text "(бадж спозиционирован абсолютно)" + Button(label="Press me") {} + } + + Panel(class="layout-row") { + Button(label="Set Admin (Lvl 9)", class="btn-custom") { + @on click { !rhei: { access_level = 9.0; } } + } + Button(label="Reset Access") { + @on click { !rhei: { access_level = 1.0; } } } } } } } - 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 { - Text "System Version: 1.0.5-stable" - - // Многострочный скрипт прямо внутри текстового узла - !rhei: { - let pct = (access_level * 10.0).to_string(); - "Access Power: " + pct + "% | Integrity: OK" + // --- Нижняя панель распределения пространства --- + Panel(class="flex-row-bar") { + Panel(class="fill-10") { Text "Grow 10%" } + Panel(class="fixed-120") { Text "Fixed 120px" } + Panel(class="fill-20") { Text "Grow 20%" } + } + } + + // ── ТЕСТ ФИКСИРОВАННЫХ СЛОЕВ ─────────────────── + @if $show_toast { + Panel(class="fixed-toast") { + Panel(class="layout-row") { + Text "🔔 [FIXED LAYER TEST]: Runtime compiled successfully!" + Button(label="❌", class="btn-close") { + @on click { !rhei: { show_toast = false; } } + } } } } + + Panel(class="scroll-test-box") { + Header "Scroll Test" + Text "Line 1: Item A" + Text "Line 2: Item B" + Text "Line 3: Item C" + Text "Line 4: Item D" + Text "Line 5: Item E" + Text "Line 6: Item F" + Text "Line 7: Item G" + Text "Line 8: Item H" + Text "Line 9: Item I" + Text "Line 10: Item J" +} + + // ── ДЕМО POSITION: ABSOLUTE (Overlay-карточка) ──────────────────── + Panel(class="absolute-demo-section") { + Header "Position: Absolute Demo" + Panel(class="abs-stage") { + Panel(class="abs-overlay") { + Text "🎯 Абсолютно спозиционирована!" + Text "top: 10px | right: 10px" + } + Text "Этот текст — часть нормального потока." + Text "Оверлей лежит поверх, не влияя на раскладку." + Text "Работает как position: absolute в CSS." + } + } + +Panel(class="sticky-container") { + Text "Scroll down to test sticky effect (внутренний скролл):" + + // Стикер внутри скролл-контейнера + Panel(class="sticky-header") { + Text "📌 STICKY: я прилип к верху!" + } + + // Большой блок текста снизу, чтобы гарантировать появление скролла + Panel(class="scroll-spacer") { + Text "Item 1: Лорем ипсум долор сит амет..." + Text "Item 2: Консектетур адипискинг элит." + Text "Item 3: Сед до эйусмод темпор." + Text "Item 4: Инсидидунт ут лаборе." + Text "Item 5: Долоре магна аликва." + Text "Item 6: Ут enim ад миним veniam." + Text "Item 7: Квис нострум экзерситатион." + Text "Item 8: Уллаmко лаборис ниси ут." + Text "Item 9: Аликип экс иа коммодо." + Text "— Конец тестового контента —" + } + } } diff --git a/display.glbc b/display.glbc new file mode 100644 index 0000000000000000000000000000000000000000..3c355da85e72ea6804cc1f7fadbf3b42779d7f8e GIT binary patch literal 9258 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 literal 0 HcmV?d00001 diff --git a/display.gtlm b/display.gtlm new file mode 100644 index 0000000..04b9210 --- /dev/null +++ b/display.gtlm @@ -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 скрыт (выше не видно красную панель)." + } + } +} diff --git a/main.glts b/main.glts index 16222e2..e04c4c7 100644 --- a/main.glts +++ b/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-panel = #1e1e2e // Базовый цвет для контейнеров и панелей -$bg-surface = #313244 // Цвет для карточек и выделенных блоков -$bg-field = #181825 // Внутренний фон для полей ввода (Input) - -$text-main = #cdd6f4 // Мягкий белый основной текст -$text-muted = #9399b2 // Серый приглушенный текст для описаний -$accent = #b4befe // Лавандовый акцентный цвет для кнопок и заголовков -$border-glow = #45475a // Цвет аккуратных рамок и разделителей - -// Сетка отступов (Размеры автоматически очищаются от "px" парсером Glint) -$pad-dense = 6px -$pad-normal = 12px -$pad-roomy = 20px -$pad-window = 28px - -// ── 2. Шаблоны стилей (Mixins) ─────────────────────────────────────────────── +$bg-app = #11111b +$bg-panel = #1e1e2e +$bg-surface = #313244 +$bg-input = #181825 +$accent = #b4befe +$text-main = #cdd6f4 +$text-muted = #9399b2 +$border-glow = #45475a @mixin text-body { - color: $text-main - font-size: 15px + font-size: 14px } @mixin standard-card { @@ -33,47 +22,94 @@ $pad-window = 28px border-radius: 12px } -// ── 3. Правила для UI-компонентов (Element Rules) ──────────────────────────── - -// Главное окно приложения Window { background: $bg-app - padding: $pad-window - spacing: $pad-roomy } -// Универсальный контейнер ( viewport, карточки, обертки списков ) Panel { - @use standard-card - padding: $pad-normal - gap: 14px + background: transparent + border-width: 0px + padding: 0px + gap: 12px direction: vertical +} + +.ui-card { + @use standard-card + padding: 14px + gap: 12px align-items: center content-align: center } -// Крупные заголовки секций -Header { - color: $accent - font-size: 22px - padding: 4px +// ── Служебные Layout-классы ────────────────────────────────────────────────── + +.layout-viewport { + width: fill + padding: 16px + gap: 16px +} + +.layout-row { + direction: horizontal + gap: 16px +} + +.layout-col { + direction: vertical + gap: 16px +} + +.layout-clean { + width: fill +} + +// ── Стили атомарных компонентов ────────────────────────────────────────────── + +Header { + color: $accent + font-size: 20px + padding: 2px } -// Обычный текст Text { @use text-body } -// Подписи, второстепенные метаданные Label { color: $text-muted - font-size: 13px + font-size: 12px } -// Интерактивные элементы управления -Button { +Input { + background-color: $bg-input + color: $text-main + font-size: 14px + padding: 10px border-radius: 8px border-width: 1px + border-color: $border-glow + width: 240px +} + +Toggle { + color: $text-main + font-size: 14px +} + +Divider { + background: $border-glow + border-width: 1px +} + +Button { + border-radius: 6px + border-width: 1px + padding: 8px +} + +.btn-custom { + background: #000000 } Image { @@ -81,26 +117,316 @@ Image { border-width: 1px } -// Текстовые поля ввода -Input { - background-color: $bg-field - color: $text-main - font-size: 14px - padding: 12px - border-radius: 16px - border-width: 1px - border-color: $border-glow +// ── Секция тестирования продвинутой геометрии Box-Model ────────────────────── + +.geometry-box { + @use standard-card + background: #24253a + border-color: #f38ba8 + border-width: 2px + padding-top: 24px + padding-bottom: 12px + padding-left: 16px + padding-right: 40px width: 320px } -// Переключатели -Toggle { - color: $text-main - font-size: 15px +.margin-test-item { + background: #a6e3a1 + padding: 8px + margin-top: 12px + margin-bottom: 4px } -// Разделительные линии (Divider / Separator) -Divider { - background: $border-glow - border-width: 1px +// ── Секция тестирования каскадного наследования (Inheritance) ──────────────── + +.inheritance-box { + @use standard-card + background: #181825 + padding: 12px + color: #f9e2af + font-size: 18px +} + +.override-style { + color: #f38ba8 + font-size: 12px +} + +// ── Секция распределения весов Flex-Grow (Fill portions) ──────────────────── + +.flex-row-bar { + background: $bg-surface + direction: horizontal + padding: 8px + gap: 8px + width: fill +} + +.fill-10 { + background: #f38ba8 + width: 10% +} + +.fixed-120 { + background: #f9e2af + width: 50px + color: #11111b +} + +.fill-20 { + background: #a6e3a1 + width: 20% + color: #11111b +} + +// ── СТИЛИ ДЛЯ ПРОВЕРКИ POSITION: FIXED ─────────────────────────────────────── + +.fixed-toast { + position: fixed + top: 20px + right: 20px + background: #a6e3a1 + border-radius: 8px + padding: 12px + border-width: 1px + border-color: #a6e3a1 + color: #000 + font-weight: bold +} + +.btn-close { + background: transparen + border-width: 0px + padding: 2px +} + + + + +.scroll-test-box { + @use standard-card + background: #313244 + height: 150px + width: 250px + overflow-y: scroll + padding: 10px +} + + +// ── Стили для тестирования Sticky Эффекта ──────────────────── + +.sticky-container { + @use standard-card + background: #181825 + padding: 12px + gap: 8px + width: fill + height: 200px + overflow-y: scroll +} + +.sticky-header { + position: sticky + top: 0px + background: #fab387 + padding: 10px 14px + border-radius: 6px + color: #11111b + font-weight: bold + font-size: 14px + width: fill +} + +.scroll-spacer { + gap: 100px + padding: 10px + width: fill +} + +// ── Стили для тестирования Opacity ──────────────────────────── + +.opacity-fade { + background: #f38ba8 + padding: 12px + border-radius: 8px + opacity: 0.3 +} + +.opacity-mid { + background: #a6e3a1 + padding: 12px + border-radius: 8px + opacity: 0.6 +} + +.opacity-subtle { + background: #89b4fa + padding: 12px + border-radius: 8px + opacity: 0.85 +} + +// ── Стили для тестирования Font-Weight ───────────────────────── + +.fw-light { + font-weight: 300 + color: $text-main +} + +.fw-normal { + font-weight: 400 + color: $text-main +} + +.fw-bold { + font-weight: 700 + color: $text-main +} + +.fw-black { + font-weight: 900 + color: $text-main +} + +// ── Стили для тестирования Text-Align ────────────────────────── + +.ta-box { + background: #313244 + padding: 8px + border-radius: 6px + width: fill +} + +.ta-left { + text-align: left +} + +.ta-center { + text-align: center +} + +.ta-right { + text-align: right +} + +// ── Стили для тестирования Line-Height ───────────────────────── + +.lh-box { + background: #313244 + padding: 8px + border-radius: 6px + width: 300px +} + +.lh-tight { + line-height: 0.8 +} + +.lh-normal { + line-height: 1.2 +} + +.lh-loose { + line-height: 2.0 +} + +// ── Стили для тестирования селекторов через запятую ──────────── + +.warning-text, .error-text { + font-weight: bold + padding: 8px + border-radius: 6px +} + +.warning-text { + color: #f9e2af + background: #3d3520 +} + +.error-text { + color: #f38ba8 + background: #3d2020 +} + +// ── Стили для тестирования Display: None ─────────────────────── + +.hidden-box { + background: #f38ba8 + padding: 12px + border-radius: 8px + display: none +} + +.test-section-header { + color: $accent + font-size: 16px + padding: 4px 0 + margin-top: 8px +} + +.test-row { + direction: horizontal + gap: 12px + align-items: center +} + +// ── Стили для тестирования POSITION: ABSOLUTE ──────────────── + +.abs-demo-container { + position: relative + background: #313244 + padding: 16px + margin-top: 8px + border-radius: 8px + width: fill + gap: 8px + min-height: 60px +} + +.abs-demo-badge { + position: absolute + top: 10px + right: 4px + background: #f38ba8 + color: #11111b + font-weight: bold + font-size: 10px + padding: 4px 8px + border-radius: 4px +} + +// ── Полноценная демка absolute-позиционирования ────────────── + +.absolute-demo-section { + @use standard-card + background: #181825 + padding: 12px + gap: 8px + width: fill +} + +.abs-stage { + position: relative + background: #24253a + border-color: $accent + border-width: 1px + border-radius: 8px + padding: 16px + gap: 6px + width: fill + min-height: 80px +} + +.abs-overlay { + position: absolute + top: 10px + right: 10px + background: #f38ba8 + border-radius: 6px + padding: 8px 12px + color: #11111b + font-weight: bold + font-size: 12px + gap: 4px } diff --git a/out.glbc b/out.glbc index 5cbaff45e318862952566780663e26a622c3aefa..1d204f206ba752adccd683f042d8f597701044a1 100644 GIT binary patch literal 12290 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 Xz8|MQ(-evULSL?)0S%;6snmY~v)hV` diff --git a/src/app.rs b/src/app.rs index c04a056..e058c5d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,13 +1,13 @@ -use iced::widget::{column, container, row, scrollable, text, Space}; -use iced::{Alignment, Length, Theme}; +use iced::widget::{column, container}; +use iced::{Length, Theme}; use crate::interpreter::{Document, Element, Interpreter, RheiContext}; use crate::renderer::render_element; use crate::Message; pub struct GlintApp { - pub doc: Document, - pub vdom_roots: Vec, + pub doc: Document<'static>, + pub vdom_roots: Vec>, pub rhei: RheiContext, } @@ -18,6 +18,9 @@ impl GlintApp { pub fn update(&mut self, message: Message) -> iced::Task { match message { + Message::WindowScrolled(y) => { + self.doc.variables.insert("__scroll_y".to_string(), y.to_string()); + } Message::EventTriggered(script) => { if !script.is_empty() { self.rhei.execute_action(&script, &mut self.doc.variables); @@ -40,56 +43,64 @@ impl GlintApp { self.vdom_roots = Interpreter::evaluate_vdom( &self.doc.roots, - &self.doc.variables, + &mut self.doc.variables, &self.doc.components, &self.rhei, &self.doc.stylesheet, + &[], ); iced::Task::none() } 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::().ok()) + .unwrap_or(0.0); for root in &self.vdom_roots { - if let Some(el) = render_element(root) { - content = content.push(el); - } + 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); +} } - let status_bar = container(self.build_status_bar(&self.doc.variables)) - .width(Length::Fill) - .padding(10); - let layout = column![ - scrollable(content).width(Length::Fill).height(Length::Fill), + content, 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, - ) -> iced::Element<'a, Message, Theme, iced::Renderer> { - let slider_val = scope.get("volume_level") - .and_then(|v| v.parse::().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) - .into() + .width(Length::Fill) + .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() + } } } diff --git a/src/cli.rs b/src/cli.rs index ec3150b..80d5957 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,7 +19,7 @@ pub fn compile_files(gltm_files: &[String], glts_files: &[String], output_path: glts_source.push_str(&read_source(path, "style")); glts_source.push('\n'); } - + let mut gltm_source = String::new(); for path in gltm_files { gltm_source.push_str(&read_source(path, "markup")); @@ -36,7 +36,7 @@ pub fn compile_files(gltm_files: &[String], glts_files: &[String], output_path: spinner.enable_steady_tick(Duration::from_millis(80)); let parse_start = Instant::now(); - + let mut module = ModuleSoA::new(); if !glts_source.is_empty() { @@ -49,7 +49,7 @@ pub fn compile_files(gltm_files: &[String], glts_files: &[String], output_path: } let mut parser = Parser::with_module(&gltm_source, module); - + match parser.parse_all() { Ok(_) => { let parse_dur = parse_start.elapsed(); @@ -136,11 +136,12 @@ pub fn compile_files(gltm_files: &[String], glts_files: &[String], output_path: } 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()); let start = Instant::now(); - match Interpreter::run(&bytecode) { + match Interpreter::run(bytecode) { Ok(doc) => { println!( "{} {} ({:?})", @@ -157,18 +158,21 @@ pub fn run_file(path: &str) -> ! { let result = iced::application( 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( - &doc.roots, - &doc.variables, - &doc.components, + &local_doc.roots, + &mut local_doc.variables, + &local_doc.components, &rhei, - &doc.stylesheet, + &local_doc.stylesheet, + &[], ); GlintApp { - doc: doc.clone(), + doc: local_doc, rhei, vdom_roots, } diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 10d13f9..18c678a 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -1,9 +1,11 @@ + pub mod opcodes; pub mod reader; pub mod rhei; pub mod style; pub mod types; +use std::borrow::Cow; pub use rhei::RheiContext; use style::StyleSheet as SS; pub use types::{ComponentDef, Document, Element, InterpError}; @@ -11,7 +13,7 @@ pub use types::{ComponentDef, Document, Element, InterpError}; use opcodes::*; use reader::Reader; use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX}; -use style::ComputedStyle; +use style::{AncestorInfo, ComputedStyle, StructuralContext}; use regex::Regex; use std::collections::HashMap; use std::sync::OnceLock; @@ -21,8 +23,8 @@ static RE_VAR: OnceLock = OnceLock::new(); pub struct Interpreter; impl Interpreter { - pub fn run(bytecode: &[u8]) -> Result { - if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); } + pub fn run<'a>(bytecode: &'a [u8]) -> Result, InterpError> { + if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); } if &bytecode[..4] != MAGIC { return Err(InterpError::BadMagic); } let mut r = Reader::new(bytecode); @@ -41,20 +43,20 @@ impl Interpreter { true, )?; - let rhei_ctx = RheiContext::new(&rhei_scripts); - rhei_ctx.initialize(&mut variables); + //let rhei_ctx = RheiContext::new(&rhei_scripts); + //rhei_ctx.initialize(&mut variables); Ok(Document { roots, components, variables, rhei_scripts, stylesheet }) } - fn parse_block_elements( - r: &mut Reader, - variables: &mut HashMap, - components: &mut HashMap, - rhei_scripts: &mut Vec, - stylesheet: &mut SS, - is_root: bool, - ) -> Result, InterpError> { + fn parse_block_elements<'a>( + r: &mut Reader<'a>, + variables: &mut HashMap, + components: &mut HashMap>, + rhei_scripts: &mut Vec, + stylesheet: &mut SS, + is_root: bool, +) -> Result>, InterpError> { let mut roots: Vec = Vec::new(); let mut stack: Vec = Vec::new(); @@ -63,7 +65,7 @@ impl Interpreter { if !is_root && op == OP_END_BLOCK { break; } 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 => { let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?; @@ -90,53 +92,63 @@ impl Interpreter { OP_CONTENT => { 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() { - let stored = if vop == OP_PROP_RHEI { - format!("{RHEI_PREFIX}{value}") - } else { - value - }; - el.properties.insert("text".to_string(), stored); + el.push_prop("text".to_string(), val); + } + } else if let Some(value) = r.read_value_as_string(vop)? { + if let Some(el) = stack.last_mut() { + el.push_prop("text".to_string(), value); } } } OP_PROP_STR => { - let (key, val) = (r.read_string()?, r.read_string()?); - if let Some(el) = stack.last_mut() { el.properties.insert(key, val); } + let key = r.read_str_ref()?; + let val = r.read_str_ref()?; + if let Some(el) = stack.last_mut() { el.push_prop(key, val); } } OP_PROP_VAR => { - let key = r.read_string()?; - let val = format!("${}", r.read_string()?); - if let Some(el) = stack.last_mut() { el.properties.insert(key, val); } + let key = r.read_str_ref()?; + let val_ref = r.read_str_ref()?; + 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 => { - let key = r.read_string()?; + let key = r.read_str_ref()?; 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 => { - let key = r.read_string()?; + let key = r.read_str_ref()?; 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 => { - let key = r.read_string()?; + let key = r.read_str_ref()?; 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 => { - let key = r.read_string()?; - let expr = r.read_string()?; + let key = r.read_str_ref()?; + 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() { - 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 => { - let key = r.read_string()?; + let key = r.read_str_ref()?; 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() { rhei_scripts.push(script); } else { - let mut text_el = Element::new("#text".to_string()); - text_el.properties.insert( + let mut text_el = Element::new("#text"); + text_el.push_prop( "text".to_string(), format!("{RHEI_PREFIX}{script}"), ); @@ -187,12 +199,12 @@ impl Interpreter { Vec::new() }; - let mut if_el = Element::new("@if".to_string()); - if_el.properties.insert("condition".to_string(), cond_val); + let mut if_el = Element::new("@if"); + if_el.push_prop("condition", cond_val); if_el.children = true_children; 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; if_el.children.push(else_el); } @@ -214,9 +226,9 @@ impl Interpreter { r, variables, components, rhei_scripts, stylesheet, false, )?; - let mut each_el = Element::new("@each".to_string()); - each_el.properties.insert("var_name".to_string(), var_name); - each_el.properties.insert("source".to_string(), source_val); + let mut each_el = Element::new("@each"); + each_el.push_prop("var_name".to_string(), var_name); + each_el.push_prop("source".to_string(), source_val); each_el.children = block_children; Self::attach(&mut stack, &mut roots, each_el); } @@ -245,7 +257,7 @@ impl Interpreter { } if let Some(el) = stack.last_mut() { - el.properties.insert( + el.push_prop( format!("__on:{event_name}"), handler_script, ); @@ -278,20 +290,54 @@ impl Interpreter { Ok(roots) } - pub fn evaluate_vdom( - templates: &[Element], - variables: &HashMap, - components: &HashMap, - rhei: &RheiContext, - stylesheet: &SS, - ) -> Vec { + pub fn evaluate_vdom<'a>( + templates: &[Element<'a>], + variables: &mut HashMap, + components: &HashMap>, + rhei: &RheiContext, + stylesheet: &SS, + ancestors: &[AncestorInfo], +) -> Vec> { let mut output = Vec::with_capacity(templates.len()); + // Precompute sibling info for structural pseudo-classes and sibling combinators + let sibling_infos: Vec = 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 { - 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" => { - let cond = el.properties.get("condition").cloned().unwrap_or_default(); - let is_true = Self::evaluate_condition(&cond, variables, rhei); + let cond = el.get_prop("condition").unwrap_or_default(); + let is_true = Self::evaluate_condition(cond, variables, rhei); let mut active_branch = Vec::new(); for child in &el.children { @@ -302,21 +348,19 @@ impl Interpreter { } } output.extend(Self::evaluate_vdom( - &active_branch, variables, components, rhei, stylesheet, + &active_branch, variables, components, rhei, stylesheet, ancestors, )); } "@each" => { - let var_name = el.properties.get("var_name").cloned().unwrap_or_default(); - let source_expr = el.properties.get("source").cloned().unwrap_or_default(); + let var_name = el.get_prop("var_name").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) { - rhei.eval_expr(expr, variables) - .map(|s| Self::normalize_rhai_array(&s)) - .unwrap_or_default() - } else { - Self::resolve_string(&source_expr, variables) - }; + Self::normalize_rhai_array(&rhei.eval_expr(expr, variables)) +} else { + Self::resolve_string(source_expr, variables).into_owned() +}; let items: Vec = if resolved_source.is_empty() { vec![] @@ -325,73 +369,84 @@ impl Interpreter { }; for item in items { - let mut local_vars = variables.clone(); - local_vars.insert(var_name.clone(), item); + let old_val = variables.insert(var_name.to_string(), item); + 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) { - // Expand custom component - let mut comp_scope = variables.clone(); + if let Some(comp) = components.get(el.type_name) { + let mut new_args = Vec::with_capacity(comp.params.len()); for (param, _) in &comp.params { - if let Some(arg) = el.properties.get(param) { - comp_scope.insert( + if let Some(arg) = el.get_prop(param) { + new_args.push(( param.clone(), 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 { 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 { - 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( - &vcomp.properties, - stylesheet.resolve(&vcomp.type_name), - ); + let matched_sheets = Self::collect_matching_styles(&vcomp, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]); + vcomp.computed_style = ComputedStyle::compute(&vcomp.properties, &matched_sheets); + + let child_ancestors = Self::build_ancestor_chain(ancestors, &vcomp); vcomp.children = Self::evaluate_vdom( - &comp.children, &comp_scope, components, rhei, stylesheet, + &comp.children, variables, components, rhei, stylesheet, &child_ancestors, ); 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 { - let mut vnode = Element::new(el.type_name.clone()); + let mut vnode = Element::new(el.type_name); for (k, v) in &el.properties { 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; } if v.starts_with('$') && !v[1..].contains(|c: char| !c.is_ascii_alphanumeric() && c != '_') { - vnode.properties.insert( - format!("__bind:{k}"), - v[1..].to_string(), - ); + vnode.set_prop(format!("__bind:{k}"), v[1..].to_string()); } - vnode.properties.insert( - k.clone(), - Self::resolve_prop(v, variables, rhei), - ); + vnode.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei)); } - vnode.computed_style = ComputedStyle::compute( - &vnode.properties, - stylesheet.resolve(&el.type_name), - ); + let matched_sheets = Self::collect_matching_styles(&vnode, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]); + vnode.computed_style = ComputedStyle::compute(&vnode.properties, &matched_sheets); + let child_ancestors = Self::build_ancestor_chain(ancestors, &vnode); vnode.children = Self::evaluate_vdom( - &el.children, variables, components, rhei, stylesheet, + &el.children, variables, components, rhei, stylesheet, &child_ancestors, ); output.push(vnode); } @@ -402,14 +457,46 @@ impl Interpreter { output } - fn resolve_prop(v: &str, variables: &HashMap, rhei: &RheiContext) -> String { - if let Some(expr) = v.strip_prefix(RHEI_PREFIX) { - rhei.eval_expr(expr, variables).unwrap_or_default() - } else { - Self::resolve_string(v, variables) - } + fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec { + let mut chain = ancestors.to_vec(); + let id = el.id().map(String::from); + let classes: Vec = el + .get_prop("class") + .map(|s| s.split_whitespace().map(String::from).collect()) + .unwrap_or_default(); + chain.push(AncestorInfo::new_with_id(el.type_name, id, classes)); + chain } + fn collect_matching_styles<'a>( + el: &Element, + stylesheet: &'a SS, + active_pseudo: &[&str], + structural: &StructuralContext, + ancestors: &[AncestorInfo], + preceding_siblings: &[AncestorInfo], + ) -> Vec<&'a HashMap> { + 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 = 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, 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( cond: &str, variables: &HashMap, @@ -476,18 +563,38 @@ impl Interpreter { } } - pub fn resolve_string(val: &str, scope: &HashMap) -> String { - let re = RE_VAR.get_or_init(|| Regex::new(r"\$([a-zA-Z0-9_]+)").unwrap()); - re.replace_all(val, |caps: ®ex::Captures| { - scope.get(&caps[1]) - .map(|s| s.as_str()) - .unwrap_or(&caps[0]) - .to_string() - }) - .to_string() + pub fn resolve_string<'a>(val: &'a str, scope: &HashMap) -> Cow<'a, str> { + if !val.contains('$') { + return Cow::Borrowed(val); } - fn attach(stack: &mut Vec, roots: &mut Vec, 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>, roots: &mut Vec>, el: Element<'a>) { match stack.last_mut() { Some(parent) => parent.children.push(el), None => roots.push(el), diff --git a/src/interpreter/reader.rs b/src/interpreter/reader.rs index 5044c75..fc3535e 100644 --- a/src/interpreter/reader.rs +++ b/src/interpreter/reader.rs @@ -7,81 +7,115 @@ pub struct Reader<'a> { } impl<'a> Reader<'a> { + #[inline(always)] pub fn new(data: &'a [u8]) -> Self { Self { data, pos: 0 } } + #[inline(always)] pub fn remaining(&self) -> usize { 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 { self.require(1)?; - let b = self.data[self.pos]; + let b = unsafe { *self.data.get_unchecked(self.pos) }; self.pos += 1; Ok(b) } + #[inline(always)] pub fn read_u32(&mut self) -> Result { 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; Ok(v) } + #[inline(always)] pub fn read_i64(&mut self) -> Result { 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; Ok(v) } + #[inline(always)] pub fn read_f64(&mut self) -> Result { 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; 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 { + self.read_str_ref().map(|s| s.to_owned()) + } + + #[inline(always)] + pub fn skip_string(&mut self) -> Result<(), InterpError> { let len = self.read_u32()? as usize; self.require(len)?; - let s = std::str::from_utf8(&self.data[self.pos..self.pos + len]) - .map_err(|_| InterpError::InvalidUtf8)? - .to_string(); self.pos += len; - Ok(s) + Ok(()) } pub fn read_value_as_string(&mut self, type_op: u8) -> Result, InterpError> { let s = match type_op { - OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_RHEI => { - Some(self.read_string()?) - } - OP_PROP_IDENT => { + OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_RHEI | OP_PROP_IDENT => { Some(self.read_string()?) } OP_PROP_VAR => { - Some(format!("${}", self.read_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()) + let name = self.read_str_ref()?; + let mut s = String::with_capacity(name.len() + 1); + s.push('$'); + s.push_str(name); + Some(s) } + 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_ARRAY => { let items = self.read_array_as_strings()?; Some(items.join(",")) } OP_PROP_UNIT => { - let num = self.read_f64()?; - let unit = self.read_string()?; + let num = self.read_f64()?; + let unit = self.read_str_ref()?; if num.fract() == 0.0 { Some(format!("{}{}", num as i64, unit)) } else { @@ -89,13 +123,12 @@ impl<'a> Reader<'a> { } } OP_PROP_CALL => { - let name = self.read_string()?; + let name = self.read_string()?; 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 { - let op = self.read_byte()?; - let val = self.read_value_as_string(op)? - .unwrap_or_default(); + let op = self.read_byte()?; + let val = self.read_value_as_string(op)?.unwrap_or_default(); args.push(val); } Some(format!("{}({})", name, args.join(","))) @@ -105,7 +138,6 @@ impl<'a> Reader<'a> { Ok(s) } - /// Read `OP_PROP_ARRAY` (opcode already consumed) and return elements as strings. pub fn read_array_as_strings(&mut self) -> Result, InterpError> { let count = self.read_u32()? as usize; let mut items = Vec::with_capacity(count); @@ -118,21 +150,15 @@ impl<'a> Reader<'a> { Ok(items) } - pub fn skip_value(&mut self, type_op: u8) -> Result<(), InterpError> { match type_op { - OP_PROP_STR - | OP_PROP_COLOR - | OP_PROP_FSPATH - | OP_PROP_VAR - | OP_PROP_RHEI - | OP_PROP_IDENT => { - self.read_string()?; + OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_VAR | OP_PROP_RHEI | OP_PROP_IDENT => { + self.skip_string()?; } - OP_PROP_INT => { self.read_i64()?; } + OP_PROP_INT => { self.read_i64()?; } OP_PROP_FLOAT => { self.read_f64()?; } - OP_PROP_BOOL => { self.read_byte()?; } - OP_PROP_NULL => {} + OP_PROP_BOOL => { self.read_byte()?; } + OP_PROP_NULL => {} OP_PROP_ARRAY => { let count = self.read_u32()?; for _ in 0..count { @@ -141,11 +167,11 @@ impl<'a> Reader<'a> { } } OP_PROP_UNIT => { - self.read_f64()?; // number - self.read_string()?; // unit suffix + self.read_f64()?; + self.skip_string()?; } OP_PROP_CALL => { - self.read_string()?; // function name + self.skip_string()?; let arg_count = self.read_u32()?; for _ in 0..arg_count { let op = self.read_byte()?; @@ -157,49 +183,38 @@ impl<'a> Reader<'a> { Ok(()) } - /// Skip a full opcode + its payload without interpreting it. pub fn skip_opcode(&mut self, op: u8) -> Result<(), InterpError> { match op { - OP_VERSION => { self.read_i64()?; } - OP_STYLE | OP_RHEI_BLK => { self.read_string()?; } + OP_VERSION => { self.read_i64()?; } + OP_STYLE | OP_RHEI_BLK => { self.skip_string()?; } - OP_PROP_STR - | OP_PROP_COLOR - | OP_PROP_FSPATH - | OP_PROP_VAR - | OP_PROP_RHEI - | 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_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_VAR | OP_PROP_RHEI | + 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.skip_string()?; + self.skip_value(op)?; } OP_GLOBAL | OP_LET => { - self.read_string()?; + self.skip_string()?; let vop = self.read_byte()?; self.skip_value(vop)?; } OP_SINGLETON => { - self.read_string()?; + self.skip_string()?; let count = self.read_u32()?; for _ in 0..count { - self.read_string()?; + self.skip_string()?; let vop = self.read_byte()?; self.skip_value(vop)?; } } OP_COMPONENT => { - self.read_string()?; + self.skip_string()?; let params = self.read_u32()?; for _ in 0..params { - self.read_string()?; - self.read_string()?; + self.skip_string()?; + self.skip_string()?; } self.skip_block()?; } @@ -210,50 +225,48 @@ impl<'a> Reader<'a> { if self.read_byte()? == 1 { self.skip_block()?; } } OP_EACH => { - self.read_string()?; + self.skip_string()?; let vop = self.read_byte()?; self.skip_value(vop)?; self.skip_block()?; } OP_ON => { - self.read_string()?; + self.skip_string()?; let args = self.read_u32()?; for _ in 0..args { - self.read_string()?; + self.skip_string()?; let vop = self.read_byte()?; self.skip_value(vop)?; } self.skip_block()?; } - OP_ELEM_PUSH => { self.read_string()?; } - OP_CONTENT => { + OP_ELEM_PUSH => { self.skip_string()?; } + OP_CONTENT => { let vop = self.read_byte()?; self.skip_value(vop)?; } - OP_STYLE_RULE => { - self.read_string()?; // selector + self.skip_string()?; let count = self.read_u32()?; for _ in 0..count { - self.read_string()?; // property key + self.skip_string()?; let vop = self.read_byte()?; - self.skip_value(vop)?; // property value + self.skip_value(vop)?; } } OP_STYLE_ANIM => { - self.read_string()?; // animation name + self.skip_string()?; let frame_count = self.read_u32()?; for _ in 0..frame_count { - self.read_string()?; // step ("from", "to", "50%", …) + self.skip_string()?; let prop_count = self.read_u32()?; for _ in 0..prop_count { - self.read_string()?; + self.skip_string()?; let vop = self.read_byte()?; self.skip_value(vop)?; } } } - _ => {} } Ok(()) @@ -266,15 +279,4 @@ impl<'a> Reader<'a> { self.skip_opcode(op)?; } } - - - #[inline] - fn require(&self, n: usize) -> Result<(), InterpError> { - if self.remaining() < n { - Err(InterpError::UnexpectedEof) - } else { - Ok(()) - } - } } - diff --git a/src/interpreter/rhei.rs b/src/interpreter/rhei.rs index 6f4b013..dc16fcb 100644 --- a/src/interpreter/rhei.rs +++ b/src/interpreter/rhei.rs @@ -1,12 +1,13 @@ -use rhai::{Dynamic, Engine, Scope, AST}; +use rhai::{Dynamic, Engine, Scope, AST, Module}; use std::collections::HashMap; +use std::cell::RefCell; pub const RHEI_PREFIX: &str = "__rhei:"; pub struct RheiContext { engine: Engine, init_ast: AST, - fn_ast: AST, + scope: RefCell>, } impl RheiContext { @@ -29,102 +30,88 @@ impl RheiContext { let mut fn_ast = combined.clone(); fn_ast.clear_statements(); - Self { - engine, - init_ast: combined, - fn_ast + 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 { + engine, + init_ast: combined, + scope: RefCell::new(Scope::new()), } } - pub fn initialize(&self, variables: &mut HashMap) { - let mut scope = Scope::new(); + pub fn sync_scope(&self, variables: &HashMap) { + let mut scope = self.scope.borrow_mut(); + for (k, v) in variables.iter() { - scope.push_dynamic(k.clone(), str_to_dyn(v)); - } - - if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &self.init_ast) { - eprintln!("⚠️ Rhei init error: {e}"); - } - - let names: Vec = scope.iter_raw() - .map(|(name, _, _)| name.to_string()) - .collect(); - for name in &names { - if let Some(val) = scope.get_value::(name) { - variables.insert(name.clone(), dyn_to_str(&val)); + if scope.contains(k) { + if let Some(old_val) = scope.get_value::(k) { + if dyn_to_str(&old_val) == *v { + continue; + } + } + scope.set_value(k, str_to_dyn(v)); + } else { + scope.push_dynamic(k.clone(), str_to_dyn(v)); } } } - pub fn eval_expr(&self, expr: &str, variables: &HashMap) -> Option { - let mut scope = Scope::new(); - for (k, v) in variables { - scope.push_dynamic(k.clone(), str_to_dyn(v)); + pub fn initialize(&self, variables: &mut HashMap) { + self.sync_scope(variables); + + 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 - .compile_expression(expr) - .or_else(|_| self.engine.compile(expr)) - .ok()?; + for (name, _, val) in scope.iter_raw() { + let s_val = dyn_to_str(&val); + if variables.get(name).map(|v| v != &s_val).unwrap_or(true) { + variables.insert(name.to_string(), s_val); + } + } + } - let full = self.fn_ast.merge(&expr_ast); + pub fn eval_expr(&self, expr: &str, variables: &HashMap) -> String { + self.sync_scope(variables); + let mut scope = self.scope.borrow_mut(); - match self.engine.eval_ast_with_scope::(&mut scope, &full) { - Ok(val) => Some(dyn_to_str(&val)), - Err(e) => { - eprintln!("⚠️ Rhei eval `{expr}`: {e}"); - None + match self.engine.eval_expression_with_scope::(&mut *scope, expr) { + Ok(val) => dyn_to_str(&val), + Err(e) => { + eprintln!("⚠️ Rhei eval_expr error: {e}"); + String::new() } } } pub fn eval_condition(&self, expr: &str, variables: &HashMap) -> bool { - let mut scope = Scope::new(); - for (k, v) in variables { - scope.push_dynamic(k.clone(), str_to_dyn(v)); - } + self.sync_scope(variables); + let mut scope = self.scope.borrow_mut(); - let ast = match self.engine.compile_expression(expr) { - Ok(a) => a, - 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::(&mut scope, &full) { - Ok(val) => { - if val.is_bool() { return val.cast::(); } - if val.is_int() { return val.cast::() != 0; } - if val.is_float(){ return val.cast::() != 0.0; } - if val.is_string(){ - let s = val.cast::(); - return !matches!(s.trim(), "" | "false" | "0" | "null"); - } - !val.is_unit() - } + match self.engine.eval_expression_with_scope::(&mut *scope, expr) { + Ok(b) => b, Err(e) => { - eprintln!("⚠️ Rhei condition eval `{expr}`: {e}"); + eprintln!("⚠️ Rhei eval_condition error: {e}"); false } } } pub fn execute_action(&self, script: &str, variables: &mut HashMap) { - let mut scope = Scope::new(); - for (k, v) in variables.iter() { - scope.push_dynamic(k.clone(), str_to_dyn(v)); - } + self.sync_scope(variables); + let mut scope = self.scope.borrow_mut(); match self.engine.compile(script) { Ok(action_ast) => { - let full = self.fn_ast.merge(&action_ast); - if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &full) { + if let Err(e) = self.engine.run_ast_with_scope(&mut *scope, &action_ast) { eprintln!("⚠️ Rhei action execution error: {e}"); } } @@ -133,12 +120,10 @@ impl RheiContext { } } - let names: Vec = scope.iter_raw() - .map(|(name, _, _)| name.to_string()) - .collect(); - for name in &names { - if let Some(val) = scope.get_value::(name) { - variables.insert(name.clone(), dyn_to_str(&val)); + for (name, _, val) in scope.iter_raw() { + let s_val = dyn_to_str(&val); + if variables.get(name).map(|v| v != &s_val).unwrap_or(true) { + variables.insert(name.to_string(), s_val); } } } @@ -150,7 +135,6 @@ impl Default for RheiContext { } } - pub fn str_to_dyn(s: &str) -> Dynamic { if let Ok(i) = s.parse::() { return Dynamic::from(i); } if let Ok(f) = s.parse::() { return Dynamic::from(f); } @@ -158,12 +142,9 @@ pub fn str_to_dyn(s: &str) -> Dynamic { Dynamic::from(s.to_owned()) } -pub fn dyn_to_str(val: &Dynamic) -> String { - if val.is_string() { - return val.clone().cast::(); +pub fn dyn_to_str(d: &Dynamic) -> String { + if d.is_string() { + return d.clone().into_string().unwrap_or_default(); } - if val.is_unit() { - return String::new(); - } - val.to_string() + d.to_string() } diff --git a/src/interpreter/style.rs b/src/interpreter/style.rs index bb0a279..f8a41c5 100644 --- a/src/interpreter/style.rs +++ b/src/interpreter/style.rs @@ -1,8 +1,465 @@ 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, + pub id: Option, + pub classes: Vec, + pub pseudo_classes: Vec, + pub attributes: Vec, +} + +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 = 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, + ) -> 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, + pub combinators: Vec, +} + +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 = parts.iter().map(|(s, _)| CompoundSelector::parse(s)).collect(); + let combinators: Vec = 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, + ) -> 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, + pub classes: Vec, +} + +impl AncestorInfo { + pub fn new(type_name: &str, classes: Vec) -> Self { + Self { type_name: type_name.to_string(), id: None, classes } + } + + pub fn new_with_id(type_name: &str, id: Option, classes: Vec) -> 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::() { + 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::().unwrap_or(0) + }; + + let b = if b_str.is_empty() { + 0 + } else if b_str.starts_with('+') { + b_str[1..].trim().parse::().unwrap_or(0) + } else if b_str.starts_with('-') { + b_str.parse::().unwrap_or(0) + } else { + b_str.parse::().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, +} + +impl StyleRule { + pub fn build(selector_str: String, properties: HashMap) -> Self { + Self { + selector: ComplexSelector::parse(&selector_str), + properties, + } + } +} + #[derive(Debug, Clone, Default)] pub struct StyleSheet { - index: HashMap>, + rules: Vec, } impl StyleSheet { @@ -11,80 +468,374 @@ impl StyleSheet { } pub fn add_rule(&mut self, selector: String, properties: HashMap) { - self.index - .entry(selector.trim().to_string()) - .or_default() - .extend(properties); + for part in split_selectors(&selector) { + self.rules.push(StyleRule::build(part, properties.clone())); + } } - #[inline] - pub fn resolve(&self, element_type: &str) -> Option<&HashMap> { - self.index.get(element_type.trim()) + pub fn matching_rules<'a>( + &'a self, + type_name: &str, + el_id: Option<&str>, + el_classes: &[&str], + active_pseudo: &[&str], + structural: &StructuralContext, + ancestors: &[AncestorInfo], + preceding_siblings: &[AncestorInfo], + el_attributes: &HashMap, + ) -> Vec<&'a HashMap> { + 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, + ) -> HashMap { + 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 { - self.index.is_empty() + self.rules.is_empty() } } +fn split_selectors(input: &str) -> Vec { + 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)] pub struct ComputedStyle { pub font_size: Option, pub color: Option, pub padding: Option, + pub padding_top: Option, + pub padding_right: Option, + pub padding_bottom:Option, + pub padding_left: Option, + + pub margin: Option, + pub margin_top: Option, + pub margin_right: Option, + pub margin_bottom: Option, + pub margin_left: Option, + pub background: Option, pub spacing: Option, pub border_radius: Option, pub border_width: Option, pub border_color: Option, - pub width: Option, + + pub width: Option, + pub height: Option, + + pub min_width: Option, + pub max_width: Option, + pub min_height: Option, + pub max_height: Option, pub direction: Option, pub align_items: Option, pub content_align: Option, + + pub flex_grow: Option, + + pub position: Option, + pub top: Option, + pub right: Option, + pub bottom: Option, + pub left: Option, + + pub overflow_x: Option, + pub overflow_y: Option, + pub display: Option, + + pub opacity: Option, + pub font_weight: Option, + pub line_height: Option, + pub text_align: Option, } impl ComputedStyle { - pub fn compute( - inline: &HashMap, - sheet: Option<&HashMap>, +pub fn compute( + inline: &[(Cow<'_, str>, Cow<'_, str>)], + matched_sheets: &[&HashMap], ) -> Self { +let overflow = lookup("overflow", inline, matched_sheets).and_then(parse_overflow); Self { - font_size: lookup("font-size", inline, sheet).and_then(parse_size), - color: lookup("color", inline, sheet).and_then(parse_color), - padding: lookup("padding", inline, sheet).and_then(parse_size), - background: lookup("background", inline, sheet) - .or_else(|| lookup("background-color", inline, sheet)) - .and_then(parse_color), - spacing: lookup("spacing", inline, sheet) - .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), + font_size: lookup("font-size", inline, matched_sheets).and_then(parse_size), + color: lookup("color", inline, matched_sheets).and_then(parse_color), + padding: lookup("padding", inline, matched_sheets).and_then(parse_size), + padding_top: lookup("padding-top", inline, matched_sheets).and_then(parse_size), + padding_right: lookup("padding-right", inline, matched_sheets).and_then(parse_size), + padding_bottom: lookup("padding-bottom", inline, matched_sheets).and_then(parse_size), + padding_left: lookup("padding-left", inline, matched_sheets).and_then(parse_size), - direction: lookup("direction", inline, sheet).and_then(parse_direction), - align_items: lookup("align-items", inline, sheet).and_then(parse_alignment), - content_align: lookup("content-align", inline, sheet).and_then(parse_content_align), + margin: lookup("margin", inline, matched_sheets).and_then(parse_size), + margin_top: lookup("margin-top", inline, matched_sheets).and_then(parse_size), + 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::().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) { + struct Override<'a>(&'a HashMap); + 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::().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] fn lookup<'a>( key: &str, - inline: &'a HashMap, - sheet: Option<&'a HashMap>, + inline: &'a [(Cow<'_, str>, Cow<'_, str>)], + matched_sheets: &[&'a HashMap], ) -> Option<&'a str> { - if let Some(v) = inline.get(key) { - return Some(v.as_str()); + for (k, v) in inline { + if **k == *key { return Some(v.as_ref()); } + if k.len() == key.len() + 6 + && k.starts_with("style:") + && &k[6..] == key + { + return Some(v.as_ref()); + } } - if let Some(v) = inline.get(&format!("style:{key}")) { - return Some(v.as_str()); + for sheet in matched_sheets.iter().rev() { + 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 { + 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 { + 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 { + 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::().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 { let s = s.trim(); @@ -148,36 +899,91 @@ pub enum ContentAlign { End, } -pub fn parse_direction(s: &str) -> Option { - match s.trim().to_lowercase().as_str() { - "horizontal" | "row" => Some(LayoutDirection::Row), - "vertical" | "column" => Some(LayoutDirection::Column), - "grid" => Some(LayoutDirection::Grid), - _ => None, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TextAlign { + Left, + Center, + Right, +} + +impl From 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 { + 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 { - match s.trim().to_lowercase().as_str() { - "start" => Some(iced::Alignment::Start), - "center" => Some(iced::Alignment::Center), - "end" => Some(iced::Alignment::End), - _ => None, - } + let s = s.trim(); + if s.eq_ignore_ascii_case("start") { Some(iced::Alignment::Start) } + else if s.eq_ignore_ascii_case("center") { Some(iced::Alignment::Center) } + else if s.eq_ignore_ascii_case("end") { Some(iced::Alignment::End) } + else { None } } pub fn parse_content_align(s: &str) -> Option { - match s.trim().to_lowercase().as_str() { - "start" | "left" | "top" => Some(ContentAlign::Start), - "center" => Some(ContentAlign::Center), - "end" | "right" | "bottom" => Some(ContentAlign::End), - _ => None, + let s = s.trim(); + if s.eq_ignore_ascii_case("start") || s.eq_ignore_ascii_case("left") || s.eq_ignore_ascii_case("top") { Some(ContentAlign::Start) } + else if s.eq_ignore_ascii_case("center") { Some(ContentAlign::Center) } + else if s.eq_ignore_ascii_case("end") || s.eq_ignore_ascii_case("right") || s.eq_ignore_ascii_case("bottom") { Some(ContentAlign::End) } + else { None } +} + +pub fn parse_display(s: &str) -> Option { + 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 { + let v = s.trim().parse::().ok()?; + Some(v.clamp(0.0, 1.0)) +} + +pub fn parse_font_weight(s: &str) -> Option { + let s = s.trim(); + match s { + "normal" => Some(400), + "bold" => Some(700), + "lighter" => Some(300), + "bolder" => Some(900), + _ => s.parse::().ok().map(|v| v as u16), } } +pub fn parse_text_align(s: &str) -> Option { + 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 { - s.trim() - .trim_end_matches(|c: char| c.is_alphabetic() || c == '%') + let s = s.trim(); + 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::() .ok() } diff --git a/src/interpreter/types.rs b/src/interpreter/types.rs index 9bd93b4..3b1e152 100644 --- a/src/interpreter/types.rs +++ b/src/interpreter/types.rs @@ -1,38 +1,45 @@ +use std::borrow::Cow; use std::collections::HashMap; use std::fmt; use super::style::{ComputedStyle, StyleSheet}; #[derive(Debug, Clone)] -pub struct Element { - pub type_name: String, - pub properties: HashMap, - pub children: Vec, +pub struct Element<'a> { + pub type_name: &'a str, + pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>, + pub children: Vec>, pub computed_style: ComputedStyle, } -impl Element { - pub fn new(type_name: String) -> Self { +impl<'a> Element<'a> { + pub fn id(&self) -> Option<&str> { + self.properties.iter().find_map(|(k, v)| { + if **k == *"id" { Some(v.as_ref()) } else { None } + }) + } + + pub fn new(type_name: &'a str) -> Self { Self { type_name, - properties: HashMap::new(), - children: Vec::new(), + properties: Vec::with_capacity(8), + children: Vec::with_capacity(4), computed_style: ComputedStyle::default(), } } } #[derive(Debug, Clone)] -pub struct ComponentDef { +pub struct ComponentDef<'a> { pub name: String, pub params: Vec<(String, String)>, - pub children: Vec, + pub children: Vec>, } #[derive(Debug, Clone)] -pub struct Document { - pub roots: Vec, - pub components: HashMap, +pub struct Document<'a> { + pub roots: Vec>, + pub components: HashMap>, pub variables: HashMap, pub rhei_scripts: Vec, pub stylesheet: StyleSheet, diff --git a/src/main.rs b/src/main.rs index a857472..ad781ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ pub enum Message { InputChanged(Option, String), ToggleChanged(Option, bool), SliderChanged(Option, f64), + WindowScrolled(f32), } #[derive(Parser)] diff --git a/src/renderer.rs b/src/renderer.rs index 3e9e70d..0ef27be 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,27 +1,207 @@ -use std::collections::HashMap; - +use crate::Message; +use crate::interpreter::Element; +use crate::interpreter::style::{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign}; +use iced::widget::container::Style as ContainerStyle; use iced::widget::{ - button, checkbox, column, container, progress_bar, row, slider, text, text_input, image, svg + button, checkbox, column, container, image, progress_bar, row, scrollable, slider, svg, text, + text::Wrapping, text_input, }; use iced::{Alignment, Background, Border, Length, Theme}; -use iced::widget::container::Style as ContainerStyle; -use crate::interpreter::style::{LayoutDirection, ContentAlign, ComputedStyle}; -use crate::interpreter::Element; -use crate::Message; +use iced::font::Weight; +use std::borrow::Cow; +use std::collections::HashMap; fn extract_var_binding(el: &Element, prop: &str) -> Option { - el.properties.get(&format!("__bind:{prop}")).cloned() + el.properties.iter() + .find_map(|(k, v)| { + k.strip_prefix("__bind:") + .filter(|&p| p == prop) + .map(|_| v.to_string()) + }) +} + +impl<'a> Element<'a> { + #[inline] + pub fn get_prop(&self, key: &str) -> Option<&str> { + self.properties.iter() + .find(|(k, _)| **k == *key) + .map(|(_, v)| v.as_ref()) + } + + pub fn push_prop>, V: Into>>(&mut self, key: K, val: V) { + self.properties.push((key.into(), val.into())); + } + + pub fn set_prop>, V: Into>>(&mut self, key: K, val: V) { + let key_cow = key.into(); + if let Some(entry) = self.properties.iter_mut().find(|(k,_)| *k == key_cow) { + entry.1 = val.into(); + } else { + self.properties.push((key_cow, val.into())); + } + } +} + +pub fn collect_hover_active<'a>( + el: &'a Element, + stylesheet: &StyleSheet, +) -> (HashMap, HashMap) { + let el_id = el.id(); + let classes: Vec<&str> = el + .get_prop("class") + .map(|s| s.split_whitespace().collect()) + .unwrap_or_default(); + + let default_struct = StructuralContext::default(); + + let el_attributes: HashMap = el.properties.iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + let hover_props = stylesheet.matching_pseudo_rules( + "hover", el.type_name, el_id, &classes, &default_struct, &[], &[], &el_attributes, + ); + let active_props = stylesheet.matching_pseudo_rules( + "active", el.type_name, el_id, &classes, &default_struct, &[], &[], &el_attributes, + ); + + (hover_props, active_props) +} + +fn make_hoverable<'a>( + widget: iced::Element<'a, crate::Message, Theme, iced::Renderer>, + el: &Element, + hover_props: &HashMap, + active_props: &HashMap, + base_cs: &ComputedStyle, +) -> iced::Element<'a, crate::Message, Theme, iced::Renderer> { + let has_hover = !hover_props.is_empty(); + let has_active = !active_props.is_empty(); + + if !has_hover && !has_active { + return widget; + } + + let hover = hover_props.clone(); + let active = active_props.clone(); + let cs = base_cs.clone(); + + let mut btn = button(widget).padding(0); + + if let Some(script) = el.get_prop("__on:click") { + btn = btn.on_press(crate::Message::EventTriggered(script.to_string())); + } else if has_hover || has_active { + btn = btn.on_press(crate::Message::EventTriggered(String::new())); + } + + btn.style(move |_, status| { + let mut style_cs = cs.clone(); + match status { + button::Status::Hovered => { + if has_hover { + style_cs.apply_overrides(&hover); + } + } + button::Status::Pressed => { + if has_active { + style_cs.apply_overrides(&active); + } else if has_hover { + style_cs.apply_overrides(&hover); + } + } + _ => {} + } + button::Style { + background: style_cs.background.map(Background::Color), + border: Border { + color: style_cs.border_color.unwrap_or(iced::Color::TRANSPARENT), + width: style_cs.border_width.unwrap_or(0.0), + radius: style_cs.border_radius.unwrap_or(0.0).into(), + }, + text_color: style_cs.color.unwrap_or(iced::Color::WHITE), + ..Default::default() + } + }) + .into() } fn render_children<'a>( children: &'a [Element], + parent_color: Option, + parent_font_size: Option, + parent_direction: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + stylesheet: &StyleSheet, ) -> Vec> { - children.iter().filter_map(render_element).collect() + children + .iter() + .filter_map(|c| { + render_element( + c, + parent_color, + parent_font_size, + parent_direction, + fixed_layers, + abs_layers, + sticky_layers, + stylesheet, + ) + }) + .collect() +} + +fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding { + let base = cs.padding.unwrap_or(default_pad); + let mut top = cs.padding_top.unwrap_or(base); + let mut right = cs.padding_right.unwrap_or(base); + let mut bottom = cs.padding_bottom.unwrap_or(base); + let mut left = cs.padding_left.unwrap_or(base); + + let bwidth = cs.border_width.unwrap_or(0.0); + + if let Some(iced::Length::Fixed(w)) = cs.width { + let total_h = left + right + bwidth * 2.0; + if total_h > w && w > 0.0 { + let scale = w / total_h; + left *= scale; + right *= scale; + } + } + + if let Some(iced::Length::Fixed(h)) = cs.height { + let total_v = top + bottom + bwidth * 2.0; + if total_v > h && h > 0.0 { + let scale = h / total_v; + top *= scale; + bottom *= scale; + } + } + + iced::Padding { + top, + right, + bottom, + left, + } +} + +fn get_margin(cs: &ComputedStyle) -> iced::Padding { + let base = cs.margin.unwrap_or(0.0); + iced::Padding { + top: cs.margin_top.unwrap_or(base), + right: cs.margin_right.unwrap_or(base), + bottom: cs.margin_bottom.unwrap_or(base), + left: cs.margin_left.unwrap_or(base), + } } fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style { - let base_bg = cs.background.unwrap_or(iced::Color::from_rgb(0.25, 0.26, 0.35)); - + let base_bg = cs + .background + .unwrap_or(iced::Color::from_rgb(0.25, 0.26, 0.35)); + let bg_color = match status { button::Status::Hovered => iced::Color { r: (base_bg.r * 1.15).min(1.0), @@ -41,8 +221,8 @@ fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style button::Style { background: Some(Background::Color(bg_color)), border: Border { - color: cs.border_color.unwrap_or(iced::Color::TRANSPARENT), - width: cs.border_width.unwrap_or(0.0), + color: cs.border_color.unwrap_or(iced::Color::TRANSPARENT), + width: cs.border_width.unwrap_or(0.0), radius: cs.border_radius.unwrap_or(0.0).into(), }, text_color: cs.color.unwrap_or(iced::Color::WHITE), @@ -52,10 +232,15 @@ fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style { text_input::Style { - background: Background::Color(cs.background.unwrap_or(iced::Color::from_rgb(0.12, 0.12, 0.14))), + background: Background::Color( + cs.background + .unwrap_or(iced::Color::from_rgb(0.12, 0.12, 0.14)), + ), border: Border { - color: cs.border_color.unwrap_or(iced::Color::from_rgb(0.25, 0.25, 0.3)), - width: cs.border_width.unwrap_or(1.0), + color: cs + .border_color + .unwrap_or(iced::Color::from_rgb(0.25, 0.25, 0.3)), + width: cs.border_width.unwrap_or(1.0), radius: cs.border_radius.unwrap_or(0.0).into(), }, icon: iced::Color::from_rgb(0.5, 0.5, 0.5), @@ -65,141 +250,558 @@ fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style { } } - -fn apply_container_style<'a>( - c: container::Container<'a, Message, Theme, iced::Renderer>, - cs: &ComputedStyle, +fn wrap_fixed_position<'a>( + widget: iced::Element<'a, Message, Theme, iced::Renderer>, + cs: &ComputedStyle, ) -> iced::Element<'a, Message, Theme, iced::Renderer> { - let bg = cs.background; + use iced::alignment::{Horizontal, Vertical}; + + let top = cs.top.unwrap_or(0.0); + let left = cs.left.unwrap_or(0.0); + let right = cs.right.unwrap_or(0.0); + let bottom = cs.bottom.unwrap_or(0.0); + + let has_top = cs.top.is_some(); + let has_bottom = cs.bottom.is_some(); + let has_left = cs.left.is_some(); + let has_right = cs.right.is_some(); + + let horiz = match (has_left, has_right) { + (true, true) => Horizontal::Left, + (false, true) => Horizontal::Right, + _ => Horizontal::Left, + }; + + let vert = match (has_top, has_bottom) { + (true, true) => Vertical::Top, + (false, true) => Vertical::Bottom, + _ => Vertical::Top, + }; + + let padding = iced::Padding { + top: if has_top { top } else { 0.0 }, + bottom: if has_bottom { bottom } else { 0.0 }, + left: if has_left { left } else { 0.0 }, + right: if has_right { right } else { 0.0 }, + }; + + container(widget) + .width(Length::Fill) + .height(Length::Fill) + .align_x(horiz) + .align_y(vert) + .padding(padding) + .into() +} + +fn apply_universal_box_model<'a>( + widget: impl Into>, + cs: &ComputedStyle, + is_window: bool, + default_padding: f32, +) -> iced::Element<'a, Message, Theme, iced::Renderer> { + let bg = cs.background; let radius = cs.border_radius.unwrap_or(0.0); let bwidth = cs.border_width.unwrap_or(0.0); - let bcolor = cs.border_color.unwrap_or(iced::Color::TRANSPARENT); + + let bcolor = cs.border_color.unwrap_or_else(|| { + if bwidth > 0.0 { + iced::Color::from_rgb(0.4, 0.4, 0.45) + } else { + iced::Color::TRANSPARENT + } + }); let needs_style = bg.is_some() || radius != 0.0 || bwidth != 0.0; + let padding = get_padding(cs, default_padding); - let c = if needs_style { - c.style(move |_theme| ContainerStyle { + let mut padded_content = container(widget).padding(padding); + + let ox = cs.overflow_x.unwrap_or(Overflow::Visible); + let oy = cs.overflow_y.unwrap_or(if is_window { Overflow::Auto } else { Overflow::Visible }); + + let scroll_v = oy == Overflow::Scroll || oy == Overflow::Auto; + let scroll_h = ox == Overflow::Scroll || ox == Overflow::Auto; + + if !scroll_h { + padded_content = padded_content.width(Length::Fill); + } + + let content_layer: iced::Element<'a, Message, Theme, iced::Renderer> = if scroll_v || scroll_h { + use iced::widget::scrollable::{Direction, Scrollbar}; + + let direction = match (scroll_v, scroll_h) { + (true, true) => Direction::Both { + vertical: Default::default(), + horizontal: Default::default(), + }, + (true, false) => Direction::Vertical(Default::default()), + (false, true) => Direction::Horizontal(Default::default()), + (false, false) => unreachable!(), + }; + + let mut s = scrollable(padded_content) + .direction(direction) + .width(Length::Fill); + + if scroll_v { + s = s.height(Length::Fill); + } + + s.into() + } else { + padded_content.into() + }; + + let mut inner = container(content_layer); + + if ox == Overflow::Hidden || oy == Overflow::Hidden { + inner = inner.clip(true); + } + + if needs_style { + inner = inner.style(move |_theme| ContainerStyle { background: bg.map(Background::Color), border: Border { - color: bcolor, - width: bwidth, + color: bcolor, + width: bwidth, radius: radius.into(), }, ..Default::default() - }) + }); } else { - c.style(|_theme| ContainerStyle::default()) + inner = inner.style(|_theme| ContainerStyle::default()); + } + + if is_window { + inner = inner.width(Length::Fill).height(Length::Fill); + } else { + if let Some(w) = cs.width { + inner = inner.width(w); + } else if cs.content_align.is_some() { + inner = inner.width(Length::Fill); + } + + if let Some(h) = cs.height { + inner = inner.height(h); + } + if let Some(max_w) = cs.max_width { + inner = inner.max_width(max_w); + } + if let Some(max_h) = cs.max_height { + inner = inner.max_height(max_h); + } + + if let Some(ca) = cs.content_align { + match ca { + ContentAlign::Start => inner = inner.align_x(iced::alignment::Horizontal::Left), + ContentAlign::Center => inner = inner.align_x(iced::alignment::Horizontal::Center), + ContentAlign::End => inner = inner.align_x(iced::alignment::Horizontal::Right), + } + } + } + + let margin = get_margin(cs); + let has_margin = margin.top > 0.0 || margin.right > 0.0 || margin.bottom > 0.0 || margin.left > 0.0; + + let result: iced::Element<'a, Message, Theme, iced::Renderer> = if has_margin && !is_window { + let mut outer = container(inner).padding(margin); + + if let Some(w) = cs.width { + if matches!(w, Length::Fill | Length::FillPortion(_)) { + outer = outer.width(w); + } + } else if cs.content_align.is_some() { + outer = outer.width(Length::Fill); + } + + if let Some(h) = cs.height { + if matches!(h, Length::Fill | Length::FillPortion(_)) { + outer = outer.height(h); + } + } + + outer.into() + } else { + inner.into() }; - match cs.width { - Some(w) => c.width(Length::Fixed(w)).into(), - None => c.into(), - } + + result } pub fn render_element<'a>( el: &'a Element, + parent_color: Option, + parent_font_size: Option, + parent_direction: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + stylesheet: &StyleSheet, ) -> Option> { - let props = &el.properties; - let cs = &el.computed_style; + let mut cs = el.computed_style; - match el.type_name.as_str() { - "Window" => { - let padding = cs.padding.unwrap_or(0.0); - let spacing = cs.spacing.unwrap_or(12.0); - let mut col = column![].spacing(spacing); - for child in render_children(&el.children) { col = col.push(child); } - Some(apply_container_style(container(col).padding(padding), cs)) + if cs.display == Some(Display::None) { + return None; + } + + let (hover_props, active_props) = collect_hover_active(el, stylesheet); + + let is_fixed = cs.position == Some(Position::Fixed) && el.type_name != "Window"; + let is_absolute = cs.position == Some(Position::Absolute) && el.type_name != "Window"; + let is_sticky = cs.position == Some(Position::Sticky) && el.type_name != "Window"; + + if is_fixed || is_absolute || is_sticky { + if cs.left.is_some() && cs.right.is_some() && cs.width.is_none() { + cs.width = Some(Length::Fill); + } + if cs.top.is_some() && cs.bottom.is_some() && cs.height.is_none() { + cs.height = Some(Length::Fill); + } + } + + if let Some(grow_value) = cs.flex_grow { + if cs.width.is_none() && parent_direction == Some(LayoutDirection::Row) { + cs.width = Some(Length::FillPortion(grow_value)); + } + if cs.height.is_none() && parent_direction == Some(LayoutDirection::Column) { + cs.height = Some(Length::FillPortion(grow_value)); + } + } + + let current_color = cs.color.or(parent_color); + let current_font_size = cs.font_size.or(parent_font_size); + + if el.type_name == "Window" { + let spacing = cs.spacing.unwrap_or(12.0); + let mut col = column![].spacing(spacing).width(Length::Fill); + + let mut window_fixed_layers = Vec::new(); + let mut window_abs_layers = Vec::new(); + let mut window_sticky_layers = Vec::new(); + + for child in render_children( + &el.children, + current_color, + current_font_size, + Some(LayoutDirection::Column), + &mut window_fixed_layers, + &mut window_abs_layers, + &mut window_sticky_layers, + stylesheet, + ) { + col = col.push(child); } - "Panel" => Some(render_panel(el)), + let main_flow = apply_universal_box_model(col, &cs, true, 0.0); + let has_abs = !window_abs_layers.is_empty(); + let has_fixed = !window_fixed_layers.is_empty(); + let has_sticky = !window_sticky_layers.is_empty(); + + if !has_abs && !has_fixed && !has_sticky { + return Some(main_flow); + } + + let mut stack_widget = iced::widget::stack![main_flow]; + for layer in window_abs_layers { + stack_widget = stack_widget.push(layer); + } + for layer in window_sticky_layers { + stack_widget = stack_widget.push(layer); + } + for layer in window_fixed_layers { + stack_widget = stack_widget.push(layer); + } + return Some(stack_widget.into()); + } + + let mut final_widget_opt = None; + + if el.type_name == "Panel" { + let mut panel_abs = Vec::new(); + let mut panel_sticky = Vec::new(); + let widget = render_panel( + el, + cs, + current_color, + current_font_size, + fixed_layers, + &mut panel_abs, + &mut panel_sticky, + stylesheet, + ); + if panel_abs.is_empty() { + final_widget_opt = Some(widget); + } else { + let mut stack_widget = iced::widget::stack![widget]; + for layer in panel_abs { + stack_widget = stack_widget.push(layer); + } + final_widget_opt = Some(stack_widget.into()); + } + } else if el.type_name == "Button" { + let mut btn_abs = Vec::new(); + let mut btn_sticky = Vec::new(); + let widget = render_button( + el, + cs, + current_color, + current_font_size, + fixed_layers, + &mut btn_abs, + &mut btn_sticky, + stylesheet, + ); + if btn_abs.is_empty() && btn_sticky.is_empty() { + final_widget_opt = Some(widget); + } else { + let mut stack_widget = iced::widget::stack![widget]; + for layer in btn_abs { + stack_widget = stack_widget.push(layer); + } + for layer in btn_sticky { + stack_widget = stack_widget.push(layer); + } + final_widget_opt = Some(stack_widget.into()); + } + } else if el.type_name == "Input" { + final_widget_opt = Some(render_input( + el, cs, current_color, current_font_size, &hover_props, &active_props, + )); + } else { +let element_opt: Option> = + match el.type_name { "Title" | "Header" => { - let content = props.get("text").cloned().unwrap_or_else(|| "Title".into()); - let mut widget = text(content).size(cs.font_size.unwrap_or(24.0)); - if let Some(col) = cs.color { widget = widget.color(col); } - Some(widget.into()) - } - - "Text" | "Label" | "#text" => { - let content = props.get("text").cloned().unwrap_or_default(); - let mut widget = text(content).size(cs.font_size.unwrap_or(16.0)); - if let Some(col) = cs.color { widget = widget.color(col); } - Some(widget.into()) - } - - "Image" => { - let src = props.get("src").cloned().unwrap_or_default(); - - let path = src.strip_prefix("fs:").unwrap_or(&src); - - let mut element: iced::Element<'a, Message, Theme, iced::Renderer> = if path.ends_with(".svg") { - let mut svg_widget = svg(svg::Handle::from_path(path)); - if let Some(w) = cs.width { - svg_widget = svg_widget.width(Length::Fixed(w)); - } else { - svg_widget = svg_widget.width(Length::Shrink); + let content = el.get_prop("text").unwrap_or("Title"); + let mut widget = text(content).size(current_font_size.unwrap_or(24.0)); + if let Some(col) = current_color { + widget = widget.color(col); + } + if let Some(fw) = cs.font_weight { + let w = if fw <= 399 { Weight::Light } + else if fw <= 599 { Weight::Normal } + else if fw <= 799 { Weight::Bold } + else { Weight::ExtraBold }; + widget = widget.font(iced::font::Font { weight: w, ..Default::default() }); + } + if let Some(ta) = cs.text_align { + widget = widget.align_x(iced::alignment::Horizontal::from(ta)); + widget = widget.width(Length::Fill); + } + if let Some(lh) = cs.line_height { + widget = widget.line_height(lh); + widget = widget.width(Length::Fill); + widget = widget.wrapping(Wrapping::Word); + } + Some(widget.into()) } - svg_widget.into() - } else { - let mut img_widget = image(path); - if let Some(w) = cs.width { - img_widget = img_widget.width(Length::Fixed(w)); - } else { - img_widget = img_widget.width(Length::Shrink); + + "Text" | "Label" | "#text" => { + let content = el.get_prop("text").unwrap_or(""); + let mut widget = text(content).size(current_font_size.unwrap_or(16.0)); + if let Some(col) = current_color { + widget = widget.color(col); + } + if let Some(fw) = cs.font_weight { + let w = if fw <= 399 { Weight::Light } + else if fw <= 599 { Weight::Normal } + else if fw <= 799 { Weight::Bold } + else { Weight::ExtraBold }; + widget = widget.font(iced::font::Font { weight: w, ..Default::default() }); + } + if let Some(ta) = cs.text_align { + widget = widget.align_x(iced::alignment::Horizontal::from(ta)); + widget = widget.width(Length::Fill); + } + if let Some(lh) = cs.line_height { + widget = widget.line_height(lh); + widget = widget.width(Length::Fill); + widget = widget.wrapping(Wrapping::Word); + } + Some(widget.into()) + } + + "Image" => { + let src = el.get_prop("src").unwrap_or(""); + let path = src.strip_prefix("fs:").unwrap_or(src); + let radius = cs.border_radius.unwrap_or(0.0); + let padding = get_padding(&cs, 0.0); + let bwidth = cs.border_width.unwrap_or(0.0); + + let content_w = cs.width.map(|len| { + if let iced::Length::Fixed(w) = len { + iced::Length::Fixed( + (w - padding.left - padding.right - bwidth * 2.0).max(0.0), + ) + } else { + len + } + }); + + let content_h = cs.height.map(|len| { + if let iced::Length::Fixed(h) = len { + iced::Length::Fixed( + (h - padding.top - padding.bottom - bwidth * 2.0).max(0.0), + ) + } else { + len + } + }); + + if path.ends_with(".svg") { + let mut svg_widget = svg(svg::Handle::from_path(path)); + if let Some(w) = content_w { + svg_widget = svg_widget.width(w); + } else { + svg_widget = svg_widget.width(Length::Shrink); + } + + if let Some(h) = content_h { + svg_widget = svg_widget.height(h); + } else { + svg_widget = svg_widget.height(Length::Shrink); + } + + Some(svg_widget.into()) + } else { + let mut img_widget = image(path); + if let Some(w) = content_w { + img_widget = img_widget.width(w); + } else { + img_widget = img_widget.width(Length::Shrink); + } + + if let Some(h) = content_h { + img_widget = img_widget.height(h); + } else { + img_widget = img_widget.height(Length::Shrink); + } + + if radius > 0.0 { + img_widget = img_widget.border_radius(radius); + } + + Some(img_widget.into()) + } + } + + "Toggle" => Some(render_toggle(el, current_color, current_font_size)), + "Slider" => Some(render_slider(el)), + + "Icon" => { + let mut widget = text("🔹").size(current_font_size.unwrap_or(18.0)); + if let Some(col) = current_color { + widget = widget.color(col); + } + Some(widget.into()) + } + + "ProgressBar" => { + let value = el.get_prop("value") + .and_then(|v| v.parse::().ok()) + .unwrap_or(0.0); + Some(progress_bar(0.0..=100.0, value).into()) + } + + "Divider" | "Separator" => Some(iced::widget::rule::horizontal(1).into()), + + _ => { + if el.children.is_empty() { + None + } else { + let spacing = cs.spacing.unwrap_or(10.0); + let mut col = column![].spacing(spacing); + let mut default_abs = Vec::new(); + let mut default_sticky = Vec::new(); + for child in render_children( + &el.children, + current_color, + current_font_size, + Some(LayoutDirection::Column), + fixed_layers, + &mut default_abs, + &mut default_sticky, + stylesheet, + ) { + col = col.push(child); + } + // Abs inside content (before box model = inside scrollable) + let content_widget: iced::Element<'a, Message, Theme, iced::Renderer> = + if default_abs.is_empty() { + col.into() + } else { + let mut stack = iced::widget::stack![col]; + for layer in default_abs { + stack = stack.push(layer); + } + stack.into() + }; + let boxed = apply_universal_box_model(content_widget, &cs, false, 0.0); + // Sticky outside (after box model = outside scrollable) + if default_sticky.is_empty() { + final_widget_opt = Some(boxed); + } else { + let mut stack = iced::widget::stack![boxed]; + for layer in default_sticky { + stack = stack.push(layer); + } + final_widget_opt = Some(stack.into()); + } + None + } } - img_widget.into() }; - if cs.background.is_some() || cs.border_radius.is_some() || cs.border_width.is_some() { - Some(apply_container_style(container(element), cs)) - } else { - Some(element) - } + if let Some(element) = element_opt { + final_widget_opt = Some(apply_universal_box_model(element, &cs, false, 0.0)); } + } - "Button" => Some(render_button(el)), - "Input" => Some(render_input(el)), - "Toggle" => Some(render_toggle(el)), - "Slider" => Some(render_slider(el)), - - "Icon" => Some(text("🔹").size(18).into()), - - "ProgressBar" => { - let value = props.get("value") - .and_then(|v| v.parse::().ok()) - .unwrap_or(0.0); - Some(progress_bar(0.0..=100.0, value).into()) + if el.type_name != "Button" && el.type_name != "Input" { + if let Some(w) = final_widget_opt.take() { + let wrapped = make_hoverable(w, el, &hover_props, &active_props, &cs); + final_widget_opt = Some(wrapped); } + } - "Divider" | "Separator" => Some(iced::widget::rule::horizontal(1).into()), - - _ => { - if el.children.is_empty() { - return None; - } - let padding = cs.padding.unwrap_or(0.0); - let spacing = cs.spacing.unwrap_or(10.0); - let mut col = column![].spacing(spacing); - for child in render_children(&el.children) { col = col.push(child); } - - if cs.background.is_none() && cs.border_radius.is_none() && cs.border_width.is_none() { - Some(col.into()) - } else { - Some(apply_container_style(container(col).padding(padding), cs)) - } + if let Some(final_widget) = final_widget_opt { + if is_fixed { + let wrapped = wrap_fixed_position(final_widget, &cs); + fixed_layers.push(wrapped); + None + } else if is_absolute { + let wrapped = wrap_fixed_position(final_widget, &cs); + abs_layers.push(wrapped); + None + } else if is_sticky { + let wrapped = wrap_fixed_position(final_widget, &cs); + sticky_layers.push(wrapped); + None + } else { + Some(final_widget) } + } else { + None } } -fn render_panel<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> { - let cs = &el.computed_style; - let padding = cs.padding.unwrap_or(5.0); +fn render_panel<'a>( + el: &'a Element, + cs: ComputedStyle, + parent_color: Option, + parent_font_size: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + stylesheet: &StyleSheet, +) -> iced::Element<'a, Message, Theme, iced::Renderer> { let spacing = cs.spacing.unwrap_or(10.0); - let is_horizontal = el.children.iter().all(|c| { +let is_horizontal = el.children.iter().all(|c| { matches!( - c.type_name.as_str(), + c.type_name, "Icon" | "Button" | "MenuItem" | "Label" | "Toggle" | "Text" | "#text" ) }); @@ -213,138 +815,344 @@ fn render_panel<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced:: let content: iced::Element<'a, Message, Theme, iced::Renderer> = match direction { LayoutDirection::Row => { let mut r = row![].spacing(spacing); - if let Some(align) = cs.align_items { - r = r.align_y(align); - } else { + if let Some(align) = cs.align_items { + r = r.align_y(align); + } else { r = r.align_y(Alignment::Center); } - for child in render_children(&el.children) { r = r.push(child); } - r.into() + for child in render_children( + &el.children, + parent_color, + parent_font_size, + Some(LayoutDirection::Row), + fixed_layers, + abs_layers, + sticky_layers, + stylesheet, + ) { + r = r.push(child); + } + if abs_layers.is_empty() { + r.into() + } else { + let layers = std::mem::take(abs_layers); + let mut stack = iced::widget::stack![r]; + for layer in layers { + stack = stack.push(layer); + } + stack.into() + } } LayoutDirection::Column => { let mut c = column![].spacing(spacing); - if let Some(align) = cs.align_items { - c = c.align_x(align); + if let Some(align) = cs.align_items { + c = c.align_x(align); + } + for child in render_children( + &el.children, + parent_color, + parent_font_size, + Some(LayoutDirection::Column), + fixed_layers, + abs_layers, + sticky_layers, + stylesheet, + ) { + c = c.push(child); + } + if abs_layers.is_empty() { + c.into() + } else { + let layers = std::mem::take(abs_layers); + let mut stack = iced::widget::stack![c]; + for layer in layers { + stack = stack.push(layer); + } + stack.into() } - for child in render_children(&el.children) { c = c.push(child); } - c.into() } LayoutDirection::Grid => { let mut c = column![].spacing(spacing); - let cols = el.properties.get("columns") + let cols = el + .get_prop("columns") .and_then(|v| v.parse::().ok()) .unwrap_or(3); - + for chunk in el.children.chunks(cols) { let mut r = row![].spacing(spacing); - if let Some(align) = cs.align_items { - r = r.align_y(align); + if let Some(align) = cs.align_items { + r = r.align_y(align); } else { r = r.align_y(Alignment::Center); } - for child in render_children(chunk) { r = r.push(child); } - c = c.push(r); + let mut chunk_abs = Vec::new(); + let mut chunk_sticky = Vec::new(); + for child in render_children( + chunk, + parent_color, + parent_font_size, + Some(LayoutDirection::Row), + fixed_layers, + &mut chunk_abs, + &mut chunk_sticky, + stylesheet, + ) { + r = r.push(child); + } + if chunk_abs.is_empty() && chunk_sticky.is_empty() { + c = c.push(r); + } else { + let mut stack = iced::widget::stack![r]; + for layer in chunk_abs { + stack = stack.push(layer); + } + for layer in chunk_sticky { + stack = stack.push(layer); + } + c = c.push(stack); + } } c.into() } }; - let mut cont = container(content).padding(padding); + let boxed = apply_universal_box_model(content, &cs, false, 5.0); - if let Some(ca) = cs.content_align { - match ca { - ContentAlign::Start => cont = cont.align_x(iced::alignment::Horizontal::Left), - ContentAlign::Center => cont = cont.align_x(iced::alignment::Horizontal::Center), - ContentAlign::End => cont = cont.align_x(iced::alignment::Horizontal::Right), + if sticky_layers.is_empty() { + boxed + } else { + let layers = std::mem::take(sticky_layers); + let mut stack = iced::widget::stack![boxed]; + for layer in layers { + stack = stack.push(layer); } - cont = cont.width(Length::Fill); + stack.into() } - - apply_container_style(cont, cs) } -fn render_button<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> { - let props = &el.properties; - let cs = &el.computed_style; +fn render_button<'a>( + el: &'a Element, + cs: ComputedStyle, + parent_color: Option, + parent_font_size: Option, + fixed_layers: &mut Vec>, + abs_layers: &mut Vec>, + sticky_layers: &mut Vec>, + stylesheet: &StyleSheet, +) -> iced::Element<'a, Message, Theme, iced::Renderer> { + let final_color = cs.color.or(parent_color); let content: iced::Element<'a, _, _, _> = if el.children.is_empty() { - let label = props.get("label").cloned().unwrap_or_else(|| "Button".into()); - let mut t = text(label).size(cs.font_size.unwrap_or(14.0)); - if let Some(col) = cs.color { t = t.color(col); } + let label = el.get_prop("label").unwrap_or("Button"); + let mut t = text(label).size(parent_font_size.unwrap_or(14.0)); + if let Some(col) = final_color { + t = t.color(col); + } t.into() } else { let mut r = row![].spacing(8); - for child in render_children(&el.children) { r = r.push(child); } - r.into() + for child in render_children( + &el.children, + final_color, + parent_font_size, + Some(LayoutDirection::Row), + fixed_layers, + abs_layers, + sticky_layers, + stylesheet, + ) { + r = r.push(child); + } + if abs_layers.is_empty() && sticky_layers.is_empty() { + r.into() + } else { + let mut stack = iced::widget::stack![r]; + for layer in std::mem::take(abs_layers) { + stack = stack.push(layer); + } + for layer in std::mem::take(sticky_layers) { + stack = stack.push(layer); + } + stack.into() + } }; - let padding = cs.padding - .map(|p| [p, p * 2.0]) - .unwrap_or([8.0, 16.0]); + let mut padding = iced::Padding { + top: cs.padding_top.or(cs.padding).unwrap_or(8.0), + bottom: cs.padding_bottom.or(cs.padding).unwrap_or(8.0), + left: cs + .padding_left + .or_else(|| cs.padding.map(|p| p * 2.0)) + .unwrap_or(16.0), + right: cs + .padding_right + .or_else(|| cs.padding.map(|p| p * 2.0)) + .unwrap_or(16.0), + }; + + let bwidth = cs.border_width.unwrap_or(0.0); + if let Some(iced::Length::Fixed(w)) = cs.width { + let total_h = padding.left + padding.right + bwidth * 2.0; + if total_h > w && w > 0.0 { + let scale = w / total_h; + padding.left *= scale; + padding.right *= scale; + } + } + if let Some(iced::Length::Fixed(h)) = cs.height { + let total_v = padding.top + padding.bottom + bwidth * 2.0; + if total_v > h && h > 0.0 { + let scale = h / total_v; + padding.top *= scale; + padding.bottom *= scale; + } + } let mut btn = button(content).padding(padding); - if let Some(script) = props.get("__on:click") { - btn = btn.on_press(Message::EventTriggered(script.clone())); - } else { - btn = btn.on_press(Message::EventTriggered(String::new())); + if let Some(script) = el.get_prop("__on:click") { + btn = btn.on_press(Message::EventTriggered(script.to_string())); } if let Some(w) = cs.width { - btn = btn.width(Length::Fixed(w)); + btn = btn.width(w); + } + if let Some(h) = cs.height { + btn = btn.height(h); } - let cs_clone = *cs; - let btn = btn.style(move |_, status| get_button_style(&cs_clone, status)); + let el_id = el.id(); + let classes: Vec<&str> = el + .get_prop("class") + .map(|s| s.split_whitespace().collect()) + .unwrap_or_default(); - btn.into() + let default_struct = StructuralContext::default(); + let el_attributes: HashMap = el.properties.iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let hover_sheets: Vec<&HashMap> = + stylesheet.matching_rules(el.type_name, el_id, &classes, &["hover"], &default_struct, &[], &[], &el_attributes); + let active_sheets: Vec<&HashMap> = + stylesheet.matching_rules(el.type_name, el_id, &classes, &["active"], &default_struct, &[], &[], &el_attributes); + + let mut hover_props = std::collections::HashMap::new(); + for sheet in &hover_sheets { + hover_props.extend((*sheet).clone()); + } + let mut active_props = std::collections::HashMap::new(); + for sheet in &active_sheets { + active_props.extend((*sheet).clone()); + } + + let mut cs_clone = cs; + cs_clone.color = final_color; + + btn.style(move |_, status| { + let mut style_cs = cs_clone.clone(); + match status { + button::Status::Hovered => { + if !hover_props.is_empty() { + style_cs.apply_overrides(&hover_props); + } + } + button::Status::Pressed => { + if !active_props.is_empty() { + style_cs.apply_overrides(&active_props); + } else if !hover_props.is_empty() { + style_cs.apply_overrides(&hover_props); + } + } + _ => {} + } + get_button_style(&style_cs, status) + }).into() } -fn render_input<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> { - let props = &el.properties; - let placeholder = props.get("placeholder") - .cloned() - .unwrap_or_else(|| "Type here…".into()); - let value = props.get("value").cloned().unwrap_or_default(); +fn render_toggle<'a>( + el: &'a Element, + parent_color: Option, + parent_font_size: Option, +) -> iced::Element<'a, Message, Theme, iced::Renderer> { + let label = el.get_prop("label").unwrap_or(""); + let value = el.get_prop("value") + .and_then(|v| v.parse::().ok()) + .unwrap_or(false); let var_name = extract_var_binding(el, "value"); - let padding = el.computed_style.padding.unwrap_or(10.0); - let cs = &el.computed_style; + + let cb = checkbox(value).on_toggle(move |v| Message::ToggleChanged(var_name.clone(), v)); + + if label.is_empty() { + cb.into() + } else { + let mut label_widget = text(label).size(parent_font_size.unwrap_or(16.0)); + if let Some(color) = parent_color { + label_widget = label_widget.color(color); + } + + row![cb, label_widget] + .spacing(8) + .align_y(iced::Alignment::Center) + .into() + } +} + +fn render_input<'a>( + el: &'a Element, + cs: ComputedStyle, + parent_color: Option, + _parent_font_size: Option, + hover_props: &HashMap, + active_props: &HashMap, +) -> iced::Element<'a, Message, Theme, iced::Renderer> { + let placeholder = el.get_prop("placeholder").unwrap_or("Type here…"); + let value = el.get_prop("value").unwrap_or(""); + let var_name = extract_var_binding(el, "value"); + + let padding = get_padding(&cs, 10.0); let mut input = text_input(&placeholder, &value) .on_input(move |v| Message::InputChanged(var_name.clone(), v)) .padding(padding); if let Some(w) = cs.width { - input = input.width(Length::Fixed(w)); + input = input.width(w); } - let cs_clone = *cs; - input = input.style(move |_, _| get_text_input_style(&cs_clone)); + let mut base_cs = cs; + base_cs.color = parent_color; + + let has_any = !hover_props.is_empty() || !active_props.is_empty(); + let hover = hover_props.clone(); + + if has_any { + input = input.style(move |_, status| { + let mut style_cs = base_cs.clone(); + match status { + text_input::Status::Hovered => { + if !hover.is_empty() { + style_cs.apply_overrides(&hover); + } + } + _ => {} + } + get_text_input_style(&style_cs) + }); + } else { + input = input.style(move |_, _| get_text_input_style(&base_cs)); + } input.into() } -fn render_toggle<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> { - let props = &el.properties; - let label = props.get("label").cloned().unwrap_or_default(); - let value = props.get("value") - .and_then(|v| v.parse::().ok()) - .unwrap_or(false); - let var_name = extract_var_binding(el, "value"); - - checkbox(value) - .label(label) - .on_toggle(move |v| Message::ToggleChanged(var_name.clone(), v)) - .into() -} - fn render_slider<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> { - let props = &el.properties; - let value = props.get("value") - .and_then(|v| v.parse::().ok()) + let value = el.get_prop("value") + .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); let var_name = extract_var_binding(el, "value"); - slider(0.0..=100.0, value, move |v| Message::SliderChanged(var_name.clone(), v)) - .into() + slider(0.0..=100.0, value, move |v| { + Message::SliderChanged(var_name.clone(), v as f64) + }) + .into() }