use crate::Message; use crate::interpreter::Element; use crate::interpreter::style::{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign, resolve_size}; 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::cell::RefCell; use std::collections::HashMap; thread_local! { static WIDGET_ID_CACHE: RefCell> = RefCell::new(HashMap::new()); } fn get_or_create_widget_id(key: u32, prefix: &'static str) -> iced::widget::Id { WIDGET_ID_CACHE.with(|cache| { cache.borrow_mut() .entry((key, prefix)) .or_insert_with(|| { let s = Box::leak(format!("{prefix}:{key}").into_boxed_str()); iced::widget::Id::new(s) }) .clone() }) } fn extract_var_binding(el: &Element, prop: &str) -> Option { 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>, V: Into>>(&mut self, key: K, val: V) { self.properties.push((key.into(), val.into())); } pub fn set_prop>, V: Into>>(&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, HashMap) { 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 = 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, active_props: &HashMap, 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 click_script = el.get_prop("__on:click").map(String::from); if click_script.is_none() { return widget; } let hover = hover_props.clone(); let active = active_props.clone(); let cs = base_cs.clone(); let mut btn = button(widget).padding(0); btn = btn.on_press(crate::Message::EventTriggered(click_script.unwrap())); 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: resolve_size(style_cs.border_width, None).unwrap_or(0.0), radius: resolve_size(style_cs.border_radius, None).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, parent_font_size: Option, parent_direction: Option, fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, scroll_positions: &HashMap, container_id: u64, stylesheet: &StyleSheet, ) -> Vec> { children .iter() .filter_map(|c| { render_element( c, parent_color, parent_font_size, parent_direction, fixed_layers, abs_layers, sticky_layers, scroll_positions, container_id, stylesheet, ) }) .collect() } fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding { let base = resolve_size(cs.padding, None).unwrap_or(default_pad); let mut top = resolve_size(cs.padding_top, None).unwrap_or(base); let mut right = resolve_size(cs.padding_right, None).unwrap_or(base); let mut bottom = resolve_size(cs.padding_bottom, None).unwrap_or(base); let mut left = resolve_size(cs.padding_left, None).unwrap_or(base); let bwidth = resolve_size(cs.border_width, None).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 = resolve_size(cs.margin, None).unwrap_or(0.0); iced::Padding { top: resolve_size(cs.margin_top, None).unwrap_or(base), right: resolve_size(cs.margin_right, None).unwrap_or(base), bottom: resolve_size(cs.margin_bottom, None).unwrap_or(base), left: resolve_size(cs.margin_left, None).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: resolve_size(cs.border_width, None).unwrap_or(0.0), radius: resolve_size(cs.border_radius, None).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: resolve_size(cs.border_width, None).unwrap_or(1.0), radius: resolve_size(cs.border_radius, None).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 estimate_element_height(cs: &ComputedStyle) -> f32 { let pad_top = resolve_size(cs.padding_top.or(cs.padding), None).unwrap_or(0.0); let pad_bottom = resolve_size(cs.padding_bottom.or(cs.padding), None).unwrap_or(0.0); let bwidth = resolve_size(cs.border_width, None).unwrap_or(0.0); let font_size = resolve_size(cs.font_size, None).unwrap_or(16.0); let line_height = resolve_size(cs.line_height, None).unwrap_or(1.2); pad_top + pad_bottom + bwidth * 2.0 + font_size * line_height } fn wrap_sticky_position<'a>( widget: iced::Element<'a, Message, Theme, iced::Renderer>, cs: &ComputedStyle, ) -> iced::Element<'a, Message, Theme, iced::Renderer> { let top = resolve_size(cs.top, None).unwrap_or(0.0); container(widget) .width(Length::Fill) .padding(iced::Padding { top, ..Default::default() }) .into() } 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 = resolve_size(cs.top, None).unwrap_or(0.0); let left = resolve_size(cs.left, None).unwrap_or(0.0); let right = resolve_size(cs.right, None).unwrap_or(0.0); let bottom = resolve_size(cs.bottom, None).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>, cs: &ComputedStyle, is_window: bool, default_padding: f32, scrollable_id: Option, ) -> iced::Element<'a, Message, Theme, iced::Renderer> { let bg = cs.background; let radius = resolve_size(cs.border_radius, None).unwrap_or(0.0); let bwidth = resolve_size(cs.border_width, None).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); } if let Some(sid) = scrollable_id { s = s.id(get_or_create_widget_id(sid as u32, "sc")) .on_scroll(move |viewport| Message::ScrollableScrolled(sid, viewport.absolute_offset().y)); } 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) = resolve_size(cs.max_width, None) { inner = inner.max_width(max_w); } if let Some(max_h) = resolve_size(cs.max_height, None) { 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, parent_font_size: Option, parent_direction: Option, fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, scroll_positions: &HashMap, container_id: u64, stylesheet: &StyleSheet, ) -> Option> { 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 = resolve_size(cs.font_size, parent_font_size).or(parent_font_size); if el.type_name == "Window" { let spacing = resolve_size(cs.spacing, None).unwrap_or(12.0); let mut col = column![].spacing(spacing).width(Length::Fill); let window_id = el.element_id.0 as u64; 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, scroll_positions, window_id, stylesheet, ) { col = col.push(child); } let main_flow = apply_universal_box_model(col, &cs, true, 0.0, Some(window_id)); 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, scroll_positions, container_id, 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, scroll_positions, container_id, 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> = 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) = resolve_size(cs.line_height, current_font_size) { 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) = resolve_size(cs.line_height, current_font_size) { 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 = resolve_size(cs.border_radius, None).unwrap_or(0.0); let padding = get_padding(&cs, 0.0); let bwidth = resolve_size(cs.border_width, None).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::().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 = resolve_size(cs.spacing, None).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, scroll_positions, container_id, 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 is_scrollable = cs.overflow_y.map(|o| o == Overflow::Scroll || o == Overflow::Auto).unwrap_or(false); let sc_id: Option = if is_scrollable { Some(el.element_id.0 as u64) } else { None }; let boxed = apply_universal_box_model(content_widget, &cs, false, 0.0, sc_id); 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 { let is_scrollable = cs.overflow_y.map(|o| o == Overflow::Scroll || o == Overflow::Auto).unwrap_or(false); let sc_id: Option = if is_scrollable { Some(el.element_id.0 as u64) } else { None }; final_widget_opt = Some(apply_universal_box_model(element, &cs, false, 0.0, sc_id)); } } 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 threshold = resolve_size(cs.top, None).unwrap_or(0.0); let scroll_y = scroll_positions.get(&container_id).copied().unwrap_or(0.0); if scroll_y > threshold { let wrapped = wrap_sticky_position(final_widget, &cs); sticky_layers.push(wrapped); let spacer_h = estimate_element_height(&cs); let spacer: iced::Element<'a, Message, Theme, iced::Renderer> = container(iced::widget::text("")) .width(Length::Fill) .height(Length::Fixed(spacer_h)) .into(); Some(spacer) } else { Some(final_widget) } } else { Some(final_widget) } } else { None } } fn render_panel<'a>( el: &'a Element, cs: ComputedStyle, parent_color: Option, parent_font_size: Option, fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, scroll_positions: &HashMap, container_id: u64, stylesheet: &StyleSheet, ) -> iced::Element<'a, Message, Theme, iced::Renderer> { let spacing = resolve_size(cs.spacing, None).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 is_scrollable = cs.overflow_y.map(|o| o == Overflow::Scroll || o == Overflow::Auto).unwrap_or(false); let child_container: u64 = if is_scrollable { el.element_id.0 as u64 } else { container_id }; let sc_id: Option = if is_scrollable { Some(el.element_id.0 as u64) } else { None }; 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, scroll_positions, child_container, 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, scroll_positions, child_container, 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::().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, scroll_positions, child_container, 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, sc_id); 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, parent_font_size: Option, fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, scroll_positions: &HashMap, container_id: u64, 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, scroll_positions, container_id, 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 base_px = resolve_size(cs.padding, None).unwrap_or(8.0); let mut padding = iced::Padding { top: resolve_size(cs.padding_top, None).unwrap_or(base_px), bottom: resolve_size(cs.padding_bottom, None).unwrap_or(base_px), left: resolve_size(cs.padding_left, None).unwrap_or(base_px * 2.0), right: resolve_size(cs.padding_right, None).unwrap_or(base_px * 2.0), }; let bwidth = resolve_size(cs.border_width, None).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 = el.properties.iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); let hover_sheets: Vec<&HashMap> = stylesheet.matching_rules(el.type_name, el_id, &classes, &["hover"], &default_struct, &[], &[], &el_attributes); let active_sheets: Vec<&HashMap> = 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, parent_font_size: Option, ) -> 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::().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, _parent_font_size: Option, hover_props: &HashMap, active_props: &HashMap, ) -> 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)) .id(get_or_create_widget_id(el.element_id.0, "ti")) .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::().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() }