git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitf48dd01aec6fe45b0744bd13f58df183b6459b2e
parentf41b2dda3d
authorLucas Galante <[email protected]>
date2026-06-26 12:58
Fix backplate drag interfering with pointer events on scrolled widgets and apply UI updates

 src/context.rs                           | 174 +++++++++++++++--
 src/layout.rs                            | 317 ++++++++++++++++++++++++++++++-
 src/main.rs                              |  71 +++++--
 src/widget/container/container.rs        |   2 +-
 src/widget/container/container_layout.rs | 161 ++++++++++++++--
 src/widget/container/paginator.rs        |  12 ++
 src/widget/container/spreadsheet.rs      |   4 +
 src/widget/container/switcher.rs         |  11 ++
 src/widget/input/button_strip.rs         |   4 +
 src/widget/input/color_selector.rs       |   4 +
 src/widget/json_layout.rs                |   4 +
 src/widget/mod.rs                        |  31 ++-
 12 files changed, 737 insertions(+), 58 deletions(-)

diff --git a/src/context.rs b/src/context.rs
index 7a1a5f8..df52778 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -3,6 +3,52 @@ use crate::widget::{Element, WidgetId, LayoutTree, Key, MouseButton, ElementStat
 use crate::widget::core::hover_animation::HoverState;
 use crate::widget::core::context_menu::ContextMenuState;
 
+pub struct SpatialGrid {
+    pub cell_size: f32,
+    pub cells: HashMap<(i32, i32), Vec<WidgetId>>,
+}
+
+impl SpatialGrid {
+    pub fn new(cell_size: f32) -> Self {
+        Self {
+            cell_size,
+            cells: HashMap::new(),
+        }
+    }
+
+    pub fn clear(&mut self) {
+        self.cells.clear();
+    }
+
+    pub fn insert(&mut self, id: WidgetId, rect: (f32, f32, f32, f32)) {
+        let (x, y, w, h) = rect;
+        if w <= 0.0 || h <= 0.0 {
+            return;
+        }
+        let start_x = (x / self.cell_size).floor() as i32;
+        let end_x = ((x + w) / self.cell_size).floor() as i32;
+        let start_y = (y / self.cell_size).floor() as i32;
+        let end_y = ((y + h) / self.cell_size).floor() as i32;
+
+        let start_x = start_x.max(-1000);
+        let end_x = end_x.min(1000);
+        let start_y = start_y.max(-1000);
+        let end_y = end_y.min(1000);
+
+        for cx in start_x..=end_x {
+            for cy in start_y..=end_y {
+                self.cells.entry((cx, cy)).or_default().push(id);
+            }
+        }
+    }
+
+    pub fn query(&self, px: f32, py: f32) -> &[WidgetId] {
+        let cx = (px / self.cell_size).floor() as i32;
+        let cy = (py / self.cell_size).floor() as i32;
+        self.cells.get(&(cx, cy)).map(|v| v.as_slice()).unwrap_or(&[])
+    }
+}
+
 pub struct UiContext {
     pub layout_tree: LayoutTree,
     pub widget_registry: HashMap<WidgetId, *mut (dyn Element + 'static)>,
@@ -15,6 +61,9 @@ pub struct UiContext {
     pub drag_start_pos: Option<(f32, f32)>,
     pub drag_target: Option<WidgetId>,
     pub is_dragging: bool,
+    pub any_dirty: bool,
+    pub tick_receivers: Vec<WidgetId>,
+    pub spatial_grid: SpatialGrid,
 }
 
 impl UiContext {
@@ -34,6 +83,9 @@ impl UiContext {
             drag_start_pos: None,
             drag_target: None,
             is_dragging: false,
+            any_dirty: false,
+            tick_receivers: Vec::new(),
+            spatial_grid: SpatialGrid::new(100.0),
         }
     }
 
@@ -49,6 +101,9 @@ impl UiContext {
         if root.is_null() {
             return false;
         }
+        if let Event::Tick(_) = event {
+            return false;
+        }
         unsafe {
             // Track drag gestures based on mouse events
             match event {
@@ -218,28 +273,89 @@ impl UiContext {
     }
 
     pub fn is_dirty(&self) -> bool {
+        self.any_dirty
+    }
+
+    pub fn clear_dirty(&mut self) {
+        self.any_dirty = false;
         for &ptr in self.widget_registry.values() {
             unsafe {
-                if let Some(b) = (*ptr).base() {
-                    if b.dirty {
-                        return true;
-                    }
+                if let Some(b) = (*ptr).base_mut() {
+                    b.dirty = false;
                 }
             }
         }
-        false
+        self.rebuild_spatial_grid();
     }
 
-    pub fn clear_dirty(&mut self) {
-        for &ptr in self.widget_registry.values() {
+    pub fn rebuild_spatial_grid(&mut self) {
+        self.spatial_grid.clear();
+        for (&id, &ptr) in &self.widget_registry {
             unsafe {
-                if let Some(b) = (*ptr).base_mut() {
-                    b.dirty = false;
+                if !ptr.is_null() {
+                    let rect = (*ptr).rect();
+                    self.spatial_grid.insert(id, rect);
                 }
             }
         }
     }
 
+    pub fn register_tick_receiver(&mut self, id: WidgetId) {
+        if !self.tick_receivers.contains(&id) {
+            self.tick_receivers.push(id);
+        }
+    }
+
+    pub fn unregister_tick_receiver(&mut self, id: WidgetId) {
+        self.tick_receivers.retain(|&x| x != id);
+    }
+
+    pub fn is_widget_visible(&self, id: WidgetId) -> bool {
+        let mut curr = id;
+        loop {
+            if let Some(w_ptr) = self.widget_registry.get(&curr) {
+                unsafe {
+                    if !(*(*w_ptr)).visible() {
+                        return false;
+                    }
+                }
+            } else {
+                return false;
+            }
+            if let Some(&parent_id) = self.layout_tree.parents.get(&curr) {
+                if let Some(parent_ptr) = self.widget_registry.get(&parent_id) {
+                    unsafe {
+                        if !(*(*parent_ptr)).is_child_visible(curr) {
+                            return false;
+                        }
+                    }
+                }
+                curr = parent_id;
+            } else {
+                break;
+            }
+        }
+        true
+    }
+
+    pub fn tick(&mut self, dt: f32) -> bool {
+        let mut changed = false;
+        let ids = self.tick_receivers.clone();
+        for id in ids {
+            if self.is_widget_visible(id) {
+                if let Some(ptr) = self.widget_registry.get(&id).copied() {
+                    unsafe {
+                        if (*ptr).tick(dt, self) {
+                            (*ptr).mark_dirty(self);
+                            changed = true;
+                        }
+                    }
+                }
+            }
+        }
+        changed
+    }
+
     // --- Focus management ---
     pub fn set_focused(&mut self, w: &mut dyn Element) {
         let new_ptr = unsafe {
@@ -375,6 +491,11 @@ impl UiContext {
     // --- Registry ---
     pub fn register_widget(&mut self, id: WidgetId, ptr: *mut (dyn Element + 'static)) {
         self.widget_registry.insert(id, ptr);
+        unsafe {
+            if !ptr.is_null() && (*ptr).wants_tick() {
+                self.register_tick_receiver(id);
+            }
+        }
     }
 
     pub fn link_ids(&mut self, parent: WidgetId, child: WidgetId) {
@@ -404,6 +525,7 @@ impl UiContext {
         self.layout_tree.parents.clear();
         self.layout_tree.children.clear();
         self.widget_registry.clear();
+        self.tick_receivers.clear();
     }
 
     // --- Popovers ---
@@ -630,17 +752,31 @@ impl UiContext {
 
     pub fn is_movable_backplate_at(&self, px: f32, py: f32) -> bool {
         let mut hit_backplate = false;
-        for &ptr in self.widget_registry.values() {
-            unsafe {
-                if !ptr.is_null() {
-                    let w = &*ptr;
-                    if w.hit_test(px, py, self) {
-                        if w.is_backplate() {
-                            if w.is_movable_backplate() {
-                                hit_backplate = true;
+        let scroll_y = self.get_scroll_offset();
+        let mut candidate_ids = self.spatial_grid.query(px, py).to_vec();
+        if scroll_y != 0.0 {
+            candidate_ids.extend_from_slice(self.spatial_grid.query(px, py + scroll_y));
+            candidate_ids.sort_unstable();
+            candidate_ids.dedup();
+        }
+        for &id in &candidate_ids {
+            if let Some(&ptr) = self.widget_registry.get(&id) {
+                unsafe {
+                    if !ptr.is_null() {
+                        let w = &*ptr;
+                        let is_hit = if w.is_backplate() {
+                            w.hit_test(px, py, self)
+                        } else {
+                            w.hit_test(px, py, self) || (scroll_y != 0.0 && w.hit_test(px, py + scroll_y, self))
+                        };
+                        if is_hit {
+                            if w.is_backplate() {
+                                if w.is_movable_backplate() {
+                                    hit_backplate = true;
+                                }
+                            } else if w.blocks_backplate_drag() {
+                                return false;
                             }
-                        } else if w.blocks_backplate_drag() {
-                            return false;
                         }
                     }
                 }
diff --git a/src/layout.rs b/src/layout.rs
index aab4b41..f31f25d 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -2358,6 +2358,7 @@ impl SplitterLayout {
     }
 }
 
+#[derive(Debug, Clone)]
 pub struct Grid {
     pub left: f32,
     pub top: f32,
@@ -2458,6 +2459,7 @@ impl CircularPaneLayout {
     }
 }
 
+#[derive(Debug, Clone)]
 pub struct Radial {
     pub center_x: f32,
     pub center_y: f32,
@@ -2529,12 +2531,45 @@ impl Radial {
     }
 }
 
-pub trait LayoutStrategy {
+use std::cell::RefCell;
+use std::collections::HashMap;
+
+thread_local! {
+    pub static GRID_STATES: RefCell<HashMap<usize, Grid>> = RefCell::new(HashMap::new());
+    pub static OVERLAY_STATES: RefCell<HashMap<usize, (f32, f32, f32, f32)>> = RefCell::new(HashMap::new());
+    pub static VERTICAL_STATES: RefCell<HashMap<usize, (f32, f32)>> = RefCell::new(HashMap::new());
+}
+
+pub fn save_grid_state(ptr: usize, grid: Grid) {
+    GRID_STATES.with(|m| m.borrow_mut().insert(ptr, grid));
+}
+
+pub fn mutate_grid_state<F, R>(ptr: usize, mut f: F) -> Option<R>
+where
+    F: FnMut(&mut Grid) -> R,
+{
+    GRID_STATES.with(|m| {
+        let mut map = m.borrow_mut();
+        map.get_mut(&ptr).map(|grid| f(grid))
+    })
+}
+
+pub trait LayoutStrategy: std::fmt::Debug {
     fn init(&mut self, left: f32, top: f32, width: f32, height: f32);
     fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32);
     fn set_section_count(&mut self, _count: usize) {}
     fn get_column_width(&self) -> Option<f32> { None }
     fn get_gap(&self) -> f32 { 20.0 }
+
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &mut crate::context::UiContext) -> f32;
+    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size;
+    fn box_clone(&self) -> Box<dyn LayoutStrategy>;
+}
+
+impl Clone for Box<dyn LayoutStrategy> {
+    fn clone(&self) -> Self {
+        self.box_clone()
+    }
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -2543,6 +2578,7 @@ pub enum FlexDirection {
     Column,
 }
 
+#[derive(Debug, Clone)]
 pub struct FlexLayout {
     left: f32,
     top: f32,
@@ -2599,8 +2635,86 @@ impl LayoutStrategy for FlexLayout {
     fn get_gap(&self) -> f32 {
         self.spacing
     }
+
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
+        let mut cur_x = x;
+        let mut cur_y = y;
+        match self.direction {
+            FlexDirection::Row => {
+                for &child_ptr in children {
+                    unsafe {
+                        let child = &mut *child_ptr;
+                        let child_w = child.rect().2;
+                        let child_h = child.preferred_height().unwrap_or(child.rect().3);
+                        let use_h = if child_h > 0.0 { child_h } else { h };
+                        child.set_rect(cur_x, cur_y, child_w, use_h);
+                        cur_x += child_w + self.spacing;
+                    }
+                }
+                (cur_x - x).max(0.0)
+            }
+            FlexDirection::Column => {
+                for &child_ptr in children {
+                    unsafe {
+                        let child = &mut *child_ptr;
+                        let child_h = child.preferred_height().unwrap_or(child.rect().3);
+                        let use_h = if child_h > 0.0 { child_h } else { 44.0 };
+                        child.set_rect(x, cur_y, w, use_h);
+                        cur_y += use_h + self.spacing;
+                    }
+                }
+                (cur_y - y).max(0.0)
+            }
+        }
+    }
+
+    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
+        match self.direction {
+            FlexDirection::Row => {
+                let mut total_w = 0.0f32;
+                let mut max_h = 0.0f32;
+                for (i, &child_ptr) in children.iter().enumerate() {
+                    unsafe {
+                        let size = (*child_ptr).measure(constraints, ctx);
+                        total_w += size.width;
+                        max_h = max_h.max(size.height);
+                        if i > 0 {
+                            total_w += self.spacing;
+                        }
+                    }
+                }
+                crate::widget::Size {
+                    width: total_w.clamp(constraints.min_width, constraints.max_width),
+                    height: max_h.clamp(constraints.min_height, constraints.max_height),
+                }
+            }
+            FlexDirection::Column => {
+                let mut total_h = 0.0f32;
+                let mut max_w = 0.0f32;
+                for (i, &child_ptr) in children.iter().enumerate() {
+                    unsafe {
+                        let size = (*child_ptr).measure(constraints, ctx);
+                        total_h += size.height;
+                        max_w = max_w.max(size.width);
+                        if i > 0 {
+                            total_h += self.spacing;
+                        }
+                    }
+                }
+                crate::widget::Size {
+                    width: max_w.clamp(constraints.min_width, constraints.max_width),
+                    height: total_h.clamp(constraints.min_height, constraints.max_height),
+                }
+            }
+        }
+    }
+
+    fn box_clone(&self) -> Box<dyn LayoutStrategy> {
+        Box::new(self.clone())
+    }
 }
 
+#[derive(Debug, Clone)]
 pub struct ColumnLayout {
     left: f32,
     top: f32,
@@ -2643,8 +2757,46 @@ impl LayoutStrategy for ColumnLayout {
     fn get_gap(&self) -> f32 {
         self.gap
     }
+
+    fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
+        let mut cur_y = y;
+        for &child_ptr in children {
+            unsafe {
+                let child = &mut *child_ptr;
+                let child_h = child.preferred_height().unwrap_or(child.rect().3);
+                let use_h = if child_h > 0.0 { child_h } else { 44.0 };
+                child.set_rect(x, cur_y, w, use_h);
+                cur_y += use_h + self.gap;
+            }
+        }
+        (cur_y - y).max(0.0)
+    }
+
+    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
+        let mut total_h = 0.0f32;
+        let mut max_w = 0.0f32;
+        for (i, &child_ptr) in children.iter().enumerate() {
+            unsafe {
+                let size = (*child_ptr).measure(constraints, ctx);
+                total_h += size.height;
+                max_w = max_w.max(size.width);
+                if i > 0 {
+                    total_h += self.gap;
+                }
+            }
+        }
+        crate::widget::Size {
+            width: max_w.clamp(constraints.min_width, constraints.max_width),
+            height: total_h.clamp(constraints.min_height, constraints.max_height),
+        }
+    }
+
+    fn box_clone(&self) -> Box<dyn LayoutStrategy> {
+        Box::new(self.clone())
+    }
 }
 
+#[derive(Debug, Clone)]
 pub struct AdaptiveGrid {
     grid: Option<Grid>,
     #[allow(dead_code)]
@@ -2737,8 +2889,94 @@ impl LayoutStrategy for AdaptiveGrid {
     fn get_gap(&self) -> f32 {
         self.gap
     }
+
+    fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
+        let usable_w = w.max(1.0);
+        let min_col_width = crate::layout::grid_min_col_width();
+        let cols = (((usable_w + self.gap) / (min_col_width + self.gap)).floor().max(1.0)) as usize;
+        let count = if let Some(n) = self.num_sections {
+            n.min(cols).max(1)
+        } else {
+            cols
+        };
+
+        let total_gap = self.gap * (count - 1) as f32;
+        let available_w = (w - total_gap).max(1.0);
+        let col_w = available_w / count as f32;
+        
+        let mut col_heights = vec![y; count];
+
+        for &child_ptr in children {
+            unsafe {
+                let child = &mut *child_ptr;
+                let ch = child.preferred_height().unwrap_or(child.rect().3);
+                let use_h = if ch > 0.0 { ch } else { 44.0 };
+                
+                let mut min_col = 0;
+                let mut min_h = col_heights[0];
+                for i in 1..count {
+                    if col_heights[i] < min_h {
+                        min_h = col_heights[i];
+                        min_col = i;
+                    }
+                }
+                
+                let cx = x + min_col as f32 * (col_w + self.gap);
+                let cy = col_heights[min_col];
+                child.set_rect(cx, cy, col_w, use_h);
+                col_heights[min_col] += use_h + self.gap;
+            }
+        }
+        
+        let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
+        (max_h - y).max(0.0)
+    }
+
+    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
+        let usable_w = constraints.max_width.max(1.0);
+        let min_col_width = crate::layout::grid_min_col_width();
+        let cols = (((usable_w + self.gap) / (min_col_width + self.gap)).floor().max(1.0)) as usize;
+        let count = if let Some(n) = self.num_sections {
+            n.min(cols).max(1)
+        } else {
+            cols
+        };
+
+        let mut col_heights = vec![0.0f32; count];
+        let total_gap = self.gap * (count - 1) as f32;
+        let available_w = (constraints.max_width - total_gap).max(1.0);
+        let col_w = available_w / count as f32;
+        
+        let child_constraints = crate::widget::LayoutConstraints::new(col_w, col_w, constraints.min_height, constraints.max_height);
+
+        for &child_ptr in children {
+            unsafe {
+                let size = (*child_ptr).measure(child_constraints, ctx);
+                let mut min_col = 0;
+                let mut min_h = col_heights[0];
+                for i in 1..count {
+                    if col_heights[i] < min_h {
+                        min_h = col_heights[i];
+                        min_col = i;
+                    }
+                }
+                col_heights[min_col] += size.height + self.gap;
+            }
+        }
+        
+        let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
+        crate::widget::Size {
+            width: constraints.max_width,
+            height: max_h.clamp(constraints.min_height, constraints.max_height),
+        }
+    }
+
+    fn box_clone(&self) -> Box<dyn LayoutStrategy> {
+        Box::new(self.clone())
+    }
 }
 
+#[derive(Debug, Clone)]
 pub struct RadialLayout {
     radial: Option<Radial>,
     aspect_ratio: f32,
@@ -2780,6 +3018,59 @@ impl LayoutStrategy for RadialLayout {
             (0.0, 0.0, ww, wh)
         }
     }
+
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
+        let cx = x + w / 2.0;
+        let cy = y + h / 2.0;
+        let aspect = if self.aspect_ratio > 0.0 {
+            self.aspect_ratio
+        } else {
+            let screen_aspect = (w / h.max(1.0)).max(0.1);
+            1.0 + (screen_aspect - 1.0) * 0.4
+        };
+        let radial = Radial::new(cx, cy, aspect, self.base_spacing);
+        for (idx, &child_ptr) in children.iter().enumerate() {
+            unsafe {
+                let child = &mut *child_ptr;
+                let cw = child.rect().2;
+                let ch = child.preferred_height().unwrap_or(child.rect().3);
+                let use_h = if ch > 0.0 { ch } else { 44.0 };
+                let (rx, ry, rw, rh) = radial.widget_rect(idx, cw, use_h);
+                child.set_rect(rx, ry, rw, rh);
+            }
+        }
+        h
+    }
+
+    fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
+        let cx = constraints.max_width / 2.0;
+        let cy = constraints.max_height / 2.0;
+        let aspect = if self.aspect_ratio > 0.0 {
+            self.aspect_ratio
+        } else {
+            let screen_aspect = (constraints.max_width / constraints.max_height.max(1.0)).max(0.1);
+            1.0 + (screen_aspect - 1.0) * 0.4
+        };
+        let radial = Radial::new(cx, cy, aspect, self.base_spacing);
+        let mut max_w = 0.0f32;
+        let mut max_h = 0.0f32;
+        for (idx, &child_ptr) in children.iter().enumerate() {
+            unsafe {
+                let size = (*child_ptr).measure(constraints, ctx);
+                let (rx, ry, rw, rh) = radial.widget_rect(idx, size.width, size.height);
+                max_w = max_w.max(rx + rw);
+                max_h = max_h.max(ry + rh);
+            }
+        }
+        crate::widget::Size {
+            width: max_w.clamp(constraints.min_width, constraints.max_width),
+            height: max_h.clamp(constraints.min_height, constraints.max_height),
+        }
+    }
+
+    fn box_clone(&self) -> Box<dyn LayoutStrategy> {
+        Box::new(self.clone())
+    }
 }
 
 pub struct PageLayoutBuilder<'a, P> {
@@ -3230,6 +3521,30 @@ pub fn get_system_monospace_font() -> &'static str {
     })
 }
 
+impl crate::widget::ContainerLayout for FlexLayout {
+    fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
+        Box::new(self.clone())
+    }
+}
+
+impl crate::widget::ContainerLayout for ColumnLayout {
+    fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
+        Box::new(self.clone())
+    }
+}
+
+impl crate::widget::ContainerLayout for AdaptiveGrid {
+    fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
+        Box::new(self.clone())
+    }
+}
+
+impl crate::widget::ContainerLayout for RadialLayout {
+    fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
+        Box::new(self.clone())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/main.rs b/src/main.rs
index 1783f56..032a285 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -322,19 +322,55 @@ fn extra_quad_vertices(
     rounded_rect_vertices_corners(qx, qy, qw, qh, r, sw, sh, qc, extra_corners)
 }
 
+#[derive(Hash, PartialEq, Eq, Clone)]
+struct BufferCacheKey {
+    text: String,
+    size_milli: u32,
+    font: Option<String>,
+}
+
+std::thread_local! {
+    static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<BufferCacheKey, Buffer>> = std::cell::RefCell::new(std::collections::HashMap::new());
+}
+
 fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
-    let metrics = Metrics::new(size, size * 1.4);
-    let mut buffer = Buffer::new(font_system, metrics);
-    buffer.set_text(font_system, text, Attrs::new(), glyphon::Shaping::Advanced);
-    buffer.shape_until_scroll(font_system, true);
-    buffer
+    make_text_buffer_with_font(font_system, text, size, None)
 }
 
 fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
-    let metrics = Metrics::new(size, size * 1.4);
+    let scale = cce_ui::scale::scale_factor();
+    let mut font_size = size;
+    let mut family_name = None;
+
+    if let Some(font_str) = font {
+        let (parsed_family, parsed_size) = cce_ui::layout::parse_font_string(font_str);
+        if let Some(ps) = parsed_size {
+            font_size = ps;
+        }
+        family_name = Some(parsed_family);
+    }
+
+    let physical_size = font_size * scale;
+    let size_key = (physical_size * 1000.0).round() as u32;
+
+    let key = BufferCacheKey {
+        text: text.to_string(),
+        size_milli: size_key,
+        font: family_name.clone(),
+    };
+
+    let cached = BUFFER_CACHE.with(|cache| {
+        cache.borrow().get(&key).cloned()
+    });
+
+    if let Some(buf) = cached {
+        return buf;
+    }
+
+    let metrics = Metrics::new(physical_size, physical_size * 1.4);
     let mut buffer = Buffer::new(font_system, metrics);
     let mut attrs = Attrs::new();
-    if let Some(font_name) = font {
+    if let Some(font_name) = family_name.as_deref() {
         let family = match font_name {
             "monospace" => glyphon::Family::Name(cce_ui::layout::get_system_monospace_font()),
             "sans-serif" => glyphon::Family::SansSerif,
@@ -345,9 +381,15 @@ fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f3
     }
     buffer.set_text(font_system, text, attrs, glyphon::Shaping::Advanced);
     buffer.shape_until_scroll(font_system, true);
+
+    BUFFER_CACHE.with(|cache| {
+        cache.borrow_mut().insert(key, buffer.clone());
+    });
+
     buffer
 }
 
+
 struct State {
     surface: wgpu::Surface<'static>,
     device: wgpu::Device,
@@ -1747,20 +1789,7 @@ fn main() {
         last_tick = now;
 
         if let Some(ref mut st) = app.state {
-            let mut tick_changed = false;
-            if st.layout_mode {
-                if let Some(ref mut jl) = &mut st.json_layout {
-                    if jl.tick(dt, &mut st.ui_context) {
-                        tick_changed = true;
-                    }
-                }
-            } else {
-                for w in &mut st.widgets {
-                    if w.tick(dt, &mut st.ui_context) {
-                        tick_changed = true;
-                    }
-                }
-            }
+            let tick_changed = st.ui_context.tick(dt);
             if tick_changed {
                 st.upload_vertices();
                 app.redraw = true;
diff --git a/src/widget/container/container.rs b/src/widget/container/container.rs
index 0d6b07e..05fc52f 100644
--- a/src/widget/container/container.rs
+++ b/src/widget/container/container.rs
@@ -15,7 +15,7 @@ impl Container {
             parent: None,
             children: Vec::new(),
             base: Widget::new(),
-            layout: Box::new(OverlayLayout),
+            layout: Box::new(OverlayLayout::default()),
         }
     }
 
diff --git a/src/widget/container/container_layout.rs b/src/widget/container/container_layout.rs
index d74b7e6..88c0df0 100644
--- a/src/widget/container/container_layout.rs
+++ b/src/widget/container/container_layout.rs
@@ -1,22 +1,36 @@
 use crate::widget::*;
 use crate::context::UiContext;
 
-pub trait ContainerLayout: std::fmt::Debug {
-    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32;
-    fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size;
-    fn box_clone(&self) -> Box<dyn ContainerLayout>;
+pub trait ContainerLayout: crate::layout::LayoutStrategy {
+    fn box_clone_container(&self) -> Box<dyn ContainerLayout>;
 }
 
 impl Clone for Box<dyn ContainerLayout> {
     fn clone(&self) -> Self {
-        self.box_clone()
+        self.box_clone_container()
     }
 }
 
-#[derive(Debug, Clone, Copy)]
-pub struct OverlayLayout;
+#[derive(Debug, Clone, Copy, Default)]
+pub struct OverlayLayout {
+    left: f32,
+    top: f32,
+    width: f32,
+    height: f32,
+}
+
+impl crate::layout::LayoutStrategy for OverlayLayout {
+    fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
+        self.left = left;
+        self.top = top;
+        self.width = width;
+        self.height = height;
+    }
+
+    fn allocate(&mut self, _ww: f32, _wh: f32) -> (f32, f32, f32, f32) {
+        (self.left, self.top, self.width, self.height)
+    }
 
-impl ContainerLayout for OverlayLayout {
     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
         for &child_ptr in children {
             unsafe {
@@ -42,7 +56,13 @@ impl ContainerLayout for OverlayLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn ContainerLayout> {
+    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+        Box::new(*self)
+    }
+}
+
+impl ContainerLayout for OverlayLayout {
+    fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
 }
@@ -64,7 +84,26 @@ impl Default for VerticalLayout {
     }
 }
 
-impl ContainerLayout for VerticalLayout {
+impl crate::layout::LayoutStrategy for VerticalLayout {
+    fn init(&mut self, left: f32, top: f32, _width: f32, _height: f32) {
+        let ptr = self as *const Self as usize;
+        crate::layout::VERTICAL_STATES.with(|m| m.borrow_mut().insert(ptr, (left, top + self.padding_y)));
+    }
+
+    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+        let ptr = self as *const Self as usize;
+        let (left, mut current_y) = crate::layout::VERTICAL_STATES.with(|m| m.borrow().get(&ptr).copied().unwrap_or((0.0, 0.0)));
+        let x = left + self.padding_x;
+        let y = current_y;
+        current_y += wh + self.spacing;
+        crate::layout::VERTICAL_STATES.with(|m| m.borrow_mut().insert(ptr, (left, current_y)));
+        (x, y, ww, wh)
+    }
+
+    fn get_gap(&self) -> f32 {
+        self.spacing
+    }
+
     fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
         let left_x = x + self.padding_x;
         let available_w = (w - 2.0 * self.padding_x).max(1.0);
@@ -104,7 +143,13 @@ impl ContainerLayout for VerticalLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn ContainerLayout> {
+    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+        Box::new(*self)
+    }
+}
+
+impl ContainerLayout for VerticalLayout {
+    fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
 }
@@ -117,7 +162,44 @@ pub struct GridLayout {
     pub padding_y: f32,
 }
 
-impl ContainerLayout for GridLayout {
+impl crate::layout::LayoutStrategy for GridLayout {
+    fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+        let count = self.columns.max(1);
+        let usable_w = (width - 2.0 * self.padding_x).max(1.0);
+        let grid = crate::layout::Grid::new(
+            left + self.padding_x,
+            top + self.padding_y,
+            usable_w,
+            usable_w / count as f32,
+            self.gap,
+            count,
+        );
+        let ptr = self as *const Self as usize;
+        crate::layout::save_grid_state(ptr, grid);
+    }
+
+    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+        let ptr = self as *const Self as usize;
+        crate::layout::mutate_grid_state(ptr, |grid| {
+            let col = grid.next_column();
+            let x = grid.col_lefts[col];
+            let y = grid.col_heights[col];
+            grid.col_heights[col] += wh + grid.gap;
+            (x, y, grid.col_width, wh)
+        }).unwrap_or((0.0, 0.0, ww, wh))
+    }
+
+    fn get_column_width(&self) -> Option<f32> {
+        let ptr = self as *const Self as usize;
+        crate::layout::GRID_STATES.with(|m| {
+            m.borrow().get(&ptr).map(|g| g.col_width)
+        })
+    }
+
+    fn get_gap(&self) -> f32 {
+        self.gap
+    }
+
     fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
         let count = children.len();
         if count == 0 {
@@ -187,7 +269,13 @@ impl ContainerLayout for GridLayout {
         }
     }
 
-    fn box_clone(&self) -> Box<dyn ContainerLayout> {
+    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+        Box::new(*self)
+    }
+}
+
+impl ContainerLayout for GridLayout {
+    fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
 }
@@ -200,7 +288,44 @@ pub struct AdaptiveGridLayout {
     pub padding_y: f32,
 }
 
-impl ContainerLayout for AdaptiveGridLayout {
+impl crate::layout::LayoutStrategy for AdaptiveGridLayout {
+    fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+        let usable_w = (width - 2.0 * self.padding_x).max(1.0);
+        let cols = (((usable_w + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0)) as usize;
+        let grid = crate::layout::Grid::new(
+            left + self.padding_x,
+            top + self.padding_y,
+            usable_w,
+            self.min_col_width,
+            self.gap,
+            cols,
+        );
+        let ptr = self as *const Self as usize;
+        crate::layout::save_grid_state(ptr, grid);
+    }
+
+    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+        let ptr = self as *const Self as usize;
+        crate::layout::mutate_grid_state(ptr, |grid| {
+            let col = grid.next_column();
+            let x = grid.col_lefts[col];
+            let y = grid.col_heights[col];
+            grid.col_heights[col] += wh + grid.gap;
+            (x, y, grid.col_width, wh)
+        }).unwrap_or((0.0, 0.0, ww, wh))
+    }
+
+    fn get_column_width(&self) -> Option<f32> {
+        let ptr = self as *const Self as usize;
+        crate::layout::GRID_STATES.with(|m| {
+            m.borrow().get(&ptr).map(|g| g.col_width)
+        })
+    }
+
+    fn get_gap(&self) -> f32 {
+        self.gap
+    }
+
     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32 {
         let usable_w = (w - 2.0 * self.padding_x).max(1.0);
         let cols = (((usable_w + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0)) as usize;
@@ -225,7 +350,13 @@ impl ContainerLayout for AdaptiveGridLayout {
         grid.measure(constraints, children, ctx)
     }
 
-    fn box_clone(&self) -> Box<dyn ContainerLayout> {
+    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+        Box::new(*self)
+    }
+}
+
+impl ContainerLayout for AdaptiveGridLayout {
+    fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
         Box::new(*self)
     }
 }
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index 9b9a943..dc487c2 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -253,6 +253,18 @@ impl Element for Paginator {
         childs
     }
 
+    fn is_child_visible(&self, child_id: WidgetId) -> bool {
+        if self.sidebar_menu.base.id() == child_id {
+            return true;
+        }
+        if let Some(plate) = self.pages.get(self.selected_page) {
+            if !self.page_hidden && plate.base.base.id() == child_id {
+                return true;
+            }
+        }
+        false
+    }
+
     fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         let mut changed = false;
         if self.sidebar_menu.cursor_moved(px, py, ctx) {
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 79e0e4c..8f1b3bb 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -265,6 +265,10 @@ impl Element for Spreadsheet {
         }
     }
 
+    fn wants_tick(&self) -> bool {
+        true
+    }
+
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         if !self.visible {
             return Vec::new();
diff --git a/src/widget/container/switcher.rs b/src/widget/container/switcher.rs
index dd0f1a2..f541615 100644
--- a/src/widget/container/switcher.rs
+++ b/src/widget/container/switcher.rs
@@ -68,6 +68,17 @@ impl Element for Switcher {
         self.children.clone()
     }
 
+    fn is_child_visible(&self, child_id: WidgetId) -> bool {
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                if let Some(b) = unsafe { (*self.children[idx]).base() } {
+                    return b.id() == child_id;
+                }
+            }
+        }
+        false
+    }
+
     fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
         self.children.push(child);
         let id = self.base.id();
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index aa00738..2c87f1a 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -264,6 +264,10 @@ impl Element for ButtonStrip {
         changed
     }
 
+    fn wants_tick(&self) -> bool {
+        true
+    }
+
     fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
         None
     }
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 026aa57..07850f5 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -259,6 +259,10 @@ impl Element for ColorSelector {
         false
     }
 
+    fn wants_tick(&self) -> bool {
+        true
+    }
+
     fn focus(&mut self) {
         self.editing = true;
         self.edit_buffer = self.get_value_string().unwrap();
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index b74a31c..38fb55e 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -309,6 +309,10 @@ impl Element for JsonLayoutWidget {
         changed
     }
 
+    fn wants_tick(&self) -> bool {
+        true
+    }
+
     fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
 
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index b7d5726..d26bf73 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -69,7 +69,7 @@ use std::collections::HashMap;
 
 pub const DROPDOWN_ITEM_H: f32 = 22.0;
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub struct WidgetId(pub usize);
 
 pub static NEXT_WIDGET_ID: AtomicUsize = AtomicUsize::new(1);
@@ -80,6 +80,33 @@ pub struct LayoutTree {
     pub children: HashMap<WidgetId, Vec<WidgetId>>,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub struct WidgetPtr(pub *mut (dyn Element + 'static));
+
+impl WidgetPtr {
+    pub fn is_null(&self) -> bool {
+        self.0.is_null()
+    }
+    pub fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+        self.0
+    }
+}
+
+impl std::ops::Deref for WidgetPtr {
+    type Target = dyn Element + 'static;
+    fn deref(&self) -> &Self::Target {
+        assert!(!self.0.is_null(), "Attempted to dereference a null WidgetPtr!");
+        unsafe { &*self.0 }
+    }
+}
+
+impl std::ops::DerefMut for WidgetPtr {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        assert!(!self.0.is_null(), "Attempted to dereference a null WidgetPtr!");
+        unsafe { &mut *self.0 }
+    }
+}
+
 pub use crate::context::UiContext;
 
 #[derive(Debug, Clone, PartialEq)]
@@ -470,6 +497,8 @@ pub trait Element {
     fn set_visible(&mut self, _visible: bool) {}
     fn visible(&self) -> bool { true }
     fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool { false }
+    fn wants_tick(&self) -> bool { false }
+    fn is_child_visible(&self, _child_id: WidgetId) -> bool { true }
     fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
 
     fn as_page_selector(&self) -> Option<&dyn PageSelector> { None }