feat: reactivityl; vdom; rhei; styles; fs; image element

This commit is contained in:
Faynot
2026-06-02 12:12:20 +03:00
commit f956b750e3
16 changed files with 7713 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

5397
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
Cargo.toml Normal file
View 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
View 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
View 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
}

BIN
out.glbc Normal file

Binary file not shown.

95
src/app.rs Normal file
View 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
View 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
View 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: &regex::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),
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}