feat: reactivityl; vdom; rhei; styles; fs; image element
This commit is contained in:
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