Files
niri/src/handlers/xdg_shell.rs
T
2024-01-14 09:28:03 +04:00

518 lines
19 KiB
Rust

use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
WindowSurfaceType,
};
use smithay::input::pointer::Focus;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge};
use smithay::reexports::wayland_server::protocol::wl_output;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::wlr_layer::Layer;
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData,
};
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
&mut self.niri.xdg_shell_state
}
fn new_toplevel(&mut self, surface: ToplevelSurface) {
let wl_surface = surface.wl_surface().clone();
let window = Window::new(surface);
// Tell the surface the preferred size and bounds for its likely output.
if let Some(ws) = self.niri.layout.active_workspace() {
ws.configure_new_window(&window);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
let config = self.niri.config.borrow();
if config.prefer_no_csd {
window.toplevel().with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// At the moment of creation, xdg toplevels must have no buffer.
let existing = self.niri.unmapped_windows.insert(wl_surface, window);
assert!(existing.is_none());
}
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
self.unconstrain_popup(&surface);
if let Err(err) = self.niri.popups.track_popup(PopupKind::Xdg(surface)) {
warn!("error tracking popup: {err:?}");
}
}
fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
// FIXME
}
fn resize_request(
&mut self,
_surface: ToplevelSurface,
_seat: WlSeat,
_serial: Serial,
_edges: ResizeEdge,
) {
// FIXME
}
fn reposition_request(
&mut self,
surface: PopupSurface,
positioner: PositionerState,
token: u32,
) {
surface.with_pending_state(|state| {
let geometry = positioner.get_geometry();
state.geometry = geometry;
state.positioner = positioner;
});
self.unconstrain_popup(&surface);
surface.send_repositioned(token);
}
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
return;
};
// We need to hand out the grab in a way consistent with what update_keyboard_focus()
// thinks the current focus is, otherwise it will desync and cause weird issues with
// keyboard focus being at the wrong place.
if self.niri.is_locked() {
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else if self.niri.screenshot_ui.is_open() {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
if let Some(layer_surface) =
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
{
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else {
if layers
.layers_on(Layer::Overlay)
.any(|l| l.can_receive_keyboard_focus())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let mon = self.niri.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& layers
.layers_on(Layer::Top)
.any(|l| l.can_receive_keyboard_focus())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let seat = &self.niri.seat;
let Ok(mut grab) = self
.niri
.popups
.grab_popup(root.clone(), popup, seat, serial)
else {
return;
};
let keyboard = seat.get_keyboard().unwrap();
let pointer = seat.get_pointer().unwrap();
let keyboard_grab_mismatches = keyboard.is_grabbed()
&& !(keyboard.has_grab(serial)
|| grab
.previous_serial()
.map_or(true, |s| keyboard.has_grab(s)));
let pointer_grab_mismatches = pointer.is_grabbed()
&& !(pointer.has_grab(serial)
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
if keyboard_grab_mismatches || pointer_grab_mismatches {
grab.ungrab(PopupUngrabStrategy::All);
return;
}
trace!("new grab for root {:?}", root);
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState { root, grab });
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
// FIXME
// The protocol demands us to always reply with a configure,
// regardless of we fulfilled the request or not
surface.send_configure();
}
fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
// FIXME
}
fn fullscreen_request(
&mut self,
surface: ToplevelSurface,
wl_output: Option<wl_output::WlOutput>,
) {
if surface
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
if let Some((window, current_output)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
{
let window = window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(window.clone(), &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
}
}
// The protocol demands us to always reply with a configure,
// regardless of we fulfilled the request or not
surface.send_configure();
}
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
if let Some((window, _)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
{
let window = window.clone();
self.niri.layout.set_fullscreen(&window, false);
}
}
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
if self
.niri
.unmapped_windows
.remove(surface.wl_surface())
.is_some()
{
// An unmapped toplevel got destroyed.
return;
}
let win_out = self
.niri
.layout
.find_window_and_output(surface.wl_surface());
let Some((window, output)) = win_out.map(clone2) else {
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
// involving laptop going to sleep and resuming.
error!("toplevel missing from both unmapped_windows and layout");
return;
};
self.niri.layout.remove_window(&window);
self.niri.queue_redraw(output);
}
fn popup_destroyed(&mut self, surface: PopupSurface) {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
self.niri.queue_redraw(output.clone());
}
}
}
delegate_xdg_shell!(State);
impl XdgDecorationHandler for State {
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
let mode = if self.niri.config.borrow().prefer_no_csd {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
} else {
None
};
toplevel.with_pending_state(|state| {
state.decoration_mode = mode;
});
}
fn request_mode(&mut self, toplevel: ToplevelSurface, mode: zxdg_toplevel_decoration_v1::Mode) {
// Set whatever the client wants, rather than our preferred mode. This especially matters
// for SDL2 which has a bug where forcing a different (client-side) decoration mode during
// their window creation sequence would leave the window permanently hidden.
//
// https://github.com/libsdl-org/SDL/issues/8173
//
// The bug has been fixed, but there's a ton of apps which will use the buggy version for a
// long while...
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(mode);
});
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
toplevel.send_configure();
}
}
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
let mode = if self.niri.config.borrow().prefer_no_csd {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
} else {
None
};
toplevel.with_pending_state(|state| {
state.decoration_mode = mode;
});
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
toplevel.send_configure();
}
}
}
delegate_xdg_decoration!(State);
impl KdeDecorationHandler for State {
fn kde_decoration_state(&self) -> &KdeDecorationState {
&self.niri.kde_decoration_state
}
}
delegate_kde_decoration!(State);
pub fn send_initial_configure_if_needed(toplevel: &ToplevelSurface) {
if !initial_configure_sent(toplevel) {
toplevel.send_configure();
}
}
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
})
}
impl State {
/// Should be called on `WlSurface::commit`
pub fn popups_handle_commit(&mut self, surface: &WlSurface) {
self.niri.popups.commit(surface);
if let Some(popup) = self.niri.popups.find_popup(surface) {
match popup {
PopupKind::Xdg(ref popup) => {
let initial_configure_sent = with_states(surface, |states| {
states
.data_map
.get::<XdgPopupSurfaceData>()
.unwrap()
.lock()
.unwrap()
.initial_configure_sent
});
if !initial_configure_sent {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(popup.clone()))
{
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
with_states(surface, |data| {
send_surface_state(surface, data, scale, transform);
});
}
popup.send_configure().expect("initial configure failed");
}
}
// Input method popups don't require a configure.
PopupKind::InputMethod(_) => (),
}
}
}
pub fn output_for_popup(&self, popup: &PopupKind) -> Option<&Output> {
let root = find_popup_root_surface(popup).ok()?;
self.niri.output_for_root(&root)
}
pub fn unconstrain_popup(&self, popup: &PopupSurface) {
let _span = tracy_client::span!("Niri::unconstrain_popup");
// Popups with a NULL parent will get repositioned in their respective protocol handlers
// (i.e. layer-shell).
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
return;
};
// Figure out if the root is a window or a layer surface.
if let Some((window, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, window, output);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
Some((layer_surface.clone(), o))
}) {
self.unconstrain_layer_shell_popup(popup, &layer_surface, output);
}
}
fn unconstrain_window_popup(&self, popup: &PopupSurface, window: &Window, output: &Output) {
let window_geo = window.geometry();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
//
// We try to keep regular window popups within the window itself horizontally (since the
// window can be scrolled to both edges of the screen), but within the whole monitor's
// height.
let mut target =
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h));
target.loc.y -= self.niri.layout.window_y(window).unwrap();
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
pub fn unconstrain_layer_shell_popup(
&self,
popup: &PopupSurface,
layer_surface: &LayerSurface,
output: &Output,
) {
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let map = layer_map_for_output(output);
let Some(layer_geo) = map.layer_geometry(layer_surface) else {
return;
};
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
let _span = tracy_client::span!("Niri::update_reactive_popups");
for (popup, _) in PopupManager::popups_for_surface(window.toplevel().wl_surface()) {
match popup {
PopupKind::Xdg(ref popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(popup, window, output);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
}
}
PopupKind::InputMethod(_) => (),
}
}
}
}
fn unconstrain_with_padding(
positioner: PositionerState,
target: Rectangle<i32, Logical>,
) -> Rectangle<i32, Logical> {
// Try unconstraining with a small padding first which looks nicer, then if it doesn't fit try
// unconstraining without padding.
const PADDING: i32 = 8;
let mut padded = target;
if PADDING * 2 < padded.size.w {
padded.loc.x += PADDING;
padded.size.w -= PADDING * 2;
}
if PADDING * 2 < padded.size.h {
padded.loc.y += PADDING;
padded.size.h -= PADDING * 2;
}
// No padding, so just unconstrain with the original target.
if padded == target {
return positioner.get_unconstrained_geometry(target);
}
// Do not try to resize to fit the padded target rectangle.
let mut no_resize = positioner;
no_resize
.constraint_adjustment
.remove(ConstraintAdjustment::ResizeX);
no_resize
.constraint_adjustment
.remove(ConstraintAdjustment::ResizeY);
let geo = no_resize.get_unconstrained_geometry(padded);
if padded.contains_rect(geo) {
return geo;
}
// Could not unconstrain into the padded target, so resort to the regular one.
positioner.get_unconstrained_geometry(target)
}