Files
feresaria/compositor/src/handlers/layer_shell.rs
T

394 lines
14 KiB
Rust

use smithay::desktop::{
LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output,
};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::shell::wlr_layer::{
KeyboardInteractivity, Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState,
};
use smithay::wayland::shell::xdg::PopupSurface;
use crate::Ferese;
impl WlrLayerShellHandler for Ferese {
fn shell_state(&mut self) -> &mut WlrLayerShellState {
&mut self.layer_shell_state
}
fn new_layer_surface(
&mut self,
surface: LayerSurface,
output: Option<WlOutput>,
_layer: Layer,
namespace: String,
) {
// A wl_output resource can outlive its registered desktop output.
// Explicit requests retain their identity; only unspecified requests
// may use the focused/default output policy.
let usable = |output: &Output| {
self.output_id(output).is_some()
&& output.current_mode().is_some()
&& self.space.outputs().any(|candidate| candidate == output)
};
let output = match output {
Some(resource) => Output::from_resource(&resource).filter(usable),
None => self
.focused_output()
.filter(|output| usable(output))
.cloned()
.or_else(|| self.space.outputs().find(|output| usable(output)).cloned()),
};
let Some(output) = output else {
tracing::warn!(%namespace, "layer surface has no available output");
surface.send_close();
return;
};
let layer = DesktopLayerSurface::new(surface, namespace.clone());
if let Err(error) = layer_map_for_output(&output).map_layer(&layer) {
tracing::warn!(%namespace, %error, "failed to map layer surface");
layer.layer_surface().send_close();
return;
}
tracing::debug!(%namespace, output = %output.name(), "mapped layer surface");
super::set_surface_tree_output(layer.wl_surface(), &output);
self.relayout_on(&[output]);
}
fn new_popup(&mut self, parent: LayerSurface, popup: PopupSurface) {
let output = self.space.outputs().find(|output| {
layer_map_for_output(output)
.layers()
.any(|layer| layer.wl_surface() == parent.wl_surface())
});
if let Some(output) = output {
super::set_surface_tree_output(popup.wl_surface(), output);
}
self.unconstrain_popup(&popup);
if let Err(error) = self.popups.track_popup(PopupKind::Xdg(popup)) {
tracing::warn!(?error, "failed to track layer-shell popup");
}
}
fn layer_destroyed(&mut self, surface: LayerSurface) {
if self
.pending_layer_focus
.as_ref()
.is_some_and(|(target, _)| target == surface.wl_surface())
{
self.pending_layer_focus = None;
}
let Some(layer) = self
.space
.layer_for_surface(surface.wl_surface(), WindowSurfaceType::TOPLEVEL)
else {
return;
};
let restore_focus = layer_has_keyboard_focus(self, &layer);
let outputs = self
.space
.outputs()
.filter(|output| {
layer_map_for_output(output)
.layer_geometry(&layer)
.is_some()
})
.cloned()
.collect::<Vec<_>>();
for output in &outputs {
layer_map_for_output(output).unmap_layer(&layer);
}
self.relayout_on(&outputs);
if restore_focus {
self.restore_keyboard_focus();
}
crate::backends::direct::render_on(self, &outputs);
}
}
#[derive(Clone, Debug, PartialEq)]
struct LayoutState {
size: smithay::utils::Size<i32, smithay::utils::Logical>,
anchor: smithay::wayland::shell::wlr_layer::Anchor,
zone: smithay::wayland::shell::wlr_layer::ExclusiveZone,
margin: [i32; 4],
layer: Layer,
bounds: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
}
impl LayoutState {
fn current(layer: &DesktopLayerSurface) -> Self {
Self::from_policy(layer.cached_state(), layer.bbox())
}
fn from_policy(
policy: smithay::wayland::shell::wlr_layer::LayerSurfaceCachedState,
bounds: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
) -> Self {
Self {
size: policy.size,
anchor: policy.anchor,
zone: policy.exclusive_zone,
margin: [
policy.margin.top,
policy.margin.right,
policy.margin.bottom,
policy.margin.left,
],
layer: policy.layer,
bounds,
}
}
}
pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
let Some(layer) = state
.space
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
else {
return;
};
let outputs = state
.space
.outputs()
.filter(|output| {
layer_map_for_output(output)
.layer_geometry(&layer)
.is_some()
})
.cloned()
.collect::<Vec<_>>();
if !state.layer_mapped_and_visible(&layer)
&& state
.pending_layer_focus
.as_ref()
.is_some_and(|(target, _)| target == surface)
{
state.pending_layer_focus = None;
}
let policy = layer.cached_state();
let current = LayoutState::current(&layer);
let previous = layer.user_data().get_or_insert_threadsafe(|| {
std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>)
});
let (changed, requested_focus) = {
let mut previous = previous
.lock()
.expect("layer configure state must remain consistent across callbacks");
let changed = previous.as_ref().map(|(layout, _)| layout) != Some(&current);
let requested_focus = policy.keyboard_interactivity == KeyboardInteractivity::OnDemand
&& previous
.as_ref()
.is_some_and(|(_, keyboard)| *keyboard == KeyboardInteractivity::None);
*previous = Some((current, policy.keyboard_interactivity));
(changed, requested_focus)
};
if changed {
state.relayout_on(&outputs);
}
// Configure acknowledgement and keyboard policy are independent of layout.
layer.layer_surface().send_pending_configure();
// Consume the opening interaction once. Later commits, pointer hover, or
// another input target must not give a panel a fresh claim to focus.
let opening_interaction = if requested_focus
&& state
.pending_layer_focus
.as_ref()
.is_some_and(|(target, _)| target == surface)
{
state
.pending_layer_focus
.take()
.is_some_and(|(_, at)| at.elapsed() <= std::time::Duration::from_secs(5))
} else {
false
};
state.reconcile_layer_keyboard_focus();
if opening_interaction
&& !state
.seat
.get_keyboard()
.is_some_and(|keyboard| keyboard.is_grabbed())
&& !state.session_lock.active()
&& !state.input_capture.captures(1)
&& state.layer_keyboard_eligible(&layer)
&& state.exclusive_keyboard_layer().is_none()
&& matches!(policy.layer, Layer::Top | Layer::Overlay)
&& !layer_has_keyboard_focus(state, &layer)
{
state
.seat
.get_keyboard()
.expect("seat has a keyboard")
.set_focus(
state,
Some(layer.wl_surface().clone()),
smithay::utils::SERIAL_COUNTER.next_serial(),
);
}
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush layer-surface configure");
}
}
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.
fn layer_mapped_and_visible(&self, layer: &DesktopLayerSurface) -> bool {
layer.layer_surface().alive()
&& smithay::backend::renderer::utils::with_renderer_surface_state(
layer.wl_surface(),
|state| state.buffer().is_some(),
)
.unwrap_or(false)
&& self.space.outputs().any(|output| {
self.layer_visible_on_output(output, layer.layer())
&& layer_map_for_output(output)
.layers()
.any(|candidate| candidate == layer)
})
}
fn keyboard_layer_root(&self) -> Option<WlSurface> {
let surface = self.seat.get_keyboard()?.current_focus()?;
let root = self
.popups
.find_popup(&surface)
.and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok())
.unwrap_or(surface);
(smithay::wayland::compositor::get_role(&root)
== Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE))
.then_some(root)
}
pub(crate) fn keyboard_focused_layer(&self) -> Option<DesktopLayerSurface> {
self.space
.layer_for_surface(&self.keyboard_layer_root()?, WindowSurfaceType::ALL)
}
pub(crate) fn exclusive_keyboard_layer(&self) -> Option<DesktopLayerSurface> {
// Overlay outranks Top. Keep the current owner among peers; the protocol
// leaves ordering within one layer to the compositor.
let focused = self.keyboard_focused_layer();
for priority in [Layer::Overlay, Layer::Top] {
if let Some(layer) = focused.as_ref().filter(|layer| {
layer.layer() == priority
&& layer.cached_state().keyboard_interactivity
== KeyboardInteractivity::Exclusive
&& self.layer_keyboard_eligible(layer)
}) {
return Some(layer.clone());
}
for output in self.space.outputs() {
let layers = layer_map_for_output(output)
.layers_on(priority)
.rev()
.cloned()
.collect::<Vec<_>>();
if let Some(layer) = layers.into_iter().find(|layer| {
layer.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive
&& self.layer_keyboard_eligible(layer)
}) {
return Some(layer.clone());
}
}
}
None
}
pub(crate) fn reconcile_layer_keyboard_focus(&mut self) {
if self.session_lock.active() || self.input_capture.captures(1) {
return;
}
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
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) {
let _ = smithay::desktop::PopupManager::dismiss_popup(&root, &popup);
}
keyboard.unset_grab(self);
self.restore_keyboard_focus();
} else if !keyboard.is_grabbed()
&& let Some(layer) = self.exclusive_keyboard_layer()
&& !layer_has_keyboard_focus(self, &layer)
{
keyboard.set_focus(
self,
Some(layer.wl_surface().clone()),
smithay::utils::SERIAL_COUNTER.next_serial(),
);
}
}
}
fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool {
state
.keyboard_focused_layer()
.is_some_and(|focused| focused == *layer)
}
#[cfg(test)]
mod tests {
use smithay::utils::Rectangle;
use smithay::wayland::shell::wlr_layer::{Anchor, ExclusiveZone, LayerSurfaceCachedState};
use super::*;
#[test]
fn only_arrangement_and_mapping_changes_require_layout() {
let policy = LayerSurfaceCachedState::default();
let bounds = Rectangle::from_size((800, 32).into());
let original = LayoutState::from_policy(policy, bounds);
assert_eq!(original, LayoutState::from_policy(policy, bounds));
let mut keyboard = policy;
keyboard.keyboard_interactivity = KeyboardInteractivity::Exclusive;
assert_eq!(original, LayoutState::from_policy(keyboard, bounds));
for mutate in [
|p: &mut LayerSurfaceCachedState| p.size.w += 1,
|p: &mut LayerSurfaceCachedState| p.anchor = Anchor::TOP,
|p: &mut LayerSurfaceCachedState| p.exclusive_zone = ExclusiveZone::Exclusive(32),
|p: &mut LayerSurfaceCachedState| p.margin.left += 1,
|p: &mut LayerSurfaceCachedState| p.layer = Layer::Overlay,
] {
let mut changed = policy;
mutate(&mut changed);
assert_ne!(original, LayoutState::from_policy(changed, bounds));
}
assert_ne!(
original,
LayoutState::from_policy(policy, Rectangle::from_size((800, 33).into()))
);
assert_ne!(
original,
LayoutState::from_policy(policy, Rectangle::default())
);
}
}
#[cfg(test)]
mod output_tests;