Fix shell focus, launcher touch dismissal, and sandbox clipboard access

This commit is contained in:
Samuel Onoja committed 2026-10-14 18:11:13 +01:00
1 parent c8dfb3074d
commit a62f1367bd
8 files changed
+126 -79

No files matched your search

+5 -29
View File
@@ -6,6 +6,7 @@ use smithay::reexports::wayland_server::{Resource, backend::ClientId};
use smithay::utils::Serial;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
#[cfg(test)]
use smithay::utils::SERIAL_COUNTER;
use smithay::wayland::xdg_activation::{
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
@@ -110,34 +111,10 @@ impl XdgActivationHandler for Ferese {
return;
}
let Some(window) = self
.space
.elements()
.find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == &surface)
})
.cloned()
else {
let Some(id) = self.windows.id_for_surface(&surface) else {
return;
};
self.focused_window = self.windows.ids().get(&window).copied();
if let Some(focused) = self.focused_window
&& let Err(error) = self.workspaces.focus_window(focused)
{
tracing::error!(%error, ?focused, "failed to update workspace focus");
return;
}
self.raise_window(&window, true);
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
keyboard.set_focus(self, Some(surface), SERIAL_COUNTER.next_serial());
self.space.elements().for_each(|window| {
if let Some(toplevel) = window.toplevel() {
toplevel.send_pending_configure();
}
});
self.activate_managed_window(id);
}
}
@@ -252,8 +229,7 @@ mod tests {
assert_ne!(approved.client_id, Some(recipient.id()));
assert!(approved.user_data.get::<ApprovedInteraction>().unwrap().0.elapsed() < TOKEN_MAX_AGE);
// Exercise the production handler with a real recipient toplevel.
// Creating the window needs no GPU or client buffer.
// A valid handoff must not focus a recipient before its first buffer.
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase;
use smithay::reexports::wayland_server::protocol::wl_compositor::WlCompositor;
use smithay::wayland::shell::xdg::XdgWmBaseUserData;
@@ -279,7 +255,7 @@ mod tests {
})
.expect("recipient's toplevel");
state.request_activation(token, approved, target.clone());
assert_eq!(state.seat.get_keyboard().unwrap().current_focus(), Some(target));
assert_eq!(state.seat.get_keyboard().unwrap().current_focus(), None);
}
#[test]
+15 -6
View File
@@ -195,11 +195,14 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
}
impl Ferese {
pub(crate) fn layer_keyboard_eligible(&self, layer: &DesktopLayerSurface) -> bool {
layer.can_receive_keyboard_focus() && self.layer_mapped_and_visible(layer)
}
/// Layer-map membership tracks the role and output, not buffer mapping.
/// Use the renderer state updated by on_commit_buffer_handler, as for windows.
pub(crate) fn layer_keyboard_eligible(&self, layer: &DesktopLayerSurface) -> bool {
fn layer_mapped_and_visible(&self, layer: &DesktopLayerSurface) -> bool {
layer.layer_surface().alive()
&& layer.can_receive_keyboard_focus()
&& smithay::backend::renderer::utils::with_renderer_surface_state(layer.wl_surface(), |state| {
state.buffer().is_some()
})
@@ -262,10 +265,16 @@ impl Ferese {
return;
}
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
if let Some(root) = self.keyboard_layer_root().filter(|_| {
!self
.keyboard_focused_layer()
.is_some_and(|layer| self.layer_keyboard_eligible(&layer))
if let Some(root) = self.keyboard_layer_root().filter(|root| {
!self.keyboard_focused_layer().is_some_and(|layer| {
// A popup owns its keyboard focus independently of the bar's
// interactivity policy, but still needs a visible, mapped parent.
if keyboard.current_focus().as_ref() != Some(root) {
self.layer_mapped_and_visible(&layer)
} else {
self.layer_keyboard_eligible(&layer)
}
})
}) {
// A popup grab cannot keep an ineligible layer alive as input owner.
for (popup, _) in smithay::desktop::PopupManager::popups_for_surface(&root) {
+1
View File
@@ -6,6 +6,7 @@ mod layer_shell;
pub(crate) mod screencopy;
pub(crate) mod screenshot;
pub(crate) mod screenshot_worker;
pub(crate) mod security_context;
pub(crate) mod window_capture;
mod xdg_shell;
@@ -0,0 +1,31 @@
use std::sync::Arc;
use smithay::reexports::wayland_server::Client;
use smithay::wayland::security_context::{SecurityContext, SecurityContextHandler, SecurityContextListenerSource};
use crate::Ferese;
use crate::state::ClientState;
pub(crate) fn unrestricted_client(client: &Client) -> bool {
client
.get_data::<ClientState>()
.is_some_and(|state| state.security_context.is_none())
}
impl SecurityContextHandler for Ferese {
fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
if let Err(error) = self.loop_handle.insert_source(source, move |stream, _, state| {
let data = ClientState {
security_context: Some(context.clone()),
..Default::default()
};
if let Err(error) = state.display_handle.insert_client(stream, Arc::new(data)) {
tracing::warn!(%error, "failed to register sandboxed Wayland client");
}
}) {
tracing::warn!(%error, "failed to register security context listener");
}
}
}
smithay::delegate_security_context!(Ferese);
+41 -14
View File
@@ -629,23 +629,10 @@ impl Ferese {
if event.state() == ButtonState::Pressed
&& !pointer.is_grabbed()
&& let Some(surface) = self.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus())
&& self
.space
.layer_for_surface(&surface, smithay::desktop::WindowSurfaceType::ALL)
.is_some_and(|layer| layer.namespace() == "ferese-launcher")
&& !self
.layer_under(pointer.current_location())
.is_some_and(|(layer, _, _)| layer.wl_surface() == &surface)
&& let Some(shell) = self
.shell_resources
.iter()
.filter_map(|shell| shell.upgrade().ok())
.find(|shell| shell.version() >= 10)
&& self.dismiss_launcher_outside(pointer.current_location())
{
// Consume both halves of the click before underlying windows or tabs see it.
self.consumed_pointer_buttons.insert(event.button_code());
shell.toggle_launcher(String::new());
return;
}
@@ -823,6 +810,10 @@ impl Ferese {
let Some(location) = self.absolute_event_position(&event) else {
return;
};
if !self.consumed_touch_slots.is_empty() || self.dismiss_launcher_outside(location) {
self.consumed_touch_slots.insert(event.slot());
return;
}
self.focus_window_at(location, SERIAL_COUNTER.next_serial(), true);
let touch = self.seat.get_touch().expect("seat has touch capability");
let serial = SERIAL_COUNTER.next_serial();
@@ -843,6 +834,9 @@ impl Ferese {
);
}
InputEvent::TouchMotion { event, .. } => {
if self.consumed_touch_slots.contains(&event.slot()) {
return;
}
let Some(location) = self.absolute_event_position(&event) else {
return;
};
@@ -859,6 +853,9 @@ impl Ferese {
);
}
InputEvent::TouchUp { event, .. } => {
if self.consumed_touch_slots.remove(&event.slot()) {
return;
}
let touch = self.seat.get_touch().expect("seat has touch capability");
touch.up(
self,
@@ -870,6 +867,7 @@ impl Ferese {
);
}
InputEvent::TouchCancel { .. } => {
self.consumed_touch_slots.clear();
let touch = self.seat.get_touch().expect("seat has touch capability");
touch.cancel(self);
}
@@ -881,6 +879,35 @@ impl Ferese {
}
}
fn dismiss_launcher_outside(&self, location: Point<f64, Logical>) -> bool {
if self.session_lock.active() || self.input_capture.active() {
return false;
}
let Some(surface) = self.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus()) else {
return false;
};
if !self
.space
.layer_for_surface(&surface, smithay::desktop::WindowSurfaceType::ALL)
.is_some_and(|layer| layer.namespace() == "ferese-launcher")
|| self
.layer_under(location)
.is_some_and(|(layer, _, _)| layer.wl_surface() == &surface)
{
return false;
}
let Some(shell) = self
.shell_resources
.iter()
.filter_map(|shell| shell.upgrade().ok())
.find(|shell| shell.version() >= 10)
else {
return false;
};
shell.toggle_launcher(String::new());
true
}
fn start_floating_pointer_grab(&mut self, pointer: &PointerHandle<Self>, button: u32, serial: Serial) -> bool {
if self.session_lock.active()
|| pointer.is_grabbed()
+21 -10
View File
@@ -470,6 +470,7 @@ pub struct Ferese {
pub(crate) tab_indicator_settings: crate::config::TabIndicatorSettings,
pub(crate) tab_bars: HashMap<WorkspaceId, Vec<ferese_layout::TabBar>>,
pub(crate) consumed_pointer_buttons: HashSet<u32>,
pub(crate) consumed_touch_slots: HashSet<smithay::backend::input::TouchSlot>,
pub(crate) focus_history: ferese_core::FocusHistory,
pub(crate) focus_cycle: Option<ferese_core::FocusCycle>,
column_width_presets: Vec<ColumnWidth>,
@@ -553,6 +554,7 @@ pub struct Ferese {
pub primary_selection_state: PrimarySelectionState,
pub wlr_data_control_state: wlr_data_control::DataControlState,
pub ext_data_control_state: ext_data_control::DataControlState,
pub security_context_state: smithay::wayland::security_context::SecurityContextState,
pub relative_pointer_state: RelativePointerManagerState,
pub seat_state: SeatState<Self>,
pub shm_state: ShmState,
@@ -659,16 +661,22 @@ impl Ferese {
let presentation_state = PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);
let data_device_state = DataDeviceState::new::<Self>(&display_handle);
let primary_selection_state = PrimarySelectionState::new::<Self>(&display_handle);
// Clipboard tools need direct selection access. Without data control,
// wl-copy maps a temporary window to obtain keyboard focus.
let wlr_data_control_state =
wlr_data_control::DataControlState::new::<Self, _>(&display_handle, Some(&primary_selection_state), |_| {
true
});
let ext_data_control_state =
ext_data_control::DataControlState::new::<Self, _>(&display_handle, Some(&primary_selection_state), |_| {
true
});
let security_context_state = smithay::wayland::security_context::SecurityContextState::new::<Self, _>(
&display_handle,
crate::handlers::security_context::unrestricted_client,
);
// Host clipboard tools retain direct access. Sandboxed clients use
// ordinary selection protocols, which follow keyboard focus.
let wlr_data_control_state = wlr_data_control::DataControlState::new::<Self, _>(
&display_handle,
Some(&primary_selection_state),
crate::handlers::security_context::unrestricted_client,
);
let ext_data_control_state = ext_data_control::DataControlState::new::<Self, _>(
&display_handle,
Some(&primary_selection_state),
crate::handlers::security_context::unrestricted_client,
);
let relative_pointer_state = RelativePointerManagerState::new::<Self>(&display_handle);
let viewporter_state = ViewporterState::new::<Self>(&display_handle);
@@ -777,6 +785,7 @@ impl Ferese {
tab_indicator_settings: config.tab_indicator,
tab_bars: HashMap::new(),
consumed_pointer_buttons: HashSet::new(),
consumed_touch_slots: HashSet::new(),
focus_history: Default::default(),
focus_cycle: None,
column_width_presets: config.column_width_presets,
@@ -855,6 +864,7 @@ impl Ferese {
primary_selection_state,
wlr_data_control_state,
ext_data_control_state,
security_context_state,
relative_pointer_state,
seat_state,
shm_state,
@@ -1264,6 +1274,7 @@ where
#[derive(Default)]
pub struct ClientState {
pub compositor_state: CompositorClientState,
pub(crate) security_context: Option<smithay::wayland::security_context::SecurityContext>,
pub(crate) capabilities: crate::private_client::ClientCapabilities,
}
+3
View File
@@ -377,6 +377,9 @@ impl Ferese {
let Some(window) = self.windows.window(id).cloned() else {
return false;
};
if !super::window_has_buffer(&window) {
return false;
}
if !activate_window_workspace(&mut self.workspaces, &mut self.output_workspaces, id) {
return false;
}
+9 -20
View File
@@ -510,7 +510,6 @@ impl FereseShell {
pub(super) fn attach_effects(&mut self, id: window::Id, surface: &wl_surface::WlSurface) {
let hidden = self.bar_hidden(id);
let islands = self.panel_for_bar(id).surface() == ferese_panel::PanelSurface::None;
let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else {
return;
};
@@ -521,12 +520,15 @@ impl FereseShell {
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
let binding = if islands {
// Wait for measured island bounds rather than briefly painting the full bar.
EffectsBinding::attach_role(&mut self.effects_context, surface, None, 1.0)
} else {
EffectsBinding::attach(&mut self.effects_context, surface, !hidden)
};
// The first panel presentation supplies the role and measured shape
// together with its buffer. Enabling the role here would paint the
// whole surface before those bounds arrive.
let binding = EffectsBinding::attach_role(
&mut self.effects_context,
surface,
None,
if hidden { 0.0 } else { 1.0 },
);
match binding {
Ok(binding) => entry.effects = Some(binding),
@@ -644,19 +646,6 @@ pub(super) struct EffectsBinding {
}
impl EffectsBinding {
pub(super) fn attach(
context: &mut Option<std::rc::Rc<EffectsContext>>,
surface: &wl_surface::WlSurface,
visible: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::attach_role(
context,
surface,
visible.then_some(ferese_surface_effects_v1::Role::Panel),
if visible { 1.0 } else { 0.0 },
)
}
pub(super) fn attach_role(
context: &mut Option<std::rc::Rc<EffectsContext>>,
surface: &wl_surface::WlSurface,