feat: reactivityl; vdom; rhei; styles; fs; image element
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
5397
Cargo.lock
generated
Normal file
5397
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "glint-runtime"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
glt = { path = "../glt" }
|
||||||
|
regex = "1.10"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
iced = { version = "0.14.0", features = ["tokio", "image", "svg"] }
|
||||||
|
colored = "2"
|
||||||
|
indicatif = "0.17"
|
||||||
|
rhai = "1.17.1"
|
||||||
|
chrono = "0.4.44"
|
||||||
207
desktop.gltm
Normal file
207
desktop.gltm
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// GLINT UI: Ultimate Reactive Showcase Architecture
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
@version 1
|
||||||
|
@style "main.glts"
|
||||||
|
|
||||||
|
// ── 1. Global Configuration & Constants ──────────────────────────────────────
|
||||||
|
|
||||||
|
@global $APP_TITLE = "Glint Design System & Rhei Demo"
|
||||||
|
@global $IS_PRODUCTION = false
|
||||||
|
|
||||||
|
// Глобальное состояние приложения
|
||||||
|
@global $access_level = 3.0
|
||||||
|
@global $username = "Guest"
|
||||||
|
@global $volume_level = 0.0
|
||||||
|
@global $is_enabled = false
|
||||||
|
@global $search_query = "Type to search..."
|
||||||
|
@global $team_members = ["Alexander", "Beatrice", "Cyrus", "Diana"]
|
||||||
|
|
||||||
|
@singleton SystemSettings {
|
||||||
|
theme = "ocean-blue",
|
||||||
|
refresh_rate = 60,
|
||||||
|
debug_mode = true,
|
||||||
|
storage_path = fs:/var/lib/glint/assets
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Logic Layer (Document-level Rhai Script) ──────────────────────────────
|
||||||
|
|
||||||
|
!rhei: {
|
||||||
|
// Вспомогательные UI-функции
|
||||||
|
fn level_label(lvl) {
|
||||||
|
if lvl >= 8.0 { "Admin" }
|
||||||
|
else if lvl >= 5.0 { "Moderator" }
|
||||||
|
else { "Guest" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn volume_icon(vol) {
|
||||||
|
if vol == 0.0 { "🔇" }
|
||||||
|
else if vol < 40.0 { "🔈" }
|
||||||
|
else if vol < 75.0 { "🔉" }
|
||||||
|
else { "🔊" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Демонстрация сложных вычислений при загрузке документа
|
||||||
|
const TARGET = 20;
|
||||||
|
fn fib(n) {
|
||||||
|
if n < 2 { return n; }
|
||||||
|
let a = 0; let b = 1;
|
||||||
|
for i in 2..=n {
|
||||||
|
let c = a + b;
|
||||||
|
a = b; b = c;
|
||||||
|
}
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
print(`Initializing Glint Engine...`);
|
||||||
|
let result = fib(TARGET);
|
||||||
|
print(`Fibonacci(${TARGET}) computed on load: ${result}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. UI Components ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@component ProfileCard(username: String, access_level: Float) {
|
||||||
|
Panel {
|
||||||
|
Header !rhei: { "User: " + username }
|
||||||
|
Label(text=$username)
|
||||||
|
|
||||||
|
// Сложное условие в Rhai: вычисляется при каждом обновлении VDOM
|
||||||
|
@if !rhei: { (access_level >= 5.0) && (username.len() > 0) } {
|
||||||
|
Text "✅ Administrator Privileges Active"
|
||||||
|
Text !rhei: { "Role: " + level_label(access_level) }
|
||||||
|
} @else {
|
||||||
|
Text "🔒 Restricted Access Mode"
|
||||||
|
Text "Role: Guest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Main Application Tree ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Window(
|
||||||
|
title=$APP_TITLE,
|
||||||
|
width=1280,
|
||||||
|
height=800,
|
||||||
|
resizable=true
|
||||||
|
) {
|
||||||
|
// Локальное состояние окна
|
||||||
|
@let $show_metrics = true
|
||||||
|
|
||||||
|
Panel(id="main_viewport", padding=24) {
|
||||||
|
|
||||||
|
Header "Dashboard Overview"
|
||||||
|
Text "Welcome to the ultimate Glint component test suite."
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// ── Секция A: Control Panel (Переменные и биндинги) ──────────────────
|
||||||
|
Header "A. Control Panel & State Bindings"
|
||||||
|
|
||||||
|
Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {}
|
||||||
|
|
||||||
|
Panel {
|
||||||
|
Input(placeholder=$search_query, value=$search_query)
|
||||||
|
Toggle(label="Enable Live Metrics", value=$is_enabled)
|
||||||
|
Text !rhei: { "Live search query: " + search_query }
|
||||||
|
}
|
||||||
|
|
||||||
|
@if $is_enabled {
|
||||||
|
Text "✅ Features are ON. You can adjust the system."
|
||||||
|
} @else {
|
||||||
|
Text "⛔ Features are OFF."
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// ── Секция B: Arithmetic Conditions & Reactive Text ──────────────────
|
||||||
|
Header "B. Rhei Arithmetic Conditions"
|
||||||
|
|
||||||
|
Slider(value=$volume_level)
|
||||||
|
ProgressBar(value=$volume_level)
|
||||||
|
|
||||||
|
// Трехуровневое ветвление с использованием Rhai-условий
|
||||||
|
@if !rhei: { volume_level == 0.0 } {
|
||||||
|
Text "🔇 Muted"
|
||||||
|
} @else {
|
||||||
|
@if !rhei: { volume_level >= 75.0 } {
|
||||||
|
Text "🔊 High volume — protect your hearing!"
|
||||||
|
} @else {
|
||||||
|
// Инлайн-текст, вызывающий функцию из глобального скрипта
|
||||||
|
Text !rhei: { volume_icon(volume_level) + " Volume OK: " + volume_level + " / 100" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// ── Секция C: Access Control & Component Instantiation ───────────────
|
||||||
|
Header "C. Multi-variable Logic & Components"
|
||||||
|
|
||||||
|
Slider(value=$access_level)
|
||||||
|
|
||||||
|
!rhei: { "Current slider level: " + access_level + " → " + level_label(access_level) }
|
||||||
|
|
||||||
|
Panel {
|
||||||
|
Button(label="Grant Admin (Level 9)") {
|
||||||
|
@on click {
|
||||||
|
!rhei: { access_level = 9.0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(label="Reset (Level 1)") {
|
||||||
|
@on click {
|
||||||
|
!rhei: { access_level = 1.0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if !rhei: {
|
||||||
|
let threshold = 5.0;
|
||||||
|
access_level >= threshold
|
||||||
|
} {
|
||||||
|
Text "🛡️ Access granted — Privileged Zone"
|
||||||
|
ProfileCard(username=$username, access_level=$access_level)
|
||||||
|
} @else {
|
||||||
|
Text "🚫 Access denied — Raise your level to 5+"
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// ── Секция D: Dynamic List Rendering (@each) ─────────────────────────
|
||||||
|
Header "D. Team Management (Reactive Grid)"
|
||||||
|
|
||||||
|
// Передаем direction и columns как свойства компонента
|
||||||
|
Panel(columns=2, direction="grid") {
|
||||||
|
@each $name in $team_members {
|
||||||
|
Panel {
|
||||||
|
ProfileCard(username=$name, access_level=$access_level)
|
||||||
|
|
||||||
|
Button(label="Promote System-wide") {
|
||||||
|
@on click {
|
||||||
|
!rhei: {
|
||||||
|
// VDOM автоматически заменит "$name" на "Alexander", "Beatrice" и т.д.
|
||||||
|
let target_name = "$name";
|
||||||
|
print("Promoting triggered by " + target_name);
|
||||||
|
|
||||||
|
if access_level < 10.0 {
|
||||||
|
access_level += 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// ── Секция E: Footer Metadata ────────────────────────────────────────
|
||||||
|
Panel {
|
||||||
|
Text "System Version: 1.0.5-stable"
|
||||||
|
|
||||||
|
// Многострочный скрипт прямо внутри текстового узла
|
||||||
|
!rhei: {
|
||||||
|
let pct = (access_level * 10.0).to_string();
|
||||||
|
"Access Power: " + pct + "% | Integrity: OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
main.glts
Normal file
106
main.glts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Glint Design System — Master Stylesheet (main.glts)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ── 1. Цветовая палитра и константы (Variables) ──────────────────────────────
|
||||||
|
$bg-app = #11111b // Глубокий темный фон для всего окна
|
||||||
|
$bg-panel = #1e1e2e // Базовый цвет для контейнеров и панелей
|
||||||
|
$bg-surface = #313244 // Цвет для карточек и выделенных блоков
|
||||||
|
$bg-field = #181825 // Внутренний фон для полей ввода (Input)
|
||||||
|
|
||||||
|
$text-main = #cdd6f4 // Мягкий белый основной текст
|
||||||
|
$text-muted = #9399b2 // Серый приглушенный текст для описаний
|
||||||
|
$accent = #b4befe // Лавандовый акцентный цвет для кнопок и заголовков
|
||||||
|
$border-glow = #45475a // Цвет аккуратных рамок и разделителей
|
||||||
|
|
||||||
|
// Сетка отступов (Размеры автоматически очищаются от "px" парсером Glint)
|
||||||
|
$pad-dense = 6px
|
||||||
|
$pad-normal = 12px
|
||||||
|
$pad-roomy = 20px
|
||||||
|
$pad-window = 28px
|
||||||
|
|
||||||
|
// ── 2. Шаблоны стилей (Mixins) ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
@mixin text-body {
|
||||||
|
color: $text-main
|
||||||
|
font-size: 15px
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin standard-card {
|
||||||
|
background: $bg-panel
|
||||||
|
border-color: $border-glow
|
||||||
|
border-width: 1px
|
||||||
|
border-radius: 12px
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. Правила для UI-компонентов (Element Rules) ────────────────────────────
|
||||||
|
|
||||||
|
// Главное окно приложения
|
||||||
|
Window {
|
||||||
|
background: $bg-app
|
||||||
|
padding: $pad-window
|
||||||
|
spacing: $pad-roomy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Универсальный контейнер ( viewport, карточки, обертки списков )
|
||||||
|
Panel {
|
||||||
|
@use standard-card
|
||||||
|
padding: $pad-normal
|
||||||
|
gap: 14px
|
||||||
|
direction: vertical
|
||||||
|
align-items: center
|
||||||
|
content-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
// Крупные заголовки секций
|
||||||
|
Header {
|
||||||
|
color: $accent
|
||||||
|
font-size: 22px
|
||||||
|
padding: 4px
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обычный текст
|
||||||
|
Text {
|
||||||
|
@use text-body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подписи, второстепенные метаданные
|
||||||
|
Label {
|
||||||
|
color: $text-muted
|
||||||
|
font-size: 13px
|
||||||
|
}
|
||||||
|
|
||||||
|
// Интерактивные элементы управления
|
||||||
|
Button {
|
||||||
|
border-radius: 8px
|
||||||
|
border-width: 1px
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
border-radius: 8px
|
||||||
|
border-width: 1px
|
||||||
|
}
|
||||||
|
|
||||||
|
// Текстовые поля ввода
|
||||||
|
Input {
|
||||||
|
background-color: $bg-field
|
||||||
|
color: $text-main
|
||||||
|
font-size: 14px
|
||||||
|
padding: 12px
|
||||||
|
border-radius: 16px
|
||||||
|
border-width: 1px
|
||||||
|
border-color: $border-glow
|
||||||
|
width: 320px
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переключатели
|
||||||
|
Toggle {
|
||||||
|
color: $text-main
|
||||||
|
font-size: 15px
|
||||||
|
}
|
||||||
|
|
||||||
|
// Разделительные линии (Divider / Separator)
|
||||||
|
Divider {
|
||||||
|
background: $border-glow
|
||||||
|
border-width: 1px
|
||||||
|
}
|
||||||
95
src/app.rs
Normal file
95
src/app.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use iced::widget::{column, container, row, scrollable, text, Space};
|
||||||
|
use iced::{Alignment, 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<Element>,
|
||||||
|
pub rhei: RheiContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlintApp {
|
||||||
|
pub fn title(&self) -> String {
|
||||||
|
"Glint Runtime - Native Renderer".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, message: Message) -> iced::Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::EventTriggered(script) => {
|
||||||
|
if !script.is_empty() {
|
||||||
|
self.rhei.execute_action(&script, &mut self.doc.variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::InputChanged(Some(var), val) => {
|
||||||
|
self.doc.variables.insert(var, val);
|
||||||
|
}
|
||||||
|
Message::ToggleChanged(Some(var), val) => {
|
||||||
|
self.doc.variables.insert(var, val.to_string());
|
||||||
|
}
|
||||||
|
Message::SliderChanged(Some(var), val) => {
|
||||||
|
self.doc.variables.insert(var.clone(), format!("{:.1}", val));
|
||||||
|
if var == "volume_level" {
|
||||||
|
self.doc.variables.insert("age".to_string(), val.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.vdom_roots = Interpreter::evaluate_vdom(
|
||||||
|
&self.doc.roots,
|
||||||
|
&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);
|
||||||
|
|
||||||
|
for root in &self.vdom_roots {
|
||||||
|
if let Some(el) = render_element(root) {
|
||||||
|
content = content.push(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_bar = container(self.build_status_bar(&self.doc.variables))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(10);
|
||||||
|
|
||||||
|
let layout = column![
|
||||||
|
scrollable(content).width(Length::Fill).height(Length::Fill),
|
||||||
|
iced::widget::rule::horizontal(1),
|
||||||
|
status_bar,
|
||||||
|
];
|
||||||
|
|
||||||
|
container(layout)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_status_bar<'a>(
|
||||||
|
&self,
|
||||||
|
scope: &std::collections::HashMap<String, String>,
|
||||||
|
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||||
|
let slider_val = scope.get("volume_level")
|
||||||
|
.and_then(|v| v.parse::<f64>().ok())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
row![
|
||||||
|
text("🟢 Runtime Active (VDOM + Rhai)").size(14),
|
||||||
|
Space::new().width(Length::Fill),
|
||||||
|
text(format!("Шаблонных узлов: {}", self.doc.roots.len())).size(14),
|
||||||
|
Space::new().width(Length::Fixed(15.0)),
|
||||||
|
text(format!("Слайдер ($volume_level): {:.1}", slider_val)).size(14),
|
||||||
|
]
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
250
src/cli.rs
Normal file
250
src/cli.rs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use colored::Colorize;
|
||||||
|
use glt::{Compiler, ModuleSoA, NodeId, Parser, StyleParser};
|
||||||
|
use glt::ast::Directive;
|
||||||
|
use iced::Theme;
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
|
||||||
|
use crate::app::GlintApp;
|
||||||
|
use crate::interpreter::{Interpreter, RheiContext};
|
||||||
|
|
||||||
|
pub fn compile_files(gltm_files: &[String], glts_files: &[String], output_path: &str) {
|
||||||
|
let mut glts_source = String::new();
|
||||||
|
for path in glts_files {
|
||||||
|
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"));
|
||||||
|
gltm_source.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
let spinner = ProgressBar::new_spinner();
|
||||||
|
spinner.set_style(
|
||||||
|
ProgressStyle::with_template("{spinner} {msg}")
|
||||||
|
.unwrap()
|
||||||
|
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
|
||||||
|
);
|
||||||
|
spinner.set_message("Parsing...");
|
||||||
|
spinner.enable_steady_tick(Duration::from_millis(80));
|
||||||
|
|
||||||
|
let parse_start = Instant::now();
|
||||||
|
|
||||||
|
let mut module = ModuleSoA::new();
|
||||||
|
|
||||||
|
if !glts_source.is_empty() {
|
||||||
|
let mut style_parser = StyleParser::new(&glts_source, &mut module);
|
||||||
|
if let Err(e) = style_parser.parse_all() {
|
||||||
|
spinner.finish_with_message("❌ Style parsing failed".red().to_string());
|
||||||
|
eprintln!("{}", format!("Style Error: {}", e).red());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parser = Parser::with_module(&gltm_source, module);
|
||||||
|
|
||||||
|
match parser.parse_all() {
|
||||||
|
Ok(_) => {
|
||||||
|
let parse_dur = parse_start.elapsed();
|
||||||
|
spinner.finish_with_message(format!("✅ Parsing done ({:?})", parse_dur));
|
||||||
|
|
||||||
|
spinner.set_message("Building AST tree...");
|
||||||
|
let tree_start = Instant::now();
|
||||||
|
|
||||||
|
let module = &parser.module;
|
||||||
|
let mut child_indices: HashSet<usize> = HashSet::new();
|
||||||
|
|
||||||
|
for (start, len) in &module.elem_child_spans {
|
||||||
|
let start_us = *start as usize;
|
||||||
|
let len_us = *len as usize;
|
||||||
|
child_indices.extend(start_us..(start_us + len_us));
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in &module.directives {
|
||||||
|
let spans: Vec<(usize, usize)> = match dir {
|
||||||
|
Directive::Component { child_span, .. } => {
|
||||||
|
vec![(child_span.0 as usize, child_span.1 as usize)]
|
||||||
|
}
|
||||||
|
Directive::If { child_span, else_span, .. } => {
|
||||||
|
let mut v = vec![(child_span.0 as usize, child_span.1 as usize)];
|
||||||
|
if let Some(es) = else_span {
|
||||||
|
v.push((es.0 as usize, es.1 as usize));
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Directive::Each { child_span, .. } => {
|
||||||
|
vec![(child_span.0 as usize, child_span.1 as usize)]
|
||||||
|
}
|
||||||
|
Directive::On { child_span, .. } => {
|
||||||
|
vec![(child_span.0 as usize, child_span.1 as usize)]
|
||||||
|
}
|
||||||
|
_ => vec![],
|
||||||
|
};
|
||||||
|
for (start, len) in spans {
|
||||||
|
child_indices.extend(start..(start + len));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let root_nodes: Vec<NodeId> = module
|
||||||
|
.hierarchy
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| !child_indices.contains(i))
|
||||||
|
.map(|(_, node_id)| *node_id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let tree_dur = tree_start.elapsed();
|
||||||
|
spinner.finish_with_message(format!("✅ Tree built ({:?})", tree_dur));
|
||||||
|
|
||||||
|
spinner.set_message("Generating bytecode...");
|
||||||
|
let compile_start = Instant::now();
|
||||||
|
let bytecode = Compiler::new(&parser.module).compile(&root_nodes);
|
||||||
|
let compile_dur = compile_start.elapsed();
|
||||||
|
spinner.finish_with_message(format!("✅ Bytecode generated ({:?})", compile_dur));
|
||||||
|
|
||||||
|
spinner.set_message("Writing to disk...");
|
||||||
|
let io_start = Instant::now();
|
||||||
|
write_bytecode(output_path, &bytecode);
|
||||||
|
let io_dur = io_start.elapsed();
|
||||||
|
spinner.finish_with_message(format!("✅ Written to {}", output_path));
|
||||||
|
|
||||||
|
let total_dur = parse_dur + tree_dur + compile_dur + io_dur;
|
||||||
|
|
||||||
|
print_stats(BuildStats {
|
||||||
|
parse: parse_dur,
|
||||||
|
tree: tree_dur,
|
||||||
|
compile: compile_dur,
|
||||||
|
io: io_dur,
|
||||||
|
total: total_dur,
|
||||||
|
size: bytecode.len(),
|
||||||
|
output: output_path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
spinner.finish_with_message("❌ Parsing failed".red().to_string());
|
||||||
|
eprintln!("{}", format!("Markup Error: {}", e).red());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_file(path: &str) -> ! {
|
||||||
|
let bytecode = read_file_or_exit(path);
|
||||||
|
println!("{} {}", "▶️".green(), format!("Rendering: {}", path).bold());
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
match Interpreter::run(&bytecode) {
|
||||||
|
Ok(doc) => {
|
||||||
|
println!(
|
||||||
|
"{} {} ({:?})",
|
||||||
|
"✅".green(),
|
||||||
|
"Bytecode decoded".bold(),
|
||||||
|
start.elapsed()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"{} {} Rhai блоков",
|
||||||
|
"📜".green(),
|
||||||
|
doc.rhei_scripts.len()
|
||||||
|
);
|
||||||
|
println!("{}", "🚀 Launching GUI...".green().bold());
|
||||||
|
|
||||||
|
let result = iced::application(
|
||||||
|
move || {
|
||||||
|
let rhei = RheiContext::new(&doc.rhei_scripts);
|
||||||
|
|
||||||
|
let vdom_roots = Interpreter::evaluate_vdom(
|
||||||
|
&doc.roots,
|
||||||
|
&doc.variables,
|
||||||
|
&doc.components,
|
||||||
|
&rhei,
|
||||||
|
&doc.stylesheet,
|
||||||
|
);
|
||||||
|
|
||||||
|
GlintApp {
|
||||||
|
doc: doc.clone(),
|
||||||
|
rhei,
|
||||||
|
vdom_roots,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlintApp::update,
|
||||||
|
GlintApp::view,
|
||||||
|
)
|
||||||
|
.title(GlintApp::title)
|
||||||
|
.window(iced::window::Settings {
|
||||||
|
size: iced::Size::new(500.0, 700.0),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.theme(|_: &GlintApp| Theme::Dark)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("{} {}", "❌".red(), format!("Iced error: {:?}", e).red());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{} {}", "❌".red(), format!("Interpreter error: {}", e).red());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_source(path: &str, kind: &str) -> String {
|
||||||
|
match fs::read_to_string(path) {
|
||||||
|
Ok(s) => {
|
||||||
|
println!("{} {} {}", "📄".green(), kind.cyan(), path.italic());
|
||||||
|
s
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{} {}: {}", "❌".red(), path.red(), e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_file_or_exit(path: &str) -> Vec<u8> {
|
||||||
|
fs::read(path).unwrap_or_else(|e| {
|
||||||
|
eprintln!("{} {}: {}", "❌".red(), path.red(), e);
|
||||||
|
std::process::exit(1);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_bytecode(path: &str, data: &[u8]) {
|
||||||
|
let mut file = File::create(path).unwrap_or_else(|e| {
|
||||||
|
eprintln!("{} {}: {}", "❌".red(), path.red(), e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
file.write_all(data).unwrap_or_else(|e| {
|
||||||
|
eprintln!("{} {}", "❌".red(), format!("Write failed: {}", e).red());
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BuildStats<'a> {
|
||||||
|
parse: Duration,
|
||||||
|
tree: Duration,
|
||||||
|
compile: Duration,
|
||||||
|
io: Duration,
|
||||||
|
total: Duration,
|
||||||
|
size: usize,
|
||||||
|
output: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_stats(s: BuildStats) {
|
||||||
|
println!("\n{}", "Compilation Statistics".bold().underline());
|
||||||
|
println!(" {:<15} {}", "Parse:".cyan(), format!("{:?}", s.parse).white());
|
||||||
|
println!(" {:<15} {}", "AST tree:".cyan(), format!("{:?}", s.tree).white());
|
||||||
|
println!(" {:<15} {}", "Bytecode gen:".cyan(), format!("{:?}", s.compile).white());
|
||||||
|
println!(" {:<15} {}", "Disk write:".cyan(), format!("{:?}", s.io).white());
|
||||||
|
println!(" {:<15} {}", "Total time:".green().bold(), format!("{:?}", s.total).green().bold());
|
||||||
|
println!(" {:<15} {}", "Output size:".yellow(), format!("{} bytes", s.size).yellow());
|
||||||
|
println!(" {:<15} {}", "Saved to:".yellow(), s.output.yellow());
|
||||||
|
}
|
||||||
496
src/interpreter/mod.rs
Normal file
496
src/interpreter/mod.rs
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
pub mod opcodes;
|
||||||
|
pub mod reader;
|
||||||
|
pub mod rhei;
|
||||||
|
pub mod style;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub use rhei::RheiContext;
|
||||||
|
use style::StyleSheet as SS;
|
||||||
|
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 regex::Regex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
static RE_VAR: OnceLock<Regex> = OnceLock::new();
|
||||||
|
|
||||||
|
pub struct Interpreter;
|
||||||
|
|
||||||
|
impl Interpreter {
|
||||||
|
pub fn run(bytecode: &[u8]) -> Result<Document, InterpError> {
|
||||||
|
if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); }
|
||||||
|
if &bytecode[..4] != MAGIC { return Err(InterpError::BadMagic); }
|
||||||
|
|
||||||
|
let mut r = Reader::new(bytecode);
|
||||||
|
r.pos = 4;
|
||||||
|
let mut variables = HashMap::new();
|
||||||
|
let mut components = HashMap::new();
|
||||||
|
let mut rhei_scripts: Vec<String> = Vec::new();
|
||||||
|
let mut stylesheet = SS::new();
|
||||||
|
|
||||||
|
let roots = Self::parse_block_elements(
|
||||||
|
&mut r,
|
||||||
|
&mut variables,
|
||||||
|
&mut components,
|
||||||
|
&mut rhei_scripts,
|
||||||
|
&mut stylesheet,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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<String, String>,
|
||||||
|
components: &mut HashMap<String, ComponentDef>,
|
||||||
|
rhei_scripts: &mut Vec<String>,
|
||||||
|
stylesheet: &mut SS,
|
||||||
|
is_root: bool,
|
||||||
|
) -> Result<Vec<Element>, InterpError> {
|
||||||
|
let mut roots: Vec<Element> = Vec::new();
|
||||||
|
let mut stack: Vec<Element> = Vec::new();
|
||||||
|
|
||||||
|
while r.remaining() > 0 {
|
||||||
|
let op = r.read_byte()?;
|
||||||
|
if !is_root && op == OP_END_BLOCK { break; }
|
||||||
|
|
||||||
|
match op {
|
||||||
|
OP_ELEM_PUSH => stack.push(Element::new(r.read_string()?)),
|
||||||
|
|
||||||
|
OP_ELEM_POP => {
|
||||||
|
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
|
||||||
|
Self::attach(&mut stack, &mut roots, finished);
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_GLOBAL | OP_LET => {
|
||||||
|
let name = r.read_string()?;
|
||||||
|
let vop = r.read_byte()?;
|
||||||
|
if let Some(value) = r.read_value_as_string(vop)? {
|
||||||
|
variables.insert(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_SINGLETON => {
|
||||||
|
let _name = r.read_string()?;
|
||||||
|
let count = r.read_u32()?;
|
||||||
|
for _ in 0..count {
|
||||||
|
r.read_string()?;
|
||||||
|
let vop = r.read_byte()?;
|
||||||
|
r.skip_value(vop)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_CONTENT => {
|
||||||
|
let vop = r.read_byte()?;
|
||||||
|
if let Some(value) = r.read_value_as_string(vop)? {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_PROP_STR => {
|
||||||
|
let (key, val) = (r.read_string()?, r.read_string()?);
|
||||||
|
if let Some(el) = stack.last_mut() { el.properties.insert(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); }
|
||||||
|
}
|
||||||
|
OP_PROP_INT => {
|
||||||
|
let key = r.read_string()?;
|
||||||
|
let val = r.read_i64()?.to_string();
|
||||||
|
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||||
|
}
|
||||||
|
OP_PROP_FLOAT => {
|
||||||
|
let key = r.read_string()?;
|
||||||
|
let val = r.read_f64()?.to_string();
|
||||||
|
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||||
|
}
|
||||||
|
OP_PROP_BOOL => {
|
||||||
|
let key = r.read_string()?;
|
||||||
|
let val = (r.read_byte()? != 0).to_string();
|
||||||
|
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||||
|
}
|
||||||
|
OP_PROP_RHEI => {
|
||||||
|
let key = r.read_string()?;
|
||||||
|
let expr = r.read_string()?;
|
||||||
|
if let Some(el) = stack.last_mut() {
|
||||||
|
el.properties.insert(key, format!("{RHEI_PREFIX}{expr}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
|
||||||
|
let key = r.read_string()?;
|
||||||
|
if let Some(val) = r.read_value_as_string(op)? {
|
||||||
|
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_RHEI_BLK => {
|
||||||
|
let script = r.read_string()?;
|
||||||
|
if is_root && stack.is_empty() {
|
||||||
|
rhei_scripts.push(script);
|
||||||
|
} else {
|
||||||
|
let mut text_el = Element::new("#text".to_string());
|
||||||
|
text_el.properties.insert(
|
||||||
|
"text".to_string(),
|
||||||
|
format!("{RHEI_PREFIX}{script}"),
|
||||||
|
);
|
||||||
|
Self::attach(&mut stack, &mut roots, text_el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_COMPONENT => {
|
||||||
|
let name = r.read_string()?;
|
||||||
|
let param_count = r.read_u32()?;
|
||||||
|
let params = (0..param_count)
|
||||||
|
.map(|_| Ok((r.read_string()?, r.read_string()?)))
|
||||||
|
.collect::<Result<Vec<_>, InterpError>>()?;
|
||||||
|
let children = Self::parse_block_elements(
|
||||||
|
r, variables, components, rhei_scripts, stylesheet, false,
|
||||||
|
)?;
|
||||||
|
components.insert(name.clone(), ComponentDef { name, params, children });
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_IF => {
|
||||||
|
let vop = r.read_byte()?;
|
||||||
|
let raw = r.read_value_as_string(vop)?.unwrap_or_default();
|
||||||
|
let cond_val = if vop == OP_PROP_RHEI {
|
||||||
|
format!("{RHEI_PREFIX}{raw}")
|
||||||
|
} else {
|
||||||
|
raw
|
||||||
|
};
|
||||||
|
|
||||||
|
let true_children = Self::parse_block_elements(
|
||||||
|
r, variables, components, rhei_scripts, stylesheet, false,
|
||||||
|
)?;
|
||||||
|
let has_else = r.read_byte()? == 1;
|
||||||
|
let false_children = if has_else {
|
||||||
|
Self::parse_block_elements(
|
||||||
|
r, variables, components, rhei_scripts, stylesheet, false,
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut if_el = Element::new("@if".to_string());
|
||||||
|
if_el.properties.insert("condition".to_string(), cond_val);
|
||||||
|
if_el.children = true_children;
|
||||||
|
|
||||||
|
if !false_children.is_empty() {
|
||||||
|
let mut else_el = Element::new("@else".to_string());
|
||||||
|
else_el.children = false_children;
|
||||||
|
if_el.children.push(else_el);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::attach(&mut stack, &mut roots, if_el);
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_EACH => {
|
||||||
|
let var_name = r.read_string()?;
|
||||||
|
let vop = r.read_byte()?;
|
||||||
|
let source_val = match vop {
|
||||||
|
OP_PROP_VAR => format!("${}", r.read_string()?),
|
||||||
|
OP_PROP_ARRAY => r.read_array_as_strings()?.join(","),
|
||||||
|
OP_PROP_RHEI => format!("{RHEI_PREFIX}{}", r.read_string()?),
|
||||||
|
_ => { r.skip_value(vop)?; String::new() }
|
||||||
|
};
|
||||||
|
|
||||||
|
let block_children = Self::parse_block_elements(
|
||||||
|
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);
|
||||||
|
each_el.children = block_children;
|
||||||
|
Self::attach(&mut stack, &mut roots, each_el);
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_ON => {
|
||||||
|
let event_name = r.read_string()?;
|
||||||
|
let arg_count = r.read_u32()?;
|
||||||
|
let mut _args: Vec<(String, String)> = Vec::with_capacity(arg_count as usize);
|
||||||
|
for _ in 0..arg_count {
|
||||||
|
let k = r.read_string()?;
|
||||||
|
let vop = r.read_byte()?;
|
||||||
|
if let Some(v) = r.read_value_as_string(vop)? {
|
||||||
|
_args.push((k, v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut handler_script = String::new();
|
||||||
|
loop {
|
||||||
|
let inner_op = r.read_byte()?;
|
||||||
|
if inner_op == OP_END_BLOCK { break; }
|
||||||
|
if inner_op == OP_RHEI_BLK {
|
||||||
|
handler_script = r.read_string()?;
|
||||||
|
} else {
|
||||||
|
r.skip_opcode(inner_op)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(el) = stack.last_mut() {
|
||||||
|
el.properties.insert(
|
||||||
|
format!("__on:{event_name}"),
|
||||||
|
handler_script,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_STYLE_RULE => {
|
||||||
|
let selector = r.read_string()?;
|
||||||
|
let prop_count = r.read_u32()?;
|
||||||
|
let mut props = HashMap::with_capacity(prop_count as usize);
|
||||||
|
|
||||||
|
for _ in 0..prop_count {
|
||||||
|
let key = r.read_string()?;
|
||||||
|
let type_op = r.read_byte()?;
|
||||||
|
if let Some(val) = r.read_value_as_string(type_op)? {
|
||||||
|
props.insert(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stylesheet.add_rule(selector, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_STYLE_ANIM => {
|
||||||
|
r.skip_opcode(OP_STYLE_ANIM)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
other => r.skip_opcode(other)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(roots)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate_vdom(
|
||||||
|
templates: &[Element],
|
||||||
|
variables: &HashMap<String, String>,
|
||||||
|
components: &HashMap<String, ComponentDef>,
|
||||||
|
rhei: &RheiContext,
|
||||||
|
stylesheet: &SS,
|
||||||
|
) -> Vec<Element> {
|
||||||
|
let mut output = Vec::with_capacity(templates.len());
|
||||||
|
|
||||||
|
for el in templates {
|
||||||
|
match el.type_name.as_str() {
|
||||||
|
"@if" => {
|
||||||
|
let cond = el.properties.get("condition").cloned().unwrap_or_default();
|
||||||
|
let is_true = Self::evaluate_condition(&cond, variables, rhei);
|
||||||
|
|
||||||
|
let mut active_branch = Vec::new();
|
||||||
|
for child in &el.children {
|
||||||
|
if child.type_name == "@else" {
|
||||||
|
if !is_true { active_branch.extend(child.children.clone()); }
|
||||||
|
} else if is_true {
|
||||||
|
active_branch.push(child.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.extend(Self::evaluate_vdom(
|
||||||
|
&active_branch, variables, components, rhei, stylesheet,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
"@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 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)
|
||||||
|
};
|
||||||
|
|
||||||
|
let items: Vec<String> = if resolved_source.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
resolved_source.split(',').map(str::trim).map(str::to_string).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
let mut local_vars = variables.clone();
|
||||||
|
local_vars.insert(var_name.clone(), item);
|
||||||
|
output.extend(Self::evaluate_vdom(
|
||||||
|
&el.children, &local_vars, components, rhei, stylesheet,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
if let Some(comp) = components.get(&el.type_name) {
|
||||||
|
// Expand custom component
|
||||||
|
let mut comp_scope = variables.clone();
|
||||||
|
for (param, _) in &comp.params {
|
||||||
|
if let Some(arg) = el.properties.get(param) {
|
||||||
|
comp_scope.insert(
|
||||||
|
param.clone(),
|
||||||
|
Self::resolve_prop(arg, variables, rhei),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut vcomp = Element::new(el.type_name.clone());
|
||||||
|
for (k, v) in &el.properties {
|
||||||
|
if k.starts_with("__on:") {
|
||||||
|
vcomp.properties.insert(k.clone(), Self::resolve_string(v, variables));
|
||||||
|
} else {
|
||||||
|
vcomp.properties.insert(k.clone(), Self::resolve_prop(v, variables, rhei));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vcomp.computed_style = ComputedStyle::compute(
|
||||||
|
&vcomp.properties,
|
||||||
|
stylesheet.resolve(&vcomp.type_name),
|
||||||
|
);
|
||||||
|
vcomp.children = Self::evaluate_vdom(
|
||||||
|
&comp.children, &comp_scope, components, rhei, stylesheet,
|
||||||
|
);
|
||||||
|
output.push(vcomp);
|
||||||
|
} else {
|
||||||
|
let mut vnode = Element::new(el.type_name.clone());
|
||||||
|
|
||||||
|
for (k, v) in &el.properties {
|
||||||
|
if k.starts_with("__on:") {
|
||||||
|
vnode.properties.insert(k.clone(), Self::resolve_string(v, variables));
|
||||||
|
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.properties.insert(
|
||||||
|
k.clone(),
|
||||||
|
Self::resolve_prop(v, variables, rhei),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
vnode.computed_style = ComputedStyle::compute(
|
||||||
|
&vnode.properties,
|
||||||
|
stylesheet.resolve(&el.type_name),
|
||||||
|
);
|
||||||
|
|
||||||
|
vnode.children = Self::evaluate_vdom(
|
||||||
|
&el.children, variables, components, rhei, stylesheet,
|
||||||
|
);
|
||||||
|
output.push(vnode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String {
|
||||||
|
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
||||||
|
rhei.eval_expr(expr, variables).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
Self::resolve_string(v, variables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_condition(
|
||||||
|
cond: &str,
|
||||||
|
variables: &HashMap<String, String>,
|
||||||
|
rhei: &RheiContext,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) {
|
||||||
|
return rhei.eval_condition(expr, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut clean = cond.to_string();
|
||||||
|
if let Some(start) = clean.find('{') {
|
||||||
|
if let Some(end) = clean.rfind('}') {
|
||||||
|
clean = clean[start + 1..end].trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolved = Self::resolve_string(&clean, variables).trim().to_string();
|
||||||
|
|
||||||
|
if Self::is_truthy(&resolved) { return true; }
|
||||||
|
if resolved == "false" || resolved == "0" || resolved.is_empty() { return false; }
|
||||||
|
|
||||||
|
let operators: [(&str, fn(f64, f64) -> bool); 6] = [
|
||||||
|
(">=", |a, b| a >= b),
|
||||||
|
("<=", |a, b| a <= b),
|
||||||
|
(">", |a, b| a > b),
|
||||||
|
("<", |a, b| a < b),
|
||||||
|
("==", |a, b| a == b),
|
||||||
|
("!=", |a, b| a != b),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (op, compare) in operators {
|
||||||
|
if let Some((left, right)) = resolved.split_once(op) {
|
||||||
|
let l = left.trim();
|
||||||
|
let r = right.trim();
|
||||||
|
if let (Ok(ln), Ok(rn)) = (l.parse::<f64>(), r.parse::<f64>()) {
|
||||||
|
return compare(ln, rn);
|
||||||
|
}
|
||||||
|
if op == "==" { return l == r; }
|
||||||
|
if op == "!=" { return l != r; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_truthy(s: &str) -> bool {
|
||||||
|
match s.trim() {
|
||||||
|
"" | "false" | "0" | "null" => false,
|
||||||
|
"true" | "1" => true,
|
||||||
|
other => other.parse::<f64>().map(|n| n != 0.0).unwrap_or(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_rhai_array(s: &str) -> String {
|
||||||
|
let trimmed = s.trim();
|
||||||
|
if trimmed.starts_with('[') && trimmed.ends_with(']') {
|
||||||
|
trimmed[1..trimmed.len() - 1]
|
||||||
|
.split(',')
|
||||||
|
.map(|item| item.trim().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
} else {
|
||||||
|
trimmed.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_string(val: &str, scope: &HashMap<String, String>) -> 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attach(stack: &mut Vec<Element>, roots: &mut Vec<Element>, el: Element) {
|
||||||
|
match stack.last_mut() {
|
||||||
|
Some(parent) => parent.children.push(el),
|
||||||
|
None => roots.push(el),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/interpreter/opcodes.rs
Normal file
41
src/interpreter/opcodes.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
pub const MAGIC: &[u8; 4] = b"GLBC";
|
||||||
|
pub const MAGIC_HEADER: [u8; 4] = *b"GLBC";
|
||||||
|
|
||||||
|
// Control directives
|
||||||
|
pub const OP_VERSION: u8 = 0x01;
|
||||||
|
pub const OP_STYLE: u8 = 0x02;
|
||||||
|
pub const OP_GLOBAL: u8 = 0x03;
|
||||||
|
pub const OP_SINGLETON: u8 = 0x04;
|
||||||
|
pub const OP_COMPONENT: u8 = 0x05;
|
||||||
|
pub const OP_LET: u8 = 0x06;
|
||||||
|
pub const OP_IF: u8 = 0x07;
|
||||||
|
pub const OP_EACH: u8 = 0x08;
|
||||||
|
pub const OP_ON: u8 = 0x09;
|
||||||
|
pub const OP_RHEI_BLK: u8 = 0x0A;
|
||||||
|
|
||||||
|
pub const OP_STYLE_RULE: u8 = 0x0B;
|
||||||
|
pub const OP_STYLE_ANIM: u8 = 0x0C;
|
||||||
|
|
||||||
|
// Element tree
|
||||||
|
pub const OP_ELEM_PUSH: u8 = 0x10;
|
||||||
|
pub const OP_ELEM_POP: u8 = 0x11;
|
||||||
|
pub const OP_CONTENT: u8 = 0x12;
|
||||||
|
|
||||||
|
// Value type tags
|
||||||
|
pub const OP_PROP_STR: u8 = 0x20; // UTF-8 string literal
|
||||||
|
pub const OP_PROP_INT: u8 = 0x21; // i64 LE
|
||||||
|
pub const OP_PROP_FLOAT: u8 = 0x22; // f64 LE
|
||||||
|
pub const OP_PROP_BOOL: u8 = 0x23; // u8 (0 or 1)
|
||||||
|
pub const OP_PROP_COLOR: u8 = 0x24; // "#RRGGBB" string
|
||||||
|
pub const OP_PROP_FSPATH: u8 = 0x25; // filesystem path string
|
||||||
|
pub const OP_PROP_VAR: u8 = 0x26; // variable name string (without "$")
|
||||||
|
pub const OP_PROP_RHEI: u8 = 0x27; // Rhai expression string
|
||||||
|
pub const OP_PROP_NULL: u8 = 0x28; // no data follows
|
||||||
|
pub const OP_PROP_ARRAY: u8 = 0x29; // count:u32 + count * (type_op + data)
|
||||||
|
|
||||||
|
pub const OP_PROP_CALL: u8 = 0x2A;
|
||||||
|
pub const OP_PROP_UNIT: u8 = 0x2B;
|
||||||
|
pub const OP_PROP_IDENT: u8 = 0x2C;
|
||||||
|
|
||||||
|
// Block terminator
|
||||||
|
pub const OP_END_BLOCK: u8 = 0xFF;
|
||||||
280
src/interpreter/reader.rs
Normal file
280
src/interpreter/reader.rs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
use super::opcodes::*;
|
||||||
|
use super::types::InterpError;
|
||||||
|
|
||||||
|
pub struct Reader<'a> {
|
||||||
|
pub data: &'a [u8],
|
||||||
|
pub pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Reader<'a> {
|
||||||
|
pub fn new(data: &'a [u8]) -> Self {
|
||||||
|
Self { data, pos: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remaining(&self) -> usize {
|
||||||
|
self.data.len() - self.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn read_byte(&mut self) -> Result<u8, InterpError> {
|
||||||
|
self.require(1)?;
|
||||||
|
let b = self.data[self.pos];
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_u32(&mut self) -> Result<u32, InterpError> {
|
||||||
|
self.require(4)?;
|
||||||
|
let v = u32::from_le_bytes(self.data[self.pos..self.pos + 4].try_into().unwrap());
|
||||||
|
self.pos += 4;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_i64(&mut self) -> Result<i64, InterpError> {
|
||||||
|
self.require(8)?;
|
||||||
|
let v = i64::from_le_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap());
|
||||||
|
self.pos += 8;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_f64(&mut self) -> Result<f64, InterpError> {
|
||||||
|
self.require(8)?;
|
||||||
|
let v = f64::from_le_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap());
|
||||||
|
self.pos += 8;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_string(&mut self) -> Result<String, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_value_as_string(&mut self, type_op: u8) -> Result<Option<String>, 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 => {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
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()?;
|
||||||
|
if num.fract() == 0.0 {
|
||||||
|
Some(format!("{}{}", num as i64, unit))
|
||||||
|
} else {
|
||||||
|
Some(format!("{}{}", num, unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OP_PROP_CALL => {
|
||||||
|
let name = self.read_string()?;
|
||||||
|
let arg_count = self.read_u32()? as usize;
|
||||||
|
let mut args = Vec::with_capacity(arg_count);
|
||||||
|
for _ in 0..arg_count {
|
||||||
|
let op = self.read_byte()?;
|
||||||
|
let val = self.read_value_as_string(op)?
|
||||||
|
.unwrap_or_default();
|
||||||
|
args.push(val);
|
||||||
|
}
|
||||||
|
Some(format!("{}({})", name, args.join(",")))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `OP_PROP_ARRAY` (opcode already consumed) and return elements as strings.
|
||||||
|
pub fn read_array_as_strings(&mut self) -> Result<Vec<String>, InterpError> {
|
||||||
|
let count = self.read_u32()? as usize;
|
||||||
|
let mut items = Vec::with_capacity(count);
|
||||||
|
for _ in 0..count {
|
||||||
|
let elem_op = self.read_byte()?;
|
||||||
|
if let Some(s) = self.read_value_as_string(elem_op)? {
|
||||||
|
items.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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_INT => { self.read_i64()?; }
|
||||||
|
OP_PROP_FLOAT => { self.read_f64()?; }
|
||||||
|
OP_PROP_BOOL => { self.read_byte()?; }
|
||||||
|
OP_PROP_NULL => {}
|
||||||
|
OP_PROP_ARRAY => {
|
||||||
|
let count = self.read_u32()?;
|
||||||
|
for _ in 0..count {
|
||||||
|
let elem_op = self.read_byte()?;
|
||||||
|
self.skip_value(elem_op)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OP_PROP_UNIT => {
|
||||||
|
self.read_f64()?; // number
|
||||||
|
self.read_string()?; // unit suffix
|
||||||
|
}
|
||||||
|
OP_PROP_CALL => {
|
||||||
|
self.read_string()?; // function name
|
||||||
|
let arg_count = self.read_u32()?;
|
||||||
|
for _ in 0..arg_count {
|
||||||
|
let op = self.read_byte()?;
|
||||||
|
self.skip_value(op)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
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_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_GLOBAL | OP_LET => {
|
||||||
|
self.read_string()?;
|
||||||
|
let vop = self.read_byte()?;
|
||||||
|
self.skip_value(vop)?;
|
||||||
|
}
|
||||||
|
OP_SINGLETON => {
|
||||||
|
self.read_string()?;
|
||||||
|
let count = self.read_u32()?;
|
||||||
|
for _ in 0..count {
|
||||||
|
self.read_string()?;
|
||||||
|
let vop = self.read_byte()?;
|
||||||
|
self.skip_value(vop)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OP_COMPONENT => {
|
||||||
|
self.read_string()?;
|
||||||
|
let params = self.read_u32()?;
|
||||||
|
for _ in 0..params {
|
||||||
|
self.read_string()?;
|
||||||
|
self.read_string()?;
|
||||||
|
}
|
||||||
|
self.skip_block()?;
|
||||||
|
}
|
||||||
|
OP_IF => {
|
||||||
|
let vop = self.read_byte()?;
|
||||||
|
self.skip_value(vop)?;
|
||||||
|
self.skip_block()?;
|
||||||
|
if self.read_byte()? == 1 { self.skip_block()?; }
|
||||||
|
}
|
||||||
|
OP_EACH => {
|
||||||
|
self.read_string()?;
|
||||||
|
let vop = self.read_byte()?;
|
||||||
|
self.skip_value(vop)?;
|
||||||
|
self.skip_block()?;
|
||||||
|
}
|
||||||
|
OP_ON => {
|
||||||
|
self.read_string()?;
|
||||||
|
let args = self.read_u32()?;
|
||||||
|
for _ in 0..args {
|
||||||
|
self.read_string()?;
|
||||||
|
let vop = self.read_byte()?;
|
||||||
|
self.skip_value(vop)?;
|
||||||
|
}
|
||||||
|
self.skip_block()?;
|
||||||
|
}
|
||||||
|
OP_ELEM_PUSH => { self.read_string()?; }
|
||||||
|
OP_CONTENT => {
|
||||||
|
let vop = self.read_byte()?;
|
||||||
|
self.skip_value(vop)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
OP_STYLE_RULE => {
|
||||||
|
self.read_string()?; // selector
|
||||||
|
let count = self.read_u32()?;
|
||||||
|
for _ in 0..count {
|
||||||
|
self.read_string()?; // property key
|
||||||
|
let vop = self.read_byte()?;
|
||||||
|
self.skip_value(vop)?; // property value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OP_STYLE_ANIM => {
|
||||||
|
self.read_string()?; // animation name
|
||||||
|
let frame_count = self.read_u32()?;
|
||||||
|
for _ in 0..frame_count {
|
||||||
|
self.read_string()?; // step ("from", "to", "50%", …)
|
||||||
|
let prop_count = self.read_u32()?;
|
||||||
|
for _ in 0..prop_count {
|
||||||
|
self.read_string()?;
|
||||||
|
let vop = self.read_byte()?;
|
||||||
|
self.skip_value(vop)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skip_block(&mut self) -> Result<(), InterpError> {
|
||||||
|
loop {
|
||||||
|
let op = self.read_byte()?;
|
||||||
|
if op == OP_END_BLOCK { return Ok(()); }
|
||||||
|
self.skip_opcode(op)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn require(&self, n: usize) -> Result<(), InterpError> {
|
||||||
|
if self.remaining() < n {
|
||||||
|
Err(InterpError::UnexpectedEof)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
169
src/interpreter/rhei.rs
Normal file
169
src/interpreter/rhei.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
use rhai::{Dynamic, Engine, Scope, AST};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub const RHEI_PREFIX: &str = "__rhei:";
|
||||||
|
|
||||||
|
pub struct RheiContext {
|
||||||
|
engine: Engine,
|
||||||
|
init_ast: AST,
|
||||||
|
fn_ast: AST,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RheiContext {
|
||||||
|
pub fn new(scripts: &[String]) -> Self {
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
engine.on_print(|s| println!("[rhei] {s}"));
|
||||||
|
engine.on_debug(|s, src, pos| {
|
||||||
|
eprintln!("[rhei debug @ {src:?}:{pos}] {s}");
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut combined = AST::empty();
|
||||||
|
for (i, script) in scripts.iter().enumerate() {
|
||||||
|
match engine.compile(script) {
|
||||||
|
Ok(ast) => combined = combined.merge(&ast),
|
||||||
|
Err(e) => eprintln!("⚠️ Rhei compile error (block #{i}): {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut fn_ast = combined.clone();
|
||||||
|
fn_ast.clear_statements();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
engine,
|
||||||
|
init_ast: combined,
|
||||||
|
fn_ast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize(&self, variables: &mut HashMap<String, String>) {
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
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<String> = scope.iter_raw()
|
||||||
|
.map(|(name, _, _)| name.to_string())
|
||||||
|
.collect();
|
||||||
|
for name in &names {
|
||||||
|
if let Some(val) = scope.get_value::<Dynamic>(name) {
|
||||||
|
variables.insert(name.clone(), dyn_to_str(&val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> Option<String> {
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
for (k, v) in variables {
|
||||||
|
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
let expr_ast = self.engine
|
||||||
|
.compile_expression(expr)
|
||||||
|
.or_else(|_| self.engine.compile(expr))
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let full = self.fn_ast.merge(&expr_ast);
|
||||||
|
|
||||||
|
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
|
||||||
|
Ok(val) => Some(dyn_to_str(&val)),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("⚠️ Rhei eval `{expr}`: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool {
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
for (k, v) in variables {
|
||||||
|
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<Dynamic>(&mut scope, &full) {
|
||||||
|
Ok(val) => {
|
||||||
|
if val.is_bool() { return val.cast::<bool>(); }
|
||||||
|
if val.is_int() { return val.cast::<i64>() != 0; }
|
||||||
|
if val.is_float(){ return val.cast::<f64>() != 0.0; }
|
||||||
|
if val.is_string(){
|
||||||
|
let s = val.cast::<String>();
|
||||||
|
return !matches!(s.trim(), "" | "false" | "0" | "null");
|
||||||
|
}
|
||||||
|
!val.is_unit()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("⚠️ Rhei condition eval `{expr}`: {e}");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) {
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
for (k, v) in variables.iter() {
|
||||||
|
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.engine.compile(script) {
|
||||||
|
Ok(action_ast) => {
|
||||||
|
let full = self.fn_ast.merge(&action_ast);
|
||||||
|
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &full) {
|
||||||
|
eprintln!("⚠️ Rhei action execution error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("⚠️ Rhei action compilation error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let names: Vec<String> = scope.iter_raw()
|
||||||
|
.map(|(name, _, _)| name.to_string())
|
||||||
|
.collect();
|
||||||
|
for name in &names {
|
||||||
|
if let Some(val) = scope.get_value::<Dynamic>(name) {
|
||||||
|
variables.insert(name.clone(), dyn_to_str(&val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RheiContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(&[])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn str_to_dyn(s: &str) -> Dynamic {
|
||||||
|
if let Ok(i) = s.parse::<i64>() { return Dynamic::from(i); }
|
||||||
|
if let Ok(f) = s.parse::<f64>() { return Dynamic::from(f); }
|
||||||
|
if let Ok(b) = s.parse::<bool>() { return Dynamic::from(b); }
|
||||||
|
Dynamic::from(s.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dyn_to_str(val: &Dynamic) -> String {
|
||||||
|
if val.is_string() {
|
||||||
|
return val.clone().cast::<String>();
|
||||||
|
}
|
||||||
|
if val.is_unit() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
val.to_string()
|
||||||
|
}
|
||||||
183
src/interpreter/style.rs
Normal file
183
src/interpreter/style.rs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct StyleSheet {
|
||||||
|
index: HashMap<String, HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleSheet {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>) {
|
||||||
|
self.index
|
||||||
|
.entry(selector.trim().to_string())
|
||||||
|
.or_default()
|
||||||
|
.extend(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn resolve(&self, element_type: &str) -> Option<&HashMap<String, String>> {
|
||||||
|
self.index.get(element_type.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.index.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct ComputedStyle {
|
||||||
|
pub font_size: Option<f32>,
|
||||||
|
pub color: Option<iced::Color>,
|
||||||
|
pub padding: Option<f32>,
|
||||||
|
pub background: Option<iced::Color>,
|
||||||
|
pub spacing: Option<f32>,
|
||||||
|
pub border_radius: Option<f32>,
|
||||||
|
pub border_width: Option<f32>,
|
||||||
|
pub border_color: Option<iced::Color>,
|
||||||
|
pub width: Option<f32>,
|
||||||
|
pub direction: Option<LayoutDirection>,
|
||||||
|
pub align_items: Option<iced::Alignment>,
|
||||||
|
pub content_align: Option<ContentAlign>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComputedStyle {
|
||||||
|
pub fn compute(
|
||||||
|
inline: &HashMap<String, String>,
|
||||||
|
sheet: Option<&HashMap<String, String>>,
|
||||||
|
) -> Self {
|
||||||
|
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),
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn lookup<'a>(
|
||||||
|
key: &str,
|
||||||
|
inline: &'a HashMap<String, String>,
|
||||||
|
sheet: Option<&'a HashMap<String, String>>,
|
||||||
|
) -> Option<&'a str> {
|
||||||
|
if let Some(v) = inline.get(key) {
|
||||||
|
return Some(v.as_str());
|
||||||
|
}
|
||||||
|
if let Some(v) = inline.get(&format!("style:{key}")) {
|
||||||
|
return Some(v.as_str());
|
||||||
|
}
|
||||||
|
sheet.and_then(|s| s.get(key)).map(String::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_color(s: &str) -> Option<iced::Color> {
|
||||||
|
let s = s.trim();
|
||||||
|
|
||||||
|
if let Some(hex) = s.strip_prefix('#') {
|
||||||
|
return match hex.len() {
|
||||||
|
3 => {
|
||||||
|
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
|
||||||
|
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
|
||||||
|
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
|
||||||
|
Some(iced::Color::from_rgb(
|
||||||
|
r as f32 / 255.0,
|
||||||
|
g as f32 / 255.0,
|
||||||
|
b as f32 / 255.0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
6 => {
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||||
|
Some(iced::Color::from_rgb(
|
||||||
|
r as f32 / 255.0,
|
||||||
|
g as f32 / 255.0,
|
||||||
|
b as f32 / 255.0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
8 => {
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||||
|
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
|
||||||
|
Some(iced::Color::from_rgba(
|
||||||
|
r as f32 / 255.0,
|
||||||
|
g as f32 / 255.0,
|
||||||
|
b as f32 / 255.0,
|
||||||
|
a as f32 / 255.0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match s {
|
||||||
|
"white" => Some(iced::Color::WHITE),
|
||||||
|
"black" => Some(iced::Color::BLACK),
|
||||||
|
"transparent" => Some(iced::Color::TRANSPARENT),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum LayoutDirection {
|
||||||
|
Column,
|
||||||
|
Row,
|
||||||
|
Grid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ContentAlign {
|
||||||
|
Start,
|
||||||
|
Center,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_direction(s: &str) -> Option<LayoutDirection> {
|
||||||
|
match s.trim().to_lowercase().as_str() {
|
||||||
|
"horizontal" | "row" => Some(LayoutDirection::Row),
|
||||||
|
"vertical" | "column" => Some(LayoutDirection::Column),
|
||||||
|
"grid" => Some(LayoutDirection::Grid),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_alignment(s: &str) -> Option<iced::Alignment> {
|
||||||
|
match s.trim().to_lowercase().as_str() {
|
||||||
|
"start" => Some(iced::Alignment::Start),
|
||||||
|
"center" => Some(iced::Alignment::Center),
|
||||||
|
"end" => Some(iced::Alignment::End),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_content_align(s: &str) -> Option<ContentAlign> {
|
||||||
|
match s.trim().to_lowercase().as_str() {
|
||||||
|
"start" | "left" | "top" => Some(ContentAlign::Start),
|
||||||
|
"center" => Some(ContentAlign::Center),
|
||||||
|
"end" | "right" | "bottom" => Some(ContentAlign::End),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_size(s: &str) -> Option<f32> {
|
||||||
|
s.trim()
|
||||||
|
.trim_end_matches(|c: char| c.is_alphabetic() || c == '%')
|
||||||
|
.parse::<f32>()
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
60
src/interpreter/types.rs
Normal file
60
src/interpreter/types.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
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<String, String>,
|
||||||
|
pub children: Vec<Element>,
|
||||||
|
pub computed_style: ComputedStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element {
|
||||||
|
pub fn new(type_name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
type_name,
|
||||||
|
properties: HashMap::new(),
|
||||||
|
children: Vec::new(),
|
||||||
|
computed_style: ComputedStyle::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ComponentDef {
|
||||||
|
pub name: String,
|
||||||
|
pub params: Vec<(String, String)>,
|
||||||
|
pub children: Vec<Element>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Document {
|
||||||
|
pub roots: Vec<Element>,
|
||||||
|
pub components: HashMap<String, ComponentDef>,
|
||||||
|
pub variables: HashMap<String, String>,
|
||||||
|
pub rhei_scripts: Vec<String>,
|
||||||
|
pub stylesheet: StyleSheet,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InterpError {
|
||||||
|
BadMagic,
|
||||||
|
UnexpectedEof,
|
||||||
|
InvalidUtf8,
|
||||||
|
UnexpectedPop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for InterpError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::BadMagic => write!(f, "Bad magic bytes — not a .glbc file"),
|
||||||
|
Self::UnexpectedEof => write!(f, "Unexpected end of bytecode"),
|
||||||
|
Self::InvalidUtf8 => write!(f, "String is not valid UTF-8"),
|
||||||
|
Self::UnexpectedPop => write!(f, "OP_ELEM_POP without matching OP_ELEM_PUSH"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for InterpError {}
|
||||||
64
src/main.rs
Normal file
64
src/main.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
mod app;
|
||||||
|
mod cli;
|
||||||
|
mod interpreter;
|
||||||
|
mod renderer;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
EventTriggered(String),
|
||||||
|
InputChanged(Option<String>, String),
|
||||||
|
ToggleChanged(Option<String>, bool),
|
||||||
|
SliderChanged(Option<String>, f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "glint-runtime")]
|
||||||
|
#[command(author, version, about = "Glint runtime – compile and run .gltm/.glts files", long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
Compile {
|
||||||
|
#[arg(required = true)]
|
||||||
|
gltm_files: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 's', long = "style")]
|
||||||
|
glts_files: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'o', long = "output")]
|
||||||
|
output: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
Run {
|
||||||
|
/// Bytecode file to execute
|
||||||
|
file: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Compile {
|
||||||
|
gltm_files,
|
||||||
|
glts_files,
|
||||||
|
output,
|
||||||
|
} => {
|
||||||
|
let output_path = output.unwrap_or_else(|| {
|
||||||
|
std::path::Path::new(&gltm_files[0])
|
||||||
|
.with_extension("glbc")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
});
|
||||||
|
cli::compile_files(&gltm_files, &glts_files, &output_path);
|
||||||
|
}
|
||||||
|
Commands::Run { file } => {
|
||||||
|
cli::run_file(&file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
350
src/renderer.rs
Normal file
350
src/renderer.rs
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use iced::widget::{
|
||||||
|
button, checkbox, column, container, progress_bar, row, slider, text, text_input, image, svg
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
|
||||||
|
fn extract_var_binding(el: &Element, prop: &str) -> Option<String> {
|
||||||
|
el.properties.get(&format!("__bind:{prop}")).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_children<'a>(
|
||||||
|
children: &'a [Element],
|
||||||
|
) -> Vec<iced::Element<'a, Message, Theme, iced::Renderer>> {
|
||||||
|
children.iter().filter_map(render_element).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bg_color = match status {
|
||||||
|
button::Status::Hovered => iced::Color {
|
||||||
|
r: (base_bg.r * 1.15).min(1.0),
|
||||||
|
g: (base_bg.g * 1.15).min(1.0),
|
||||||
|
b: (base_bg.b * 1.15).min(1.0),
|
||||||
|
a: base_bg.a,
|
||||||
|
},
|
||||||
|
button::Status::Pressed => iced::Color {
|
||||||
|
r: base_bg.r * 0.85,
|
||||||
|
g: base_bg.g * 0.85,
|
||||||
|
b: base_bg.b * 0.85,
|
||||||
|
a: base_bg.a,
|
||||||
|
},
|
||||||
|
_ => base_bg,
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
radius: cs.border_radius.unwrap_or(0.0).into(),
|
||||||
|
},
|
||||||
|
text_color: cs.color.unwrap_or(iced::Color::WHITE),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))),
|
||||||
|
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),
|
||||||
|
radius: cs.border_radius.unwrap_or(0.0).into(),
|
||||||
|
},
|
||||||
|
icon: iced::Color::from_rgb(0.5, 0.5, 0.5),
|
||||||
|
placeholder: iced::Color::from_rgb(0.4, 0.4, 0.45),
|
||||||
|
value: cs.color.unwrap_or(iced::Color::WHITE),
|
||||||
|
selection: iced::Color::from_rgb(0.3, 0.3, 0.5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn apply_container_style<'a>(
|
||||||
|
c: container::Container<'a, Message, Theme, iced::Renderer>,
|
||||||
|
cs: &ComputedStyle,
|
||||||
|
) -> 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 needs_style = bg.is_some() || radius != 0.0 || bwidth != 0.0;
|
||||||
|
|
||||||
|
let c = if needs_style {
|
||||||
|
c.style(move |_theme| ContainerStyle {
|
||||||
|
background: bg.map(Background::Color),
|
||||||
|
border: Border {
|
||||||
|
color: bcolor,
|
||||||
|
width: bwidth,
|
||||||
|
radius: radius.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.style(|_theme| ContainerStyle::default())
|
||||||
|
};
|
||||||
|
|
||||||
|
match cs.width {
|
||||||
|
Some(w) => c.width(Length::Fixed(w)).into(),
|
||||||
|
None => c.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_element<'a>(
|
||||||
|
el: &'a Element,
|
||||||
|
) -> Option<iced::Element<'a, Message, Theme, iced::Renderer>> {
|
||||||
|
let props = &el.properties;
|
||||||
|
let 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
"Panel" => Some(render_panel(el)),
|
||||||
|
|
||||||
|
"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);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"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::<f32>().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() {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let spacing = cs.spacing.unwrap_or(10.0);
|
||||||
|
|
||||||
|
let is_horizontal = el.children.iter().all(|c| {
|
||||||
|
matches!(
|
||||||
|
c.type_name.as_str(),
|
||||||
|
"Icon" | "Button" | "MenuItem" | "Label" | "Toggle" | "Text" | "#text"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let direction = cs.direction.unwrap_or(if is_horizontal {
|
||||||
|
LayoutDirection::Row
|
||||||
|
} else {
|
||||||
|
LayoutDirection::Column
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
|
r = r.align_y(Alignment::Center);
|
||||||
|
}
|
||||||
|
for child in render_children(&el.children) { r = r.push(child); }
|
||||||
|
r.into()
|
||||||
|
}
|
||||||
|
LayoutDirection::Column => {
|
||||||
|
let mut c = column![].spacing(spacing);
|
||||||
|
if let Some(align) = cs.align_items {
|
||||||
|
c = c.align_x(align);
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
.and_then(|v| v.parse::<usize>().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);
|
||||||
|
} else {
|
||||||
|
r = r.align_y(Alignment::Center);
|
||||||
|
}
|
||||||
|
for child in render_children(chunk) { r = r.push(child); }
|
||||||
|
c = c.push(r);
|
||||||
|
}
|
||||||
|
c.into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cont = container(content).padding(padding);
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
cont = cont.width(Length::Fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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); }
|
||||||
|
t.into()
|
||||||
|
} else {
|
||||||
|
let mut r = row![].spacing(8);
|
||||||
|
for child in render_children(&el.children) { r = r.push(child); }
|
||||||
|
r.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let padding = cs.padding
|
||||||
|
.map(|p| [p, p * 2.0])
|
||||||
|
.unwrap_or([8.0, 16.0]);
|
||||||
|
|
||||||
|
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(w) = cs.width {
|
||||||
|
btn = btn.width(Length::Fixed(w));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cs_clone = *cs;
|
||||||
|
let btn = btn.style(move |_, status| get_button_style(&cs_clone, status));
|
||||||
|
|
||||||
|
btn.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();
|
||||||
|
let var_name = extract_var_binding(el, "value");
|
||||||
|
let padding = el.computed_style.padding.unwrap_or(10.0);
|
||||||
|
let cs = &el.computed_style;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cs_clone = *cs;
|
||||||
|
input = input.style(move |_, _| get_text_input_style(&cs_clone));
|
||||||
|
|
||||||
|
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::<bool>().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::<f64>().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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user