GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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
}