Files
Glint-Runtime/src/renderer.rs
2026-07-08 23:45:37 +03:00

1159 lines
39 KiB
Rust

use crate::Message;
use crate::interpreter::Element;
use crate::interpreter::style::{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign};
use iced::widget::container::Style as ContainerStyle;
use iced::widget::{
button, checkbox, column, container, image, progress_bar, row, scrollable, slider, svg, text,
text::Wrapping, text_input,
};
use iced::{Alignment, Background, Border, Length, Theme};
use iced::font::Weight;
use std::borrow::Cow;
use std::collections::HashMap;
fn extract_var_binding(el: &Element, prop: &str) -> Option<String> {
el.properties.iter()
.find_map(|(k, v)| {
k.strip_prefix("__bind:")
.filter(|&p| p == prop)
.map(|_| v.to_string())
})
}
impl<'a> Element<'a> {
#[inline]
pub fn get_prop(&self, key: &str) -> Option<&str> {
self.properties.iter()
.find(|(k, _)| **k == *key)
.map(|(_, v)| v.as_ref())
}
pub fn push_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V) {
self.properties.push((key.into(), val.into()));
}
pub fn set_prop<K: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(&mut self, key: K, val: V) {
let key_cow = key.into();
if let Some(entry) = self.properties.iter_mut().find(|(k,_)| *k == key_cow) {
entry.1 = val.into();
} else {
self.properties.push((key_cow, val.into()));
}
}
}
pub fn collect_hover_active<'a>(
el: &'a Element,
stylesheet: &StyleSheet,
) -> (HashMap<String, String>, HashMap<String, String>) {
let el_id = el.id();
let classes: Vec<&str> = el
.get_prop("class")
.map(|s| s.split_whitespace().collect())
.unwrap_or_default();
let default_struct = StructuralContext::default();
let el_attributes: HashMap<String, String> = el.properties.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let hover_props = stylesheet.matching_pseudo_rules(
"hover", el.type_name, el_id, &classes, &default_struct, &[], &[], &el_attributes,
);
let active_props = stylesheet.matching_pseudo_rules(
"active", el.type_name, el_id, &classes, &default_struct, &[], &[], &el_attributes,
);
(hover_props, active_props)
}
fn make_hoverable<'a>(
widget: iced::Element<'a, crate::Message, Theme, iced::Renderer>,
el: &Element,
hover_props: &HashMap<String, String>,
active_props: &HashMap<String, String>,
base_cs: &ComputedStyle,
) -> iced::Element<'a, crate::Message, Theme, iced::Renderer> {
let has_hover = !hover_props.is_empty();
let has_active = !active_props.is_empty();
if !has_hover && !has_active {
return widget;
}
let hover = hover_props.clone();
let active = active_props.clone();
let cs = base_cs.clone();
let mut btn = button(widget).padding(0);
if let Some(script) = el.get_prop("__on:click") {
btn = btn.on_press(crate::Message::EventTriggered(script.to_string()));
} else if has_hover || has_active {
btn = btn.on_press(crate::Message::EventTriggered(String::new()));
}
btn.style(move |_, status| {
let mut style_cs = cs.clone();
match status {
button::Status::Hovered => {
if has_hover {
style_cs.apply_overrides(&hover);
}
}
button::Status::Pressed => {
if has_active {
style_cs.apply_overrides(&active);
} else if has_hover {
style_cs.apply_overrides(&hover);
}
}
_ => {}
}
button::Style {
background: style_cs.background.map(Background::Color),
border: Border {
color: style_cs.border_color.unwrap_or(iced::Color::TRANSPARENT),
width: style_cs.border_width.unwrap_or(0.0),
radius: style_cs.border_radius.unwrap_or(0.0).into(),
},
text_color: style_cs.color.unwrap_or(iced::Color::WHITE),
..Default::default()
}
})
.into()
}
fn render_children<'a>(
children: &'a [Element],
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
parent_direction: Option<LayoutDirection>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
stylesheet: &StyleSheet,
) -> Vec<iced::Element<'a, Message, Theme, iced::Renderer>> {
children
.iter()
.filter_map(|c| {
render_element(
c,
parent_color,
parent_font_size,
parent_direction,
fixed_layers,
abs_layers,
sticky_layers,
stylesheet,
)
})
.collect()
}
fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding {
let base = cs.padding.unwrap_or(default_pad);
let mut top = cs.padding_top.unwrap_or(base);
let mut right = cs.padding_right.unwrap_or(base);
let mut bottom = cs.padding_bottom.unwrap_or(base);
let mut left = cs.padding_left.unwrap_or(base);
let bwidth = cs.border_width.unwrap_or(0.0);
if let Some(iced::Length::Fixed(w)) = cs.width {
let total_h = left + right + bwidth * 2.0;
if total_h > w && w > 0.0 {
let scale = w / total_h;
left *= scale;
right *= scale;
}
}
if let Some(iced::Length::Fixed(h)) = cs.height {
let total_v = top + bottom + bwidth * 2.0;
if total_v > h && h > 0.0 {
let scale = h / total_v;
top *= scale;
bottom *= scale;
}
}
iced::Padding {
top,
right,
bottom,
left,
}
}
fn get_margin(cs: &ComputedStyle) -> iced::Padding {
let base = cs.margin.unwrap_or(0.0);
iced::Padding {
top: cs.margin_top.unwrap_or(base),
right: cs.margin_right.unwrap_or(base),
bottom: cs.margin_bottom.unwrap_or(base),
left: cs.margin_left.unwrap_or(base),
}
}
fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style {
let base_bg = cs
.background
.unwrap_or(iced::Color::from_rgb(0.25, 0.26, 0.35));
let 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 wrap_fixed_position<'a>(
widget: iced::Element<'a, Message, Theme, iced::Renderer>,
cs: &ComputedStyle,
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
use iced::alignment::{Horizontal, Vertical};
let top = cs.top.unwrap_or(0.0);
let left = cs.left.unwrap_or(0.0);
let right = cs.right.unwrap_or(0.0);
let bottom = cs.bottom.unwrap_or(0.0);
let has_top = cs.top.is_some();
let has_bottom = cs.bottom.is_some();
let has_left = cs.left.is_some();
let has_right = cs.right.is_some();
let horiz = match (has_left, has_right) {
(true, true) => Horizontal::Left,
(false, true) => Horizontal::Right,
_ => Horizontal::Left,
};
let vert = match (has_top, has_bottom) {
(true, true) => Vertical::Top,
(false, true) => Vertical::Bottom,
_ => Vertical::Top,
};
let padding = iced::Padding {
top: if has_top { top } else { 0.0 },
bottom: if has_bottom { bottom } else { 0.0 },
left: if has_left { left } else { 0.0 },
right: if has_right { right } else { 0.0 },
};
container(widget)
.width(Length::Fill)
.height(Length::Fill)
.align_x(horiz)
.align_y(vert)
.padding(padding)
.into()
}
fn apply_universal_box_model<'a>(
widget: impl Into<iced::Element<'a, Message, Theme, iced::Renderer>>,
cs: &ComputedStyle,
is_window: bool,
default_padding: f32,
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
let bg = cs.background;
let radius = cs.border_radius.unwrap_or(0.0);
let bwidth = cs.border_width.unwrap_or(0.0);
let bcolor = cs.border_color.unwrap_or_else(|| {
if bwidth > 0.0 {
iced::Color::from_rgb(0.4, 0.4, 0.45)
} else {
iced::Color::TRANSPARENT
}
});
let needs_style = bg.is_some() || radius != 0.0 || bwidth != 0.0;
let padding = get_padding(cs, default_padding);
let mut padded_content = container(widget).padding(padding);
let ox = cs.overflow_x.unwrap_or(Overflow::Visible);
let oy = cs.overflow_y.unwrap_or(if is_window { Overflow::Auto } else { Overflow::Visible });
let scroll_v = oy == Overflow::Scroll || oy == Overflow::Auto;
let scroll_h = ox == Overflow::Scroll || ox == Overflow::Auto;
if !scroll_h {
padded_content = padded_content.width(Length::Fill);
}
let content_layer: iced::Element<'a, Message, Theme, iced::Renderer> = if scroll_v || scroll_h {
use iced::widget::scrollable::{Direction, Scrollbar};
let direction = match (scroll_v, scroll_h) {
(true, true) => Direction::Both {
vertical: Default::default(),
horizontal: Default::default(),
},
(true, false) => Direction::Vertical(Default::default()),
(false, true) => Direction::Horizontal(Default::default()),
(false, false) => unreachable!(),
};
let mut s = scrollable(padded_content)
.direction(direction)
.width(Length::Fill);
if scroll_v {
s = s.height(Length::Fill);
}
s.into()
} else {
padded_content.into()
};
let mut inner = container(content_layer);
if ox == Overflow::Hidden || oy == Overflow::Hidden {
inner = inner.clip(true);
}
if needs_style {
inner = inner.style(move |_theme| ContainerStyle {
background: bg.map(Background::Color),
border: Border {
color: bcolor,
width: bwidth,
radius: radius.into(),
},
..Default::default()
});
} else {
inner = inner.style(|_theme| ContainerStyle::default());
}
if is_window {
inner = inner.width(Length::Fill).height(Length::Fill);
} else {
if let Some(w) = cs.width {
inner = inner.width(w);
} else if cs.content_align.is_some() {
inner = inner.width(Length::Fill);
}
if let Some(h) = cs.height {
inner = inner.height(h);
}
if let Some(max_w) = cs.max_width {
inner = inner.max_width(max_w);
}
if let Some(max_h) = cs.max_height {
inner = inner.max_height(max_h);
}
if let Some(ca) = cs.content_align {
match ca {
ContentAlign::Start => inner = inner.align_x(iced::alignment::Horizontal::Left),
ContentAlign::Center => inner = inner.align_x(iced::alignment::Horizontal::Center),
ContentAlign::End => inner = inner.align_x(iced::alignment::Horizontal::Right),
}
}
}
let margin = get_margin(cs);
let has_margin = margin.top > 0.0 || margin.right > 0.0 || margin.bottom > 0.0 || margin.left > 0.0;
let result: iced::Element<'a, Message, Theme, iced::Renderer> = if has_margin && !is_window {
let mut outer = container(inner).padding(margin);
if let Some(w) = cs.width {
if matches!(w, Length::Fill | Length::FillPortion(_)) {
outer = outer.width(w);
}
} else if cs.content_align.is_some() {
outer = outer.width(Length::Fill);
}
if let Some(h) = cs.height {
if matches!(h, Length::Fill | Length::FillPortion(_)) {
outer = outer.height(h);
}
}
outer.into()
} else {
inner.into()
};
result
}
pub fn render_element<'a>(
el: &'a Element,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
parent_direction: Option<LayoutDirection>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
stylesheet: &StyleSheet,
) -> Option<iced::Element<'a, Message, Theme, iced::Renderer>> {
let mut cs = el.computed_style;
if cs.display == Some(Display::None) {
return None;
}
let (hover_props, active_props) = collect_hover_active(el, stylesheet);
let is_fixed = cs.position == Some(Position::Fixed) && el.type_name != "Window";
let is_absolute = cs.position == Some(Position::Absolute) && el.type_name != "Window";
let is_sticky = cs.position == Some(Position::Sticky) && el.type_name != "Window";
if is_fixed || is_absolute || is_sticky {
if cs.left.is_some() && cs.right.is_some() && cs.width.is_none() {
cs.width = Some(Length::Fill);
}
if cs.top.is_some() && cs.bottom.is_some() && cs.height.is_none() {
cs.height = Some(Length::Fill);
}
}
if let Some(grow_value) = cs.flex_grow {
if cs.width.is_none() && parent_direction == Some(LayoutDirection::Row) {
cs.width = Some(Length::FillPortion(grow_value));
}
if cs.height.is_none() && parent_direction == Some(LayoutDirection::Column) {
cs.height = Some(Length::FillPortion(grow_value));
}
}
let current_color = cs.color.or(parent_color);
let current_font_size = cs.font_size.or(parent_font_size);
if el.type_name == "Window" {
let spacing = cs.spacing.unwrap_or(12.0);
let mut col = column![].spacing(spacing).width(Length::Fill);
let mut window_fixed_layers = Vec::new();
let mut window_abs_layers = Vec::new();
let mut window_sticky_layers = Vec::new();
for child in render_children(
&el.children,
current_color,
current_font_size,
Some(LayoutDirection::Column),
&mut window_fixed_layers,
&mut window_abs_layers,
&mut window_sticky_layers,
stylesheet,
) {
col = col.push(child);
}
let main_flow = apply_universal_box_model(col, &cs, true, 0.0);
let has_abs = !window_abs_layers.is_empty();
let has_fixed = !window_fixed_layers.is_empty();
let has_sticky = !window_sticky_layers.is_empty();
if !has_abs && !has_fixed && !has_sticky {
return Some(main_flow);
}
let mut stack_widget = iced::widget::stack![main_flow];
for layer in window_abs_layers {
stack_widget = stack_widget.push(layer);
}
for layer in window_sticky_layers {
stack_widget = stack_widget.push(layer);
}
for layer in window_fixed_layers {
stack_widget = stack_widget.push(layer);
}
return Some(stack_widget.into());
}
let mut final_widget_opt = None;
if el.type_name == "Panel" {
let mut panel_abs = Vec::new();
let mut panel_sticky = Vec::new();
let widget = render_panel(
el,
cs,
current_color,
current_font_size,
fixed_layers,
&mut panel_abs,
&mut panel_sticky,
stylesheet,
);
if panel_abs.is_empty() {
final_widget_opt = Some(widget);
} else {
let mut stack_widget = iced::widget::stack![widget];
for layer in panel_abs {
stack_widget = stack_widget.push(layer);
}
final_widget_opt = Some(stack_widget.into());
}
} else if el.type_name == "Button" {
let mut btn_abs = Vec::new();
let mut btn_sticky = Vec::new();
let widget = render_button(
el,
cs,
current_color,
current_font_size,
fixed_layers,
&mut btn_abs,
&mut btn_sticky,
stylesheet,
);
if btn_abs.is_empty() && btn_sticky.is_empty() {
final_widget_opt = Some(widget);
} else {
let mut stack_widget = iced::widget::stack![widget];
for layer in btn_abs {
stack_widget = stack_widget.push(layer);
}
for layer in btn_sticky {
stack_widget = stack_widget.push(layer);
}
final_widget_opt = Some(stack_widget.into());
}
} else if el.type_name == "Input" {
final_widget_opt = Some(render_input(
el, cs, current_color, current_font_size, &hover_props, &active_props,
));
} else {
let element_opt: Option<iced::Element<'a, Message, Theme, iced::Renderer>> =
match el.type_name {
"Title" | "Header" => {
let content = el.get_prop("text").unwrap_or("Title");
let mut widget = text(content).size(current_font_size.unwrap_or(24.0));
if let Some(col) = current_color {
widget = widget.color(col);
}
if let Some(fw) = cs.font_weight {
let w = if fw <= 399 { Weight::Light }
else if fw <= 599 { Weight::Normal }
else if fw <= 799 { Weight::Bold }
else { Weight::ExtraBold };
widget = widget.font(iced::font::Font { weight: w, ..Default::default() });
}
if let Some(ta) = cs.text_align {
widget = widget.align_x(iced::alignment::Horizontal::from(ta));
widget = widget.width(Length::Fill);
}
if let Some(lh) = cs.line_height {
widget = widget.line_height(lh);
widget = widget.width(Length::Fill);
widget = widget.wrapping(Wrapping::Word);
}
Some(widget.into())
}
"Text" | "Label" | "#text" => {
let content = el.get_prop("text").unwrap_or("");
let mut widget = text(content).size(current_font_size.unwrap_or(16.0));
if let Some(col) = current_color {
widget = widget.color(col);
}
if let Some(fw) = cs.font_weight {
let w = if fw <= 399 { Weight::Light }
else if fw <= 599 { Weight::Normal }
else if fw <= 799 { Weight::Bold }
else { Weight::ExtraBold };
widget = widget.font(iced::font::Font { weight: w, ..Default::default() });
}
if let Some(ta) = cs.text_align {
widget = widget.align_x(iced::alignment::Horizontal::from(ta));
widget = widget.width(Length::Fill);
}
if let Some(lh) = cs.line_height {
widget = widget.line_height(lh);
widget = widget.width(Length::Fill);
widget = widget.wrapping(Wrapping::Word);
}
Some(widget.into())
}
"Image" => {
let src = el.get_prop("src").unwrap_or("");
let path = src.strip_prefix("fs:").unwrap_or(src);
let radius = cs.border_radius.unwrap_or(0.0);
let padding = get_padding(&cs, 0.0);
let bwidth = cs.border_width.unwrap_or(0.0);
let content_w = cs.width.map(|len| {
if let iced::Length::Fixed(w) = len {
iced::Length::Fixed(
(w - padding.left - padding.right - bwidth * 2.0).max(0.0),
)
} else {
len
}
});
let content_h = cs.height.map(|len| {
if let iced::Length::Fixed(h) = len {
iced::Length::Fixed(
(h - padding.top - padding.bottom - bwidth * 2.0).max(0.0),
)
} else {
len
}
});
if path.ends_with(".svg") {
let mut svg_widget = svg(svg::Handle::from_path(path));
if let Some(w) = content_w {
svg_widget = svg_widget.width(w);
} else {
svg_widget = svg_widget.width(Length::Shrink);
}
if let Some(h) = content_h {
svg_widget = svg_widget.height(h);
} else {
svg_widget = svg_widget.height(Length::Shrink);
}
Some(svg_widget.into())
} else {
let mut img_widget = image(path);
if let Some(w) = content_w {
img_widget = img_widget.width(w);
} else {
img_widget = img_widget.width(Length::Shrink);
}
if let Some(h) = content_h {
img_widget = img_widget.height(h);
} else {
img_widget = img_widget.height(Length::Shrink);
}
if radius > 0.0 {
img_widget = img_widget.border_radius(radius);
}
Some(img_widget.into())
}
}
"Toggle" => Some(render_toggle(el, current_color, current_font_size)),
"Slider" => Some(render_slider(el)),
"Icon" => {
let mut widget = text("🔹").size(current_font_size.unwrap_or(18.0));
if let Some(col) = current_color {
widget = widget.color(col);
}
Some(widget.into())
}
"ProgressBar" => {
let value = el.get_prop("value")
.and_then(|v| v.parse::<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() {
None
} else {
let spacing = cs.spacing.unwrap_or(10.0);
let mut col = column![].spacing(spacing);
let mut default_abs = Vec::new();
let mut default_sticky = Vec::new();
for child in render_children(
&el.children,
current_color,
current_font_size,
Some(LayoutDirection::Column),
fixed_layers,
&mut default_abs,
&mut default_sticky,
stylesheet,
) {
col = col.push(child);
}
// Abs inside content (before box model = inside scrollable)
let content_widget: iced::Element<'a, Message, Theme, iced::Renderer> =
if default_abs.is_empty() {
col.into()
} else {
let mut stack = iced::widget::stack![col];
for layer in default_abs {
stack = stack.push(layer);
}
stack.into()
};
let boxed = apply_universal_box_model(content_widget, &cs, false, 0.0);
// Sticky outside (after box model = outside scrollable)
if default_sticky.is_empty() {
final_widget_opt = Some(boxed);
} else {
let mut stack = iced::widget::stack![boxed];
for layer in default_sticky {
stack = stack.push(layer);
}
final_widget_opt = Some(stack.into());
}
None
}
}
};
if let Some(element) = element_opt {
final_widget_opt = Some(apply_universal_box_model(element, &cs, false, 0.0));
}
}
if el.type_name != "Button" && el.type_name != "Input" {
if let Some(w) = final_widget_opt.take() {
let wrapped = make_hoverable(w, el, &hover_props, &active_props, &cs);
final_widget_opt = Some(wrapped);
}
}
if let Some(final_widget) = final_widget_opt {
if is_fixed {
let wrapped = wrap_fixed_position(final_widget, &cs);
fixed_layers.push(wrapped);
None
} else if is_absolute {
let wrapped = wrap_fixed_position(final_widget, &cs);
abs_layers.push(wrapped);
None
} else if is_sticky {
let wrapped = wrap_fixed_position(final_widget, &cs);
sticky_layers.push(wrapped);
None
} else {
Some(final_widget)
}
} else {
None
}
}
fn render_panel<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
stylesheet: &StyleSheet,
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
let spacing = cs.spacing.unwrap_or(10.0);
let is_horizontal = el.children.iter().all(|c| {
matches!(
c.type_name,
"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,
parent_color,
parent_font_size,
Some(LayoutDirection::Row),
fixed_layers,
abs_layers,
sticky_layers,
stylesheet,
) {
r = r.push(child);
}
if abs_layers.is_empty() {
r.into()
} else {
let layers = std::mem::take(abs_layers);
let mut stack = iced::widget::stack![r];
for layer in layers {
stack = stack.push(layer);
}
stack.into()
}
}
LayoutDirection::Column => {
let mut c = column![].spacing(spacing);
if let Some(align) = cs.align_items {
c = c.align_x(align);
}
for child in render_children(
&el.children,
parent_color,
parent_font_size,
Some(LayoutDirection::Column),
fixed_layers,
abs_layers,
sticky_layers,
stylesheet,
) {
c = c.push(child);
}
if abs_layers.is_empty() {
c.into()
} else {
let layers = std::mem::take(abs_layers);
let mut stack = iced::widget::stack![c];
for layer in layers {
stack = stack.push(layer);
}
stack.into()
}
}
LayoutDirection::Grid => {
let mut c = column![].spacing(spacing);
let cols = el
.get_prop("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);
}
let mut chunk_abs = Vec::new();
let mut chunk_sticky = Vec::new();
for child in render_children(
chunk,
parent_color,
parent_font_size,
Some(LayoutDirection::Row),
fixed_layers,
&mut chunk_abs,
&mut chunk_sticky,
stylesheet,
) {
r = r.push(child);
}
if chunk_abs.is_empty() && chunk_sticky.is_empty() {
c = c.push(r);
} else {
let mut stack = iced::widget::stack![r];
for layer in chunk_abs {
stack = stack.push(layer);
}
for layer in chunk_sticky {
stack = stack.push(layer);
}
c = c.push(stack);
}
}
c.into()
}
};
let boxed = apply_universal_box_model(content, &cs, false, 5.0);
if sticky_layers.is_empty() {
boxed
} else {
let layers = std::mem::take(sticky_layers);
let mut stack = iced::widget::stack![boxed];
for layer in layers {
stack = stack.push(layer);
}
stack.into()
}
}
fn render_button<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
fixed_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
abs_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
sticky_layers: &mut Vec<iced::Element<'a, Message, Theme, iced::Renderer>>,
stylesheet: &StyleSheet,
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
let final_color = cs.color.or(parent_color);
let content: iced::Element<'a, _, _, _> = if el.children.is_empty() {
let label = el.get_prop("label").unwrap_or("Button");
let mut t = text(label).size(parent_font_size.unwrap_or(14.0));
if let Some(col) = final_color {
t = t.color(col);
}
t.into()
} else {
let mut r = row![].spacing(8);
for child in render_children(
&el.children,
final_color,
parent_font_size,
Some(LayoutDirection::Row),
fixed_layers,
abs_layers,
sticky_layers,
stylesheet,
) {
r = r.push(child);
}
if abs_layers.is_empty() && sticky_layers.is_empty() {
r.into()
} else {
let mut stack = iced::widget::stack![r];
for layer in std::mem::take(abs_layers) {
stack = stack.push(layer);
}
for layer in std::mem::take(sticky_layers) {
stack = stack.push(layer);
}
stack.into()
}
};
let mut padding = iced::Padding {
top: cs.padding_top.or(cs.padding).unwrap_or(8.0),
bottom: cs.padding_bottom.or(cs.padding).unwrap_or(8.0),
left: cs
.padding_left
.or_else(|| cs.padding.map(|p| p * 2.0))
.unwrap_or(16.0),
right: cs
.padding_right
.or_else(|| cs.padding.map(|p| p * 2.0))
.unwrap_or(16.0),
};
let bwidth = cs.border_width.unwrap_or(0.0);
if let Some(iced::Length::Fixed(w)) = cs.width {
let total_h = padding.left + padding.right + bwidth * 2.0;
if total_h > w && w > 0.0 {
let scale = w / total_h;
padding.left *= scale;
padding.right *= scale;
}
}
if let Some(iced::Length::Fixed(h)) = cs.height {
let total_v = padding.top + padding.bottom + bwidth * 2.0;
if total_v > h && h > 0.0 {
let scale = h / total_v;
padding.top *= scale;
padding.bottom *= scale;
}
}
let mut btn = button(content).padding(padding);
if let Some(script) = el.get_prop("__on:click") {
btn = btn.on_press(Message::EventTriggered(script.to_string()));
}
if let Some(w) = cs.width {
btn = btn.width(w);
}
if let Some(h) = cs.height {
btn = btn.height(h);
}
let el_id = el.id();
let classes: Vec<&str> = el
.get_prop("class")
.map(|s| s.split_whitespace().collect())
.unwrap_or_default();
let default_struct = StructuralContext::default();
let el_attributes: HashMap<String, String> = el.properties.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let hover_sheets: Vec<&HashMap<String, String>> =
stylesheet.matching_rules(el.type_name, el_id, &classes, &["hover"], &default_struct, &[], &[], &el_attributes);
let active_sheets: Vec<&HashMap<String, String>> =
stylesheet.matching_rules(el.type_name, el_id, &classes, &["active"], &default_struct, &[], &[], &el_attributes);
let mut hover_props = std::collections::HashMap::new();
for sheet in &hover_sheets {
hover_props.extend((*sheet).clone());
}
let mut active_props = std::collections::HashMap::new();
for sheet in &active_sheets {
active_props.extend((*sheet).clone());
}
let mut cs_clone = cs;
cs_clone.color = final_color;
btn.style(move |_, status| {
let mut style_cs = cs_clone.clone();
match status {
button::Status::Hovered => {
if !hover_props.is_empty() {
style_cs.apply_overrides(&hover_props);
}
}
button::Status::Pressed => {
if !active_props.is_empty() {
style_cs.apply_overrides(&active_props);
} else if !hover_props.is_empty() {
style_cs.apply_overrides(&hover_props);
}
}
_ => {}
}
get_button_style(&style_cs, status)
}).into()
}
fn render_toggle<'a>(
el: &'a Element,
parent_color: Option<iced::Color>,
parent_font_size: Option<f32>,
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
let label = el.get_prop("label").unwrap_or("");
let value = el.get_prop("value")
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(false);
let var_name = extract_var_binding(el, "value");
let cb = checkbox(value).on_toggle(move |v| Message::ToggleChanged(var_name.clone(), v));
if label.is_empty() {
cb.into()
} else {
let mut label_widget = text(label).size(parent_font_size.unwrap_or(16.0));
if let Some(color) = parent_color {
label_widget = label_widget.color(color);
}
row![cb, label_widget]
.spacing(8)
.align_y(iced::Alignment::Center)
.into()
}
}
fn render_input<'a>(
el: &'a Element,
cs: ComputedStyle,
parent_color: Option<iced::Color>,
_parent_font_size: Option<f32>,
hover_props: &HashMap<String, String>,
active_props: &HashMap<String, String>,
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
let placeholder = el.get_prop("placeholder").unwrap_or("Type here…");
let value = el.get_prop("value").unwrap_or("");
let var_name = extract_var_binding(el, "value");
let padding = get_padding(&cs, 10.0);
let mut input = text_input(&placeholder, &value)
.on_input(move |v| Message::InputChanged(var_name.clone(), v))
.padding(padding);
if let Some(w) = cs.width {
input = input.width(w);
}
let mut base_cs = cs;
base_cs.color = parent_color;
let has_any = !hover_props.is_empty() || !active_props.is_empty();
let hover = hover_props.clone();
if has_any {
input = input.style(move |_, status| {
let mut style_cs = base_cs.clone();
match status {
text_input::Status::Hovered => {
if !hover.is_empty() {
style_cs.apply_overrides(&hover);
}
}
_ => {}
}
get_text_input_style(&style_cs)
});
} else {
input = input.style(move |_, _| get_text_input_style(&base_cs));
}
input.into()
}
fn render_slider<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> {
let value = el.get_prop("value")
.and_then(|v| v.parse::<f32>().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 as f64)
})
.into()
}