fix(input): match layer keyboard focus to fullscreen visibility (#49)
* fix(input): require mapped layers for keyboard focus * fix(input): match layer keyboard focus to fullscreen visibility --------- Co-authored-by: Samuel Onoja <[email protected]>
This commit is contained in:
1 parent
b5f7793870
commit
9251522a69
8 files changed
+312
-4
No files matched your search
@@ -205,7 +205,8 @@ impl Ferese {
|
|||||||
})
|
})
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
&& self.space.outputs().any(|output| {
|
&& self.space.outputs().any(|output| {
|
||||||
layer_map_for_output(output)
|
self.layer_visible_on_output(output, layer.layer())
|
||||||
|
&& layer_map_for_output(output)
|
||||||
.layers()
|
.layers()
|
||||||
.any(|candidate| candidate == layer)
|
.any(|candidate| candidate == layer)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -246,10 +246,14 @@ fn on_demand_layer_focus_follows_opening_and_outside_clicks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fixture() -> (Ferese, EventLoop<'static, Ferese>, Output) {
|
fn fixture() -> (Ferese, EventLoop<'static, Ferese>, Output) {
|
||||||
|
fixture_with_animations(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_with_animations(animations: bool) -> (Ferese, EventLoop<'static, Ferese>, Output) {
|
||||||
let mut events = EventLoop::try_new().unwrap();
|
let mut events = EventLoop::try_new().unwrap();
|
||||||
let mut config = crate::config::Config::default().runtime_config().unwrap();
|
let mut config = crate::config::Config::default().runtime_config().unwrap();
|
||||||
config.wallpaper.path = None;
|
config.wallpaper.path = None;
|
||||||
config.animations_enabled = false;
|
config.animations_enabled = animations;
|
||||||
let mut state = Ferese::new(
|
let mut state = Ferese::new(
|
||||||
&mut events,
|
&mut events,
|
||||||
smithay::reexports::wayland_server::Display::new().unwrap(),
|
smithay::reexports::wayland_server::Display::new().unwrap(),
|
||||||
@@ -466,3 +470,292 @@ fn layer_mapping_commits_do_not_override_session_lock() {
|
|||||||
"layer destruction preserves lock-owned focus policy"
|
"layer destruction preserves lock-owned focus policy"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fullscreen_hidden_top_commits_cannot_steal_keyboard_focus() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"input::layer_focus_tests::fullscreen_hidden_top_commits_cannot_steal_keyboard_focus",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut state, mut events, output) = fixture();
|
||||||
|
let (window, _window_wire) = window(&mut state, &mut events, 0xff112233);
|
||||||
|
let id = state.windows.ids()[&window];
|
||||||
|
assert!(state.activate_managed_window(id));
|
||||||
|
let fallback = Some(window.toplevel().unwrap().wl_surface().clone());
|
||||||
|
let keyboard = state.seat.get_keyboard().unwrap();
|
||||||
|
state.set_window_fullscreen(id, true);
|
||||||
|
let (top, mut top_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 2);
|
||||||
|
assert!(state.output_has_fullscreen(&output));
|
||||||
|
assert!(
|
||||||
|
state.layer_under((20., 20.).into()).is_none(),
|
||||||
|
"Top is excluded from hit testing by fullscreen policy"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
fallback,
|
||||||
|
"a hidden Top layer's map commit cannot steal keyboard focus"
|
||||||
|
);
|
||||||
|
commit_policy(&mut state, &mut events, &mut top_wire, 1);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
fallback,
|
||||||
|
"hidden animation commits cannot steal focus"
|
||||||
|
);
|
||||||
|
state.set_window_fullscreen(id, false);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
Some(top.wl_surface().clone()),
|
||||||
|
"fullscreen exit restores visible exclusive layers"
|
||||||
|
);
|
||||||
|
assert_eq!(state.layer_under((20., 20.).into()).unwrap().0, top);
|
||||||
|
state.set_window_fullscreen(id, true);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
fallback,
|
||||||
|
"fullscreen entry revokes hidden Top focus immediately"
|
||||||
|
);
|
||||||
|
let (overlay, mut overlay_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 3);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
Some(overlay.wl_surface().clone()),
|
||||||
|
"Overlay remains eligible over fullscreen"
|
||||||
|
);
|
||||||
|
assert_eq!(state.layer_under((20., 20.).into()).unwrap().0, overlay);
|
||||||
|
request(&mut overlay_wire, 3, 7, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
fallback,
|
||||||
|
"destroying Overlay must not restore hidden Top"
|
||||||
|
);
|
||||||
|
request(&mut top_wire, 2, 1, &[0, 0, 0], None);
|
||||||
|
request(&mut top_wire, 2, 6, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
request(&mut top_wire, 3, 0, &[65, 48], None);
|
||||||
|
request(&mut top_wire, 2, 6, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
ack_latest_configure(&mut top_wire);
|
||||||
|
request(&mut top_wire, 2, 1, &[5, 0, 0], None);
|
||||||
|
request(&mut top_wire, 2, 6, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
fallback,
|
||||||
|
"hidden remapping cannot reclaim focus"
|
||||||
|
);
|
||||||
|
state.set_window_fullscreen(id, false);
|
||||||
|
commit_policy(&mut state, &mut events, &mut top_wire, 2);
|
||||||
|
state.focus_window_at((20., 20.).into(), SERIAL_COUNTER.next_serial(), true);
|
||||||
|
assert_eq!(keyboard.current_focus(), Some(top.wl_surface().clone()));
|
||||||
|
let pointer = state.seat.get_pointer().unwrap();
|
||||||
|
pointer.motion(
|
||||||
|
&mut state,
|
||||||
|
Some((top.wl_surface().clone(), (0., 0.).into())),
|
||||||
|
&MotionEvent {
|
||||||
|
location: (20., 20.).into(),
|
||||||
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
state.set_window_fullscreen(id, true);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
fallback,
|
||||||
|
"fullscreen also revokes on-demand Top focus"
|
||||||
|
);
|
||||||
|
commit_policy(&mut state, &mut events, &mut top_wire, 0);
|
||||||
|
commit_policy(&mut state, &mut events, &mut top_wire, 2);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
fallback,
|
||||||
|
"a stale pointer owner cannot activate a hidden panel"
|
||||||
|
);
|
||||||
|
state.set_window_fullscreen(id, false);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
fallback,
|
||||||
|
"on-demand layers require interaction after fullscreen exit"
|
||||||
|
);
|
||||||
|
state.focus_window_at((20., 20.).into(), SERIAL_COUNTER.next_serial(), true);
|
||||||
|
assert_eq!(keyboard.current_focus(), Some(top.wl_surface().clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid_layer_popup_grab_survives_commits_but_not_parent_unmapping() {
|
||||||
|
use smithay::desktop::{PopupKeyboardGrab, PopupKind};
|
||||||
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase;
|
||||||
|
use smithay::wayland::shell::xdg::{XdgShellHandler, XdgWmBaseUserData};
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"input::layer_focus_tests::valid_layer_popup_grab_survives_commits_but_not_parent_unmapping",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut state, mut events, output) = fixture();
|
||||||
|
let (top, mut wire) = layer_with_policy(&mut state, &mut events, &output, 2, 2);
|
||||||
|
state.focus_window_at((20., 20.).into(), SERIAL_COUNTER.next_serial(), true);
|
||||||
|
let client = top.wl_surface().client().unwrap();
|
||||||
|
let compositor = client
|
||||||
|
.create_resource::<WlCompositor, (), Ferese>(&state.display_handle, 6, ())
|
||||||
|
.unwrap();
|
||||||
|
let shell = client
|
||||||
|
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default())
|
||||||
|
.unwrap();
|
||||||
|
request(&mut wire, shell.id().protocol_id(), 1, &[6], None); // Positioner.
|
||||||
|
request(&mut wire, 6, 1, &[32, 24], None);
|
||||||
|
request(&mut wire, 6, 2, &[0, 0, 32, 24], None);
|
||||||
|
request(&mut wire, compositor.id().protocol_id(), 0, &[7], None);
|
||||||
|
request(&mut wire, shell.id().protocol_id(), 2, &[8, 7], None);
|
||||||
|
request(&mut wire, 8, 2, &[9, 0, 6], None); // Popup with layer parent assigned next.
|
||||||
|
request(&mut wire, 3, 5, &[9], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
let popup = state
|
||||||
|
.xdg_shell_state()
|
||||||
|
.popup_surfaces()
|
||||||
|
.iter()
|
||||||
|
.find(|popup| {
|
||||||
|
popup
|
||||||
|
.wl_surface()
|
||||||
|
.client()
|
||||||
|
.is_some_and(|owner| owner.id() == client.id())
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
|
let seat = state.seat.clone();
|
||||||
|
let grab = state
|
||||||
|
.popups
|
||||||
|
.grab_popup(top.wl_surface().clone(), PopupKind::Xdg(popup.clone()), &seat, serial)
|
||||||
|
.unwrap();
|
||||||
|
let keyboard = state.seat.get_keyboard().unwrap();
|
||||||
|
keyboard.set_grab(&mut state, PopupKeyboardGrab::new(&grab), serial);
|
||||||
|
keyboard.set_focus(&mut state, grab.current_grab(), serial);
|
||||||
|
let popup_focus = Some(popup.wl_surface().clone());
|
||||||
|
assert_eq!(keyboard.current_focus(), popup_focus);
|
||||||
|
commit_policy(&mut state, &mut events, &mut wire, 2);
|
||||||
|
assert!(keyboard.is_grabbed());
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
popup_focus,
|
||||||
|
"valid popup grabs survive parent commits"
|
||||||
|
);
|
||||||
|
let (_overlay, _overlay_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 3);
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
popup_focus,
|
||||||
|
"new exclusive layers do not interrupt a valid popup grab"
|
||||||
|
);
|
||||||
|
request(&mut wire, 2, 1, &[0, 0, 0], None);
|
||||||
|
request(&mut wire, 2, 6, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
assert!(!keyboard.is_grabbed(), "unmapping revokes the parent popup grab");
|
||||||
|
assert_ne!(keyboard.current_focus(), popup_focus);
|
||||||
|
assert_ne!(keyboard.current_focus(), Some(top.wl_surface().clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fullscreen_layer_eligibility_is_per_output_and_respects_overview() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"input::layer_focus_tests::fullscreen_layer_eligibility_is_per_output_and_respects_overview",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut state, mut events, output) = fixture();
|
||||||
|
let (window, _window_wire) = window(&mut state, &mut events, 0xff112233);
|
||||||
|
let id = state.windows.ids()[&window];
|
||||||
|
assert!(state.activate_managed_window(id));
|
||||||
|
state.set_window_fullscreen(id, true);
|
||||||
|
let (top, _top_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 2);
|
||||||
|
assert!(!state.layer_keyboard_eligible(&top));
|
||||||
|
assert!(!state.layer_visible_for_frame(&output, smithay::wayland::shell::wlr_layer::Layer::Top, false));
|
||||||
|
assert!(state.layer_visible_for_frame(&output, smithay::wayland::shell::wlr_layer::Layer::Top, true));
|
||||||
|
state.set_overview_active(true);
|
||||||
|
assert!(state.layer_keyboard_eligible(&top));
|
||||||
|
assert_eq!(
|
||||||
|
state.seat.get_keyboard().unwrap().current_focus(),
|
||||||
|
Some(top.wl_surface().clone())
|
||||||
|
);
|
||||||
|
state.set_overview_active(false);
|
||||||
|
assert!(!state.layer_keyboard_eligible(&top));
|
||||||
|
assert_eq!(
|
||||||
|
state.seat.get_keyboard().unwrap().current_focus(),
|
||||||
|
Some(window.toplevel().unwrap().wl_surface().clone())
|
||||||
|
);
|
||||||
|
let other = Output::new(
|
||||||
|
"other".into(),
|
||||||
|
PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
other.change_current_state(
|
||||||
|
Some(Mode {
|
||||||
|
size: (800, 600).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
state.space.map_output(&other, (800, 0));
|
||||||
|
state.register_output(&other, "other".into());
|
||||||
|
// Move the test role into the second output's map to isolate visibility
|
||||||
|
// policy from the separately tested output-selection policy.
|
||||||
|
let (other_top, mut wire) = layer_with_policy(&mut state, &mut events, &output, 0, 2);
|
||||||
|
layer_map_for_output(&output).unmap_layer(&other_top);
|
||||||
|
layer_map_for_output(&other).map_layer(&other_top).unwrap();
|
||||||
|
commit_policy(&mut state, &mut events, &mut wire, 1);
|
||||||
|
assert!(
|
||||||
|
state.layer_keyboard_eligible(&other_top),
|
||||||
|
"fullscreen on the first output does not hide Top on another"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.seat.get_keyboard().unwrap().current_focus(),
|
||||||
|
Some(other_top.wl_surface().clone())
|
||||||
|
);
|
||||||
|
state.unregister_output(&other);
|
||||||
|
assert!(!state.layer_keyboard_eligible(&other_top));
|
||||||
|
assert_eq!(
|
||||||
|
state.seat.get_keyboard().unwrap().current_focus(),
|
||||||
|
Some(window.toplevel().unwrap().wl_surface().clone())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn animated_overview_exit_revokes_top_focus_when_it_becomes_hidden() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"input::layer_focus_tests::animated_overview_exit_revokes_top_focus_when_it_becomes_hidden",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut state, mut events, output) = fixture_with_animations(true);
|
||||||
|
let (window, _window_wire) = window(&mut state, &mut events, 0xff112233);
|
||||||
|
let id = state.windows.ids()[&window];
|
||||||
|
assert!(state.activate_managed_window(id));
|
||||||
|
state.set_window_fullscreen(id, true);
|
||||||
|
let (top, _wire) = layer_with_policy(&mut state, &mut events, &output, 1, 2);
|
||||||
|
let keyboard = state.seat.get_keyboard().unwrap();
|
||||||
|
state.set_overview_active(true);
|
||||||
|
assert!(state.layer_keyboard_eligible(&top));
|
||||||
|
assert_eq!(keyboard.current_focus(), Some(top.wl_surface().clone()));
|
||||||
|
let tick = std::time::Instant::now();
|
||||||
|
for step in 1..=300 {
|
||||||
|
state.advance_animations(tick + std::time::Duration::from_millis(16 * step));
|
||||||
|
}
|
||||||
|
state.set_overview_active(false);
|
||||||
|
assert!(
|
||||||
|
state.layer_keyboard_eligible(&top),
|
||||||
|
"Top remains visible during the overview exit animation"
|
||||||
|
);
|
||||||
|
for step in 1..=300 {
|
||||||
|
state.advance_animations(tick + std::time::Duration::from_millis(4800 + 16 * step));
|
||||||
|
}
|
||||||
|
assert!(!state.layer_keyboard_eligible(&top));
|
||||||
|
assert_eq!(
|
||||||
|
keyboard.current_focus(),
|
||||||
|
Some(window.toplevel().unwrap().wl_surface().clone()),
|
||||||
|
"completion of the visibility transition revokes focus without a client commit"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -637,6 +637,7 @@ impl Ferese {
|
|||||||
self.overview.exit(normal, self.animations_enabled());
|
self.overview.exit(normal, self.animations_enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.reconcile_layer_keyboard_focus();
|
||||||
self.notify_overview_state();
|
self.notify_overview_state();
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ fn scene_elements(
|
|||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let upper_layers: &[Layer] = if state.output_has_fullscreen_for_frame(output, frame.overview.is_presenting()) {
|
let upper_layers: &[Layer] = if !state.layer_visible_for_frame(output, Layer::Top, frame.overview.is_presenting()) {
|
||||||
&[Layer::Overlay]
|
&[Layer::Overlay]
|
||||||
} else {
|
} else {
|
||||||
&[Layer::Overlay, Layer::Top]
|
&[Layer::Overlay, Layer::Top]
|
||||||
|
|||||||
@@ -519,9 +519,13 @@ impl Ferese {
|
|||||||
if !self.overview.is_active() {
|
if !self.overview.is_active() {
|
||||||
self.retarget_overview();
|
self.retarget_overview();
|
||||||
}
|
}
|
||||||
|
let overview_was_presenting = self.overview.is_presenting();
|
||||||
active_animation |= self
|
active_animation |= self
|
||||||
.overview
|
.overview
|
||||||
.advance(delta, self.spring_config, self.animations_enabled);
|
.advance(delta, self.spring_config, self.animations_enabled);
|
||||||
|
if overview_was_presenting != self.overview.is_presenting() {
|
||||||
|
self.reconcile_layer_keyboard_focus();
|
||||||
|
}
|
||||||
self.refresh_workspace_slide_offsets();
|
self.refresh_workspace_slide_offsets();
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,14 @@ impl Ferese {
|
|||||||
.is_some_and(|workspace| workspace.fullscreen.is_some())
|
.is_some_and(|workspace| workspace.fullscreen.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn layer_visible_on_output(&self, output: &Output, layer: Layer) -> bool {
|
||||||
|
layer != Layer::Top || !self.output_has_fullscreen(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn layer_visible_for_frame(&self, output: &Output, layer: Layer, overview: bool) -> bool {
|
||||||
|
layer != Layer::Top || !self.output_has_fullscreen_for_frame(output, overview)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) {
|
pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) {
|
||||||
match self.workspaces.set_fullscreen(window, enabled) {
|
match self.workspaces.set_fullscreen(window, enabled) {
|
||||||
Ok(true) => self.relayout_window(window),
|
Ok(true) => self.relayout_window(window),
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ impl Ferese {
|
|||||||
let map = layer_map_for_output(output);
|
let map = layer_map_for_output(output);
|
||||||
|
|
||||||
for requested in layers {
|
for requested in layers {
|
||||||
if *requested == Layer::Top && self.output_has_fullscreen(output) {
|
if !self.layer_visible_on_output(output, *requested) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for layer in map.layers_on(*requested).rev() {
|
for layer in map.layers_on(*requested).rev() {
|
||||||
|
|||||||
@@ -641,6 +641,7 @@ impl Ferese {
|
|||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
self.retarget_overview();
|
self.retarget_overview();
|
||||||
self.reconcile_tab_indicator();
|
self.reconcile_tab_indicator();
|
||||||
|
self.reconcile_layer_keyboard_focus();
|
||||||
if !finalize {
|
if !finalize {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user