input/keyboard: let the app intercept arrow / Tab keys before the default text-edit and focus-shift behaviours
The keysym dispatcher used to give the focused widget first refusal on Left / Right / Up / Down / Tab. A `text_edit` swallowed the four arrows for cursor movement, and Tab walked the keyboard-focus ring through `next_focusable_index`. Apps only got `on_key_with_modifiers` for those keys when no text input was focused (Tab never), so a search field with autocomplete couldn't drive a selection cursor through arrows or Tab without putting focus on something else first — which is exactly the wrong UX for a search-as-you-type list. Three arms in `AppData::handle_key_press` now query `self.app.on_key_with_modifiers( keysym, ctrl, shift )` *first* and only fall back to the default behaviour if the app returns `None`: - `Left | Right`: previously branched on `is_text_input` and routed the keypress to `handle_cursor_left/right` for text fields, otherwise asked the app. Now: app first; if app says `None`, the existing text-cursor path runs for text inputs and non-text widgets get nothing (their previous fallback already produced no useful action without an app handler). - `Up | Down`: the previous logic was a four-way decision that tried text-cursor movement, then `move_keyboard_hover` for combo / list widgets, then the app handler. Now the app gets first refusal too. When it declines, the original cascade (text-cursor → hover navigation) still runs, so multiline `text_edit` cursor walking and combo / scrollable-list keyboard nav are unchanged for any app that doesn't intercept these keys. - `Tab | ISO_Left_Tab`: app first; on `None` the focus-shift path (`next_focusable_index` + `set_focus`) runs. Apps that want Tab as a navigation message between custom UI states (a search field cycling through results, a wizard advancing pages) finally have a hook; apps that don't get the standard tab-through-focusable behaviour by leaving `on_key_with_modifiers` returning `None` for Tab, which is the trait's default. The interception is keysym-only — modifier state is forwarded so an app can distinguish `Tab` from `Shift+Tab`, `Right` from `Ctrl+Right`. The text-input check is preserved as a local before the dispatch so the fallback doesn't lose the "is this even a text edit?" question; it just runs *after* the app instead of before. Net effect: a list / autocomplete that needs arrow / Tab navigation can now be implemented purely by overriding `App::on_key_with_modifiers`, with no changes to `text_edit` itself and no risk of breaking apps that don't need it. The Up / Down branch's old comment about the "fall through to list hover-navigation" rationale is dropped — the cascade still exists, but the app-first ordering is the new contract and the comment was about the previous one.
This commit is contained in:
@@ -245,28 +245,26 @@ impl<A: App> AppData<A>
|
|||||||
}
|
}
|
||||||
Keysym::Down | Keysym::Up =>
|
Keysym::Down | Keysym::Up =>
|
||||||
{
|
{
|
||||||
// When a text input is focused, Up/Down move the cursor
|
|
||||||
// between lines first. They only fall through to list
|
|
||||||
// hover-navigation (combo / scrollable list) when the
|
|
||||||
// cursor was already on the topmost / bottommost line —
|
|
||||||
// that lets a user keyboard-step out of a multiline
|
|
||||||
// `text_edit` into surrounding navigable items without
|
|
||||||
// changing focus first.
|
|
||||||
let is_text = focused.and_then( |idx|
|
let is_text = focused.and_then( |idx|
|
||||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||||
let reverse = event.keysym == Keysym::Up;
|
let reverse = event.keysym == Keysym::Up;
|
||||||
let extend = self.shift_pressed;
|
let extend = self.shift_pressed;
|
||||||
let consumed = if is_text
|
let intercepted = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed );
|
||||||
|
if let Some( msg ) = intercepted
|
||||||
{
|
{
|
||||||
if reverse { self.handle_cursor_up( focus, extend ) }
|
self.pending_msgs.push( msg );
|
||||||
else { self.handle_cursor_down( focus, extend ) }
|
}
|
||||||
} else { false };
|
else
|
||||||
if !consumed && !self.move_keyboard_hover( focus, reverse )
|
|
||||||
{
|
{
|
||||||
if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
|
let consumed = if is_text
|
||||||
{
|
{
|
||||||
self.pending_msgs.push( msg );
|
if reverse { self.handle_cursor_up( focus, extend ) }
|
||||||
|
else { self.handle_cursor_down( focus, extend ) }
|
||||||
|
} else { false };
|
||||||
|
if !consumed && !self.move_keyboard_hover( focus, reverse )
|
||||||
|
{
|
||||||
|
// already None, no extra fire
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +274,12 @@ impl<A: App> AppData<A>
|
|||||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||||
let extend = self.shift_pressed;
|
let extend = self.shift_pressed;
|
||||||
if is_text
|
let intercepted = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed );
|
||||||
|
if let Some( msg ) = intercepted
|
||||||
|
{
|
||||||
|
self.pending_msgs.push( msg );
|
||||||
|
}
|
||||||
|
else if is_text
|
||||||
{
|
{
|
||||||
if event.keysym == Keysym::Left
|
if event.keysym == Keysym::Left
|
||||||
{
|
{
|
||||||
@@ -284,9 +287,6 @@ impl<A: App> AppData<A>
|
|||||||
} else {
|
} else {
|
||||||
self.handle_cursor_right( focus, extend );
|
self.handle_cursor_right( focus, extend );
|
||||||
}
|
}
|
||||||
} else if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
|
|
||||||
{
|
|
||||||
self.pending_msgs.push( msg );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Keysym::Home =>
|
Keysym::Home =>
|
||||||
@@ -334,11 +334,19 @@ impl<A: App> AppData<A>
|
|||||||
Keysym::Tab | Keysym::ISO_Left_Tab =>
|
Keysym::Tab | Keysym::ISO_Left_Tab =>
|
||||||
{
|
{
|
||||||
let reverse = event.keysym == Keysym::ISO_Left_Tab || self.shift_pressed;
|
let reverse = event.keysym == Keysym::ISO_Left_Tab || self.shift_pressed;
|
||||||
let ss = self.surface( focus );
|
let intercepted = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed );
|
||||||
let next_idx = next_focusable_index( &ss.widget_rects, ss.focused_idx, reverse );
|
if let Some( msg ) = intercepted
|
||||||
if let Some( next_idx ) = next_idx
|
|
||||||
{
|
{
|
||||||
self.set_focus( focus, Some( next_idx ), qh );
|
self.pending_msgs.push( msg );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let ss = self.surface( focus );
|
||||||
|
let next_idx = next_focusable_index( &ss.widget_rects, ss.focused_idx, reverse );
|
||||||
|
if let Some( next_idx ) = next_idx
|
||||||
|
{
|
||||||
|
self.set_focus( focus, Some( next_idx ), qh );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Keysym::Escape =>
|
Keysym::Escape =>
|
||||||
|
|||||||
Reference in New Issue
Block a user