GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: Add Page Up, Page Down, Home, and End keys to scrolling interfaces
src/widget/container/page.rs | 114 +++++++++++++++++++++++++++++++++++
src/widget/container/scroll_box.rs | 115 ++++++++++++++++++++++++++++++++++--
src/widget/container/spreadsheet.rs | 40 +++++++++++++
src/widget/json_layout.rs | 39 ++++++++++++
4 files changed, 302 insertions(+), 6 deletions(-)
diff --git a/src/widget/container/page.rs b/src/widget/container/page.rs
index a430380..ef8394a 100644
--- a/src/widget/container/page.rs
+++ b/src/widget/container/page.rs
@@ -429,6 +429,52 @@ impl Element for Page {
}
false
}
+
+ fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if event.state != ElementState::Pressed {
+ return false;
+ }
+ let (x, y, w, h) = self.rect();
+ let max_scroll = (self.content_h - h).max(0.0);
+ if max_scroll <= 0.0 {
+ return false;
+ }
+
+ let old_scroll = self.scroll_y;
+ match &event.logical_key {
+ Key::Named(NamedKey::ArrowDown) => {
+ self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max_scroll);
+ }
+ Key::Named(NamedKey::ArrowUp) => {
+ self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max_scroll);
+ }
+ Key::Named(NamedKey::PageDown) => {
+ self.scroll_y = (self.scroll_y + h).clamp(0.0, max_scroll);
+ }
+ Key::Named(NamedKey::PageUp) => {
+ self.scroll_y = (self.scroll_y - h).clamp(0.0, max_scroll);
+ }
+ Key::Named(NamedKey::Home) => {
+ self.scroll_y = 0.0;
+ }
+ Key::Named(NamedKey::End) => {
+ self.scroll_y = max_scroll;
+ }
+ _ => return false,
+ }
+
+ self.scroll_bar.scroll_y = self.scroll_y;
+ if (self.scroll_y - old_scroll).abs() > 0.01 {
+ self.set_rect(x, y, w, h);
+ self.mark_dirty(ctx);
+ true
+ } else {
+ false
+ }
+ }
}
#[cfg(test)]
@@ -494,5 +540,73 @@ mod tests {
assert!(page.check_out_of_bounds(&event_scrolled_out, &ctx));
assert!(!page.hit_test(100.0, 1000.0, &ctx));
}
+
+ #[test]
+ fn test_page_keyboard_input() {
+ let mut page = Page::new(0.0, 0.0, 800.0, 600.0);
+ page.content_h = 1000.0; // max_scroll = 400.0
+
+ let mut ctx = UiContext::new();
+
+ // 1. ArrowDown
+ let event_down = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::ArrowDown),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(page.keyboard_input(&event_down, &mut ctx));
+ assert_eq!(page.scroll_y, 24.0);
+
+ // 2. PageDown
+ let event_pgdown = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::PageDown),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(page.keyboard_input(&event_pgdown, &mut ctx));
+ assert_eq!(page.scroll_y, 400.0);
+
+ // 3. PageUp
+ let event_pgup = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::PageUp),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(page.keyboard_input(&event_pgup, &mut ctx));
+ assert_eq!(page.scroll_y, 0.0);
+
+ // 4. End
+ let event_end = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::End),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(page.keyboard_input(&event_end, &mut ctx));
+ assert_eq!(page.scroll_y, 400.0);
+
+ // 5. Home
+ let event_home = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::Home),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(page.keyboard_input(&event_home, &mut ctx));
+ assert_eq!(page.scroll_y, 0.0);
+ }
}
diff --git a/src/widget/container/scroll_box.rs b/src/widget/container/scroll_box.rs
index a948fc1..06ff7c3 100644
--- a/src/widget/container/scroll_box.rs
+++ b/src/widget/container/scroll_box.rs
@@ -146,24 +146,38 @@ impl Element for ScrollBox {
quads
}
- fn keyboard_input(&mut self, event: &KeyEvent, _ctx: &mut UiContext) -> bool {
- if !focus::is_focused(self) {
+ fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
+ let self_addr = self as *const Self as *const () as usize;
+ let has_focus = ctx.is_focused_addr(self_addr) || {
+ let mut current = ctx.focused_widget;
+ let mut found = false;
+ while let Some(ptr) = current {
+ let ptr_addr = ptr as *const () as usize;
+ if ptr_addr == self_addr {
+ found = true;
+ break;
+ }
+ current = unsafe { (*ptr).parent(ctx) };
+ }
+ found
+ };
+
+ if !has_focus {
return false;
}
if event.state != ElementState::Pressed {
return false;
}
+ let max_scroll = (self.content_h - self.viewport_h).max(0.0);
if event.ctrl {
match &event.logical_key {
Key::Character(c) if c == "n" || c == "N" => {
let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max_scroll);
(self.scroll_y - old_scroll).abs() > 0.01
}
Key::Character(c) if c == "p" || c == "P" => {
let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max_scroll);
(self.scroll_y - old_scroll).abs() > 0.01
}
@@ -173,16 +187,34 @@ impl Element for ScrollBox {
match &event.logical_key {
Key::Named(NamedKey::ArrowDown) => {
let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max_scroll);
(self.scroll_y - old_scroll).abs() > 0.01
}
Key::Named(NamedKey::ArrowUp) => {
let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max_scroll);
(self.scroll_y - old_scroll).abs() > 0.01
}
+ Key::Named(NamedKey::PageDown) => {
+ let old_scroll = self.scroll_y;
+ self.scroll_y = (self.scroll_y + self.viewport_h).clamp(0.0, max_scroll);
+ (self.scroll_y - old_scroll).abs() > 0.01
+ }
+ Key::Named(NamedKey::PageUp) => {
+ let old_scroll = self.scroll_y;
+ self.scroll_y = (self.scroll_y - self.viewport_h).clamp(0.0, max_scroll);
+ (self.scroll_y - old_scroll).abs() > 0.01
+ }
+ Key::Named(NamedKey::Home) => {
+ let old_scroll = self.scroll_y;
+ self.scroll_y = 0.0;
+ (self.scroll_y - old_scroll).abs() > 0.01
+ }
+ Key::Named(NamedKey::End) => {
+ let old_scroll = self.scroll_y;
+ self.scroll_y = max_scroll;
+ (self.scroll_y - old_scroll).abs() > 0.01
+ }
_ => false,
}
}
@@ -245,4 +277,75 @@ mod tests {
// 30 >= 22.0 and 30 + 24 <= 118.0, so it should return Some(30.0)
assert_eq!(sb.get_item_draw_y(60.0, 24.0), Some(30.0));
}
+
+ #[test]
+ fn test_scroll_box_keyboard_input() {
+ 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();
+ // Focus the scroll box
+ ctx.set_focused(&mut sb);
+
+ // 1. ArrowDown key
+ let event_down = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::ArrowDown),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(sb.keyboard_input(&event_down, &mut ctx));
+ assert_eq!(sb.scroll_y, 24.0);
+
+ // 2. PageDown key
+ let event_pgdown = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::PageDown),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(sb.keyboard_input(&event_pgdown, &mut ctx));
+ assert_eq!(sb.scroll_y, 124.0);
+
+ // 3. End key
+ let event_end = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::End),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(sb.keyboard_input(&event_end, &mut ctx));
+ assert_eq!(sb.scroll_y, 200.0); // clamps at max_scroll = 200.0
+
+ // 4. PageUp key
+ let event_pgup = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::PageUp),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(sb.keyboard_input(&event_pgup, &mut ctx));
+ assert_eq!(sb.scroll_y, 100.0);
+
+ // 5. Home key
+ let event_home = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Named(NamedKey::Home),
+ text: None,
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ assert!(sb.keyboard_input(&event_home, &mut ctx));
+ assert_eq!(sb.scroll_y, 0.0);
+ }
}
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 8f1b3bb..a33d1e7 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -398,6 +398,46 @@ impl Element for Spreadsheet {
}
labels
}
+
+ fn keyboard_input(&mut self, event: &KeyEvent, _ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if event.state != ElementState::Pressed {
+ return false;
+ }
+ let content_h = self.rows.len() as f32 * 24.0;
+ let visible_h = (self.h - 24.0).max(0.0);
+ if visible_h <= 0.0 || content_h <= visible_h {
+ return false;
+ }
+
+ let max_scroll_y = content_h - visible_h;
+ let old_scroll_y = self.scroll_y;
+ match &event.logical_key {
+ Key::Named(NamedKey::ArrowDown) => {
+ self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max_scroll_y);
+ }
+ Key::Named(NamedKey::ArrowUp) => {
+ self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max_scroll_y);
+ }
+ Key::Named(NamedKey::PageDown) => {
+ self.scroll_y = (self.scroll_y + visible_h).clamp(0.0, max_scroll_y);
+ }
+ Key::Named(NamedKey::PageUp) => {
+ self.scroll_y = (self.scroll_y - visible_h).clamp(0.0, max_scroll_y);
+ }
+ Key::Named(NamedKey::Home) => {
+ self.scroll_y = 0.0;
+ }
+ Key::Named(NamedKey::End) => {
+ self.scroll_y = max_scroll_y;
+ }
+ _ => return false,
+ }
+
+ (self.scroll_y - old_scroll_y).abs() > 0.01
+ }
}
impl SpreadsheetController for Spreadsheet {
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index 38fb55e..39f81f0 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -1,5 +1,6 @@
use crate::widget::{
Element, Widget, Checkbox, Button, Label, Spinbox, ColorSelector, TextLabel, MouseButton, ElementState, Slider, Event, UiContext,
+ Key, NamedKey,
};
use serde::Deserialize;
@@ -516,6 +517,44 @@ impl Element for JsonLayoutWidget {
}
}
+ if let Event::KeyInput(key_event) = event {
+ if key_event.state == ElementState::Pressed {
+ if active_page < self.page_total_heights.len() {
+ let total_height = self.page_total_heights[active_page];
+ let (_, _, _, bh) = self.rect();
+ let max_scroll_y = (total_height - bh).max(0.0);
+ if max_scroll_y > 0.0 {
+ let old_scroll = self.page_scroll_y[active_page];
+ match &key_event.logical_key {
+ Key::Named(NamedKey::PageDown) => {
+ self.page_scroll_y[active_page] = (old_scroll + bh).clamp(0.0, max_scroll_y);
+ }
+ Key::Named(NamedKey::PageUp) => {
+ self.page_scroll_y[active_page] = (old_scroll - bh).clamp(0.0, max_scroll_y);
+ }
+ Key::Named(NamedKey::Home) => {
+ self.page_scroll_y[active_page] = 0.0;
+ }
+ Key::Named(NamedKey::End) => {
+ self.page_scroll_y[active_page] = max_scroll_y;
+ }
+ Key::Named(NamedKey::ArrowDown) => {
+ self.page_scroll_y[active_page] = (old_scroll + 24.0).clamp(0.0, max_scroll_y);
+ }
+ Key::Named(NamedKey::ArrowUp) => {
+ self.page_scroll_y[active_page] = (old_scroll - 24.0).clamp(0.0, max_scroll_y);
+ }
+ _ => {}
+ }
+ if (self.page_scroll_y[active_page] - old_scroll).abs() > 0.01 {
+ self.layout_children();
+ changed = true;
+ }
+ }
+ }
+ }
+ }
+
if let Event::Tick(dt) = event {
for w in &mut self.widgets {
if w.page_idx != active_page {