From 3d237039c64b6cd450420a94bf216ee0f8251a13 Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Fri, 15 May 2026 00:40:01 +0200 Subject: [PATCH] =?UTF-8?q?input/keyboard:=20let=20the=20app=20intercept?= =?UTF-8?q?=20arrow=20/=20Tab=20keys=20before=20the=20default=20text-edit?= =?UTF-8?q?=20and=20focus-shift=20behaviours=20The=20keysym=20dispatcher?= =?UTF-8?q?=20used=20to=20give=20the=20focused=20widget=20first=20refusal?= =?UTF-8?q?=20on=20Left=20/=20Right=20/=20Up=20/=20Down=20/=20Tab.=20A=20`?= =?UTF-8?q?text=5Fedit`=20swallowed=20the=20four=20arrows=20for=20cursor?= =?UTF-8?q?=20movement,=20and=20Tab=20walked=20the=20keyboard-focus=20ring?= =?UTF-8?q?=20through=20`next=5Ffocusable=5Findex`.=20Apps=20only=20got=20?= =?UTF-8?q?`on=5Fkey=5Fwith=5Fmodifiers`=20for=20those=20keys=20when=20no?= =?UTF-8?q?=20text=20input=20was=20focused=20(Tab=20never),=20so=20a=20sea?= =?UTF-8?q?rch=20field=20with=20autocomplete=20couldn't=20drive=20a=20sele?= =?UTF-8?q?ction=20cursor=20through=20arrows=20or=20Tab=20without=20puttin?= =?UTF-8?q?g=20focus=20on=20something=20else=20first=20=E2=80=94=20which?= =?UTF-8?q?=20is=20exactly=20the=20wrong=20UX=20for=20a=20search-as-you-ty?= =?UTF-8?q?pe=20list.=20Three=20arms=20in=20`AppData::handle=5Fkey=5Fpress?= =?UTF-8?q?`=20now=20query=20`self.app.on=5Fkey=5Fwith=5Fmodifiers(=20keys?= =?UTF-8?q?ym,=20ctrl,=20shift=20)`=20*first*=20and=20only=20fall=20back?= =?UTF-8?q?=20to=20the=20default=20behaviour=20if=20the=20app=20returns=20?= =?UTF-8?q?`None`:=20=20=20-=20`Left=20|=20Right`:=20previously=20branched?= =?UTF-8?q?=20on=20`is=5Ftext=5Finput`=20and=20routed=20the=20keypress=20t?= =?UTF-8?q?o=20`handle=5Fcursor=5Fleft/right`=20for=20text=20fields,=20oth?= =?UTF-8?q?erwise=20asked=20the=20app.=20Now:=20app=20first;=20if=20app=20?= =?UTF-8?q?says=20`None`,=20the=20existing=20text-cursor=20path=20runs=20f?= =?UTF-8?q?or=20text=20inputs=20and=20non-text=20widgets=20get=20nothing?= =?UTF-8?q?=20(their=20previous=20fallback=20already=20produced=20no=20use?= =?UTF-8?q?ful=20action=20without=20an=20app=20handler).=20=20=20-=20`Up?= =?UTF-8?q?=20|=20Down`:=20the=20previous=20logic=20was=20a=20four-way=20d?= =?UTF-8?q?ecision=20that=20tried=20text-cursor=20movement,=20then=20`move?= =?UTF-8?q?=5Fkeyboard=5Fhover`=20for=20combo=20/=20list=20widgets,=20then?= =?UTF-8?q?=20the=20app=20handler.=20Now=20the=20app=20gets=20first=20refu?= =?UTF-8?q?sal=20too.=20When=20it=20declines,=20the=20original=20cascade?= =?UTF-8?q?=20(text-cursor=20=E2=86=92=20hover=20navigation)=20still=20run?= =?UTF-8?q?s,=20so=20multiline=20`text=5Fedit`=20cursor=20walking=20and=20?= =?UTF-8?q?combo=20/=20scrollable-list=20keyboard=20nav=20are=20unchanged?= =?UTF-8?q?=20for=20any=20app=20that=20doesn't=20intercept=20these=20keys.?= =?UTF-8?q?=20=20=20-=20`Tab=20|=20ISO=5FLeft=5FTab`:=20app=20first;=20on?= =?UTF-8?q?=20`None`=20the=20focus-shift=20path=20(`next=5Ffocusable=5Find?= =?UTF-8?q?ex`=20+=20`set=5Ffocus`)=20runs.=20Apps=20that=20want=20Tab=20a?= =?UTF-8?q?s=20a=20navigation=20message=20between=20custom=20UI=20states?= =?UTF-8?q?=20(a=20search=20field=20cycling=20through=20results,=20a=20wiz?= =?UTF-8?q?ard=20advancing=20pages)=20finally=20have=20a=20hook;=20apps=20?= =?UTF-8?q?that=20don't=20get=20the=20standard=20tab-through-focusable=20b?= =?UTF-8?q?ehaviour=20by=20leaving=20`on=5Fkey=5Fwith=5Fmodifiers`=20retur?= =?UTF-8?q?ning=20`None`=20for=20Tab,=20which=20is=20the=20trait's=20defau?= =?UTF-8?q?lt.=20The=20interception=20is=20keysym-only=20=E2=80=94=20modif?= =?UTF-8?q?ier=20state=20is=20forwarded=20so=20an=20app=20can=20distinguis?= =?UTF-8?q?h=20`Tab`=20from=20`Shift+Tab`,=20`Right`=20from=20`Ctrl+Right`?= =?UTF-8?q?.=20The=20text-input=20check=20is=20preserved=20as=20a=20local?= =?UTF-8?q?=20before=20the=20dispatch=20so=20the=20fallback=20doesn't=20lo?= =?UTF-8?q?se=20the=20"is=20this=20even=20a=20text=20edit=3F"=20question;?= =?UTF-8?q?=20it=20just=20runs=20*after*=20the=20app=20instead=20of=20befo?= =?UTF-8?q?re.=20Net=20effect:=20a=20list=20/=20autocomplete=20that=20need?= =?UTF-8?q?s=20arrow=20/=20Tab=20navigation=20can=20now=20be=20implemented?= =?UTF-8?q?=20purely=20by=20overriding=20`App::on=5Fkey=5Fwith=5Fmodifiers?= =?UTF-8?q?`,=20with=20no=20changes=20to=20`text=5Fedit`=20itself=20and=20?= =?UTF-8?q?no=20risk=20of=20breaking=20apps=20that=20don't=20need=20it.=20?= =?UTF-8?q?The=20Up=20/=20Down=20branch's=20old=20comment=20about=20the=20?= =?UTF-8?q?"fall=20through=20to=20list=20hover-navigation"=20rationale=20i?= =?UTF-8?q?s=20dropped=20=E2=80=94=20the=20cascade=20still=20exists,=20but?= =?UTF-8?q?=20the=20app-first=20ordering=20is=20the=20new=20contract=20and?= =?UTF-8?q?=20the=20comment=20was=20about=20the=20previous=20one.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/keyboard.rs | 52 +++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs index a12b704..3c0ef09 100644 --- a/src/input/keyboard.rs +++ b/src/input/keyboard.rs @@ -245,28 +245,26 @@ impl AppData } 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| find_handlers( &self.surface( focus ).widget_rects, idx ) .map( |h| h.is_text_input() ) ).unwrap_or( false ); let reverse = event.keysym == Keysym::Up; 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 ) } - else { self.handle_cursor_down( focus, extend ) } - } else { false }; - if !consumed && !self.move_keyboard_hover( focus, reverse ) + self.pending_msgs.push( msg ); + } + else { - 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 AppData find_handlers( &self.surface( focus ).widget_rects, idx ) .map( |h| h.is_text_input() ) ).unwrap_or( false ); 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 { @@ -284,9 +287,6 @@ impl AppData } else { 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 => @@ -334,11 +334,19 @@ impl AppData Keysym::Tab | Keysym::ISO_Left_Tab => { let reverse = event.keysym == Keysym::ISO_Left_Tab || self.shift_pressed; - 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 + let intercepted = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed ); + if let Some( msg ) = intercepted { - 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 =>