fix(panel): close Morph after outside clicks

This commit is contained in:
Samuel Onoja committed 2026-10-13 03:29:54 +01:00
1 parent f57c68454a
commit 8007f7a107
5 files changed
+311 -7

No files matched your search

+25 -7
View File
@@ -123,15 +123,20 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let policy = layer.cached_state();
let current = LayoutState::current(&layer); let current = LayoutState::current(&layer);
let previous = layer let previous = layer
.user_data() .user_data()
.get_or_insert_threadsafe(|| std::sync::Mutex::new(None::<LayoutState>)); .get_or_insert_threadsafe(|| std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>));
let changed = { let (changed, requested_focus) = {
let mut previous = previous.lock().unwrap(); let mut previous = previous.lock().unwrap();
let changed = previous.as_ref() != Some(&current); let changed = previous.as_ref().map(|(layout, _)| layout) != Some(&current);
*previous = Some(current); let requested_focus = policy.keyboard_interactivity == KeyboardInteractivity::OnDemand
changed && previous
.as_ref()
.is_some_and(|(_, keyboard)| *keyboard == KeyboardInteractivity::None);
*previous = Some((current, policy.keyboard_interactivity));
(changed, requested_focus)
}; };
if changed { if changed {
@@ -140,10 +145,23 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
// Configure acknowledgement and keyboard policy are independent of layout. // Configure acknowledgement and keyboard policy are independent of layout.
layer.layer_surface().send_pending_configure(); layer.layer_surface().send_pending_configure();
let policy = layer.cached_state(); // A click can enable keyboard interaction after its button event was
// delivered. Give that layer focus without requiring a second click.
let pointer_on_layer = requested_focus
&& !state.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed())
&& state
.seat
.get_pointer()
.and_then(|pointer| pointer.current_focus())
.is_some_and(|surface| {
state
.space
.layer_for_surface(&surface, WindowSurfaceType::ALL)
.is_some_and(|focused| focused == layer)
});
if !state.session_lock.active() if !state.session_lock.active()
&& !state.input_capture.captures(1) && !state.input_capture.captures(1)
&& policy.keyboard_interactivity == KeyboardInteractivity::Exclusive && (policy.keyboard_interactivity == KeyboardInteractivity::Exclusive || pointer_on_layer)
&& matches!(policy.layer, Layer::Top | Layer::Overlay) && matches!(policy.layer, Layer::Top | Layer::Overlay)
&& !layer_has_keyboard_focus(state, &layer) && !layer_has_keyboard_focus(state, &layer)
{ {
+26
View File
@@ -27,6 +27,8 @@ const BTN_RIGHT: u32 = 0x111;
#[cfg(test)] #[cfg(test)]
pub(crate) mod binding_mode_tests; pub(crate) mod binding_mode_tests;
#[cfg(test)]
mod layer_focus_tests;
pub(crate) struct LockedPointerHint { pub(crate) struct LockedPointerHint {
pub(crate) surface: WlSurface, pub(crate) surface: WlSurface,
@@ -938,6 +940,16 @@ impl Ferese {
if let Some((layer, _, _)) = self.layer_under(position) { if let Some((layer, _, _)) = self.layer_under(position) {
if layer.can_receive_keyboard_focus() { if layer.can_receive_keyboard_focus() {
keyboard.set_focus(self, Some(layer.wl_surface().clone()), serial); keyboard.set_focus(self, Some(layer.wl_surface().clone()), serial);
} else if raise
&& keyboard.current_focus().is_some_and(|surface| {
self.space
.layer_for_surface(&surface, smithay::desktop::WindowSurfaceType::ALL)
.is_some()
})
{
// Clicking a noninteractive layer (such as wallpaper) must
// release an expanded panel's focus too.
self.restore_keyboard_focus();
} }
return; return;
} }
@@ -1049,6 +1061,20 @@ impl Ferese {
if self.layer_under(position).is_some() { if self.layer_under(position).is_some() {
return; return;
} }
// An on-demand layer owns keyboard interaction until the next click.
// Moving the pointer away must not dismiss its expanded content.
if self
.seat
.get_keyboard()
.and_then(|keyboard| keyboard.current_focus())
.is_some_and(|surface| {
self.space
.layer_for_surface(&surface, smithay::desktop::WindowSurfaceType::ALL)
.is_some()
})
{
return;
}
let Some(window) = self.window_under_visual(position) else { let Some(window) = self.window_under_visual(position) else {
return; return;
+223
View File
@@ -0,0 +1,223 @@
use super::*;
use crate::state::ClientState;
use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window as window};
use smithay::desktop::{LayerSurface, layer_map_for_output};
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_shell_v1::ZwlrLayerShellV1;
use smithay::reexports::wayland_server::protocol::{wl_compositor::WlCompositor, wl_shm::WlShm};
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::Arc;
fn layer(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, output: &Output) -> (LayerSurface, UnixStream) {
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(ClientState::default()))
.unwrap();
let compositor = client
.create_resource::<WlCompositor, (), Ferese>(&state.display_handle, 6, ())
.unwrap();
let layers = client
.create_resource::<ZwlrLayerShellV1, (), Ferese>(&state.display_handle, 4, ())
.unwrap();
let shm = client
.create_resource::<WlShm, (), Ferese>(&state.display_handle, 1, ())
.unwrap();
request(&mut wire, compositor.id().protocol_id(), 0, &[2], None);
request(
&mut wire,
layers.id().protocol_id(),
0,
&[3, 2, 0, 2, 4, u32::from_ne_bytes(*b"bar\0")],
None,
);
request(&mut wire, 3, 0, &[64, 48], None);
request(&mut wire, 3, 1, &[1 | 4], None); // Top left.
request(&mut wire, 2, 6, &[], None);
dispatch(events, state);
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
assert_eq!(
wire.read_to_end(&mut bytes).unwrap_err().kind(),
std::io::ErrorKind::WouldBlock
);
wire.set_nonblocking(false).unwrap();
let mut serial = None;
let mut offset = 0;
while offset < bytes.len() {
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
if object == 3 && header & 0xffff == 0 {
serial = Some(u32::from_ne_bytes(bytes[offset + 8..offset + 12].try_into().unwrap()));
}
offset += (header >> 16) as usize;
}
request(&mut wire, 3, 6, &[serial.expect("layer configure")], None);
let mut file = tempfile::tempfile().unwrap();
file.write_all(&0xff112233u32.to_ne_bytes().repeat(64 * 48)).unwrap();
request(
&mut wire,
shm.id().protocol_id(),
0,
&[4, 64 * 48 * 4],
Some(file.as_raw_fd()),
);
request(&mut wire, 4, 0, &[5, 0, 64, 48, 64 * 4, 0], None);
request(&mut wire, 2, 1, &[5, 0, 0], None);
request(&mut wire, 2, 6, &[], None);
dispatch(events, state);
let layer = layer_map_for_output(output)
.layers()
.find(|layer| {
layer
.wl_surface()
.client()
.is_some_and(|owner| owner.id() == client.id())
})
.unwrap()
.clone();
(layer, wire)
}
#[test]
fn on_demand_layer_focus_follows_opening_and_outside_clicks() {
if !crate::startup_tests::private_runtime(
"input::layer_focus_tests::on_demand_layer_focus_follows_opening_and_outside_clicks",
) {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut config = crate::config::Config::default().runtime_config().unwrap();
config.wallpaper.path = None;
config.animations_enabled = false;
let mut state = Ferese::new(
&mut events,
smithay::reexports::wayland_server::Display::new().unwrap(),
config,
)
.unwrap();
let output = Output::new(
"focus".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(Mode {
size: (800, 600).into(),
refresh: 60_000,
}),
None,
None,
None,
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "focus".into());
let (panel, mut wire) = layer(&mut state, &mut events, &output);
let pointer = state.seat.get_pointer().unwrap();
let keyboard = state.seat.get_keyboard().unwrap();
let inside = Point::from((20., 20.));
pointer.motion(
&mut state,
Some((panel.wl_surface().clone(), (0., 0.).into())),
&MotionEvent {
location: inside,
serial: SERIAL_COUNTER.next_serial(),
time: 0,
},
);
// The opening click arrives while the collapsed panel is noninteractive.
state.focus_window_at(inside, SERIAL_COUNTER.next_serial(), true);
assert_eq!(keyboard.current_focus(), None);
request(&mut wire, 3, 4, &[2], None); // OnDemand.
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert_eq!(
keyboard.current_focus(),
Some(panel.wl_surface().clone()),
"opening must not require a second click"
);
state.focus_window_at(inside, SERIAL_COUNTER.next_serial(), true);
assert_eq!(
keyboard.current_focus(),
Some(panel.wl_surface().clone()),
"inside clicks keep the panel open"
);
let outside = Point::from((400., 300.));
pointer.motion(
&mut state,
None,
&MotionEvent {
location: outside,
serial: SERIAL_COUNTER.next_serial(),
time: 1,
},
);
state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true);
assert_eq!(keyboard.current_focus(), None, "empty-desktop clicks dismiss the panel");
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert_eq!(
keyboard.current_focus(),
None,
"later animation commits must not reclaim focus"
);
let (wallpaper, _wallpaper_wire) = layer(&mut state, &mut events, &output);
keyboard.set_focus(
&mut state,
Some(panel.wl_surface().clone()),
SERIAL_COUNTER.next_serial(),
);
assert_eq!(state.layer_under(inside).unwrap().0, wallpaper);
state.focus_window_at(inside, SERIAL_COUNTER.next_serial(), true);
assert_eq!(
keyboard.current_focus(),
None,
"noninteractive-layer clicks dismiss the panel"
);
let (first, _first_wire) = window(&mut state, &mut events, 0xff112233);
let (second, _second_wire) = window(&mut state, &mut events, 0xff334455);
let first_id = state.windows.ids()[&first];
assert!(state.activate_managed_window(first_id));
let second_bounds = state.presented_window_rect(state.windows.ids()[&second]).unwrap();
let on_window = Point::from((second_bounds.x + 20., second_bounds.y + 20.));
state.input_settings.focus_follows_mouse = true;
keyboard.set_focus(
&mut state,
Some(panel.wl_surface().clone()),
SERIAL_COUNTER.next_serial(),
);
state.focus_window_under_pointer(&pointer, on_window);
assert_eq!(
keyboard.current_focus(),
Some(panel.wl_surface().clone()),
"pointer leave must not dismiss the panel"
);
state.focus_window_at(on_window, SERIAL_COUNTER.next_serial(), true);
assert_eq!(
keyboard.current_focus(),
Some(second.toplevel().unwrap().wl_surface().clone()),
"an outside click must still focus its window"
);
request(&mut wire, 3, 4, &[0], None); // None.
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
request(&mut wire, 3, 4, &[2], None); // OnDemand, without a pointer on the panel.
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert_eq!(
keyboard.current_focus(),
Some(second.toplevel().unwrap().wl_surface().clone()),
"enabling a panel away from the pointer must not steal window focus"
);
}
+4
View File
@@ -1,5 +1,9 @@
mod animation; mod animation;
mod capture_privacy; mod capture_privacy;
#[cfg(test)]
pub(crate) use capture_privacy::tests::{
dispatch as dispatch_test_client, request as test_client_request, window as test_window,
};
mod floating; mod floating;
mod focus; mod focus;
mod hit_testing; mod hit_testing;
+33
View File
@@ -2828,6 +2828,39 @@ mod tests {
assert!(!runtime.hub.active()); assert!(!runtime.hub.active());
} }
#[test]
fn losing_layer_focus_collapses_only_the_expanded_hub_on_that_output() {
use cosmic::iced::event::{PlatformSpecific, wayland};
use wayland_client::Proxy;
let mut shell = shell();
let id = shell.outputs[0].bar;
let (socket, peer) = std::os::unix::net::UnixStream::pair().unwrap();
let connection = wayland_client::Connection::from_socket(socket).unwrap();
let surface = wayland_client::protocol::wl_surface::WlSurface::inert(connection.backend().downgrade());
drop(peer);
let unfocused = |id| {
cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(
wayland::LayerEvent::Unfocused,
surface.clone(),
id,
)))
};
drop(shell.open_hub(id, status_ui::Menu::Calendar));
let other = window::Id::unique();
drop(shell.handle_event(unfocused(other), other));
assert_eq!(shell.hub_mode(id), Some(Mode::Expanded(Content::Calendar)));
drop(shell.handle_event(unfocused(id), id));
assert_eq!(shell.hub_mode(id), Some(Mode::Idle));
assert_eq!(shell.outputs[0].runtime.hub.as_ref().unwrap().borrow().menu, None);
drop(shell.hub_hover(id, true));
drop(shell.handle_event(unfocused(id), id));
assert_eq!(
shell.hub_mode(id),
Some(Mode::Peek),
"focus changes must not interrupt hover peeks"
);
}
#[test] #[test]
fn cursor_leave_closes_peek_and_escape_closes_expanded_content() { fn cursor_leave_closes_peek_and_escape_closes_expanded_content() {
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Location, Modifiers, key}; use cosmic::iced::keyboard::{Event as KeyEvent, Key, Location, Modifiers, key};