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, 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, ) { 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, } 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, ) { 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::() .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.output_by_name.get(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::() .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) { 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, ) -> Rectangle { // 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::(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::(); 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::() .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); } }) }