1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Fix #2142 - lost_focus not firing after a mid-frame focus transfer (#8210)

### Problem
`Response::lost_focus()` could silently fail to fire when keyboard focus
moved from one widget to another *within the same frame* — for example,
clicking a `TextEdit` that was added to the UI *after* the
currently-focused one.

### Fix
This widens the detection window by one extra frame, which is exactly
enough for the deferred loss signal to reach the previously focused
widget on its next render.

### Notes
* The `test_demo_app` test fails, but it has nothing to do with this PR;
it fails on the current master branch, too.

* This PR replaces https://github.com/emilk/egui/pull/3247
* Closes <https://github.com/emilk/egui/issues/2142>
* [x] I have followed the instructions in the PR template
This commit is contained in:
Francesco Cinà
2026-06-10 10:00:55 +02:00
committed by GitHub
parent 704d86e4aa
commit b3e4cde85a

View File

@@ -490,6 +490,13 @@ pub(crate) struct Focus {
/// The ID of a widget that had keyboard focus during the previous frame.
id_previous_frame: Option<Id>,
/// The ID of a widget that had keyboard focus *two* frames ago.
///
/// Kept so `Response::lost_focus` can still fire after a mid-frame
/// focus transition (e.g. clicking a `TextEdit` that was added to
/// the UI later than the currently focused one).
id_two_frames_ago: Option<Id>,
/// The ID of a widget to give the focus to in the next frame.
id_next_frame: Option<Id>,
@@ -545,6 +552,7 @@ impl Focus {
}
fn begin_pass(&mut self, new_input: &crate::data::input::RawInput) {
self.id_two_frames_ago = self.id_previous_frame;
self.id_previous_frame = self.focused();
if let Some(id) = self.id_next_frame.take() {
self.focused_widget = Some(FocusWidget::new(id));
@@ -831,10 +839,21 @@ impl Memory {
self.focus().and_then(|f| f.id_previous_frame) == Some(id)
}
/// Check if the layer lost focus last frame.
/// returns `true` if the layer lost focus last frame, but not this one.
/// Check if the widget lost keyboard focus.
///
/// Returns `true` when `id` was the focused widget at the start
/// of this frame *or* the start of the previous frame — but is
/// not focused now. The two-frame window matters when focus
/// transfers mid-frame: the previously-focused widget has
/// usually already been rendered by the time another widget
/// claims focus, so the loss signal can only reach it on its
/// next render pass.
pub(crate) fn lost_focus(&self, id: Id) -> bool {
self.had_focus_last_frame(id) && !self.has_focus(id)
let had_recent_focus = self
.focus()
.map(|f| f.id_previous_frame == Some(id) || f.id_two_frames_ago == Some(id))
.unwrap_or(false);
had_recent_focus && !self.has_focus(id)
}
/// Check if the layer gained focus this frame.
@@ -1368,6 +1387,54 @@ fn memory_impl_send_sync() {
assert_send_sync::<Memory>();
}
// Regression test for https://github.com/emilk/egui/issues/2142.
#[test]
fn lost_focus_fires_after_mid_frame_focus_transfer() {
use crate::data::input::RawInput;
let a = Id::new("A");
let b = Id::new("B");
let mut focus = Focus::default();
let raw = RawInput::default();
fn lost_focus_check(focus: &Focus, id: Id) -> bool {
let was_focused =
focus.id_previous_frame == Some(id) || focus.id_two_frames_ago == Some(id);
was_focused && focus.focused() != Some(id)
}
// Frame N-1
{
focus.begin_pass(&raw);
focus.focused_widget = Some(FocusWidget::new(a));
}
// Frame N: `A` is focused at start; user clicks `B` mid-frame
{
focus.begin_pass(&raw);
assert_eq!(focus.id_previous_frame, Some(a));
assert!(!lost_focus_check(&focus, a));
focus.focused_widget = Some(FocusWidget::new(b));
}
// Frame N+1: `A` deferred lost_focus signal must fire
{
focus.begin_pass(&raw);
assert_eq!(focus.id_two_frames_ago, Some(a));
assert_eq!(focus.id_previous_frame, Some(b));
assert!(lost_focus_check(&focus, a), "`A` lost_focus must fire");
assert!(!lost_focus_check(&focus, b));
}
// Frame N+2
{
focus.begin_pass(&raw);
assert!(
!lost_focus_check(&focus, a),
"A's lost_focus must stop firing once the two-frame window passes",
);
}
}
#[test]
fn order_map_total_ordering() {
let mut layers = [