event_loop: retry focus requests that land before the view is rebuilt
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

`take_focus_request()` looks up the target widget by `WidgetId` in `widget_rects`. Read-only `TextEdit` widgets are not tracked as interactive and therefore absent from `widget_rects`. A swipe-reveal flips the fields from read-only to interactive on `SlideRevealed`, but an intervening input event (e.g. the touch-up at the end of the swipe) can cause `dispatch()` to return before the vblank frame callback clears `frame_pending`, skipping the draw that would have rebuilt the widget tree. When the focus request fires in that same iteration it finds nothing and is consumed without effect.
`AppData` gains a `focus_retry` field. When `take_focus_request()` (or a previous retry) returns an id but the widget is not in `widget_rects`, the id is stored in `focus_retry`, `view_dirty` is set and a redraw is requested. The next time around the view has been rebuilt with interactive fields and the widget is present.
This commit is contained in:
2026-06-02 14:10:37 +02:00
parent e40ab637a6
commit ae8380b1ac
2 changed files with 10 additions and 1 deletions

View File

@@ -248,6 +248,9 @@ pub struct AppData<A: App>
/// Set after the first commit of a rendered buffer on the main surface. /// Set after the first commit of a rendered buffer on the main surface.
/// Used to drive `App::on_first_frame_committed` exactly once. /// Used to drive `App::on_first_frame_committed` exactly once.
pub first_frame_committed: bool, pub first_frame_committed: bool,
/// Focus request deferred because the target widget was absent from
/// `widget_rects` (e.g. read_only, not yet rebuilt). Retried next draw.
pub focus_retry: Option<crate::types::WidgetId>,
} }
impl<A: App> AppData<A> impl<A: App> AppData<A>

View File

@@ -329,6 +329,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
view_dirty: true, view_dirty: true,
overlays_dirty: true, overlays_dirty: true,
first_frame_committed: false, first_frame_committed: false,
focus_retry: None,
}; };
// Register a calloop channel so the app can send messages from any thread. // Register a calloop channel so the app can send messages from any thread.
@@ -809,7 +810,8 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
// Walks main + every overlay because the targeted widget can live // Walks main + every overlay because the targeted widget can live
// on any of them (a search field on a launcher overlay, a text // on any of them (a search field on a launcher overlay, a text
// edit on a dialog modal, …). // edit on a dialog modal, …).
if let Some( id ) = data.app.take_focus_request() let focus_id = data.app.take_focus_request().or_else( || data.focus_retry.take() );
if let Some( id ) = focus_id
{ {
let mut hit: Option<( SurfaceFocus, usize )> = data.main.widget_rects.iter() let mut hit: Option<( SurfaceFocus, usize )> = data.main.widget_rects.iter()
.find( |w| w.id == Some( id ) ) .find( |w| w.id == Some( id ) )
@@ -840,6 +842,10 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
} }
} }
} }
} else {
data.focus_retry = Some( id );
data.view_dirty = true;
data.main.request_redraw();
} }
} }
} }