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

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