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

commit5023fae255eecf34e59733cae17b1043898f5020
parent0af492fe2e
authorLucas Galante <[email protected]>
date2026-06-26 08:21
Revamp cce-ui event handling with decoupling, coordinate relativization, pointer grabbing, and semantic drag/hover/focus events

 src/context.rs                       | 184 ++++++++++++++++++++++++++---------
 src/widget/container/page.rs         |  32 ++++++
 src/widget/input/keybinds_control.rs |   6 +-
 src/widget/input/multi_control.rs    |  10 +-
 src/widget/json_layout.rs            |  16 +--
 src/widget/mod.rs                    |  49 ++++++++--
 6 files changed, 227 insertions(+), 70 deletions(-)

diff --git a/src/context.rs b/src/context.rs
index 54ec527..7a1a5f8 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -1,5 +1,5 @@
 use std::collections::HashMap;
-use crate::widget::{Element, WidgetId, LayoutTree, Key, MouseButton, ElementState, Event, ScrollBar};
+use crate::widget::{Element, WidgetId, LayoutTree, Key, MouseButton, ElementState, Event};
 use crate::widget::core::hover_animation::HoverState;
 use crate::widget::core::context_menu::ContextMenuState;
 
@@ -11,6 +11,10 @@ pub struct UiContext {
     pub hover_state: HoverState,
     pub cursor_pos: (f32, f32),
     pub context_menu: ContextMenuState,
+    pub active_grab: Option<WidgetId>,
+    pub drag_start_pos: Option<(f32, f32)>,
+    pub drag_target: Option<WidgetId>,
+    pub is_dragging: bool,
 }
 
 impl UiContext {
@@ -26,40 +30,108 @@ impl UiContext {
             hover_state: HoverState::new(),
             cursor_pos: (0.0, 0.0),
             context_menu: ContextMenuState::new(),
+            active_grab: None,
+            drag_start_pos: None,
+            drag_target: None,
+            is_dragging: false,
         }
     }
 
