GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: Route scroll navigation keys to hovered scrollable widgets if they are not focused
src/context.rs | 59 ++++++++++++++++++++++++++++++++++++-
src/widget/container/page.rs | 1 +
src/widget/container/scroll_box.rs | 30 ++++++++++++++++++-
src/widget/container/spreadsheet.rs | 1 +
src/widget/json_layout.rs | 1 +
src/widget/mod.rs | 1 +
6 files changed, 91 insertions(+), 2 deletions(-)
diff --git a/src/context.rs b/src/context.rs
index 277652b..60638b6 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};
+use crate::widget::{Element, WidgetId, LayoutTree, Key, NamedKey, MouseButton, ElementState, Event};
use crate::widget::core::hover_animation::HoverState;
use crate::widget::core::context_menu::ContextMenuState;
@@ -121,6 +121,40 @@ impl UiContext {
self.last_scroll_time = Some(now);
}
}
+ if let Event::KeyInput(ref key_event) = event {
+ let is_scroll_key = match &key_event.logical_key {
+ Key::Named(NamedKey::PageUp)
+ | Key::Named(NamedKey::PageDown)
+ | Key::Named(NamedKey::Home)
+ | Key::Named(NamedKey::End)
+ | Key::Named(NamedKey::ArrowUp)
+ | Key::Named(NamedKey::ArrowDown) => true,
+ _ => false,
+ };
+ if is_scroll_key {
+ let mut handled = false;
+ if let Some(focused) = self.focused_widget {
+ unsafe {
+ if (*focused).handle_event(event, self) {
+ (*focused).mark_dirty(self);
+ handled = true;
+ }
+ }
+ }
+ if handled {
+ return true;
+ }
+ let (cx, cy) = self.cursor_pos;
+ if let Some(scrollable) = self.find_hovered_scrollable(root, cx, cy) {
+ unsafe {
+ if (*scrollable).handle_event(event, self) {
+ (*scrollable).mark_dirty(self);
+ return true;
+ }
+ }
+ }
+ }
+ }
self.propagate_event_impl(event, root)
}
@@ -811,4 +845,27 @@ impl UiContext {
}
hit_backplate
}
+
+ fn find_hovered_scrollable(&self, root: *mut (dyn Element + 'static), cx: f32, cy: f32) -> Option<*mut (dyn Element + 'static)> {
+ unsafe {
+ if root.is_null() {
+ return None;
+ }
+ if !(*root).visible() {
+ return None;
+ }
+ if !(*root).hit_test(cx, cy, self) {
+ return None;
+ }
+ for child in (*root).children(self).into_iter().rev() {
+ if let Some(scrollable) = self.find_hovered_scrollable(child, cx, cy) {
+ return Some(scrollable);
+ }
+ }
+ if (*root).is_scrollable() {
+ return Some(root);
+ }
+ }
+ None
+ }
}
diff --git a/src/widget/container/page.rs b/src/widget/container/page.rs
index ef8394a..3996ae2 100644
--- a/src/widget/container/page.rs
+++ b/src/widget/container/page.rs
@@ -168,6 +168,7 @@ fn clip_quad(
impl Element for Page {
fn base(&self) -> Option<&Widget> { Some(&self.base.base) }
fn is_page(&self) -> bool { true }
+ fn is_scrollable(&self) -> bool { true }
fn blocks_backplate_drag(&self) -> bool { false }
fn check_out_of_bounds(&self, event: &Event, _ctx: &UiContext) -> bool {
diff --git a/src/widget/container/scroll_box.rs b/src/widget/container/scroll_box.rs
index 06ff7c3..9f2d1b9 100644
--- a/src/widget/container/scroll_box.rs
+++ b/src/widget/container/scroll_box.rs
@@ -52,6 +52,7 @@ impl ScrollBox {
impl Element for ScrollBox {
crate::impl_widget_base!(ScrollBox);
+ fn is_scrollable(&self) -> bool { true }
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.base.x = x;
@@ -162,7 +163,8 @@ impl Element for ScrollBox {
found
};
- if !has_focus {
+ let is_hovered = self.hit_test(ctx.cursor_pos.0, ctx.cursor_pos.1, ctx);
+ if !has_focus && !is_hovered {
return false;
}
if event.state != ElementState::Pressed {
@@ -348,4 +350,30 @@ mod tests {
assert!(sb.keyboard_input(&event_home, &mut ctx));
assert_eq!(sb.scroll_y, 0.0);
}
+
+ #[test]
+ fn test_scroll_box_non_focused_hovered_scrolling() {
+ let mut sb = ScrollBox::new();
+ sb.set_rect(10.0, 20.0, 100.0, 100.0);
+ sb.update_bounds(300.0, 20.0, 100.0); // max_scroll = 200.0
+
+ let mut ctx = UiContext::new();
+ ctx.register_widget(sb.base.id(), &mut sb);
+
+ // Set cursor position over the scroll box
+ ctx.set_cursor_pos(50.0, 50.0);
+
+ let event_down = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::ArrowDown),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+
+ let root_ptr = &mut sb as *mut ScrollBox as *mut (dyn Element + 'static);
+ assert!(ctx.propagate_event(&Event::KeyInput(event_down), root_ptr));
+ assert_eq!(sb.scroll_y, 24.0);
+ }
}
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index a33d1e7..b39e155 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -41,6 +41,7 @@ impl Spreadsheet {
}
impl Element for Spreadsheet {
+ fn is_scrollable(&self) -> bool { true }
fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, true, true) }
fn as_ptr(&self) -> *mut (dyn Element + 'static) {
self as *const Self as *mut Self as *mut (dyn Element + 'static)
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index 39f81f0..d2b08e2 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -273,6 +273,7 @@ impl JsonLayoutWidget {
}
impl Element for JsonLayoutWidget {
+ fn is_scrollable(&self) -> bool { true }
fn as_any(&self) -> &dyn std::any::Any { self }
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
fn as_ptr(&self) -> *mut (dyn Element + 'static) {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 7762722..1886f9f 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -581,6 +581,7 @@ pub trait Element {
fn is_layer(&self) -> bool { false }
fn is_backplate(&self) -> bool { false }
fn is_movable_backplate(&self) -> bool { false }
+ fn is_scrollable(&self) -> bool { false }
fn blocks_backplate_drag(&self) -> bool { true }
fn rounded_corners(&self) -> (bool, bool, bool, bool) { (false, false, false, false) }