use smithay::desktop::{ find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface, PopupKind, PopupManager, Window, WindowSurfaceType, }; 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::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::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) { // FIXME popup grabs } 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, ) { if surface .current_state() .capabilities .contains(xdg_toplevel::WmCapabilities::Fullscreen) { // NOTE: This is only one part of the solution. We can set the // location and configure size here, but the surface should be rendered fullscreen // independently from its buffer size 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) { toplevel.with_pending_state(|state| { state.decoration_mode = Some(mode); }); // Only send configure if it's non-initial. if initial_configure_sent(&toplevel) { toplevel.send_pending_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; }); // Only send configure if it's non-initial. if initial_configure_sent(&toplevel) { toplevel.send_pending_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::() .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::() .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, ) -> Rectangle { // 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) }