From 32beb81ac4f0176ef202d18f0fa89f1172697b1b Mon Sep 17 00:00:00 2001 From: Glint Dev Date: Fri, 24 Jul 2026 16:46:00 +0300 Subject: [PATCH] demo: layout fixes, sticky removal, style overrides, css-like rendering improvements --- examples/demo/app.gltm | 17 ++- examples/demo/styles.glts | 16 ++- examples/dock/dock.gltm | 17 +++ examples/dock/styles.glts | 56 ++++++++++ src/app.rs | 26 +++-- src/interpreter/style.rs | 80 ++++++++++----- src/main.rs | 1 + src/renderer.rs | 210 ++++++++++++++++++++++++-------------- 8 files changed, 289 insertions(+), 134 deletions(-) create mode 100644 examples/dock/dock.gltm create mode 100644 examples/dock/styles.glts diff --git a/examples/demo/app.gltm b/examples/demo/app.gltm index bf223f2..40e1263 100644 --- a/examples/demo/app.gltm +++ b/examples/demo/app.gltm @@ -4,8 +4,7 @@ // Showcases: @global, @singleton, !rhei:, @component, @if/@else, @each, // @on click, variable binding, positioning (fixed, absolute), // typography, box model, opacity, overflow, grid layout, style inheritance. -// Note: true CSS position:sticky requires scroll-tracking not exposed by Iced. -// "Always-visible header above scrollable content" pattern used instead. +// Note: position:sticky approximated via in-flow layout + overlay // ============================================================================ @version 1 @@ -66,7 +65,7 @@ // ── 3. Layout ──────────────────────────────────────────────────────────────── Window(title=$title) { - // ── Header (always visible; body scrolls beneath — closest to sticky in Iced) ─ + // ── Header (sticky — stays at top while body scrolls beneath) ─ Panel(class="header") { Panel(class="header-inner") { Icon "◇" @@ -76,8 +75,7 @@ Window(title=$title) { } } - // ── Scrollable body ───────────────────────────────────────────────────── - Panel(class="body-scroll") { + // ── Main body ──────────────────────────────────────────────────────────── Panel(class="body") { // ── LEFT COLUMN ────────────────────────────────────────────────────── @@ -199,13 +197,13 @@ Window(title=$title) { Text(class="gauge-label") "* Opacity depends on Iced widget support — works on containers, not on text" } - // Panel: Scrollable Log with always-visible header + // Panel: Scrollable Log with sticky header Panel(class="card") { Header "Scrollable Log" - Panel(class="log-header") { - Text "Event Log (always visible — above scrollable area)" - } Panel(class="log") { + Panel(class="log-header") { + Text "Event Log (sticky header — scroll below)" + } Text "Init: system boot" Text "Info: all modules OK" Text "Warn: CPU at $cpu%" @@ -253,7 +251,6 @@ Window(title=$title) { } } } - } // ── Fixed Toast ────────────────────────────────────────────────────────── @if $notif { diff --git a/examples/demo/styles.glts b/examples/demo/styles.glts index 8dc6972..6e6855d 100644 --- a/examples/demo/styles.glts +++ b/examples/demo/styles.glts @@ -45,8 +45,10 @@ Panel { direction: vertical } -// ── Header ─────────────────────────────────────────────────────────────────── +// ── Header (sticky — sticks at top of Window when scrolled) ────────────────── .header { + position: sticky + top: 0 background: $card padding: 8px 20px border-bottom: 1px solid $border @@ -60,13 +62,6 @@ Panel { } // ── Layout ─────────────────────────────────────────────────────────────────── -.body-scroll { - overflow-y: auto - flex-grow: 1 - padding: 0px - gap: 0px -} - .body { direction: horizontal padding: 12px @@ -370,6 +365,8 @@ Slider { } .log-header { + position: sticky + top: 0 background: $surface padding: 6px 10px border-radius: 4px @@ -379,8 +376,7 @@ Slider { width: fill } -// Note: position:sticky is not supported by Iced. "Always visible header above -// scrollable content" is the working pattern. +// Note: position:sticky approximated via in-flow spacer + overlay (no scroll-tracking) // ═══════════════════════════════════════════════════════════════════════════════ // ── Style Inheritance ──────────────────────────────────────────────────────── diff --git a/examples/dock/dock.gltm b/examples/dock/dock.gltm new file mode 100644 index 0000000..129b9e9 --- /dev/null +++ b/examples/dock/dock.gltm @@ -0,0 +1,17 @@ +@version 1 +@style "styles.glts" + +Window(title="Dock") { + Panel(class="wallpaper") + + Panel(class="dock-outer") { + Panel(class="dock") { + Button(label=" Finder ", class="dock-icon") + Button(label=" Safari ", class="dock-icon") + Button(label=" Mail ", class="dock-icon") + Button(label=" Music ", class="dock-icon") + Button(label=" Photos ", class="dock-icon") + Button(label=" Trash ", class="dock-icon") + } + } +} diff --git a/examples/dock/styles.glts b/examples/dock/styles.glts new file mode 100644 index 0000000..214d964 --- /dev/null +++ b/examples/dock/styles.glts @@ -0,0 +1,56 @@ +$dock-bg = #2c2c2e +$icon-bg = #3a3a3c +$text = #ffffff +$accent = #0a84ff + +Window { + background: #1c1c1e + height: 100vh +} + +Panel { + background: transparent + border-width: 0px + padding: 0px + gap: 0px + direction: vertical +} + +.wallpaper { + width: fill + height: fill +} + +.dock-outer { + position: fixed + bottom: 12px + width: 50% + direction: horizontal + content-align: center + align-items: center +} + +.dock { + direction: horizontal + gap: 6px + background: $dock-bg + padding: 8px 14px + border-radius: 16px + border-width: 1px + border-color: #444 +} + +.dock-icon { + background: $icon-bg + color: $text + font-size: 13px + font-weight: 600 + padding: 6px 10px + border-width: 0px + border-radius: 8px +} + +.dock-icon:hover { + background: $accent + color: #fff +} diff --git a/src/app.rs b/src/app.rs index 1fe54ee..14b143b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use iced::widget::{column, container}; use iced::{Length, Theme}; @@ -23,6 +25,11 @@ impl GlintApp { self.doc.variables.insert(var.clone(), Value::Float(y as f64)); self.doc.tracker.on_variable_changed(&var); } + Message::ScrollableScrolled(id, y) => { + let var = format!("__scroll_{}", id); + self.doc.variables.insert(var.clone(), Value::Float(y as f64)); + self.doc.tracker.on_variable_changed(&var); + } Message::EventTriggered(script) => { if !script.is_empty() { let old_vars: Vec = self.doc.variables.keys().cloned().collect(); @@ -74,18 +81,23 @@ impl GlintApp { .width(Length::Fill) .height(Length::Fill); + let mut scroll_positions: HashMap = HashMap::new(); + for (key, val) in &self.doc.variables { + if let Some(id_str) = key.strip_prefix("__scroll_") { + if let (Ok(id), Value::Float(y)) = (id_str.parse::(), val) { + scroll_positions.insert(id, *y as f32); + } + } + } + let mut global_fixed_layers = Vec::new(); let mut global_abs_layers = Vec::new(); let mut global_sticky_layers = Vec::new(); - let _scroll_y = self.doc.variables.get("__scroll_y") - .and_then(|v| if let Value::Float(f) = v { Some(*f as f32) } else { None }) - .unwrap_or(0.0); - for root in &self.vdom_roots { - if let Some(el) = render_element(root, None, None, None, &mut global_fixed_layers, &mut global_abs_layers, &mut global_sticky_layers, &self.doc.stylesheet) { - content = content.push(el); -} + if let Some(el) = render_element(root, None, None, None, &mut global_fixed_layers, &mut global_abs_layers, &mut global_sticky_layers, &scroll_positions, 0, &self.doc.stylesheet) { + content = content.push(el); + } } let layout = column![ diff --git a/src/interpreter/style.rs b/src/interpreter/style.rs index 5b033ff..f03c95f 100644 --- a/src/interpreter/style.rs +++ b/src/interpreter/style.rs @@ -815,35 +815,50 @@ pub enum Display { None, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SizeValue { + Px(f32), + Percent(f32), +} + +impl SizeValue { + pub fn resolve(self, relative_to: Option) -> f32 { + match self { + SizeValue::Px(v) => v, + SizeValue::Percent(p) => relative_to.map(|base| base * p / 100.0).unwrap_or(p), + } + } +} + #[derive(Debug, Clone, Copy, Default)] pub struct ComputedStyle { - pub font_size: Option, + pub font_size: Option, pub color: Option, - pub padding: Option, - pub padding_top: Option, - pub padding_right: Option, - pub padding_bottom:Option, - pub padding_left: Option, + pub padding: Option, + pub padding_top: Option, + pub padding_right: Option, + pub padding_bottom:Option, + pub padding_left: Option, - pub margin: Option, - pub margin_top: Option, - pub margin_right: Option, - pub margin_bottom: Option, - pub margin_left: Option, + pub margin: Option, + pub margin_top: Option, + pub margin_right: Option, + pub margin_bottom: Option, + pub margin_left: Option, pub background: Option, - pub spacing: Option, - pub border_radius: Option, - pub border_width: Option, + pub spacing: Option, + pub border_radius: Option, + pub border_width: Option, pub border_color: Option, pub width: Option, pub height: Option, - pub min_width: Option, - pub max_width: Option, - pub min_height: Option, - pub max_height: Option, + pub min_width: Option, + pub max_width: Option, + pub min_height: Option, + pub max_height: Option, pub direction: Option, pub align_items: Option, pub content_align: Option, @@ -851,10 +866,10 @@ pub struct ComputedStyle { pub flex_grow: Option, pub position: Option, - pub top: Option, - pub right: Option, - pub bottom: Option, - pub left: Option, + pub top: Option, + pub right: Option, + pub bottom: Option, + pub left: Option, pub overflow_x: Option, pub overflow_y: Option, @@ -862,7 +877,7 @@ pub struct ComputedStyle { pub opacity: Option, pub font_weight: Option, - pub line_height: Option, + pub line_height: Option, pub text_align: Option, } @@ -1196,18 +1211,27 @@ pub fn parse_text_align(s: &str) -> Option { else { None } } -pub fn parse_size(s: &str) -> Option { +pub fn parse_size(s: &str) -> Option { let s = s.trim(); - if s.ends_with('%') || s.eq_ignore_ascii_case("auto") || + if s.eq_ignore_ascii_case("auto") || s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("stretch") { return None; } + if s.ends_with('%') { + let val = s.trim_end_matches(|c: char| c == '%' || c.is_alphabetic()) + .parse::().ok()?; + return Some(SizeValue::Percent(val)); + } if s.eq_ignore_ascii_case("vw") || s.eq_ignore_ascii_case("vh") { return None; } - s.trim_end_matches(|c: char| c.is_alphabetic()) - .parse::() - .ok() + let val = s.trim_end_matches(|c: char| c.is_alphabetic()) + .parse::().ok()?; + Some(SizeValue::Px(val)) +} + +pub fn resolve_size(v: Option, relative_to: Option) -> Option { + v.map(|sv| sv.resolve(relative_to)) } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index ad781ac..79ae87f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ pub enum Message { ToggleChanged(Option, bool), SliderChanged(Option, f64), WindowScrolled(f32), + ScrollableScrolled(u64, f32), } #[derive(Parser)] diff --git a/src/renderer.rs b/src/renderer.rs index 9b2eca7..af60c1b 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,6 +1,6 @@ use crate::Message; use crate::interpreter::Element; -use crate::interpreter::style::{ComputedStyle, ContentAlign, Display, LayoutDirection, Position, Overflow, StructuralContext, StyleSheet, TextAlign}; +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, @@ -116,8 +116,8 @@ fn make_hoverable<'a>( 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(), + 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() @@ -134,6 +134,8 @@ fn render_children<'a>( fixed_layers: &mut Vec>, abs_layers: &mut Vec>, sticky_layers: &mut Vec>, + scroll_positions: &HashMap, + container_id: u64, stylesheet: &StyleSheet, ) -> Vec> { children @@ -147,6 +149,8 @@ fn render_children<'a>( fixed_layers, abs_layers, sticky_layers, + scroll_positions, + container_id, stylesheet, ) }) @@ -154,13 +158,13 @@ fn render_children<'a>( } 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 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 = cs.border_width.unwrap_or(0.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 = left + right + bwidth * 2.0; @@ -189,12 +193,12 @@ fn get_padding(cs: &ComputedStyle, default_pad: f32) -> iced::Padding { } fn get_margin(cs: &ComputedStyle) -> iced::Padding { - let base = cs.margin.unwrap_or(0.0); + let base = resolve_size(cs.margin, None).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), + 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), } } @@ -223,8 +227,8 @@ fn get_button_style(cs: &ComputedStyle, status: button::Status) -> 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(), + 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() @@ -241,8 +245,8 @@ fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style { 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(), + 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), @@ -251,16 +255,39 @@ fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style { } } +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 = 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 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(); @@ -300,10 +327,11 @@ fn apply_universal_box_model<'a>( cs: &ComputedStyle, is_window: bool, default_padding: f32, + scrollable_id: Option, ) -> 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 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 { @@ -349,6 +377,10 @@ fn apply_universal_box_model<'a>( s = s.height(Length::Fill); } + if let Some(sid) = scrollable_id { + s = s.on_scroll(move |viewport| Message::ScrollableScrolled(sid, viewport.absolute_offset().y)); + } + s.into() } else { padded_content.into() @@ -386,10 +418,10 @@ fn apply_universal_box_model<'a>( if let Some(h) = cs.height { inner = inner.height(h); } - if let Some(max_w) = cs.max_width { + if let Some(max_w) = resolve_size(cs.max_width, None) { inner = inner.max_width(max_w); } - if let Some(max_h) = cs.max_height { + if let Some(max_h) = resolve_size(cs.max_height, None) { inner = inner.max_height(max_h); } @@ -439,6 +471,8 @@ pub fn render_element<'a>( 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; @@ -472,13 +506,13 @@ pub fn render_element<'a>( } let current_color = cs.color.or(parent_color); - let current_font_size = cs.font_size.or(parent_font_size); + let current_font_size = resolve_size(cs.font_size, parent_font_size).or(parent_font_size); if el.type_name == "Window" { - let spacing = cs.spacing.unwrap_or(12.0); + let spacing = resolve_size(cs.spacing, None).unwrap_or(12.0); let mut col = column![].spacing(spacing).width(Length::Fill); - if let Some(h) = cs.height { col = col.height(h); } + 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(); @@ -491,20 +525,14 @@ pub fn render_element<'a>( &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); - - 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 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 { @@ -532,6 +560,8 @@ pub fn render_element<'a>( fixed_layers, &mut panel_abs, &mut panel_sticky, + scroll_positions, + container_id, stylesheet, ); if panel_abs.is_empty() { @@ -554,6 +584,8 @@ pub fn render_element<'a>( fixed_layers, &mut btn_abs, &mut btn_sticky, + scroll_positions, + container_id, stylesheet, ); if btn_abs.is_empty() && btn_sticky.is_empty() { @@ -592,7 +624,7 @@ let element_opt: Option> = widget = widget.align_x(iced::alignment::Horizontal::from(ta)); widget = widget.width(Length::Fill); } - if let Some(lh) = cs.line_height { + 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); @@ -617,7 +649,7 @@ let element_opt: Option> = widget = widget.align_x(iced::alignment::Horizontal::from(ta)); widget = widget.width(Length::Fill); } - if let Some(lh) = cs.line_height { + 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); @@ -628,9 +660,9 @@ let element_opt: Option> = "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 radius = resolve_size(cs.border_radius, None).unwrap_or(0.0); let padding = get_padding(&cs, 0.0); - let bwidth = cs.border_width.unwrap_or(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 { @@ -713,7 +745,7 @@ let element_opt: Option> = if el.children.is_empty() { None } else { - let spacing = cs.spacing.unwrap_or(10.0); + 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(); @@ -725,6 +757,8 @@ let element_opt: Option> = fixed_layers, &mut default_abs, &mut default_sticky, + scroll_positions, + container_id, stylesheet, ) { col = col.push(child); @@ -740,24 +774,23 @@ let element_opt: Option> = } 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()); + 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 { - final_widget_opt = Some(apply_universal_box_model(element, &cs, false, 0.0)); + 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)); } } @@ -778,9 +811,21 @@ let element_opt: Option> = abs_layers.push(wrapped); None } else if is_sticky { - let wrapped = wrap_fixed_position(final_widget, &cs); - sticky_layers.push(wrapped); - None + 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) } @@ -797,9 +842,11 @@ fn render_panel<'a>( 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 = cs.spacing.unwrap_or(10.0); + let spacing = resolve_size(cs.spacing, None).unwrap_or(10.0); let is_horizontal = el.children.iter().all(|c| { matches!( @@ -814,6 +861,10 @@ let is_horizontal = el.children.iter().all(|c| { 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); @@ -830,6 +881,8 @@ let is_horizontal = el.children.iter().all(|c| { fixed_layers, abs_layers, sticky_layers, + scroll_positions, + child_container, stylesheet, ) { r = r.push(child); @@ -858,6 +911,8 @@ let is_horizontal = el.children.iter().all(|c| { fixed_layers, abs_layers, sticky_layers, + scroll_positions, + child_container, stylesheet, ) { c = c.push(child); @@ -897,6 +952,8 @@ let is_horizontal = el.children.iter().all(|c| { fixed_layers, &mut chunk_abs, &mut chunk_sticky, + scroll_positions, + child_container, stylesheet, ) { r = r.push(child); @@ -918,18 +975,14 @@ let is_horizontal = el.children.iter().all(|c| { } }; - let boxed = apply_universal_box_model(content, &cs, false, 5.0); + let boxed = apply_universal_box_model(content, &cs, false, 5.0, sc_id); - 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() + 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>( @@ -940,6 +993,8 @@ fn render_button<'a>( 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); @@ -961,6 +1016,8 @@ fn render_button<'a>( fixed_layers, abs_layers, sticky_layers, + scroll_positions, + container_id, stylesheet, ) { r = r.push(child); @@ -979,20 +1036,15 @@ fn render_button<'a>( } }; + let base_px = resolve_size(cs.padding, None).unwrap_or(8.0); 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), + 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 = cs.border_width.unwrap_or(0.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 {