Files
niri/src/handlers/xdg_shell.rs
T

1141 lines
44 KiB
Rust

use std::cell::Cell;
use calloop::Interest;
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, 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};
use smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration;
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::reexports::wayland_server::{self, Resource, WEnum};
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{
add_blocker, add_pre_commit_hook, with_states, BufferAssignment, CompositorHandler as _,
HookId, SurfaceAttributes,
};
use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::wlr_layer::{self, Layer};
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData,
};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
};
use tracing::field::Empty;
use crate::input::resize_grab::ResizeGrab;
use crate::input::DOUBLE_CLICK_TIME;
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::transaction::Transaction;
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
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 unmapped = Unmapped::new(Window::new_wayland_window(surface));
let existing = self.niri.unmapped_windows.insert(wl_surface, unmapped);
assert!(existing.is_none());
}
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
let popup = PopupKind::Xdg(surface);
self.unconstrain_popup(&popup);
if let Err(err) = self.niri.popups.track_popup(popup) {
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: xdg_toplevel::ResizeEdge,
) {
let pointer = self.niri.seat.get_pointer().unwrap();
if !pointer.has_grab(serial) {
return;
}
let Some(start_data) = pointer.grab_start_data() else {
return;
};
let Some((focus, _)) = &start_data.focus else {
return;
};
let wl_surface = surface.wl_surface();
if !focus.id().same_client_as(&wl_surface.id()) {
return;
}
let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
let edges = ResizeEdge::from(edges);
let window = mapped.window.clone();
// See if we got a double resize-click gesture.
let time = get_monotonic_time();
let last_cell = mapped.last_interactive_resize_start();
let last = last_cell.get();
last_cell.set(Some((time, edges)));
if let Some((last_time, last_edges)) = last {
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
// Allow quick resize after a triple click.
last_cell.set(None);
let intersection = edges.intersection(last_edges);
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
// FIXME: don't activate once we can pass specific windows to actions.
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.layout.toggle_full_width();
}
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
// FIXME: don't activate once we can pass specific windows to actions.
self.niri.layout.activate_window(&window);
self.niri.layer_shell_on_demand_focus = None;
self.niri.layout.reset_window_height();
}
// FIXME: granular.
self.niri.queue_redraw_all();
return;
}
}
let grab = ResizeGrab::new(start_data, window.clone());
if !self.niri.layout.interactive_resize_begin(window, edges) {
return;
}
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
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(&PopupKind::Xdg(surface.clone()));
surface.send_repositioned(token);
}
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
// a grab. It will likely need refactors in Smithay to support properly since grabs just
// replace each other.
// FIXME: do this properly.
if self.niri.seat.input_method().keyboard_grabbed() {
trace!("ignoring popup grab because IME has keyboard grabbed");
return;
}
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;
}
// FIXME: popup grabs for on-demand bottom and background layers.
} else {
if layers.layers_on(Layer::Overlay).any(|l| {
l.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
}) {
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.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::Exclusive
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
})
{
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(self, 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
// 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(&surface) {
surface.send_configure();
}
}
fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
// FIXME
}
fn fullscreen_request(
&mut self,
toplevel: ToplevelSurface,
wl_output: Option<wl_output::WlOutput>,
) {
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
if let Some((mapped, current_output)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
let window = mapped.window.clone();
if let Some(requested_output) = requested_output {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(&window, &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
*wants_fullscreen = Some(requested_output);
// The required configure will be the initial configure.
}
InitialConfigureState::Configured { rules, output, .. } => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = requested_output
.as_ref()
// If none requested, try currently configured output.
.or(output.as_ref())
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
ws.configure_new_window(&unmapped.window, None, rules);
}
// We already sent the initial configure, so we need to reconfigure.
toplevel.send_configure();
}
}
} else {
error!("couldn't find the toplevel in fullscreen_request()");
toplevel.send_configure();
}
}
fn unfullscreen_request(&mut self, toplevel: ToplevelSurface) {
if let Some((mapped, _)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
*wants_fullscreen = None;
// The required configure will be the initial configure.
}
InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
workspace_name,
} => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = workspace_name
.as_deref()
.and_then(|name| self.niri.layout.monitor_for_workspace(name))
.map(|mon| (mon, false));
let mon = mon.or_else(|| {
output
.as_ref()
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| {
self.niri.layout.find_window_and_output(&parent)
})
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
})
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = workspace_name
.as_deref()
.and_then(|name| mon.map(|mon| mon.find_named_workspace(name)))
.unwrap_or_else(|| {
mon.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace())
});
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
let configure_width = if *is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
*width
};
ws.configure_new_window(&unmapped.window, configure_width, rules);
}
// We already sent the initial configure, so we need to reconfigure.
toplevel.send_configure();
}
}
} else {
error!("couldn't find the toplevel in unfullscreen_request()");
toplevel.send_configure();
}
}
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((mapped, output)) = win_out 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;
};
let window = mapped.window.clone();
let output = output.clone();
#[cfg(feature = "xdp-gnome-screencast")]
self.niri
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
id: mapped.id().get(),
});
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
let transaction = Transaction::new();
let blocker = transaction.blocker();
self.backend.with_primary_renderer(|renderer| {
self.niri
.layout
.start_close_animation_for_window(renderer, &window, blocker);
});
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface.wl_surface());
// If this is the only instance, then this transaction will complete immediately, so no
// need to set the timer.
if !transaction.is_last() {
transaction.register_deadline_timer(&self.niri.event_loop);
}
if was_active {
self.maybe_warp_cursor_to_focus();
}
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());
}
}
fn app_id_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
fn title_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
}
delegate_xdg_shell!(State);
impl XdgDecorationHandler for State {
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
}
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) {
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
// 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);
/// Whether KDE server decorations are in use.
#[derive(Default)]
pub struct KdeDecorationsModeState {
server: Cell<bool>,
}
impl KdeDecorationsModeState {
pub fn is_server(&self) -> bool {
self.server.get()
}
}
impl KdeDecorationHandler for State {
fn kde_decoration_state(&self) -> &KdeDecorationState {
&self.niri.kde_decoration_state
}
fn request_mode(
&mut self,
surface: &WlSurface,
decoration: &org_kde_kwin_server_decoration::OrgKdeKwinServerDecoration,
mode: wayland_server::WEnum<org_kde_kwin_server_decoration::Mode>,
) {
let WEnum::Value(mode) = mode else {
return;
};
decoration.mode(mode);
with_states(surface, |states| {
let state = states
.data_map
.get_or_insert(KdeDecorationsModeState::default);
state
.server
.set(mode == org_kde_kwin_server_decoration::Mode::Server);
});
}
}
delegate_kde_decoration!(State);
impl XdgForeignHandler for State {
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {
&mut self.niri.xdg_foreign_state
}
}
delegate_xdg_foreign!(State);
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 {
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
let _span = tracy_client::span!("State::send_initial_configure");
let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) else {
error!("window must be present in unmapped_windows in send_initial_configure()");
return;
};
let config = self.niri.config.borrow();
let rules = ResolvedWindowRules::compute(
&config.window_rules,
WindowRef::Unmapped(unmapped),
self.niri.is_at_startup,
);
let Unmapped { window, state } = unmapped;
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
error!("window must not be already configured in send_initial_configure()");
return;
};
// Pick the target monitor. First, check if we had a workspace set in the window rules.
let mon = rules
.open_on_workspace
.as_deref()
.and_then(|name| self.niri.layout.monitor_for_workspace(name));
// If not, check if we had an output set in the window rules.
let mon = mon.or_else(|| {
rules
.open_on_output
.as_deref()
.and_then(|name| {
self.niri
.global_space
.outputs()
.find(|output| output_matches_name(output, name))
})
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
// If not, check if the window requested one for fullscreen.
let mon = mon.or_else(|| {
wants_fullscreen
.as_ref()
.and_then(|x| x.as_ref())
// The monitor might not exist if the output was disconnected.
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
// If not, check if this is a dialog with a parent, to place it next to the parent.
let mon = mon.map(|mon| (mon, false)).or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
});
// If not, use the active monitor.
let mon = mon.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
// If we're following the parent, don't set the target output, so that when the window is
// mapped, it fetches the possibly changed parent's output again, and shows up there.
let output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
let is_full_width = rules.open_maximized.unwrap_or(false);
// Tell the surface the preferred size and bounds for its likely output.
let ws = rules
.open_on_workspace
.as_deref()
.and_then(|name| mon.map(|mon| mon.find_named_workspace(name)))
.unwrap_or_else(|| {
mon.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace())
});
if let Some(ws) = ws {
// Set a fullscreen state based on window request and window rule.
if (wants_fullscreen.is_some() && rules.open_fullscreen.is_none())
|| rules.open_fullscreen == Some(true)
{
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
width = ws.resolve_default_width(rules.default_width);
let configure_width = if is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
width
};
ws.configure_new_window(window, configure_width, &rules);
}
// 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.
if config.prefer_no_csd {
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);
});
}
// Set the configured settings.
*state = InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name.clone()),
};
toplevel.send_configure();
}
pub fn queue_initial_configure(&self, toplevel: ToplevelSurface) {
// Send the initial configure in an idle, in case the client sent some more info after the
// initial commit.
self.niri.event_loop.insert_idle(move |state| {
if !toplevel.alive() {
return;
}
if let Some(unmapped) = state.niri.unmapped_windows.get(toplevel.wl_surface()) {
if unmapped.needs_initial_configure() {
state.send_initial_configure(&toplevel);
}
}
});
}
/// 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();
let transform = output.current_transform();
with_states(surface, |data| {
send_scale_transform(surface, data, scale, transform);
});
}
popup.send_configure().expect("initial configure failed");
}
}
// Input method popup can arbitrary change its geometry, so we need to unconstrain
// it on commit.
PopupKind::InputMethod(_) => {
self.unconstrain_popup(&popup);
}
}
}
}
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: &PopupKind) {
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(popup) else {
return;
};
// Figure out if the root is a window or a layer surface.
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.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: &PopupKind, 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)).to_f64();
target.loc -= self.niri.layout.window_loc(window).unwrap();
target.loc -= get_popup_toplevel_coords(popup).to_f64();
self.position_popup_within_rect(popup, target);
}
pub fn unconstrain_layer_shell_popup(
&self,
popup: &PopupKind,
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(popup);
self.position_popup_within_rect(popup, target.to_f64());
}
fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle<f64, Logical>) {
match popup {
PopupKind::Xdg(popup) => {
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
PopupKind::InputMethod(popup) => {
let text_input_rectangle = popup.text_input_rectangle();
let mut bbox =
utils::bbox_from_surface_tree(popup.wl_surface(), text_input_rectangle.loc)
.to_f64();
// Position bbox horizontally first.
let overflow_x = (bbox.loc.x + bbox.size.w) - (target.loc.x + target.size.w);
if overflow_x > 0. {
bbox.loc.x -= overflow_x;
}
// Ensure that the popup starts within the window.
bbox.loc.x = f64::max(bbox.loc.x, target.loc.x);
// Try to position IME popup below the text input rectangle.
let mut below = bbox;
below.loc.y += f64::from(text_input_rectangle.size.h);
let mut above = bbox;
above.loc.y -= bbox.size.h;
if target.loc.y + target.size.h >= below.loc.y + below.size.h {
popup.set_location(below.loc.to_i32_round());
} else {
popup.set_location(above.loc.to_i32_round());
}
}
}
}
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().expect("no x11 support").wl_surface(),
) {
match &popup {
xdg_popup @ PopupKind::Xdg(popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(xdg_popup, window, output);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
}
}
PopupKind::InputMethod(_) => (),
}
}
}
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
let config = self.niri.config.borrow();
let window_rules = &config.window_rules;
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
let new_rules = ResolvedWindowRules::compute(
window_rules,
WindowRef::Unmapped(unmapped),
self.niri.is_at_startup,
);
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
*rules = new_rules;
}
} else if let Some((mapped, output)) = self
.niri
.layout
.find_window_and_output_mut(toplevel.wl_surface())
{
if mapped.recompute_window_rules(window_rules, self.niri.is_at_startup) {
drop(config);
let output = output.cloned();
let window = mapped.window.clone();
self.niri.layout.update_window(&window, None);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
}
}
}
fn unconstrain_with_padding(
positioner: PositionerState,
target: Rectangle<f64, 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: f64 = 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.to_i32_round());
}
// 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.to_i32_round());
if padded.contains_rect(geo.to_f64()) {
return geo;
}
// Could not unconstrain into the padded target, so resort to the regular one.
positioner.get_unconstrained_geometry(target.to_i32_round())
}
pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId {
add_pre_commit_hook::<State, _>(toplevel.wl_surface(), move |state, _dh, surface| {
let _span = tracy_client::span!("mapped toplevel pre-commit");
let span =
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
return;
};
let (got_unmapped, dmabuf, commit_serial) = with_states(surface, |states| {
let (got_unmapped, dmabuf) = {
let mut guard = states.cached_state.get::<SurfaceAttributes>();
match guard.pending().buffer.as_ref() {
Some(BufferAssignment::NewBuffer(buffer)) => {
let dmabuf = get_dmabuf(buffer).cloned().ok();
(false, dmabuf)
}
Some(BufferAssignment::Removed) => (true, None),
None => (false, None),
}
};
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
(got_unmapped, dmabuf, role.configure_serial)
});
let mut transaction_for_dmabuf = None;
let mut animate = false;
if let Some(serial) = commit_serial {
if !span.is_disabled() {
span.record("serial", format!("{serial:?}"));
}
trace!("taking pending transaction");
if let Some(transaction) = mapped.take_pending_transaction(serial) {
// Transaction can be already completed if it ran past the deadline.
let disable = state.niri.config.borrow().debug.disable_transactions;
if !transaction.is_completed() && !disable {
// Register the deadline even if this is the last pending, since dmabuf
// rendering can still run over the deadline.
transaction.register_deadline_timer(&state.niri.event_loop);
let is_last = transaction.is_last();
// If this is the last transaction, we don't need to add a separate
// notification, because the transaction will complete in our dmabuf blocker
// callback, which already calls blocker_cleared(), or by the end of this
// function, in which case there would be no blocker in the first place.
if !is_last {
// Waiting for some other surface; register a notification and add a
// transaction blocker.
if let Some(client) = surface.client() {
transaction.add_notification(
state.niri.blocker_cleared_tx.clone(),
client.clone(),
);
add_blocker(surface, transaction.blocker());
}
}
// Delay dropping (and completing) the transaction until the dmabuf is ready.
// If there's no dmabuf, this will be dropped by the end of this pre-commit
// hook.
transaction_for_dmabuf = Some(transaction);
}
}
animate = mapped.should_animate_commit(serial);
} else {
error!("commit on a mapped surface without a configured serial");
};
if let Some((blocker, source)) =
dmabuf.and_then(|dmabuf| dmabuf.generate_blocker(Interest::READ).ok())
{
if let Some(client) = surface.client() {
let res = state
.niri
.event_loop
.insert_source(source, move |_, _, state| {
// This surface is now ready for the transaction.
drop(transaction_for_dmabuf.take());
let display_handle = state.niri.display_handle.clone();
state
.client_compositor_state(&client)
.blocker_cleared(state, &display_handle);
Ok(())
});
if res.is_ok() {
add_blocker(surface, blocker);
trace!("added dmabuf blocker");
}
}
}
let window = mapped.window.clone();
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
state.niri.layout.store_unmap_snapshot(renderer, &window);
});
} else {
if animate {
state.backend.with_primary_renderer(|renderer| {
mapped.store_animation_snapshot(renderer);
});
}
// The toplevel remains mapped; clear any stored unmap snapshot.
state.niri.layout.clear_unmap_snapshot(&window);
}
})
}