diff --git a/Cargo.lock b/Cargo.lock index 00016501..db2ed667 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,6 +1229,7 @@ dependencies = [ "ahash", "backtrace", "document-features", + "egui_kittest", "emath", "epaint", "log", diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 487a4eac..d000c0da 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -98,3 +98,7 @@ log = { workspace = true, optional = true } puffin = { workspace = true, optional = true } ron = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["derive", "rc"] } + + +[dev-dependencies] +egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 331430c7..99a1b536 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1160,6 +1160,8 @@ impl Context { /// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)). #[allow(clippy::too_many_arguments)] pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response { + let interested_in_focus = w.enabled && w.sense.focusable && w.layer_id.allow_interaction(); + // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1169,12 +1171,12 @@ impl Context { // but also to know when we have reached the widget we are checking for cover. viewport.this_pass.widgets.insert(w.layer_id, w); - if allow_focus && w.sense.focusable { + if allow_focus && interested_in_focus { ctx.memory.interested_in_focus(w.id); } }); - if allow_focus && (!w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction()) { + if allow_focus && !interested_in_focus { // Not interested or allowed input: self.memory_mut(|mem| mem.surrender_focus(w.id)); } diff --git a/crates/egui/tests/regression_tests.rs b/crates/egui/tests/regression_tests.rs new file mode 100644 index 00000000..b2423571 --- /dev/null +++ b/crates/egui/tests/regression_tests.rs @@ -0,0 +1,30 @@ +use egui::Button; +use egui_kittest::kittest::Queryable; +use egui_kittest::Harness; + +#[test] +pub fn focus_should_skip_over_disabled_buttons() { + let mut harness = Harness::new_ui(|ui| { + ui.add(Button::new("Button 1")); + ui.add_enabled(false, Button::new("Button Disabled")); + ui.add(Button::new("Button 3")); + }); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_1 = harness.get_by_name("Button 1"); + assert!(button_1.is_focused()); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_3 = harness.get_by_name("Button 3"); + assert!(button_3.is_focused()); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_1 = harness.get_by_name("Button 1"); + assert!(button_1.is_focused()); +} diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 8e410d84..2c75732b 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -249,6 +249,25 @@ impl<'a, State> Harness<'a, State> { pub fn state_mut(&mut self) -> &mut State { &mut self.state } + + /// Press a key. + /// This will create a key down event and a key up event. + pub fn press_key(&mut self, key: egui::Key) { + self.input.events.push(egui::Event::Key { + key, + pressed: true, + modifiers: Default::default(), + repeat: false, + physical_key: None, + }); + self.input.events.push(egui::Event::Key { + key, + pressed: false, + modifiers: Default::default(), + repeat: false, + physical_key: None, + }); + } } /// Utilities for stateless harnesses.