+    pub fn get_widget(&self, id: WidgetId) -> Option<&(dyn Element + 'static)> {
+        self.widget_registry.get(&id).map(|&ptr| unsafe { &*ptr })
+    }
+
+    pub fn get_widget_mut(&mut self, id: WidgetId) -> Option<&mut (dyn Element + 'static)> {
+        self.widget_registry.get(&id).map(|&ptr| unsafe { &mut *ptr })
+    }
+
     pub fn propagate_event(&mut self, event: &Event, root: *mut (dyn Element + 'static)) -> bool {
         if root.is_null() {
             return false;
         }
         unsafe {
-            let mut out_of_bounds = false;
-            if (*root).is_page() {
-                let mut scrollbar_dragging = false;
-                if let Some(page) = (*root).as_any().downcast_ref::<crate::widget::Page>() {
-                    if page.scroll_bar.dragging {
-                        scrollbar_dragging = true;
+            // Track drag gestures based on mouse events
+            match event {
+                Event::MouseButton { button, state, x, y, .. } if *button == MouseButton::Left => {
+                    if *state == ElementState::Pressed {
+                        self.drag_start_pos = Some((*x, *y));
+                        self.is_dragging = false;
+                        self.drag_target = None;
+                    } else if *state == ElementState::Released {
+                        if self.is_dragging {
+                            if let Some(target_id) = self.drag_target {
+                                if let Some(target_ptr) = self.widget_registry.get(&target_id).copied() {
+                                    (*target_ptr).handle_event(&Event::DragEnd, self);
+                                    (*target_ptr).mark_dirty(self);
+                                }
+                            }
+                            self.active_grab = None;
+                        }
+                        self.drag_start_pos = None;
+                        self.drag_target = None;
+                        self.is_dragging = false;
+                    }
+                }
+                Event::PointerMove { x, y, .. } => {
+                    if let Some((sx, sy)) = self.drag_start_pos {
+                        if let Some(target_id) = self.drag_target {
+                            if self.is_dragging {
+                                let dx = *x - sx;
+                                let dy = *y - sy;
+                                if let Some(target_ptr) = self.widget_registry.get(&target_id).copied() {
+                                    let (cx, cy, _, _) = (*target_ptr).rect();
+                                    let drag_evt = Event::DragUpdate { dx, dy, x: *x, y: *y, local_x: *x - cx, local_y: *y - cy };
+                                    let adjusted = (*root).transform_event_for_child(target_ptr, drag_evt, self);
+                                    (*target_ptr).handle_event(&adjusted, self);
+                                    (*target_ptr).mark_dirty(self);
+                                }
+                            } else {
+                                let dx = *x - sx;
+                                let dy = *y - sy;
+                                if (dx * dx + dy * dy).sqrt() > 3.0 {
+                                    self.is_dragging = true;
+                                    self.active_grab = Some(target_id);
+                                    if let Some(target_ptr) = self.widget_registry.get(&target_id).copied() {
+                                        (*target_ptr).handle_event(&Event::DragStart { start_x: sx, start_y: sy }, self);
+                                        (*target_ptr).mark_dirty(self);
+                                    }
+                                }
+                            }
+                        }
                     }
                 }
-                
-                if !scrollbar_dragging {
-                    if let Event::PointerMove { x, y }
-                    | Event::MouseButton { x, y, .. }
-                    | Event::MouseWheel { delta: _, x, y } = event
-                    {
-                        let (rx, ry, rw, rh) = (*root).rect();
-                        if *x < rx || *x > rx + rw || *y < ry || *y > ry + rh {
-                            out_of_bounds = true;
+                _ => {}
+            }
+
+            // Normal grab redirection for mouse events if active
+            if let Some(grabbed_id) = self.active_grab {
+                if let Event::PointerMove { .. }
+                | Event::MouseButton { .. }
+                | Event::MouseWheel { .. }
+                | Event::DragStart { .. }
+                | Event::DragUpdate { .. }
+                | Event::DragEnd = event
+                {
+                    if let Some(grabbed_ptr) = self.widget_registry.get(&grabbed_id).copied() {
+                        let handled = (*grabbed_ptr).handle_event(event, self);
+                        if handled {
+                            (*grabbed_ptr).mark_dirty(self);
                         }
+                        return handled;
                     }
                 }
             }
 
-            if out_of_bounds {
+            if (*root).check_out_of_bounds(event, self) {
                 return false;
             }
 
+            // 1. Capture Phase: parent intercepts
+            if (*root).capture_event(event, self) {
+                (*root).mark_dirty(self);
+                return true;
+            }
+
             // For KeyInput, send directly to focused widget if it exists
             if let Event::KeyInput(_) = event {
                 if let Some(focused) = self.focused_widget {
@@ -72,26 +144,31 @@ impl UiContext {
 
             let mut handled = false;
             let children = (*root).children(self);
-            
+
+            // Determine if we should record a drag target candidate
+            let mut check_drag_target = false;
+            if let Event::MouseButton { button, state, .. } = event {
+                if *button == MouseButton::Left && *state == ElementState::Pressed {
+                    check_drag_target = true;
+                }
+            }
+
             match event {
                 Event::PointerMove { .. } | Event::Tick(_) => {
                     for child in children.into_iter().rev() {
-                        let mut adjusted_event = event.clone();
-                        if (*root).is_page() {
-                            if let Some(page) = (*root).as_any().downcast_ref::<crate::widget::Page>() {
-                                let sb_ptr = &page.scroll_bar as *const ScrollBar as *mut ScrollBar as *mut (dyn Element + 'static);
-                                if std::ptr::addr_eq(child, sb_ptr) {
-                                    match &mut adjusted_event {
-                                        Event::PointerMove { y, .. }
-                                        | Event::MouseButton { y, .. }
-                                        | Event::MouseWheel { y, .. } => {
-                                            *y -= page.scroll_y;
-                                        }
-                                        _ => {}
-                                    }
-                                }
+                        let (cx, cy, _, _) = (*child).rect();
+                        let mut local_adjusted = event.clone();
+                        match &mut local_adjusted {
+                            Event::PointerMove { local_x, local_y, .. }
+                            | Event::MouseButton { local_x, local_y, .. }
+                            | Event::MouseWheel { local_x, local_y, .. }
+                            | Event::DragUpdate { local_x, local_y, .. } => {
+                                *local_x -= cx;
+                                *local_y -= cy;
                             }
+                            _ => {}
                         }
+                        let adjusted_event = (*root).transform_event_for_child(child, local_adjusted, self);
                         if self.propagate_event(&adjusted_event, child) {
                             handled = true;
                         }
@@ -103,28 +180,35 @@ impl UiContext {
                 }
                 _ => {
                     for child in children.into_iter().rev() {
-                        let mut adjusted_event = event.clone();
-                        if (*root).is_page() {
-                            if let Some(page) = (*root).as_any().downcast_ref::<crate::widget::Page>() {
-                                let sb_ptr = &page.scroll_bar as *const ScrollBar as *mut ScrollBar as *mut (dyn Element + 'static);
-                                if std::ptr::addr_eq(child, sb_ptr) {
-                                    match &mut adjusted_event {
-                                        Event::PointerMove { y, .. }
-                                        | Event::MouseButton { y, .. }
-                                        | Event::MouseWheel { y, .. } => {
-                                            *y -= page.scroll_y;
-                                        }
-                                        _ => {}
-                                    }
-                                }
+                        let (cx, cy, _, _) = (*child).rect();
+                        let mut local_adjusted = event.clone();
+                        match &mut local_adjusted {
+                            Event::PointerMove { local_x, local_y, .. }
+                            | Event::MouseButton { local_x, local_y, .. }
+                            | Event::MouseWheel { local_x, local_y, .. }
+                            | Event::DragUpdate { local_x, local_y, .. } => {
+                                *local_x -= cx;
+                                *local_y -= cy;
                             }
+                            _ => {}
                         }
+                        let adjusted_event = (*root).transform_event_for_child(child, local_adjusted, self);
                         if self.propagate_event(&adjusted_event, child) {
+                            if check_drag_target {
+                                if let Some(b) = (*child).base() {
+                                    self.drag_target = Some(b.id());
+                                }
+                            }
                             return true;
                         }
                     }
                     if (*root).handle_event(event, self) {
                         (*root).mark_dirty(self);
+                        if check_drag_target {
+                            if let Some(b) = (*root).base() {
+                                self.drag_target = Some(b.id());
+                            }
+                        }
                         return true;
                     }
                 }
@@ -171,11 +255,18 @@ impl UiContext {
             if old_data != new_data {
                 unsafe {
                     (*old_ptr).unfocus();
+                    (*old_ptr).handle_event(&Event::FocusOut, self);
                 }
                 self.focused_widget = Some(new_ptr);
+                unsafe {
+                    (*new_ptr).handle_event(&Event::FocusIn, self);
+                }
             }
         } else {
             self.focused_widget = Some(new_ptr);
+            unsafe {
+                (*new_ptr).handle_event(&Event::FocusIn, self);
+            }
         }
     }
 
@@ -197,6 +288,7 @@ impl UiContext {
         if let Some(ptr) = self.focused_widget.take() {
             unsafe {
                 (*ptr).unfocus();
+                (*ptr).handle_event(&Event::FocusOut, self);
             }
         }
     }
diff --git a/src/widget/container/page.rs b/src/widget/container/page.rs
index d8c0ca0..84e3120 100644
--- a/src/widget/container/page.rs
+++ b/src/widget/container/page.rs
@@ -170,6 +170,38 @@ impl Element for Page {
     fn is_page(&self) -> bool { true }
     fn blocks_backplate_drag(&self) -> bool { false }
 
+    fn check_out_of_bounds(&self, event: &Event, _ctx: &UiContext) -> bool {
+        if self.scroll_bar.dragging {
+            return false;
+        }
+        if let Event::PointerMove { x, y, .. }
+        | Event::MouseButton { x, y, .. }
+        | Event::MouseWheel { x, y, .. } = event
+        {
+            let (rx, ry, rw, rh) = self.rect();
+            if *x < rx || *x > rx + rw || *y < ry || *y > ry + rh {
+                return true;
+            }
+        }
+        false
+    }
+
+    fn transform_event_for_child(&self, child: *mut (dyn Element + 'static), mut event: Event, _ctx: &UiContext) -> Event {
+        let sb_ptr = &self.scroll_bar as *const ScrollBar as *mut ScrollBar as *mut (dyn Element + 'static);
+        if std::ptr::addr_eq(child, sb_ptr) {
+            match &mut event {
+                Event::PointerMove { y, local_y, .. }
+                | Event::MouseButton { y, local_y, .. }
+                | Event::MouseWheel { y, local_y, .. } => {
+                    *y -= self.scroll_y;
+                    *local_y -= self.scroll_y;
+                }
+                _ => {}
+            }
+        }
+        event
+    }
+
     fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base.base) }
     fn as_any(&self) -> &dyn std::any::Any { self }
     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
diff --git a/src/widget/input/keybinds_control.rs b/src/widget/input/keybinds_control.rs
index 52f79af..8cd2a81 100644
--- a/src/widget/input/keybinds_control.rs
+++ b/src/widget/input/keybinds_control.rs
@@ -311,7 +311,7 @@ impl Element for KeybindsControl {
 
         if !handled {
             match event {
-                Event::PointerMove { x, y } => {
+                Event::PointerMove { x, y, .. } => {
                     let is_hit = self.hit_test(*x, *y, ctx);
                     let was = self.hovered();
                     self.set_hovered(is_hit);
@@ -333,7 +333,7 @@ impl Element for KeybindsControl {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.handle_event(&Event::MouseButton { button, state, x: px, y: py }, ctx)
+        self.handle_event(&Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py }, ctx)
     }
 
     fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
@@ -341,7 +341,7 @@ impl Element for KeybindsControl {
     }
 
     fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.handle_event(&Event::PointerMove { x: px, y: py }, ctx)
+        self.handle_event(&Event::PointerMove { x: px, y: py, local_x: px, local_y: py }, ctx)
     }
 
     fn unfocus(&mut self) {
diff --git a/src/widget/input/multi_control.rs b/src/widget/input/multi_control.rs
index 64dd807..1b0d7c8 100644
--- a/src/widget/input/multi_control.rs
+++ b/src/widget/input/multi_control.rs
@@ -418,7 +418,7 @@ impl Element for MultiControl {
             let dy = by + bh;
             let dh = 4.0 * 24.0;
             match event {
-                Event::PointerMove { x, y } => {
+                Event::PointerMove { x, y, .. } => {
                     if *x >= bx && *x <= bx + bw && *y >= dy && *y <= dy + dh {
                         let idx = ((*y - dy) / 24.0).floor() as usize;
                         if idx < 4 {
@@ -428,7 +428,7 @@ impl Element for MultiControl {
                     }
                     self.add_popover_hovered_idx = None;
                 }
-                Event::MouseButton { button, state, x, y } => {
+                Event::MouseButton { button, state, x, y, .. } => {
                     if *button == MouseButton::Left && *state == ElementState::Pressed {
                         if *x >= bx && *x <= bx + bw && *y >= dy && *y <= dy + dh {
                             let idx = ((*y - dy) / 24.0).floor() as usize;
@@ -559,7 +559,7 @@ impl Element for MultiControl {
 
         if !handled {
             match event {
-                Event::PointerMove { x, y } => {
+                Event::PointerMove { x, y, .. } => {
                     let is_hit = self.hit_test(*x, *y, ctx);
                     let was = self.hovered();
                     self.set_hovered(is_hit);
@@ -581,7 +581,7 @@ impl Element for MultiControl {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.handle_event(&Event::MouseButton { button, state, x: px, y: py }, ctx)
+        self.handle_event(&Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py }, ctx)
     }
 
     fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
@@ -589,7 +589,7 @@ impl Element for MultiControl {
     }
 
     fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.handle_event(&Event::PointerMove { x: px, y: py }, ctx)
+        self.handle_event(&Event::PointerMove { x: px, y: py, local_x: px, local_y: py }, ctx)
     }
 
     fn unfocus(&mut self) {
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index 3b65fc2..b74a31c 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -398,7 +398,7 @@ impl Element for JsonLayoutWidget {
         let mut changed = false;
 
         match event {
-            Event::PointerMove { x, y } => {
+            Event::PointerMove { x, y, .. } => {
                 if let Some(idx) = self.dragging_slider_idx {
                     if let Some(w) = self.widgets.get_mut(idx) {
                         if let Some(sl) = w.widget.as_any_mut().downcast_mut::<Slider>() {
@@ -409,7 +409,7 @@ impl Element for JsonLayoutWidget {
                     }
                 }
             }
-            Event::MouseButton { button, state, x: _, y: _ } => {
+            Event::MouseButton { button, state, x: _, y: _, .. } => {
                 if *button == MouseButton::Left && *state == ElementState::Released {
                     if let Some(idx) = self.dragging_slider_idx {
                         if let Some(w) = self.widgets.get_mut(idx) {
@@ -430,7 +430,7 @@ impl Element for JsonLayoutWidget {
                 continue;
             }
 
-            if let Event::MouseButton { button, state, x, y } = event {
+            if let Event::MouseButton { button, state, x, y, .. } = event {
                 if *button == MouseButton::Left && *state == ElementState::Pressed {
                     let hit = *x >= w.x && *x <= w.x + w.w && *y >= w.y && *y <= w.y + w.h;
                     if hit && w.widget_type == "slider" {
@@ -441,7 +441,7 @@ impl Element for JsonLayoutWidget {
 
             if w.widget_type == "checkbox" {
                 match event {
-                    Event::PointerMove { x, y } => {
+                    Event::PointerMove { x, y, .. } => {
                         if let Some(cb) = w.widget.as_any_mut().downcast_mut::<Checkbox>() {
                             let was = cb.hovered();
                             let hit = *x >= w.x && *x <= w.x + w.w && *y >= w.y && *y <= w.y + w.h;
@@ -451,7 +451,7 @@ impl Element for JsonLayoutWidget {
                             }
                         }
                     }
-                    Event::MouseButton { button, state, x, y } => {
+                    Event::MouseButton { button, state, x, y, .. } => {
                         if *button == MouseButton::Left {
                             let hit = *x >= w.x && *x <= w.x + w.w && *y >= w.y && *y <= w.y + w.h;
                             if hit {
@@ -489,7 +489,7 @@ impl Element for JsonLayoutWidget {
             changed = true;
         }
 
-        if let Event::MouseWheel { delta, x, y } = event {
+        if let Event::MouseWheel { delta, x, y, .. } = event {
             let (bx, by, bw, bh) = self.rect();
             if *x >= bx && *x <= bx + bw && *y >= by && *y <= by + bh {
                 if active_page < self.page_total_heights.len() {
@@ -533,10 +533,10 @@ impl Element for JsonLayoutWidget {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.handle_event(&Event::MouseButton { button, state, x: px, y: py }, ctx)
+        self.handle_event(&Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py }, ctx)
     }
 
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.handle_event(&Event::PointerMove { x: px, y: py }, ctx)
+        self.handle_event(&Event::PointerMove { x: px, y: py, local_x: px, local_y: py }, ctx)
     }
 }
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 46a768e..b7d5726 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -84,11 +84,19 @@ pub use crate::context::UiContext;
 
 #[derive(Debug, Clone, PartialEq)]
 pub enum Event {
-    PointerMove { x: f32, y: f32 },
-    MouseButton { button: MouseButton, state: ElementState, x: f32, y: f32 },
-    MouseWheel { delta: MouseScrollDelta, x: f32, y: f32 },
+    PointerMove { x: f32, y: f32, local_x: f32, local_y: f32 },
+    MouseButton { button: MouseButton, state: ElementState, x: f32, y: f32, local_x: f32, local_y: f32 },
+    MouseWheel { delta: MouseScrollDelta, x: f32, y: f32, local_x: f32, local_y: f32 },
     KeyInput(KeyEvent),
     Tick(f32),
+
+    MouseEnter,
+    MouseLeave,
+    DragStart { start_x: f32, start_y: f32 },
+    DragUpdate { dx: f32, dy: f32, x: f32, y: f32, local_x: f32, local_y: f32 },
+    DragEnd,
+    FocusIn,
+    FocusOut,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq)]
@@ -126,6 +134,18 @@ pub trait Element {
     fn base_mut(&mut self) -> Option<&mut Widget> { None }
     fn preferred_height(&self) -> Option<f32> { None }
 
+    fn check_out_of_bounds(&self, _event: &Event, _ctx: &UiContext) -> bool {
+        false
+    }
+
+    fn transform_event_for_child(&self, _child: *mut (dyn Element + 'static), event: Event, _ctx: &UiContext) -> Event {
+        event
+    }
+
+    fn capture_event(&mut self, _event: &Event, _ctx: &mut UiContext) -> bool {
+        false
+    }
+
     fn mark_dirty(&mut self, ctx: &mut UiContext) {
         let mut parent_id = None;
         if let Some(b) = self.base_mut() {
@@ -175,13 +195,13 @@ pub trait Element {
 
     fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
         match event {
-            Event::PointerMove { x, y } => {
+            Event::PointerMove { x, y, .. } => {
                 self.cursor_moved(*x, *y, ctx)
             }
-            Event::MouseButton { button, state, x, y } => {
+            Event::MouseButton { button, state, x, y, .. } => {
                 self.mouse_input(*button, *state, *x, *y, ctx)
             }
-            Event::MouseWheel { delta, x, y } => {
+            Event::MouseWheel { delta, x, y, .. } => {
                 self.mouse_wheel(delta, *x, *y, ctx)
             }
             Event::KeyInput(key_event) => {
@@ -190,6 +210,7 @@ pub trait Element {
             Event::Tick(dt) => {
                 self.tick(*dt, ctx)
             }
+            _ => false,
         }
     }
 
@@ -286,7 +307,10 @@ pub trait Element {
         ctx.set_cursor_pos(px, py);
         if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
             let was = self.hovered();
-            self.set_hovered(false);
+            if was {
+                self.set_hovered(false);
+                self.handle_event(&Event::MouseLeave, ctx);
+            }
             return was;
         }
         self.on_cursor_moved(px, py, ctx)
@@ -297,7 +321,16 @@ pub trait Element {
             let was = self.hovered();
             let is_hit = self.hit_test(px, py, ctx);
             self.set_hovered(is_hit);
-            was != is_hit
+            if was != is_hit {
+                if is_hit {
+                    self.handle_event(&Event::MouseEnter, ctx);
+                } else {
+                    self.handle_event(&Event::MouseLeave, ctx);
+                }
+                was != is_hit
+            } else {
+                false
+            }
         } else {
             false
         }