Fix shell focus, launcher touch dismissal, and sandbox clipboard access
This commit is contained in:
1 parent
c8dfb3074d
commit
a62f1367bd
8 files changed
+126
-79
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user