diff --git a/compositor/src/handlers/activation.rs b/compositor/src/handlers/activation.rs index b4686e6..740dd79 100644 --- a/compositor/src/handlers/activation.rs +++ b/compositor/src/handlers/activation.rs @@ -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::().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] diff --git a/compositor/src/handlers/layer_shell.rs b/compositor/src/handlers/layer_shell.rs index 105d9b5..dd0c65a 100644 --- a/compositor/src/handlers/layer_shell.rs +++ b/compositor/src/handlers/layer_shell.rs @@ -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) { diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index b9ea219..be26387 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -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; diff --git a/compositor/src/handlers/security_context.rs b/compositor/src/handlers/security_context.rs new file mode 100644 index 0000000..61b7e85 --- /dev/null +++ b/compositor/src/handlers/security_context.rs @@ -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::() + .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); diff --git a/compositor/src/input.rs b/compositor/src/input.rs index f27aeaf..f2b8b47 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -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) -> 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, button: u32, serial: Serial) -> bool { if self.session_lock.active() || pointer.is_grabbed() diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 7aeeaf8..f3de58a 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -470,6 +470,7 @@ pub struct Ferese { pub(crate) tab_indicator_settings: crate::config::TabIndicatorSettings, pub(crate) tab_bars: HashMap>, pub(crate) consumed_pointer_buttons: HashSet, + pub(crate) consumed_touch_slots: HashSet, pub(crate) focus_history: ferese_core::FocusHistory, pub(crate) focus_cycle: Option, column_width_presets: Vec, @@ -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, pub shm_state: ShmState, @@ -659,16 +661,22 @@ impl Ferese { let presentation_state = PresentationState::new::(&display_handle, libc::CLOCK_MONOTONIC as u32); let data_device_state = DataDeviceState::new::(&display_handle); let primary_selection_state = PrimarySelectionState::new::(&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::(&display_handle, Some(&primary_selection_state), |_| { - true - }); - let ext_data_control_state = - ext_data_control::DataControlState::new::(&display_handle, Some(&primary_selection_state), |_| { - true - }); + let security_context_state = smithay::wayland::security_context::SecurityContextState::new::( + &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::( + &display_handle, + Some(&primary_selection_state), + crate::handlers::security_context::unrestricted_client, + ); + let ext_data_control_state = ext_data_control::DataControlState::new::( + &display_handle, + Some(&primary_selection_state), + crate::handlers::security_context::unrestricted_client, + ); let relative_pointer_state = RelativePointerManagerState::new::(&display_handle); let viewporter_state = ViewporterState::new::(&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, pub(crate) capabilities: crate::private_client::ClientCapabilities, } diff --git a/compositor/src/state/navigation.rs b/compositor/src/state/navigation.rs index f6f0b81..4261b82 100644 --- a/compositor/src/state/navigation.rs +++ b/compositor/src/state/navigation.rs @@ -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; } diff --git a/shell/ferese-shell/src/surfaces.rs b/shell/ferese-shell/src/surfaces.rs index 03af00b..33d76ee 100644 --- a/shell/ferese-shell/src/surfaces.rs +++ b/shell/ferese-shell/src/surfaces.rs @@ -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>, - surface: &wl_surface::WlSurface, - visible: bool, - ) -> Result> { - 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>, surface: &wl_surface::WlSurface,