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

commit02f782a9f063849c527681a7886d03b39ba68f6e
parent09444aa2be
authorLucas Galante <[email protected]>
date2026-06-24 10:25
Support default draggable Window objects via ui_context provider

 src/backend/window_runner.rs   | 13 +++++++++++++
 src/context.rs                 | 21 +++++++++++++++++++++
 src/layout.rs                  |  4 ++++
 src/widget/container/window.rs | 15 +++++++++++++++
 src/widget/mod.rs              |  1 +
 5 files changed, 54 insertions(+)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 097422f..d877d7e 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -900,6 +900,10 @@ pub trait Application: Sized + 'static {
     fn text_items(&self) -> &[TextItem];
     fn render_popovers(&self, _pc: &mut dyn crate::layout::RenderTarget) {}
     
+    fn ui_context(&self) -> Option<&crate::context::UiContext> {
+        None
+    }
+    
     fn text_areas(&self, scale_f32: f32, bounds: TextBounds) -> Vec<TextArea<'_>> {
         let overlay_rects: Vec<(f32, f32, f32, f32)> = Vec::new();
 
@@ -1687,7 +1691,16 @@ impl<A: Application> PointerHandler for EngineState<A> {
                         }
 
                         // Titlebar drag check: y is in [8.0, 32.0], and x is not in the top-right button area
+                        let mut should_move = false;
                         if ly >= border && ly < 32.0 && lx < self.logical_width - 70.0 {
+                            should_move = true;
+                        } else if let Some(ctx) = self.inner.ui_context() {
+                            if ctx.is_movable_window_at(lx, ly) {
+                                should_move = true;
+                            }
+                        }
+
+                        if should_move {
                             if let Some(ref window) = self.window {
                                 if let Some(seat) = self.seats.first() {
                                     window.move_(seat, *serial);
diff --git a/src/context.rs b/src/context.rs
index e8d5d3b..0465fe1 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -477,4 +477,25 @@ impl UiContext {
     pub fn context_menu_labels(&self) -> Vec<crate::widget::display::TextLabel> {
         crate::widget::context_menu::text_labels()
     }
+
+    pub fn is_movable_window_at(&self, px: f32, py: f32) -> bool {
+        let mut hit_window = 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_window() {
+                            if w.is_movable_window() {
+                                hit_window = true;
+                            }
+                        } else {
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+        hit_window
+    }
 }
diff --git a/src/layout.rs b/src/layout.rs
index 81fcce2..228d7d8 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1744,6 +1744,10 @@ fn get_keybinds_control_sub_widget_info(
 }
 
 pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
+    let id = w.base().map(|b| b.id());
+    if let Some(w_id) = id {
+        ctx.register_widget(w_id, w.as_ptr_mut());
+    }
     w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
     let corners = w.rounded_corners();
     let r = if corners != (false, false, false, false) {
diff --git a/src/widget/container/window.rs b/src/widget/container/window.rs
index 0d66fc7..4ee5bfe 100644
--- a/src/widget/container/window.rs
+++ b/src/widget/container/window.rs
@@ -12,6 +12,7 @@ pub struct Window {
     pub radius: f32,
     pub background_color: Option<[f32; 4]>,
     pub visible: bool,
+    pub movable: bool,
 }
 
 impl Window {
@@ -25,9 +26,15 @@ impl Window {
             radius: crate::color::window_corner_radius(),
             background_color: None,
             visible: true,
+            movable: true,
         }
     }
 
+    pub fn with_movable(mut self, movable: bool) -> Self {
+        self.movable = movable;
+        self
+    }
+
     pub fn with_border(mut self, color: [f32; 4], thickness: f32) -> Self {
         self.border_color = Some(color);
         self.border_thickness = thickness;
@@ -299,6 +306,10 @@ impl Element for Window {
     fn is_window(&self) -> bool {
         true
     }
+
+    fn is_movable_window(&self) -> bool {
+        self.movable
+    }
 }
 
 impl Drop for Window {
@@ -323,6 +334,10 @@ mod tests {
         assert_eq!(win.solid_border(), Some(([1.0, 0.0, 0.0, 1.0], 2.5)));
         assert_eq!(win.corner_radius(), 8.0);
         assert_eq!(win.rounded_corners(), (true, true, true, true));
+        assert!(win.is_movable_window());
+
+        let non_movable_win = win.with_movable(false);
+        assert!(!non_movable_win.is_movable_window());
     }
 
     #[test]
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 02482ab..3770534 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -517,6 +517,7 @@ pub trait Element {
     fn is_page(&self) -> bool { false }
     fn is_layer(&self) -> bool { false }
     fn is_window(&self) -> bool { false }
+    fn is_movable_window(&self) -> bool { false }
     fn rounded_corners(&self) -> (bool, bool, bool, bool) { (false, false, false, false) }
 
     fn corner_radius(&self) -> f32 { 12.0 }