use std::cell::{Cell, OnceCell, RefCell}; use std::collections::{HashMap, HashSet}; use std::ffi::OsString; use std::path::PathBuf; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::{env, mem, thread}; use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode; use anyhow::{bail, ensure, Context}; use calloop::futures::Scheduler; use niri_config::{ Config, FloatOrInt, Key, Modifiers, PreviewRender, TrackLayout, WorkspaceReference, DEFAULT_BACKGROUND_COLOR, }; use niri_ipc::Workspace; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::damage::OutputDamageTracker; use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::surface::{ render_elements_from_surface_tree, WaylandSurfaceRenderElement, }; use smithay::backend::renderer::element::utils::{ select_dmabuf_feedback, Relocate, RelocateRenderElement, }; use smithay::backend::renderer::element::{ default_primary_scanout_output_compare, AsRenderElements, Element as _, Id, Kind, PrimaryScanoutOutput, RenderElementStates, }; use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::sync::SyncPoint; use smithay::backend::renderer::Unbind; use smithay::desktop::utils::{ bbox_from_surface_tree, output_update, send_dmabuf_feedback_surface_tree, send_frames_surface_tree, surface_presentation_feedback_flags_from_states, surface_primary_scanout_output, take_presentation_feedback_surface_tree, under_from_surface_tree, update_surface_primary_scanout_output, OutputPresentationFeedback, }; use smithay::desktop::{ layer_map_for_output, LayerSurface, PopupGrab, PopupManager, PopupUngrabStrategy, Space, Window, WindowSurfaceType, }; use smithay::input::keyboard::{Layout as KeyboardLayout, XkbContextHandler}; use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus, MotionEvent}; use smithay::input::{Seat, SeatState}; use smithay::output::{self, Output, OutputModeSource, PhysicalProperties, Subpixel}; use smithay::reexports::calloop::generic::Generic; use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; use smithay::reexports::calloop::{ Interest, LoopHandle, LoopSignal, Mode, PostAction, RegistrationToken, }; use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1::ExtSessionLockV1; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities; use smithay::reexports::wayland_protocols_misc::server_decoration as _server_decoration; use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; use smithay::reexports::wayland_server::backend::{ ClientData, ClientId, DisconnectReason, GlobalId, }; use smithay::reexports::wayland_server::protocol::wl_shm; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource}; use smithay::utils::{ ClockSource, IsAlive as _, Logical, Monotonic, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER, }; use smithay::wayland::compositor::{ with_states, with_surface_tree_downward, CompositorClientState, CompositorState, SurfaceData, TraversalAction, }; use smithay::wayland::cursor_shape::CursorShapeManagerState; use smithay::wayland::dmabuf::DmabufState; use smithay::wayland::fractional_scale::FractionalScaleManagerState; use smithay::wayland::idle_inhibit::IdleInhibitManagerState; use smithay::wayland::idle_notify::IdleNotifierState; use smithay::wayland::input_method::{InputMethodManagerState, InputMethodSeat}; use smithay::wayland::output::OutputManagerState; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsState}; use smithay::wayland::pointer_gestures::PointerGesturesState; use smithay::wayland::presentation::PresentationState; use smithay::wayland::relative_pointer::RelativePointerManagerState; use smithay::wayland::security_context::SecurityContextState; use smithay::wayland::selection::data_device::{set_data_device_selection, DataDeviceState}; use smithay::wayland::selection::primary_selection::PrimarySelectionState; use smithay::wayland::selection::wlr_data_control::DataControlState; use smithay::wayland::session_lock::{LockSurface, SessionLockManagerState, SessionLocker}; use smithay::wayland::shell::kde::decoration::KdeDecorationState; use smithay::wayland::shell::wlr_layer::{self, Layer, WlrLayerShellState}; use smithay::wayland::shell::xdg::decoration::XdgDecorationState; use smithay::wayland::shell::xdg::XdgShellState; use smithay::wayland::shm::ShmState; use smithay::wayland::socket::ListeningSocketSource; use smithay::wayland::tablet_manager::TabletManagerState; use smithay::wayland::text_input::TextInputManagerState; use smithay::wayland::viewporter::ViewporterState; use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState; use smithay::wayland::xdg_activation::XdgActivationState; use smithay::wayland::xdg_foreign::XdgForeignState; use crate::backend::tty::SurfaceDmabufFeedback; use crate::backend::{Backend, RenderResult, Tty, Winit}; use crate::cursor::{CursorManager, CursorTextureCache, RenderCursor, XCursor}; #[cfg(feature = "dbus")] use crate::dbus::gnome_shell_introspect::{self, IntrospectToNiri, NiriToIntrospect}; #[cfg(feature = "dbus")] use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri}; #[cfg(feature = "xdp-gnome-screencast")] use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri}; use crate::frame_clock::FrameClock; use crate::handlers::configure_lock_surface; use crate::input::scroll_tracker::ScrollTracker; use crate::input::{ apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_wheel_binds, TabletData, }; use crate::ipc::server::IpcServer; use crate::layout::{Layout, LayoutElement as _, MonitorRenderElement}; use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; use crate::protocols::gamma_control::GammaControlManagerState; use crate::protocols::mutter_x11_interop::MutterX11InteropManagerState; use crate::protocols::output_management::OutputManagementManagerState; use crate::protocols::screencopy::{Screencopy, ScreencopyBuffer, ScreencopyManagerState}; use crate::pw_utils::{Cast, PipeWire}; #[cfg(feature = "xdp-gnome-screencast")] use crate::pw_utils::{CastSizeChange, CastTarget, PwToNiri}; use crate::render_helpers::debug::draw_opaque_regions; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::texture::TextureBuffer; use crate::render_helpers::{ render_to_dmabuf, render_to_encompassing_texture, render_to_shm, render_to_texture, render_to_vec, shaders, RenderTarget, }; use crate::ui::config_error_notification::ConfigErrorNotification; use crate::ui::exit_confirm_dialog::ExitConfirmDialog; use crate::ui::hotkey_overlay::HotkeyOverlay; use crate::ui::screen_transition::{self, ScreenTransition}; use crate::ui::screenshot_ui::{OutputScreenshot, ScreenshotUi, ScreenshotUiRenderElement}; use crate::utils::scale::{closest_representable_scale, guess_monitor_scale}; use crate::utils::spawning::CHILD_ENV; use crate::utils::{ center, center_f64, get_monotonic_time, ipc_transform_to_smithay, logical_output, make_screenshot_path, output_size, send_scale_transform, write_png_rgba8, }; use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped, WindowRef}; use crate::{animation, niri_render_elements}; const CLEAR_COLOR_LOCKED: [f32; 4] = [0.3, 0.1, 0.1, 1.]; // We'll try to send frame callbacks at least once a second. We'll make a timer that fires once a // second, so with the worst timing the maximum interval between two frame callbacks for a surface // should be ~1.995 seconds. const FRAME_CALLBACK_THROTTLE: Option = Some(Duration::from_millis(995)); pub struct Niri { pub config: Rc>, /// Output config from the config file. /// /// This does not include transient output config changes done via IPC. It is only used when /// reloading the config from disk to determine if the output configuration should be reloaded /// (and transient changes dropped). pub config_file_output_config: niri_config::Outputs, pub event_loop: LoopHandle<'static, State>, pub scheduler: Scheduler<()>, pub stop_signal: LoopSignal, pub display_handle: DisplayHandle, pub socket_name: OsString, pub start_time: Instant, /// Whether the at-startup=true window rules are active. pub is_at_startup: bool, // Each workspace corresponds to a Space. Each workspace generally has one Output mapped to it, // however it may have none (when there are no outputs connected) or multiple (when mirroring). pub layout: Layout, // This space does not actually contain any windows, but all outputs are mapped into it // according to their global position. pub global_space: Space, // Windows which don't have a buffer attached yet. pub unmapped_windows: HashMap, // Cached root surface for every surface, so that we can access it in destroyed() where the // normal get_parent() is cleared out. pub root_surface: HashMap, pub output_state: HashMap, pub output_by_name: HashMap, // When false, we're idling with monitors powered off. pub monitors_active: bool, pub devices: HashSet, pub tablets: HashMap, pub touch: HashSet, // Smithay state. pub compositor_state: CompositorState, pub xdg_shell_state: XdgShellState, pub xdg_decoration_state: XdgDecorationState, pub kde_decoration_state: KdeDecorationState, pub layer_shell_state: WlrLayerShellState, pub session_lock_state: SessionLockManagerState, pub foreign_toplevel_state: ForeignToplevelManagerState, pub screencopy_state: ScreencopyManagerState, pub output_management_state: OutputManagementManagerState, pub viewporter_state: ViewporterState, pub xdg_foreign_state: XdgForeignState, pub shm_state: ShmState, pub output_manager_state: OutputManagerState, pub dmabuf_state: DmabufState, pub fractional_scale_manager_state: FractionalScaleManagerState, pub seat_state: SeatState, pub tablet_state: TabletManagerState, pub text_input_state: TextInputManagerState, pub input_method_state: InputMethodManagerState, pub virtual_keyboard_state: VirtualKeyboardManagerState, pub pointer_gestures_state: PointerGesturesState, pub relative_pointer_state: RelativePointerManagerState, pub pointer_constraints_state: PointerConstraintsState, pub idle_notifier_state: IdleNotifierState, pub idle_inhibit_manager_state: IdleInhibitManagerState, pub data_device_state: DataDeviceState, pub primary_selection_state: PrimarySelectionState, pub data_control_state: DataControlState, pub popups: PopupManager, pub popup_grab: Option, pub presentation_state: PresentationState, pub security_context_state: SecurityContextState, pub gamma_control_manager_state: GammaControlManagerState, pub activation_state: XdgActivationState, pub mutter_x11_interop_state: MutterX11InteropManagerState, pub seat: Seat, /// Scancodes of the keys to suppress. pub suppressed_keys: HashSet, pub bind_cooldown_timers: HashMap, pub bind_repeat_timer: Option, pub keyboard_focus: KeyboardFocus, pub layer_shell_on_demand_focus: Option, pub idle_inhibiting_surfaces: HashSet, pub is_fdo_idle_inhibited: Arc, pub cursor_manager: CursorManager, pub cursor_texture_cache: CursorTextureCache, pub cursor_shape_manager_state: CursorShapeManagerState, pub dnd_icon: Option, pub pointer_focus: PointerFocus, /// Whether the pointer is hidden, for example due to a previous touch input. /// /// When this happens, the pointer also loses any focus. This is so that touch can prevent /// various tooltips from sticking around. pub pointer_hidden: bool, // FIXME: this should be able to be removed once PointerFocus takes grabs into account. pub pointer_grab_ongoing: bool, pub tablet_cursor_location: Option>, pub gesture_swipe_3f_cumulative: Option<(f64, f64)>, pub vertical_wheel_tracker: ScrollTracker, pub horizontal_wheel_tracker: ScrollTracker, pub mods_with_wheel_binds: HashSet, pub vertical_finger_scroll_tracker: ScrollTracker, pub horizontal_finger_scroll_tracker: ScrollTracker, pub mods_with_finger_scroll_binds: HashSet, pub lock_state: LockState, pub screenshot_ui: ScreenshotUi, pub config_error_notification: ConfigErrorNotification, pub hotkey_overlay: HotkeyOverlay, pub exit_confirm_dialog: Option, pub debug_draw_opaque_regions: bool, pub debug_draw_damage: bool, #[cfg(feature = "dbus")] pub dbus: Option, #[cfg(feature = "dbus")] pub inhibit_power_key_fd: Option, pub ipc_server: Option, pub ipc_outputs_changed: bool, pub ipc_focused_window: Arc>>, // Casts are dropped before PipeWire to prevent a double-free (yay). pub casts: Vec, pub pipewire: Option, // Screencast output for each mapped window. #[cfg(feature = "xdp-gnome-screencast")] pub mapped_cast_output: HashMap, } pub struct OutputState { pub global: GlobalId, pub frame_clock: FrameClock, pub redraw_state: RedrawState, // After the last redraw, some ongoing animations still remain. pub unfinished_animations_remain: bool, /// Last sequence received in a vblank event. pub last_drm_sequence: Option, /// Sequence for frame callback throttling. /// /// We want to send frame callbacks for each surface at most once per monitor refresh cycle. /// /// Even if a surface commit resulted in empty damage to the monitor, we want to delay the next /// frame callback until roughly when a VBlank would occur, had the monitor been damaged. This /// is necessary to prevent clients busy-looping with frame callbacks that result in empty /// damage. /// /// This counter wrapping-increments by 1 every time we move into the next refresh cycle, as /// far as frame callback throttling is concerned. Specifically, it happens: /// /// 1. Upon a successful DRM frame submission. Notably, we don't wait for the VBlank here, /// because the client buffers are already "latched" at the point of submission. Even if a /// client submits a new buffer right away, we will wait for a VBlank to draw it, which /// means that busy looping is avoided. /// 2. If a frame resulted in empty damage, a timer is queued to fire roughly when a VBlank /// would occur, based on the last presentation time and output refresh interval. Sequence /// is incremented in that timer, before attempting a redraw or sending frame callbacks. pub frame_callback_sequence: u32, /// Solid color buffer for the background that we use instead of clearing to avoid damage /// tracking issues and make screenshots easier. pub background_buffer: SolidColorBuffer, pub lock_render_state: LockRenderState, pub lock_surface: Option, pub lock_color_buffer: SolidColorBuffer, screen_transition: Option, /// Damage tracker used for the debug damage visualization. pub debug_damage_tracker: OutputDamageTracker, } #[derive(Default)] pub enum RedrawState { /// The compositor is idle. #[default] Idle, /// A redraw is queued. Queued, /// We submitted a frame to the KMS and waiting for it to be presented. WaitingForVBlank { redraw_needed: bool }, /// We did not submit anything to KMS and made a timer to fire at the estimated VBlank. WaitingForEstimatedVBlank(RegistrationToken), /// A redraw is queued on top of the above. WaitingForEstimatedVBlankAndQueued(RegistrationToken), } pub struct PopupGrabState { pub root: WlSurface, pub grab: PopupGrab, } // The surfaces here are always toplevel surfaces focused as far as niri's logic is concerned, even // when popup grabs are active (which means the real keyboard focus is on a popup descending from // that toplevel surface). #[derive(Debug, Clone, PartialEq, Eq)] pub enum KeyboardFocus { // Layout is focused by default if there's nothing else to focus. Layout { surface: Option }, LayerShell { surface: WlSurface }, LockScreen { surface: Option }, ScreenshotUi, } #[derive(Default, Clone, PartialEq)] pub struct PointerFocus { // Output under pointer. pub output: Option, // Surface under pointer and its location in global coordinate space. pub surface: Option<(WlSurface, Point)>, // If surface belongs to a window, this is that window. pub window: Option, // If surface belongs to a layer surface, this is that layer surface. pub layer: Option, } #[derive(Default)] pub enum LockState { #[default] Unlocked, Locking(SessionLocker), Locked(ExtSessionLockV1), } #[derive(PartialEq, Eq)] pub enum LockRenderState { /// The output displays a normal session frame. Unlocked, /// The output displays a locked frame. Locked, } // Not related to the one in Smithay. // // This state keeps track of when a surface last received a frame callback. struct SurfaceFrameThrottlingState { /// Output and sequence that the frame callback was last sent at. last_sent_at: RefCell>, } pub enum CenterCoords { Separately, Both, } #[derive(Default)] pub struct WindowOffscreenId(pub RefCell>); impl RedrawState { fn queue_redraw(self) -> Self { match self { RedrawState::Idle => RedrawState::Queued, RedrawState::WaitingForEstimatedVBlank(token) => { RedrawState::WaitingForEstimatedVBlankAndQueued(token) } // A redraw is already queued. value @ (RedrawState::Queued | RedrawState::WaitingForEstimatedVBlankAndQueued(_)) => { value } // We're waiting for VBlank, request a redraw afterwards. RedrawState::WaitingForVBlank { .. } => RedrawState::WaitingForVBlank { redraw_needed: true, }, } } } impl Default for SurfaceFrameThrottlingState { fn default() -> Self { Self { last_sent_at: RefCell::new(None), } } } impl KeyboardFocus { pub fn surface(&self) -> Option<&WlSurface> { match self { KeyboardFocus::Layout { surface } => surface.as_ref(), KeyboardFocus::LayerShell { surface } => Some(surface), KeyboardFocus::LockScreen { surface } => surface.as_ref(), KeyboardFocus::ScreenshotUi => None, } } pub fn into_surface(self) -> Option { match self { KeyboardFocus::Layout { surface } => surface, KeyboardFocus::LayerShell { surface } => Some(surface), KeyboardFocus::LockScreen { surface } => surface, KeyboardFocus::ScreenshotUi => None, } } pub fn is_layout(&self) -> bool { matches!(self, KeyboardFocus::Layout { .. }) } } pub struct State { pub backend: Backend, pub niri: Niri, } impl State { pub fn new( config: Config, event_loop: LoopHandle<'static, State>, stop_signal: LoopSignal, display: Display, ) -> Result> { let _span = tracy_client::span!("State::new"); let config = Rc::new(RefCell::new(config)); let has_display = env::var_os("WAYLAND_DISPLAY").is_some() || env::var_os("DISPLAY").is_some(); let mut backend = if has_display { let winit = Winit::new(config.clone(), event_loop.clone())?; Backend::Winit(winit) } else { let tty = Tty::new(config.clone(), event_loop.clone()) .context("error initializing the TTY backend")?; Backend::Tty(tty) }; let mut niri = Niri::new(config.clone(), event_loop, stop_signal, display, &backend); backend.init(&mut niri); Ok(Self { backend, niri }) } pub fn refresh_and_flush_clients(&mut self) { let _span = tracy_client::span!("State::refresh_and_flush_clients"); self.refresh(); self.niri.redraw_queued_outputs(&mut self.backend); { let _span = tracy_client::span!("flush_clients"); self.niri.display_handle.flush_clients().unwrap(); } } fn refresh(&mut self) { let _span = tracy_client::span!("State::refresh"); // These should be called periodically, before flushing the clients. self.niri.layout.refresh(); self.niri.cursor_manager.check_cursor_image_surface_alive(); self.niri.refresh_pointer_outputs(); self.niri.popups.cleanup(); self.niri.global_space.refresh(); self.niri.refresh_idle_inhibit(); self.refresh_popup_grab(); self.update_keyboard_focus(); self.refresh_pointer_focus(); foreign_toplevel::refresh(self); self.niri.refresh_window_rules(); self.refresh_ipc_outputs(); #[cfg(feature = "xdp-gnome-screencast")] self.niri.refresh_mapped_cast_outputs(); } pub fn move_cursor(&mut self, location: Point) { let under = self.niri.surface_under_and_global_space(location); self.niri .maybe_activate_pointer_constraint(location, &under); self.niri.pointer_focus.clone_from(&under); let pointer = &self.niri.seat.get_pointer().unwrap(); pointer.motion( self, under.surface, &MotionEvent { location, serial: SERIAL_COUNTER.next_serial(), time: get_monotonic_time().as_millis() as u32, }, ); pointer.frame(self); // We moved the pointer, show it. self.niri.pointer_hidden = false; // FIXME: granular self.niri.queue_redraw_all(); } /// Moves cursor within the specified rectangle, only adjusting coordinates if needed. fn move_cursor_to_rect(&mut self, rect: Rectangle, mode: CenterCoords) -> bool { let pointer = &self.niri.seat.get_pointer().unwrap(); let cur_loc = pointer.current_location(); let x_in_bound = cur_loc.x >= rect.loc.x && cur_loc.x <= rect.loc.x + rect.size.w; let y_in_bound = cur_loc.y >= rect.loc.y && cur_loc.y <= rect.loc.y + rect.size.h; let p = match mode { CenterCoords::Separately => { if x_in_bound && y_in_bound { return false; } else if y_in_bound { // adjust x Point::from((rect.loc.x + rect.size.w / 2.0, cur_loc.y)) } else if x_in_bound { // adjust y Point::from((cur_loc.x, rect.loc.y + rect.size.h / 2.0)) } else { // adjust x and y center_f64(rect) } } CenterCoords::Both => { if x_in_bound && y_in_bound { return false; } else { // adjust x and y center_f64(rect) } } }; self.move_cursor(p); true } pub fn move_cursor_to_focused_tile(&mut self, mode: CenterCoords) -> bool { if !self.niri.keyboard_focus.is_layout() { return false; } if self.niri.tablet_cursor_location.is_some() { return false; } let Some(output) = self.niri.layout.active_output() else { return false; }; let output = output.clone(); let monitor = self.niri.layout.monitor_for_output(&output).unwrap(); let mut rv = false; let rect = monitor.active_tile_visual_rectangle(); if let Some(rect) = rect { let output_geo = self.niri.global_space.output_geometry(&output).unwrap(); let mut rect = rect; rect.loc += output_geo.loc.to_f64(); rv = self.move_cursor_to_rect(rect, mode); } rv } pub fn maybe_warp_cursor_to_focus(&mut self) -> bool { if !self.niri.config.borrow().input.warp_mouse_to_focus { return false; } self.move_cursor_to_focused_tile(CenterCoords::Separately) } pub fn maybe_warp_cursor_to_focus_centered(&mut self) -> bool { if !self.niri.config.borrow().input.warp_mouse_to_focus { return false; } self.move_cursor_to_focused_tile(CenterCoords::Both) } pub fn refresh_pointer_focus(&mut self) { let _span = tracy_client::span!("Niri::refresh_pointer_focus"); let pointer = &self.niri.seat.get_pointer().unwrap(); let location = pointer.current_location(); if !self.niri.is_locked() && !self.niri.screenshot_ui.is_open() { // Don't refresh cursor focus during transitions. if let Some((output, _)) = self.niri.output_under(location) { let monitor = self.niri.layout.monitor_for_output(output).unwrap(); if monitor.are_transitions_ongoing() { return; } } } if !self.update_pointer_focus() { return; } pointer.frame(self); // FIXME: granular self.niri.queue_redraw_all(); } pub fn update_pointer_focus(&mut self) -> bool { let _span = tracy_client::span!("Niri::update_pointer_focus"); let pointer = &self.niri.seat.get_pointer().unwrap(); let location = pointer.current_location(); let under = if self.niri.pointer_hidden { PointerFocus::default() } else { self.niri.surface_under_and_global_space(location) }; // We're not changing the global cursor location here, so if the focus did not change, then // nothing changed. if self.niri.pointer_focus == under { return false; } self.niri .maybe_activate_pointer_constraint(location, &under); self.niri.pointer_focus.clone_from(&under); pointer.motion( self, under.surface, &MotionEvent { location, serial: SERIAL_COUNTER.next_serial(), time: get_monotonic_time().as_millis() as u32, }, ); true } pub fn move_cursor_to_output(&mut self, output: &Output) { let geo = self.niri.global_space.output_geometry(output).unwrap(); self.move_cursor(center(geo).to_f64()); } pub fn refresh_popup_grab(&mut self) { let keyboard_grabbed = self.niri.seat.input_method().keyboard_grabbed(); if let Some(grab) = &mut self.niri.popup_grab { if grab.grab.has_ended() { self.niri.popup_grab = None; } else if keyboard_grabbed { // HACK: remove popup grab if IME grabbed the keyboard, because we can't yet do // popup grabs together with an IME grab. // FIXME: do this properly. grab.grab.ungrab(PopupUngrabStrategy::All); self.niri.seat.get_pointer().unwrap().unset_grab( self, SERIAL_COUNTER.next_serial(), get_monotonic_time().as_millis() as u32, ); self.niri.popup_grab = None; } } } pub fn update_keyboard_focus(&mut self) { // Clean up on-demand layer surface focus if necessary. if let Some(surface) = &self.niri.layer_shell_on_demand_focus { // Still alive and has on-demand interactivity. let good = surface.alive() && surface.cached_state().keyboard_interactivity == wlr_layer::KeyboardInteractivity::OnDemand; if !good { self.niri.layer_shell_on_demand_focus = None; } } // Compute the current focus. let focus = if self.niri.is_locked() { KeyboardFocus::LockScreen { surface: self.niri.lock_surface_focus(), } } else if self.niri.screenshot_ui.is_open() { KeyboardFocus::ScreenshotUi } else if let Some(output) = self.niri.layout.active_output() { let mon = self.niri.layout.monitor_for_output(output).unwrap(); let layers = layer_map_for_output(output); // Explicitly check for layer-shell popup grabs here, our keyboard focus will stay on // the root layer surface while it has grabs. let layer_grab = self.niri.popup_grab.as_ref().and_then(|g| { layers .layer_for_surface(&g.root, WindowSurfaceType::TOPLEVEL) .map(|l| (&g.root, l.layer())) }); let grab_on_layer = |layer: Layer| { layer_grab .and_then(move |(s, l)| if l == layer { Some(s.clone()) } else { None }) .map(|surface| KeyboardFocus::LayerShell { surface }) }; let layout_focus = || { self.niri .layout .focus() .map(|win| win.toplevel().wl_surface().clone()) .map(|surface| KeyboardFocus::Layout { surface: Some(surface), }) }; let layer_focus = |surface: &LayerSurface| { let can_receive_exclusive_focus = surface.cached_state().keyboard_interactivity == wlr_layer::KeyboardInteractivity::Exclusive; let is_on_demand_surface = Some(surface) == self.niri.layer_shell_on_demand_focus.as_ref(); (can_receive_exclusive_focus || is_on_demand_surface) .then(|| surface.wl_surface().clone()) .map(|surface| KeyboardFocus::LayerShell { surface }) }; let on_d_focus = |surface: &LayerSurface| { let is_on_demand_surface = Some(surface) == self.niri.layer_shell_on_demand_focus.as_ref(); is_on_demand_surface .then(|| surface.wl_surface().clone()) .map(|surface| KeyboardFocus::LayerShell { surface }) }; let mut surface = grab_on_layer(Layer::Overlay); // FIXME: we shouldn't prioritize the top layer grabs over regular overlay input or a // fullscreen layout window. This will need tracking in grab() to avoid handing it out // in the first place. Or a better way to structure this code. surface = surface.or_else(|| grab_on_layer(Layer::Top)); surface = surface.or_else(|| layers.layers_on(Layer::Overlay).find_map(layer_focus)); if mon.render_above_top_layer() { surface = surface.or_else(layout_focus); surface = surface.or_else(|| layers.layers_on(Layer::Top).find_map(layer_focus)); } else { surface = surface.or_else(|| layers.layers_on(Layer::Top).find_map(layer_focus)); surface = surface.or_else(layout_focus); } // Bottom and background layers can receive on-demand focus only. surface = surface.or_else(|| layers.layers_on(Layer::Bottom).find_map(on_d_focus)); surface = surface.or_else(|| layers.layers_on(Layer::Background).find_map(on_d_focus)); surface.unwrap_or(KeyboardFocus::Layout { surface: None }) } else { KeyboardFocus::Layout { surface: None } }; let keyboard = self.niri.seat.get_keyboard().unwrap(); if self.niri.keyboard_focus != focus { trace!( "keyboard focus changed from {:?} to {:?}", self.niri.keyboard_focus, focus ); let mut newly_focused_window = None; // Tell the windows their new focus state for window rule purposes. if let KeyboardFocus::Layout { surface: Some(surface), } = &self.niri.keyboard_focus { if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) { mapped.set_is_focused(false); } } if let KeyboardFocus::Layout { surface: Some(surface), } = &focus { if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) { mapped.set_is_focused(true); newly_focused_window = Some(mapped.window.clone()); } } *self.niri.ipc_focused_window.lock().unwrap() = newly_focused_window; if let Some(grab) = self.niri.popup_grab.as_mut() { if Some(&grab.root) != focus.surface() { trace!( "grab root {:?} is not the new focus {:?}, ungrabbing", grab.root, focus ); grab.grab.ungrab(PopupUngrabStrategy::All); keyboard.unset_grab(self); self.niri.seat.get_pointer().unwrap().unset_grab( self, SERIAL_COUNTER.next_serial(), get_monotonic_time().as_millis() as u32, ); self.niri.popup_grab = None; } } if self.niri.config.borrow().input.keyboard.track_layout == TrackLayout::Window { let current_layout = keyboard.with_xkb_state(self, |context| context.active_layout()); let mut new_layout = current_layout; // Store the currently active layout for the surface. if let Some(current_focus) = self.niri.keyboard_focus.surface() { with_states(current_focus, |data| { let cell = data .data_map .get_or_insert::, _>(Cell::default); cell.set(current_layout); }); } if let Some(focus) = focus.surface() { new_layout = with_states(focus, |data| { let cell = data.data_map.get_or_insert::, _>(|| { // The default layout is effectively the first layout in the // keymap, so use it for new windows. Cell::new(KeyboardLayout::default()) }); cell.get() }); } if new_layout != current_layout && focus.surface().is_some() { keyboard.set_focus(self, None, SERIAL_COUNTER.next_serial()); keyboard.with_xkb_state(self, |mut context| { context.set_layout(new_layout); }); } } self.niri.keyboard_focus.clone_from(&focus); keyboard.set_focus(self, focus.into_surface(), SERIAL_COUNTER.next_serial()); // FIXME: can be more granular. self.niri.queue_redraw_all(); } } pub fn reload_config(&mut self, path: PathBuf) { let _span = tracy_client::span!("State::reload_config"); let mut config = match Config::load(&path) { Ok(config) => config, Err(err) => { warn!("{:?}", err.context("error loading config")); self.niri.config_error_notification.show(); self.niri.queue_redraw_all(); return; } }; self.niri.config_error_notification.hide(); // Find & orphan removed named workspaces. let mut removed_workspaces: Vec = vec![]; for ws in &self.niri.config.borrow().workspaces { if !config.workspaces.iter().any(|w| w.name == ws.name) { removed_workspaces.push(ws.name.0.clone()); } } for name in removed_workspaces { self.niri.layout.unname_workspace(&name); } self.niri.layout.update_config(&config); // Create new named workspaces. for ws_config in &config.workspaces { self.niri.layout.ensure_named_workspace(ws_config); } let slowdown = if config.animations.off { 0. } else { config.animations.slowdown.clamp(0., 100.) }; animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed); *CHILD_ENV.write().unwrap() = mem::take(&mut config.environment); let mut reload_xkb = None; let mut libinput_config_changed = false; let mut output_config_changed = false; let mut preserved_output_config = None; let mut window_rules_changed = false; let mut debug_config_changed = false; let mut shaders_changed = false; let mut old_config = self.niri.config.borrow_mut(); // Reload the cursor. if config.cursor != old_config.cursor { self.niri .cursor_manager .reload(&config.cursor.xcursor_theme, config.cursor.xcursor_size); self.niri.cursor_texture_cache.clear(); } // We need &mut self to reload the xkb config, so just store it here. if config.input.keyboard.xkb != old_config.input.keyboard.xkb { reload_xkb = Some(config.input.keyboard.xkb.clone()); } // Reload the repeat info. if config.input.keyboard.repeat_rate != old_config.input.keyboard.repeat_rate || config.input.keyboard.repeat_delay != old_config.input.keyboard.repeat_delay { let keyboard = self.niri.seat.get_keyboard().unwrap(); keyboard.change_repeat_info( config.input.keyboard.repeat_rate.into(), config.input.keyboard.repeat_delay.into(), ); } if config.input.touchpad != old_config.input.touchpad || config.input.mouse != old_config.input.mouse || config.input.trackpoint != old_config.input.trackpoint { libinput_config_changed = true; } if config.outputs != self.niri.config_file_output_config { output_config_changed = true; self.niri .config_file_output_config .clone_from(&config.outputs); } else { // Output config did not change from the last disk load, so we need to preserve the // transient changes. preserved_output_config = Some(mem::take(&mut old_config.outputs)); } if config.binds != old_config.binds { self.niri.hotkey_overlay.on_hotkey_config_updated(); self.niri.mods_with_wheel_binds = mods_with_wheel_binds(self.backend.mod_key(), &config.binds); self.niri.mods_with_finger_scroll_binds = mods_with_finger_scroll_binds(self.backend.mod_key(), &config.binds); } if config.window_rules != old_config.window_rules { window_rules_changed = true; } if config.animations.window_resize.custom_shader != old_config.animations.window_resize.custom_shader { let src = config.animations.window_resize.custom_shader.as_deref(); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_resize_program(renderer, src); }); shaders_changed = true; } if config.animations.window_close.custom_shader != old_config.animations.window_close.custom_shader { let src = config.animations.window_close.custom_shader.as_deref(); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_close_program(renderer, src); }); shaders_changed = true; } if config.animations.window_open.custom_shader != old_config.animations.window_open.custom_shader { let src = config.animations.window_open.custom_shader.as_deref(); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_open_program(renderer, src); }); shaders_changed = true; } if config.debug != old_config.debug { debug_config_changed = true; } *old_config = config; if let Some(outputs) = preserved_output_config { old_config.outputs = outputs; } // Release the borrow. drop(old_config); // Now with a &mut self we can reload the xkb config. if let Some(xkb) = reload_xkb { let keyboard = self.niri.seat.get_keyboard().unwrap(); if let Err(err) = keyboard.set_xkb_config(self, xkb.to_xkb_config()) { warn!("error updating xkb config: {err:?}"); } } if libinput_config_changed { let config = self.niri.config.borrow(); for mut device in self.niri.devices.iter().cloned() { apply_libinput_settings(&config.input, &mut device); } } if output_config_changed { self.reload_output_config(); } if debug_config_changed { self.backend.on_debug_config_changed(); } if window_rules_changed { self.niri.recompute_window_rules(); } if shaders_changed { self.niri.layout.update_shaders(); } // Can't really update xdg-decoration settings since we have to hide the globals for CSD // due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration // global suddenly appearing? Either way, right now it's live-reloaded in a sense that new // clients will use the new xdg-decoration setting. self.niri.queue_redraw_all(); } pub fn reload_output_config(&mut self) { let mut resized_outputs = vec![]; let mut recolored_outputs = vec![]; for output in self.niri.global_space.outputs() { let name = output.name(); let config = self.niri.config.borrow_mut(); let config = config.outputs.find(&name); let scale = config .and_then(|c| c.scale) .map(|s| s.0) .unwrap_or_else(|| { let size_mm = output.physical_properties().size; let resolution = output.current_mode().unwrap().size; guess_monitor_scale(size_mm, resolution) }); let scale = closest_representable_scale(scale.clamp(0.1, 10.)); let mut transform = config .map(|c| ipc_transform_to_smithay(c.transform)) .unwrap_or(Transform::Normal); // FIXME: fix winit damage on other transforms. if name == "winit" { transform = Transform::Flipped180; } if output.current_scale().fractional_scale() != scale || output.current_transform() != transform { output.change_current_state( None, Some(transform), Some(output::Scale::Fractional(scale)), None, ); self.niri.ipc_outputs_changed = true; resized_outputs.push(output.clone()); } let mut background_color = config .map(|c| c.background_color) .unwrap_or(DEFAULT_BACKGROUND_COLOR) .to_array_unpremul(); background_color[3] = 1.; if let Some(state) = self.niri.output_state.get_mut(output) { if state.background_buffer.color() != background_color { state.background_buffer.set_color(background_color); recolored_outputs.push(output.clone()); } } } for output in resized_outputs { self.niri.output_resized(&output); } for output in recolored_outputs { self.niri.queue_redraw(&output); } self.backend.on_output_config_changed(&mut self.niri); self.niri.reposition_outputs(None); if let Some(touch) = self.niri.seat.get_touch() { touch.cancel(self); } let config = self.niri.config.borrow().outputs.clone(); self.niri.output_management_state.on_config_changed(config); } pub fn apply_transient_output_config(&mut self, name: &str, action: niri_ipc::OutputAction) { { let mut config = self.niri.config.borrow_mut(); let config = if let Some(config) = config.outputs.find_mut(name) { config } else { config.outputs.0.push(niri_config::Output { name: String::from(name), ..Default::default() }); config.outputs.0.last_mut().unwrap() }; match action { niri_ipc::OutputAction::Off => config.off = true, niri_ipc::OutputAction::On => config.off = false, niri_ipc::OutputAction::Mode { mode } => { config.mode = match mode { niri_ipc::ModeToSet::Automatic => None, niri_ipc::ModeToSet::Specific(mode) => Some(mode), } } niri_ipc::OutputAction::Scale { scale } => { config.scale = match scale { niri_ipc::ScaleToSet::Automatic => None, niri_ipc::ScaleToSet::Specific(scale) => Some(FloatOrInt(scale)), } } niri_ipc::OutputAction::Transform { transform } => config.transform = transform, niri_ipc::OutputAction::Position { position } => { config.position = match position { niri_ipc::PositionToSet::Automatic => None, niri_ipc::PositionToSet::Specific(position) => { Some(niri_config::Position { x: position.x, y: position.y, }) } } } niri_ipc::OutputAction::Vrr { enable } => { config.variable_refresh_rate = enable; } } } self.reload_output_config(); } pub fn refresh_ipc_outputs(&mut self) { if !self.niri.ipc_outputs_changed { return; } self.niri.ipc_outputs_changed = false; let _span = tracy_client::span!("State::refresh_ipc_outputs"); for ipc_output in self.backend.ipc_outputs().lock().unwrap().values_mut() { let logical = self .niri .global_space .outputs() .find(|output| output.name() == ipc_output.name) .map(logical_output); ipc_output.logical = logical; } #[cfg(feature = "dbus")] self.niri.on_ipc_outputs_changed(); let new_config = self.backend.ipc_outputs().lock().unwrap().clone(); self.niri.output_management_state.notify_changes(new_config); } pub fn open_screenshot_ui(&mut self) { if self.niri.is_locked() || self.niri.screenshot_ui.is_open() { return; } let default_output = self .niri .output_under_cursor() .or_else(|| self.niri.layout.active_output().cloned()); let Some(default_output) = default_output else { return; }; self.niri.layout.update_render_elements_all(); let Some(screenshots) = self .backend .with_primary_renderer(|renderer| self.niri.capture_screenshots(renderer).collect()) else { return; }; // Now that we captured the screenshots, clear grabs like drag-and-drop, etc. self.niri.seat.get_pointer().unwrap().unset_grab( self, SERIAL_COUNTER.next_serial(), get_monotonic_time().as_millis() as u32, ); self.backend.with_primary_renderer(|renderer| { self.niri .screenshot_ui .open(renderer, screenshots, default_output) }); self.niri .cursor_manager .set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair)); self.niri.queue_redraw_all(); } #[cfg(feature = "xdp-gnome-screencast")] pub fn on_pw_msg(&mut self, msg: PwToNiri) { match msg { PwToNiri::StopCast { session_id } => self.niri.stop_cast(session_id), PwToNiri::Redraw(target) => match target { CastTarget::Output(weak) => { if let Some(output) = weak.upgrade() { self.niri.queue_redraw(&output); } } CastTarget::Window { id } => { self.backend.with_primary_renderer(|renderer| { // FIXME: target presentation time at the time of window commit? self.niri .render_window_for_screen_cast(renderer, id, get_monotonic_time()); }); } }, } } #[cfg(feature = "xdp-gnome-screencast")] pub fn on_screen_cast_msg(&mut self, msg: ScreenCastToNiri) { use crate::dbus::mutter_screen_cast::StreamTargetId; match msg { ScreenCastToNiri::StartCast { session_id, target, cursor_mode, signal_ctx, } => { let _span = tracy_client::span!("StartCast"); debug!(session_id, "StartCast"); let gbm = match self.backend.gbm_device() { Some(gbm) => gbm, None => { debug!("no GBM device available"); return; } }; let Some(pw) = &self.niri.pipewire else { error!("screencasting must be disabled if PipeWire is missing"); return; }; let (target, size, refresh, alpha) = match target { StreamTargetId::Output { name } => { let global_space = &self.niri.global_space; let output = global_space.outputs().find(|out| out.name() == name); let Some(output) = output else { warn!("error starting screencast: requested output is missing"); self.niri.stop_cast(session_id); return; }; let mode = output.current_mode().unwrap(); let transform = output.current_transform(); let size = transform.transform_size(mode.size); let refresh = mode.refresh as u32; (CastTarget::Output(output.downgrade()), size, refresh, false) } StreamTargetId::Window { id } => { let mut window = None; self.niri.layout.with_windows(|mapped, _| { if u64::from(mapped.id().get()) != id { return; } window = Some(mapped.window.clone()); }); let Some(window) = window else { warn!("error starting screencast: requested window is missing"); self.niri.stop_cast(session_id); return; }; // Use the cached output since it will be present even if the output was // currently disconnected. let Some(output) = self.niri.mapped_cast_output.get(&window) else { warn!("error starting screencast: requested window is missing"); self.niri.stop_cast(session_id); return; }; let scale = Scale::from(output.current_scale().fractional_scale()); let bbox = window.bbox_with_popups().to_physical_precise_up(scale); let refresh = output.current_mode().unwrap().refresh as u32; (CastTarget::Window { id }, bbox.size, refresh, true) } }; let render_formats = self .backend .with_primary_renderer(|renderer| { renderer.egl_context().dmabuf_render_formats().clone() }) .unwrap_or_default(); let res = pw.start_cast( gbm, render_formats, session_id, target, size, refresh, alpha, cursor_mode, signal_ctx, ); match res { Ok(cast) => { self.niri.casts.push(cast); } Err(err) => { warn!("error starting screencast: {err:?}"); self.niri.stop_cast(session_id); } } } ScreenCastToNiri::StopCast { session_id } => self.niri.stop_cast(session_id), } } #[cfg(feature = "dbus")] pub fn on_screen_shot_msg( &mut self, to_screenshot: &async_channel::Sender, msg: ScreenshotToNiri, ) { let ScreenshotToNiri::TakeScreenshot { include_cursor } = msg; let _span = tracy_client::span!("TakeScreenshot"); let rv = self.backend.with_primary_renderer(|renderer| { let on_done = { let to_screenshot = to_screenshot.clone(); move |path| { let msg = NiriToScreenshot::ScreenshotResult(Some(path)); if let Err(err) = to_screenshot.send_blocking(msg) { warn!("error sending path to screenshot: {err:?}"); } } }; let res = self .niri .screenshot_all_outputs(renderer, include_cursor, on_done); if let Err(err) = res { warn!("error taking a screenshot: {err:?}"); let msg = NiriToScreenshot::ScreenshotResult(None); if let Err(err) = to_screenshot.send_blocking(msg) { warn!("error sending None to screenshot: {err:?}"); } } }); if rv.is_none() { let msg = NiriToScreenshot::ScreenshotResult(None); if let Err(err) = to_screenshot.send_blocking(msg) { warn!("error sending None to screenshot: {err:?}"); } } } #[cfg(feature = "dbus")] pub fn on_introspect_msg( &mut self, to_introspect: &async_channel::Sender, msg: IntrospectToNiri, ) { use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; let IntrospectToNiri::GetWindows = msg; let _span = tracy_client::span!("GetWindows"); let mut windows = HashMap::new(); self.niri.layout.with_windows(|mapped, _| { let wl_surface = mapped .window .toplevel() .expect("no X11 support") .wl_surface(); let id = u64::from(mapped.id().get()); let props = with_states(wl_surface, |states| { let role = states .data_map .get::() .unwrap() .lock() .unwrap(); gnome_shell_introspect::WindowProperties { title: role.title.clone().unwrap_or_default(), app_id: role.app_id.clone().unwrap_or_default(), } }); windows.insert(id, props); }); let msg = NiriToIntrospect::Windows(windows); if let Err(err) = to_introspect.send_blocking(msg) { warn!("error sending windows to introspect: {err:?}"); } } } impl Niri { pub fn new( config: Rc>, event_loop: LoopHandle<'static, State>, stop_signal: LoopSignal, display: Display, backend: &Backend, ) -> Self { let _span = tracy_client::span!("Niri::new"); let (executor, scheduler) = calloop::futures::executor().unwrap(); event_loop.insert_source(executor, |_, _, _| ()).unwrap(); let display_handle = display.handle(); let config_ = config.borrow(); let config_file_output_config = config_.outputs.clone(); let layout = Layout::new(&config_); let compositor_state = CompositorState::new_v6::(&display_handle); let xdg_shell_state = XdgShellState::new_with_capabilities::( &display_handle, [WmCapabilities::Fullscreen], ); let xdg_decoration_state = XdgDecorationState::new_with_filter::(&display_handle, |client| { client .get_data::() .unwrap() .can_view_decoration_globals }); let kde_decoration_state = KdeDecorationState::new_with_filter::( &display_handle, // If we want CSD we will hide the global. KdeDecorationsMode::Server, |client| { client .get_data::() .unwrap() .can_view_decoration_globals }, ); let layer_shell_state = WlrLayerShellState::new_with_filter::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); let session_lock_state = SessionLockManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); let shm_state = ShmState::new::( &display_handle, vec![wl_shm::Format::Xbgr8888, wl_shm::Format::Abgr8888], ); let output_manager_state = OutputManagerState::new_with_xdg_output::(&display_handle); let dmabuf_state = DmabufState::new(); let fractional_scale_manager_state = FractionalScaleManagerState::new::(&display_handle); let mut seat_state = SeatState::new(); let tablet_state = TabletManagerState::new::(&display_handle); let pointer_gestures_state = PointerGesturesState::new::(&display_handle); let relative_pointer_state = RelativePointerManagerState::new::(&display_handle); let pointer_constraints_state = PointerConstraintsState::new::(&display_handle); let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.clone()); let idle_inhibit_manager_state = IdleInhibitManagerState::new::(&display_handle); let data_device_state = DataDeviceState::new::(&display_handle); let primary_selection_state = PrimarySelectionState::new::(&display_handle); let data_control_state = DataControlState::new::( &display_handle, Some(&primary_selection_state), |client| !client.get_data::().unwrap().restricted, ); let presentation_state = PresentationState::new::(&display_handle, Monotonic::ID as u32); let security_context_state = SecurityContextState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); let text_input_state = TextInputManagerState::new::(&display_handle); let input_method_state = InputMethodManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); let virtual_keyboard_state = VirtualKeyboardManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); let foreign_toplevel_state = ForeignToplevelManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); let mut output_management_state = OutputManagementManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); output_management_state.on_config_changed(config_.outputs.clone()); let screencopy_state = ScreencopyManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); let viewporter_state = ViewporterState::new::(&display_handle); let xdg_foreign_state = XdgForeignState::new::(&display_handle); let is_tty = matches!(backend, Backend::Tty(_)); let gamma_control_manager_state = GammaControlManagerState::new::(&display_handle, move |client| { is_tty && !client.get_data::().unwrap().restricted }); let activation_state = XdgActivationState::new::(&display_handle); let mutter_x11_interop_state = MutterX11InteropManagerState::new::(&display_handle, move |_| true); let mut seat: Seat = seat_state.new_wl_seat(&display_handle, backend.seat_name()); seat.add_keyboard( config_.input.keyboard.xkb.to_xkb_config(), config_.input.keyboard.repeat_delay.into(), config_.input.keyboard.repeat_rate.into(), ) .unwrap(); seat.add_pointer(); let cursor_shape_manager_state = CursorShapeManagerState::new::(&display_handle); let cursor_manager = CursorManager::new(&config_.cursor.xcursor_theme, config_.cursor.xcursor_size); let mods_with_wheel_binds = mods_with_wheel_binds(backend.mod_key(), &config_.binds); let mods_with_finger_scroll_binds = mods_with_finger_scroll_binds(backend.mod_key(), &config_.binds); let screenshot_ui = ScreenshotUi::new(config.clone()); let config_error_notification = ConfigErrorNotification::new(config.clone()); let mut hotkey_overlay = HotkeyOverlay::new(config.clone(), backend.mod_key()); if !config_.hotkey_overlay.skip_at_startup { hotkey_overlay.show(); } let exit_confirm_dialog = match ExitConfirmDialog::new() { Ok(x) => Some(x), Err(err) => { warn!("error creating the exit confirm dialog: {err:?}"); None } }; event_loop .insert_source( Timer::from_duration(Duration::from_secs(1)), |_, _, state| { state.niri.send_frame_callbacks_on_fallback_timer(); TimeoutAction::ToDuration(Duration::from_secs(1)) }, ) .unwrap(); let socket_source = ListeningSocketSource::new_auto().unwrap(); let socket_name = socket_source.socket_name().to_os_string(); event_loop .insert_source(socket_source, move |client, _, state| { let config = state.niri.config.borrow(); let data = Arc::new(ClientState { compositor_state: Default::default(), can_view_decoration_globals: config.prefer_no_csd, restricted: false, }); if let Err(err) = state.niri.display_handle.insert_client(client, data) { warn!("error inserting client: {err}"); } }) .unwrap(); let ipc_server = match IpcServer::start(&event_loop, &socket_name.to_string_lossy()) { Ok(server) => Some(server), Err(err) => { warn!("error starting IPC server: {err:?}"); None } }; let pipewire = match PipeWire::new(&event_loop) { Ok(pipewire) => Some(pipewire), Err(err) => { warn!("error connecting to PipeWire, screencasting will not work: {err:?}"); None } }; let display_source = Generic::new(display, Interest::READ, Mode::Level); event_loop .insert_source(display_source, |_, display, state| { // SAFETY: we don't drop the display. unsafe { display.get_mut().dispatch_clients(state).unwrap(); } Ok(PostAction::Continue) }) .unwrap(); event_loop .insert_source( Timer::from_duration(Duration::from_secs(60)), |_, _, state| { let _span = tracy_client::span!("startup timeout"); state.niri.is_at_startup = false; state.niri.recompute_window_rules(); TimeoutAction::Drop }, ) .unwrap(); drop(config_); Self { config, config_file_output_config, event_loop, scheduler, stop_signal, socket_name, display_handle, start_time: Instant::now(), is_at_startup: true, layout, global_space: Space::default(), output_state: HashMap::new(), output_by_name: HashMap::new(), unmapped_windows: HashMap::new(), root_surface: HashMap::new(), monitors_active: true, devices: HashSet::new(), tablets: HashMap::new(), touch: HashSet::new(), compositor_state, xdg_shell_state, xdg_decoration_state, kde_decoration_state, layer_shell_state, session_lock_state, foreign_toplevel_state, output_management_state, screencopy_state, viewporter_state, xdg_foreign_state, text_input_state, input_method_state, virtual_keyboard_state, shm_state, output_manager_state, dmabuf_state, fractional_scale_manager_state, seat_state, tablet_state, pointer_gestures_state, relative_pointer_state, pointer_constraints_state, idle_notifier_state, idle_inhibit_manager_state, data_device_state, primary_selection_state, data_control_state, popups: PopupManager::default(), popup_grab: None, suppressed_keys: HashSet::new(), bind_cooldown_timers: HashMap::new(), bind_repeat_timer: Option::default(), presentation_state, security_context_state, gamma_control_manager_state, activation_state, mutter_x11_interop_state, seat, keyboard_focus: KeyboardFocus::Layout { surface: None }, layer_shell_on_demand_focus: None, idle_inhibiting_surfaces: HashSet::new(), is_fdo_idle_inhibited: Arc::new(AtomicBool::new(false)), cursor_manager, cursor_texture_cache: Default::default(), cursor_shape_manager_state, dnd_icon: None, pointer_focus: PointerFocus::default(), pointer_hidden: false, pointer_grab_ongoing: false, tablet_cursor_location: None, gesture_swipe_3f_cumulative: None, vertical_wheel_tracker: ScrollTracker::new(120), horizontal_wheel_tracker: ScrollTracker::new(120), mods_with_wheel_binds, // 10 is copied from Clutter: DISCRETE_SCROLL_STEP. vertical_finger_scroll_tracker: ScrollTracker::new(10), horizontal_finger_scroll_tracker: ScrollTracker::new(10), mods_with_finger_scroll_binds, lock_state: LockState::Unlocked, screenshot_ui, config_error_notification, hotkey_overlay, exit_confirm_dialog, debug_draw_opaque_regions: false, debug_draw_damage: false, #[cfg(feature = "dbus")] dbus: None, #[cfg(feature = "dbus")] inhibit_power_key_fd: None, ipc_server, ipc_outputs_changed: false, ipc_focused_window: Arc::new(Mutex::new(None)), pipewire, casts: vec![], #[cfg(feature = "xdp-gnome-screencast")] mapped_cast_output: HashMap::new(), } } #[cfg(feature = "dbus")] pub fn inhibit_power_key(&mut self) -> anyhow::Result<()> { let conn = zbus::blocking::ConnectionBuilder::system()?.build()?; let message = conn.call_method( Some("org.freedesktop.login1"), "/org/freedesktop/login1", Some("org.freedesktop.login1.Manager"), "Inhibit", &("handle-power-key", "niri", "Power key handling", "block"), )?; let fd = message.body()?; self.inhibit_power_key_fd = Some(fd); Ok(()) } /// Repositions all outputs, optionally adding a new output. pub fn reposition_outputs(&mut self, new_output: Option<&Output>) { let _span = tracy_client::span!("Niri::reposition_outputs"); #[derive(Debug)] struct Data { output: Output, name: String, position: Option>, config: Option, } let config = self.config.borrow(); let mut outputs = vec![]; for output in self.global_space.outputs().chain(new_output) { let name = output.name(); let position = self.global_space.output_geometry(output).map(|geo| geo.loc); let config = config.outputs.find(&name).and_then(|c| c.position); outputs.push(Data { output: output.clone(), name, position, config, }); } drop(config); for Data { output, .. } in &outputs { self.global_space.unmap_output(output); } // Connectors can appear in udev in any order. If we sort by name then we get output // positioning that does not depend on the order they appeared. // // All outputs must have different (connector) names. outputs.sort_unstable_by(|a, b| Ord::cmp(&a.name, &b.name)); // Place all outputs with explicitly configured position first, then the unconfigured ones. outputs.sort_by_key(|d| d.config.is_none()); trace!( "placing outputs in order: {:?}", outputs.iter().map(|d| &d.name) ); for data in outputs.into_iter() { let Data { output, name, position, config, } = data; let size = output_size(&output).to_i32_round(); let new_position = config .map(|pos| Point::from((pos.x, pos.y))) .filter(|pos| { // Ensure that the requested position does not overlap any existing output. let target_geom = Rectangle::from_loc_and_size(*pos, size); let overlap = self .global_space .outputs() .map(|output| self.global_space.output_geometry(output).unwrap()) .find(|geom| geom.overlaps(target_geom)); if let Some(overlap) = overlap { warn!( "output {name} at x={} y={} sized {}x{} \ overlaps an existing output at x={} y={} sized {}x{}, \ falling back to automatic placement", pos.x, pos.y, size.w, size.h, overlap.loc.x, overlap.loc.y, overlap.size.w, overlap.size.h, ); false } else { true } }) .unwrap_or_else(|| { let x = self .global_space .outputs() .map(|output| self.global_space.output_geometry(output).unwrap()) .map(|geom| geom.loc.x + geom.size.w) .max() .unwrap_or(0); Point::from((x, 0)) }); self.global_space.map_output(&output, new_position); // By passing new_output as an Option, rather than mapping it into a bogus location // in global_space, we ensure that this branch always runs for it. if Some(new_position) != position { debug!( "putting output {name} at x={} y={}", new_position.x, new_position.y ); output.change_current_state(None, None, None, Some(new_position)); self.ipc_outputs_changed = true; self.queue_redraw(&output); } } } pub fn add_output(&mut self, output: Output, refresh_interval: Option, vrr: bool) { let global = output.create_global::(&self.display_handle); let name = output.name(); let config = self.config.borrow(); let c = config.outputs.find(&name); let scale = c.and_then(|c| c.scale).map(|s| s.0).unwrap_or_else(|| { let size_mm = output.physical_properties().size; let resolution = output.current_mode().unwrap().size; guess_monitor_scale(size_mm, resolution) }); let scale = closest_representable_scale(scale.clamp(0.1, 10.)); let mut transform = c .map(|c| ipc_transform_to_smithay(c.transform)) .unwrap_or(Transform::Normal); let mut background_color = c .map(|c| c.background_color) .unwrap_or(DEFAULT_BACKGROUND_COLOR) .to_array_unpremul(); background_color[3] = 1.; // FIXME: fix winit damage on other transforms. if name == "winit" { transform = Transform::Flipped180; } drop(config); // Set scale and transform before adding to the layout since that will read the output size. output.change_current_state( None, Some(transform), Some(output::Scale::Fractional(scale)), None, ); self.layout.add_output(output.clone()); let lock_render_state = if self.is_locked() { // We haven't rendered anything yet so it's as good as locked. LockRenderState::Locked } else { LockRenderState::Unlocked }; let size = output_size(&output).to_i32_round(); let state = OutputState { global, redraw_state: RedrawState::Idle, unfinished_animations_remain: false, frame_clock: FrameClock::new(refresh_interval, vrr), last_drm_sequence: None, frame_callback_sequence: 0, background_buffer: SolidColorBuffer::new(size, background_color), lock_render_state, lock_surface: None, lock_color_buffer: SolidColorBuffer::new(size, CLEAR_COLOR_LOCKED), screen_transition: None, debug_damage_tracker: OutputDamageTracker::from_output(&output), }; let rv = self.output_state.insert(output.clone(), state); assert!(rv.is_none(), "output was already tracked"); let rv = self.output_by_name.insert(name, output.clone()); assert!(rv.is_none(), "output was already tracked"); // Must be last since it will call queue_redraw(output) which needs things to be filled-in. self.reposition_outputs(Some(&output)); } pub fn remove_output(&mut self, output: &Output) { for layer in layer_map_for_output(output).layers() { layer.layer_surface().send_close(); } self.layout.remove_output(output); self.global_space.unmap_output(output); self.reposition_outputs(None); self.gamma_control_manager_state.output_removed(output); let state = self.output_state.remove(output).unwrap(); self.output_by_name.remove(&output.name()).unwrap(); match state.redraw_state { RedrawState::Idle => (), RedrawState::Queued => (), RedrawState::WaitingForVBlank { .. } => (), RedrawState::WaitingForEstimatedVBlank(token) => self.event_loop.remove(token), RedrawState::WaitingForEstimatedVBlankAndQueued(token) => self.event_loop.remove(token), } #[cfg(feature = "xdp-gnome-screencast")] self.stop_casts_for_target(CastTarget::Output(output.downgrade())); self.remove_screencopy_output(output); // Disable the output global and remove some time later to give the clients some time to // process it. let global = state.global; self.display_handle.disable_global::(global.clone()); self.event_loop .insert_source( Timer::from_duration(Duration::from_secs(10)), move |_, _, state| { state .niri .display_handle .remove_global::(global.clone()); TimeoutAction::Drop }, ) .unwrap(); match mem::take(&mut self.lock_state) { LockState::Locking(confirmation) => { // We're locking and an output was removed, check if the requirements are now met. let all_locked = self .output_state .values() .all(|state| state.lock_render_state == LockRenderState::Locked); if all_locked { let lock = confirmation.ext_session_lock().clone(); confirmation.lock(); self.lock_state = LockState::Locked(lock); } else { // Still waiting. self.lock_state = LockState::Locking(confirmation); } } lock_state => self.lock_state = lock_state, } if self.screenshot_ui.close() { self.cursor_manager .set_cursor_image(CursorImageStatus::default_named()); self.queue_redraw_all(); } } pub fn output_resized(&mut self, output: &Output) { let output_size = output_size(output).to_i32_round(); let is_locked = self.is_locked(); layer_map_for_output(output).arrange(); self.layout.update_output_size(output); if let Some(state) = self.output_state.get_mut(output) { state.background_buffer.resize(output_size); state.lock_color_buffer.resize(output_size); if is_locked { if let Some(lock_surface) = &state.lock_surface { configure_lock_surface(lock_surface, output); } } } // If the output size changed with an open screenshot UI, close the screenshot UI. if let Some((old_size, old_scale, old_transform)) = self.screenshot_ui.output_size(output) { let transform = output.current_transform(); let output_mode = output.current_mode().unwrap(); let size = transform.transform_size(output_mode.size); let scale = output.current_scale().fractional_scale(); // FIXME: scale changes and transform flips shouldn't matter but they currently do since // I haven't quite figured out how to draw the screenshot textures in // physical coordinates. if old_size != size || old_scale != scale || old_transform != transform { self.screenshot_ui.close(); self.cursor_manager .set_cursor_image(CursorImageStatus::default_named()); self.queue_redraw_all(); return; } } self.queue_redraw(output); } pub fn deactivate_monitors(&mut self, backend: &mut Backend) { if !self.monitors_active { return; } self.monitors_active = false; backend.set_monitors_active(false); } pub fn activate_monitors(&mut self, backend: &mut Backend) { if self.monitors_active { return; } self.monitors_active = true; backend.set_monitors_active(true); self.queue_redraw_all(); } pub fn output_under(&self, pos: Point) -> Option<(&Output, Point)> { let output = self.global_space.output_under(pos).next()?; let pos_within_output = pos - self .global_space .output_geometry(output) .unwrap() .loc .to_f64(); Some((output, pos_within_output)) } /// Returns the window under the position to be activated. /// /// The cursor may be inside the window's activation region, but not within the window's input /// region. pub fn window_under(&self, pos: Point) -> Option<&Mapped> { if self.is_locked() || self.screenshot_ui.is_open() { return None; } let (output, pos_within_output) = self.output_under(pos)?; // Check if some layer-shell surface is on top. let layers = layer_map_for_output(output); let layer_under = |layer| layers.layer_under(layer, pos_within_output).is_some(); if layer_under(Layer::Overlay) { return None; } let mon = self.layout.monitor_for_output(output).unwrap(); if !mon.render_above_top_layer() && layer_under(Layer::Top) { return None; } let (window, _loc) = self.layout.window_under(output, pos_within_output)?; Some(window) } /// Returns the window under the cursor to be activated. /// /// The cursor may be inside the window's activation region, but not within the window's input /// region. pub fn window_under_cursor(&self) -> Option<&Mapped> { let pos = self.seat.get_pointer().unwrap().current_location(); self.window_under(pos) } /// Returns the surface under cursor and its position in the global space. /// /// Pointer needs location in global space, and focused window location compatible with that /// global space. We don't have a global space for all windows, but this function converts the /// window location temporarily to the current global space. pub fn surface_under_and_global_space(&mut self, pos: Point) -> PointerFocus { let mut rv = PointerFocus::default(); let Some((output, pos_within_output)) = self.output_under(pos) else { return rv; }; rv.output = Some(output.clone()); let output_pos_in_global_space = self.global_space.output_geometry(output).unwrap().loc; if self.is_locked() { let Some(state) = self.output_state.get(output) else { return rv; }; let Some(surface) = state.lock_surface.as_ref() else { return rv; }; rv.surface = under_from_surface_tree( surface.wl_surface(), pos_within_output, // We put lock surfaces at (0, 0). (0, 0), WindowSurfaceType::ALL, ) .map(|(surface, pos_within_output)| { ( surface, (pos_within_output + output_pos_in_global_space).to_f64(), ) }); return rv; } if self.screenshot_ui.is_open() { return rv; } let layers = layer_map_for_output(output); let layer_surface_under = |layer| { layers .layer_under(layer, pos_within_output) .and_then(|layer| { let layer_pos_within_output = layers.layer_geometry(layer).unwrap().loc.to_f64(); layer .surface_under( pos_within_output - layer_pos_within_output, WindowSurfaceType::ALL, ) .map(|(surface, pos_within_layer)| { ( (surface, pos_within_layer.to_f64() + layer_pos_within_output), layer, ) }) }) .map(|(s, l)| (s, (None, Some(l.clone())))) }; let window_under = || { self.layout .window_under(output, pos_within_output) .and_then(|(mapped, win_pos_within_output)| { let win_pos_within_output = win_pos_within_output?; let window = &mapped.window; window .surface_under( pos_within_output - win_pos_within_output, WindowSurfaceType::ALL, ) .map(|(s, pos_within_window)| { (s, pos_within_window.to_f64() + win_pos_within_output) }) .map(|s| (s, (Some(window.clone()), None))) }) }; let mon = self.layout.monitor_for_output(output).unwrap(); let mut under = layer_surface_under(Layer::Overlay); if mon.render_above_top_layer() { under = under .or_else(window_under) .or_else(|| layer_surface_under(Layer::Top)); } else { under = under .or_else(|| layer_surface_under(Layer::Top)) .or_else(window_under); } let Some(((surface, surface_pos_within_output), (window, layer))) = under .or_else(|| layer_surface_under(Layer::Bottom)) .or_else(|| layer_surface_under(Layer::Background)) else { return rv; }; let surface_loc_in_global_space = surface_pos_within_output + output_pos_in_global_space.to_f64(); rv.surface = Some((surface, surface_loc_in_global_space)); rv.window = window; rv.layer = layer; rv } pub fn output_under_cursor(&self) -> Option { let pos = self.seat.get_pointer().unwrap().current_location(); self.global_space.output_under(pos).next().cloned() } pub fn output_left(&self) -> Option { let active = self.layout.active_output()?; let active_geo = self.global_space.output_geometry(active).unwrap(); let extended_geo = Rectangle::from_loc_and_size( (i32::MIN / 2, active_geo.loc.y), (i32::MAX, active_geo.size.h), ); self.global_space .outputs() .map(|output| (output, self.global_space.output_geometry(output).unwrap())) .filter(|(_, geo)| center(*geo).x < center(active_geo).x && geo.overlaps(extended_geo)) .min_by_key(|(_, geo)| center(active_geo).x - center(*geo).x) .map(|(output, _)| output) .cloned() } pub fn output_right(&self) -> Option { let active = self.layout.active_output()?; let active_geo = self.global_space.output_geometry(active).unwrap(); let extended_geo = Rectangle::from_loc_and_size( (i32::MIN / 2, active_geo.loc.y), (i32::MAX, active_geo.size.h), ); self.global_space .outputs() .map(|output| (output, self.global_space.output_geometry(output).unwrap())) .filter(|(_, geo)| center(*geo).x > center(active_geo).x && geo.overlaps(extended_geo)) .min_by_key(|(_, geo)| center(*geo).x - center(active_geo).x) .map(|(output, _)| output) .cloned() } pub fn output_up(&self) -> Option { let active = self.layout.active_output()?; let active_geo = self.global_space.output_geometry(active).unwrap(); let extended_geo = Rectangle::from_loc_and_size( (active_geo.loc.x, i32::MIN / 2), (active_geo.size.w, i32::MAX), ); self.global_space .outputs() .map(|output| (output, self.global_space.output_geometry(output).unwrap())) .filter(|(_, geo)| center(*geo).y < center(active_geo).y && geo.overlaps(extended_geo)) .min_by_key(|(_, geo)| center(active_geo).y - center(*geo).y) .map(|(output, _)| output) .cloned() } pub fn output_by_name(&self, name: &str) -> Option { self.global_space .outputs() .find(|output| output.name().eq_ignore_ascii_case(name)) .cloned() } pub fn find_output_and_workspace_index( &self, workspace_reference: WorkspaceReference, ) -> Option<(Option, usize)> { let workspace_name = match workspace_reference { WorkspaceReference::Index(index) => { return Some((None, index.saturating_sub(1) as usize)); } WorkspaceReference::Name(name) => name, }; let (target_workspace_index, target_workspace) = self.layout.find_workspace_by_name(&workspace_name)?; // FIXME: when we do fixes for no connected outputs, this will need fixing too. let active_workspace = self.layout.active_workspace()?; if target_workspace.current_output() == active_workspace.current_output() { return Some((None, target_workspace_index)); } let target_output = target_workspace.current_output()?; Some((Some(target_output.clone()), target_workspace_index)) } pub fn output_down(&self) -> Option { let active = self.layout.active_output()?; let active_geo = self.global_space.output_geometry(active).unwrap(); let extended_geo = Rectangle::from_loc_and_size( (active_geo.loc.x, i32::MIN / 2), (active_geo.size.w, i32::MAX), ); self.global_space .outputs() .map(|output| (output, self.global_space.output_geometry(output).unwrap())) .filter(|(_, geo)| center(active_geo).y < center(*geo).y && geo.overlaps(extended_geo)) .min_by_key(|(_, geo)| center(*geo).y - center(active_geo).y) .map(|(output, _)| output) .cloned() } pub fn output_for_tablet(&self) -> Option<&Output> { let config = self.config.borrow(); let map_to_output = config.input.tablet.map_to_output.as_ref(); map_to_output.and_then(|name| self.output_by_name.get(name)) } pub fn output_for_touch(&self) -> Option<&Output> { let config = self.config.borrow(); let map_to_output = config.input.touch.map_to_output.as_ref(); map_to_output .and_then(|name| self.output_by_name.get(name)) .or_else(|| self.global_space.outputs().next()) } pub fn output_for_root(&self, root: &WlSurface) -> Option<&Output> { // Check the main layout. let win_out = self.layout.find_window_and_output(root); let layout_output = win_out.map(|(_, output)| output); // Check layer-shell. let has_layer_surface = |o: &&Output| { layer_map_for_output(o) .layer_for_surface(root, WindowSurfaceType::TOPLEVEL) .is_some() }; let layer_shell_output = || self.layout.outputs().find(has_layer_surface); layout_output.or_else(layer_shell_output) } pub fn lock_surface_focus(&self) -> Option { let output_under_cursor = self.output_under_cursor(); let output = output_under_cursor .as_ref() .or_else(|| self.layout.active_output()) .or_else(|| self.global_space.outputs().next())?; let state = self.output_state.get(output)?; state.lock_surface.as_ref().map(|s| s.wl_surface()).cloned() } /// Schedules an immediate redraw on all outputs if one is not already scheduled. pub fn queue_redraw_all(&mut self) { for state in self.output_state.values_mut() { state.redraw_state = mem::take(&mut state.redraw_state).queue_redraw(); } } /// Schedules an immediate redraw if one is not already scheduled. pub fn queue_redraw(&mut self, output: &Output) { let state = self.output_state.get_mut(output).unwrap(); state.redraw_state = mem::take(&mut state.redraw_state).queue_redraw(); } pub fn redraw_queued_outputs(&mut self, backend: &mut Backend) { let _span = tracy_client::span!("Niri::redraw_queued_outputs"); while let Some((output, _)) = self.output_state.iter().find(|(_, state)| { matches!( state.redraw_state, RedrawState::Queued | RedrawState::WaitingForEstimatedVBlankAndQueued(_) ) }) { let output = output.clone(); self.redraw(backend, &output); } } pub fn pointer_element( &self, renderer: &mut R, output: &Output, ) -> Vec> { if self.pointer_hidden { return vec![]; } let _span = tracy_client::span!("Niri::pointer_element"); let output_scale = output.current_scale(); let output_pos = self.global_space.output_geometry(output).unwrap().loc; // Check whether we need to draw the tablet cursor or the regular cursor. let pointer_pos = self .tablet_cursor_location .unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location()); let pointer_pos = pointer_pos - output_pos.to_f64(); // Get the render cursor to draw. let cursor_scale = output_scale.integer_scale(); let render_cursor = self.cursor_manager.get_render_cursor(cursor_scale); let output_scale = Scale::from(output.current_scale().fractional_scale()); let (mut pointer_elements, pointer_pos) = match render_cursor { RenderCursor::Hidden => (vec![], pointer_pos.to_physical_precise_round(output_scale)), RenderCursor::Surface { surface, hotspot } => { let pointer_pos = (pointer_pos - hotspot.to_f64()).to_physical_precise_round(output_scale); let pointer_elements = render_elements_from_surface_tree( renderer, &surface, pointer_pos, output_scale, 1., Kind::Cursor, ); (pointer_elements, pointer_pos) } RenderCursor::Named { icon, scale, cursor, } => { let (idx, frame) = cursor.frame(self.start_time.elapsed().as_millis() as u32); let hotspot = XCursor::hotspot(frame).to_logical(scale); let pointer_pos = (pointer_pos - hotspot.to_f64()).to_physical_precise_round(output_scale); let texture = self.cursor_texture_cache.get(icon, scale, &cursor, idx); let mut pointer_elements = vec![]; let pointer_element = match MemoryRenderBufferRenderElement::from_buffer( renderer, pointer_pos.to_f64(), &texture, None, None, None, Kind::Cursor, ) { Ok(element) => Some(element), Err(err) => { warn!("error importing a cursor texture: {err:?}"); None } }; if let Some(element) = pointer_element { pointer_elements.push(OutputRenderElements::NamedPointer(element)); } (pointer_elements, pointer_pos) } }; if let Some(dnd_icon) = &self.dnd_icon { pointer_elements.extend(render_elements_from_surface_tree( renderer, dnd_icon, pointer_pos, output_scale, 1., Kind::Unspecified, )); } pointer_elements } pub fn refresh_pointer_outputs(&mut self) { if self.pointer_hidden { return; } let _span = tracy_client::span!("Niri::refresh_pointer_outputs"); // Check whether we need to draw the tablet cursor or the regular cursor. let pointer_pos = self .tablet_cursor_location .unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location()); match self.cursor_manager.cursor_image().clone() { CursorImageStatus::Surface(ref surface) => { let hotspot = with_states(surface, |states| { states .data_map .get::>() .unwrap() .lock() .unwrap() .hotspot }); let surface_pos = pointer_pos.to_i32_round() - hotspot; let bbox = bbox_from_surface_tree(surface, surface_pos); let dnd = self .dnd_icon .as_ref() .map(|surface| (surface, bbox_from_surface_tree(surface, surface_pos))); // FIXME we basically need to pick the largest scale factor across the overlapping // outputs, this is how it's usually done in clients as well. let mut cursor_scale = 1.; let mut cursor_transform = Transform::Normal; let mut dnd_scale = 1.; let mut dnd_transform = Transform::Normal; for output in self.global_space.outputs() { let geo = self.global_space.output_geometry(output).unwrap(); // Compute pointer surface overlap. if let Some(mut overlap) = geo.intersection(bbox) { overlap.loc -= surface_pos; cursor_scale = f64::max(cursor_scale, output.current_scale().fractional_scale()); // FIXME: using the largest overlapping or "primary" output transform would // make more sense here. cursor_transform = output.current_transform(); output_update(output, Some(overlap), surface); } else { output_update(output, None, surface); } // Compute DnD icon surface overlap. if let Some((surface, bbox)) = dnd { if let Some(mut overlap) = geo.intersection(bbox) { overlap.loc -= surface_pos; dnd_scale = f64::max(dnd_scale, output.current_scale().fractional_scale()); // FIXME: using the largest overlapping or "primary" output transform // would make more sense here. dnd_transform = output.current_transform(); output_update(output, Some(overlap), surface); } else { output_update(output, None, surface); } } } with_states(surface, |data| { send_scale_transform( surface, data, output::Scale::Fractional(cursor_scale), cursor_transform, ) }); if let Some((surface, _)) = dnd { with_states(surface, |data| { send_scale_transform( surface, data, output::Scale::Fractional(dnd_scale), dnd_transform, ); }); } } cursor_image => { // There's no cursor surface, but there might be a DnD icon. let Some(surface) = &self.dnd_icon else { return; }; let icon = if let CursorImageStatus::Named(icon) = cursor_image { icon } else { Default::default() }; let mut dnd_scale = 1.; let mut dnd_transform = Transform::Normal; for output in self.global_space.outputs() { let geo = self.global_space.output_geometry(output).unwrap(); // The default cursor is rendered at the right scale for each output, which // means that it may have a different hotspot for each output. let output_scale = output.current_scale().integer_scale(); let cursor = self .cursor_manager .get_cursor_with_name(icon, output_scale) .unwrap_or_else(|| self.cursor_manager.get_default_cursor(output_scale)); // For simplicity, we always use frame 0 for this computation. Let's hope the // hotspot doesn't change between frames. let hotspot = XCursor::hotspot(&cursor.frames()[0]).to_logical(output_scale); let surface_pos = pointer_pos.to_i32_round() - hotspot; let bbox = bbox_from_surface_tree(surface, surface_pos); if let Some(mut overlap) = geo.intersection(bbox) { overlap.loc -= surface_pos; dnd_scale = f64::max(dnd_scale, output.current_scale().fractional_scale()); // FIXME: using the largest overlapping or "primary" output transform would // make more sense here. dnd_transform = output.current_transform(); output_update(output, Some(overlap), surface); } else { output_update(output, None, surface); } } with_states(surface, |data| { send_scale_transform( surface, data, output::Scale::Fractional(dnd_scale), dnd_transform, ); }); } } } pub fn refresh_idle_inhibit(&mut self) { let _span = tracy_client::span!("Niri::refresh_idle_inhibit"); self.idle_inhibiting_surfaces.retain(|s| s.is_alive()); let is_inhibited = self.is_fdo_idle_inhibited.load(Ordering::SeqCst) || self.idle_inhibiting_surfaces.iter().any(|surface| { with_states(surface, |states| { surface_primary_scanout_output(surface, states).is_some() }) }); self.idle_notifier_state.set_is_inhibited(is_inhibited); } pub fn refresh_window_rules(&mut self) { let _span = tracy_client::span!("Niri::refresh_window_rules"); let config = self.config.borrow(); let window_rules = &config.window_rules; let mut windows = vec![]; let mut outputs = HashSet::new(); self.layout.with_windows_mut(|mapped, output| { if mapped.recompute_window_rules_if_needed(window_rules, self.is_at_startup) { windows.push(mapped.window.clone()); if let Some(output) = output { outputs.insert(output.clone()); } } }); drop(config); for win in windows { self.layout.update_window(&win, None); win.toplevel() .expect("no X11 support") .send_pending_configure(); } for output in outputs { self.queue_redraw(&output); } } #[cfg(feature = "xdp-gnome-screencast")] pub fn refresh_mapped_cast_outputs(&mut self) { use std::collections::hash_map::Entry; let mut seen = HashSet::new(); let mut output_changed = vec![]; self.layout.with_windows(|mapped, output| { seen.insert(mapped.window.clone()); let Some(output) = output else { return; }; match self.mapped_cast_output.entry(mapped.window.clone()) { Entry::Occupied(mut entry) => { if entry.get() != output { entry.insert(output.clone()); output_changed.push((mapped.id(), output.clone())); } } Entry::Vacant(entry) => { entry.insert(output.clone()); } } }); self.mapped_cast_output.retain(|win, _| seen.contains(win)); let mut to_stop = vec![]; for (id, out) in output_changed { let refresh = out.current_mode().unwrap().refresh as u32; let target = CastTarget::Window { id: u64::from(id.get()), }; for cast in self.casts.iter_mut().filter(|cast| cast.target == target) { if let Err(err) = cast.set_refresh(refresh) { warn!("error changing cast FPS: {err:?}"); to_stop.push(cast.session_id); }; } } for session_id in to_stop { self.stop_cast(session_id); } } pub fn render( &self, renderer: &mut R, output: &Output, include_pointer: bool, mut target: RenderTarget, ) -> Vec> { let _span = tracy_client::span!("Niri::render"); if target == RenderTarget::Output { if let Some(preview) = self.config.borrow().debug.preview_render { target = match preview { PreviewRender::Screencast => RenderTarget::Screencast, PreviewRender::ScreenCapture => RenderTarget::ScreenCapture, }; } } let output_scale = Scale::from(output.current_scale().fractional_scale()); // The pointer goes on the top. let mut elements = vec![]; if include_pointer { elements = self.pointer_element(renderer, output); } // Next, the screen transition texture. { let state = self.output_state.get(output).unwrap(); if let Some(transition) = &state.screen_transition { elements.push(transition.render(target).into()); } } // Next, the exit confirm dialog. if let Some(dialog) = &self.exit_confirm_dialog { if let Some(element) = dialog.render(renderer, output) { elements.push(element.into()); } } // Next, the config error notification too. if let Some(element) = self.config_error_notification.render(renderer, output) { elements.push(element.into()); } // If the session is locked, draw the lock surface. if self.is_locked() { let state = self.output_state.get(output).unwrap(); if let Some(surface) = state.lock_surface.as_ref() { elements.extend(render_elements_from_surface_tree( renderer, surface.wl_surface(), (0, 0), output_scale, 1., Kind::Unspecified, )); } // Draw the solid color background. elements.push( SolidColorRenderElement::from_buffer( &state.lock_color_buffer, (0, 0), output_scale, 1., Kind::Unspecified, ) .into(), ); if self.debug_draw_opaque_regions { draw_opaque_regions(&mut elements, output_scale); } return elements; } // Prepare the background element. let state = self.output_state.get(output).unwrap(); let background = SolidColorRenderElement::from_buffer( &state.background_buffer, (0, 0), output_scale, 1., Kind::Unspecified, ) .into(); // If the screenshot UI is open, draw it. if self.screenshot_ui.is_open() { elements.extend( self.screenshot_ui .render_output(output, target) .into_iter() .map(OutputRenderElements::from), ); // Add the background for outputs that were connected while the screenshot UI was open. elements.push(background); if self.debug_draw_opaque_regions { draw_opaque_regions(&mut elements, output_scale); } return elements; } // Draw the hotkey overlay on top. if let Some(element) = self.hotkey_overlay.render(renderer, output) { elements.push(element.into()); } // Get monitor elements. let mon = self.layout.monitor_for_output(output).unwrap(); let monitor_elements = mon.render_elements(renderer, target); // Get layer-shell elements. let layer_map = layer_map_for_output(output); let mut extend_from_layer = |elements: &mut Vec>, layer| { let iter = layer_map .layers_on(layer) .filter_map(|surface| { layer_map .layer_geometry(surface) .map(|geo| (geo.loc, surface)) }) .flat_map(|(loc, surface)| { surface .render_elements( renderer, loc.to_physical_precise_round(output_scale), output_scale, 1., ) .into_iter() .map(OutputRenderElements::Wayland) }); elements.extend(iter); }; // The upper layer-shell elements go next. extend_from_layer(&mut elements, Layer::Overlay); // Then the regular monitor elements and the top layer in varying order. if mon.render_above_top_layer() { elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from)); extend_from_layer(&mut elements, Layer::Top); } else { extend_from_layer(&mut elements, Layer::Top); elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from)); } // Then the lower layer-shell elements. extend_from_layer(&mut elements, Layer::Bottom); extend_from_layer(&mut elements, Layer::Background); // Then the background. elements.push(background); if self.debug_draw_opaque_regions { draw_opaque_regions(&mut elements, output_scale); } elements } fn redraw(&mut self, backend: &mut Backend, output: &Output) { let _span = tracy_client::span!("Niri::redraw"); // Verify our invariant. let state = self.output_state.get_mut(output).unwrap(); assert!(matches!( state.redraw_state, RedrawState::Queued | RedrawState::WaitingForEstimatedVBlankAndQueued(_) )); let target_presentation_time = state.frame_clock.next_presentation_time(); let mut res = RenderResult::Skipped; if self.monitors_active { // Update from the config and advance the animations. self.layout.advance_animations(target_presentation_time); if let Some(transition) = &mut state.screen_transition { transition.advance_animations(target_presentation_time); if transition.is_done() { state.screen_transition = None; } } state.unfinished_animations_remain = self .layout .monitor_for_output(output) .unwrap() .are_animations_ongoing(); self.config_error_notification .advance_animations(target_presentation_time); state.unfinished_animations_remain |= self.config_error_notification.are_animations_ongoing(); self.screenshot_ui .advance_animations(target_presentation_time); state.unfinished_animations_remain |= self.screenshot_ui.are_animations_ongoing(); // Also keep redrawing if the current cursor is animated. state.unfinished_animations_remain |= self .cursor_manager .is_current_cursor_animated(output.current_scale().integer_scale()); // Also keep redrawing during a screen transition. state.unfinished_animations_remain |= state.screen_transition.is_some(); self.layout.update_render_elements(output); // Render. res = backend.render(self, output, target_presentation_time); } let is_locked = self.is_locked(); let state = self.output_state.get_mut(output).unwrap(); if res == RenderResult::Skipped { // Update the redraw state on failed render. state.redraw_state = if let RedrawState::WaitingForEstimatedVBlank(token) | RedrawState::WaitingForEstimatedVBlankAndQueued(token) = state.redraw_state { RedrawState::WaitingForEstimatedVBlank(token) } else { RedrawState::Idle }; } // Update the lock render state on successful render, or if monitors are inactive. When // monitors are inactive on a TTY, they have no framebuffer attached, so no sensitive data // from a last render will be visible. if res != RenderResult::Skipped || !self.monitors_active { state.lock_render_state = if is_locked { LockRenderState::Locked } else { LockRenderState::Unlocked }; } // If we're in process of locking the session, check if the requirements were met. match mem::take(&mut self.lock_state) { LockState::Locking(confirmation) => { if state.lock_render_state == LockRenderState::Unlocked { // We needed to render a locked frame on this output but failed. self.unlock(); } else { // Check if all outputs are now locked. let all_locked = self .output_state .values() .all(|state| state.lock_render_state == LockRenderState::Locked); if all_locked { // All outputs are locked, report success. let lock = confirmation.ext_session_lock().clone(); confirmation.lock(); self.lock_state = LockState::Locked(lock); } else { // Still waiting for other outputs. self.lock_state = LockState::Locking(confirmation); } } } lock_state => self.lock_state = lock_state, } // Send the frame callbacks. // // FIXME: The logic here could be a bit smarter. Currently, during an animation, the // surfaces that are visible for the very last frame (e.g. because the camera is moving // away) will receive frame callbacks, and the surfaces that are invisible but will become // visible next frame will not receive frame callbacks (so they will show stale contents for // one frame). We could advance the animations for the next frame and send frame callbacks // according to the expected new positions. // // However, this should probably be restricted to sending frame callbacks to more surfaces, // to err on the safe side. self.send_frame_callbacks(output); backend.with_primary_renderer(|renderer| { #[cfg(feature = "xdp-gnome-screencast")] { // Render and send to PipeWire screencast streams. self.render_for_screen_cast(renderer, output, target_presentation_time); // FIXME: when a window is hidden, it should probably still receive frame callbacks // and get rendered for screen cast. This is currently // unimplemented, but happens to work by chance, since output // redrawing is more eager than it should be. self.render_windows_for_screen_cast(renderer, output, target_presentation_time); } self.render_for_screencopy_with_damage(renderer, output); }); } pub fn update_primary_scanout_output( &self, output: &Output, render_element_states: &RenderElementStates, ) { // FIXME: potentially tweak the compare function. The default one currently always prefers a // higher refresh-rate output, which is not always desirable (i.e. with a very small // overlap). // // While we only have cursors and DnD icons crossing output boundaries though, it doesn't // matter all that much. if let CursorImageStatus::Surface(surface) = &self.cursor_manager.cursor_image() { with_surface_tree_downward( surface, (), |_, _, _| TraversalAction::DoChildren(()), |surface, states, _| { update_surface_primary_scanout_output( surface, output, states, render_element_states, default_primary_scanout_output_compare, ); }, |_, _, _| true, ); } if let Some(surface) = &self.dnd_icon { with_surface_tree_downward( surface, (), |_, _, _| TraversalAction::DoChildren(()), |surface, states, _| { update_surface_primary_scanout_output( surface, output, states, render_element_states, default_primary_scanout_output_compare, ); }, |_, _, _| true, ); } // We're only updating the current output's windows and layer surfaces. This should be fine // as in niri they can only be rendered on a single output at a time. // // The reason to do this at all is that it keeps track of whether the surface is visible or // not in a unified way with the pointer surfaces, which makes the logic elsewhere simpler. for mapped in self.layout.windows_for_output(output) { let win = &mapped.window; let offscreen_id = win .user_data() .get_or_insert(WindowOffscreenId::default) .0 .borrow(); let offscreen_id = offscreen_id.as_ref(); win.with_surfaces(|surface, states| { states .data_map .insert_if_missing_threadsafe(Mutex::::default); let surface_primary_scanout_output = states .data_map .get::>() .unwrap(); surface_primary_scanout_output .lock() .unwrap() .update_from_render_element_states( offscreen_id.cloned().unwrap_or_else(|| surface.into()), output, render_element_states, |_, _, output, _| output, ); }); } for surface in layer_map_for_output(output).layers() { surface.with_surfaces(|surface, states| { update_surface_primary_scanout_output( surface, output, states, render_element_states, // Layer surfaces are shown only on one output at a time. |_, _, output, _| output, ); }); } if let Some(surface) = &self.output_state[output].lock_surface { with_surface_tree_downward( surface.wl_surface(), (), |_, _, _| TraversalAction::DoChildren(()), |surface, states, _| { update_surface_primary_scanout_output( surface, output, states, render_element_states, default_primary_scanout_output_compare, ); }, |_, _, _| true, ); } } pub fn send_dmabuf_feedbacks( &self, output: &Output, feedback: &SurfaceDmabufFeedback, render_element_states: &RenderElementStates, ) { let _span = tracy_client::span!("Niri::send_dmabuf_feedbacks"); // We can unconditionally send the current output's feedback to regular and layer-shell // surfaces, as they can only be displayed on a single output at a time. Even if a surface // is currently invisible, this is the DMABUF feedback that it should know about. for mapped in self.layout.windows_for_output(output) { mapped.window.send_dmabuf_feedback( output, |_, _| Some(output.clone()), |surface, _| { select_dmabuf_feedback( surface, render_element_states, &feedback.render, &feedback.scanout, ) }, ); } for surface in layer_map_for_output(output).layers() { surface.send_dmabuf_feedback( output, |_, _| Some(output.clone()), |surface, _| { select_dmabuf_feedback( surface, render_element_states, &feedback.render, &feedback.scanout, ) }, ); } if let Some(surface) = &self.output_state[output].lock_surface { send_dmabuf_feedback_surface_tree( surface.wl_surface(), output, |_, _| Some(output.clone()), |surface, _| { select_dmabuf_feedback( surface, render_element_states, &feedback.render, &feedback.scanout, ) }, ); } if let Some(surface) = &self.dnd_icon { send_dmabuf_feedback_surface_tree( surface, output, surface_primary_scanout_output, |surface, _| { select_dmabuf_feedback( surface, render_element_states, &feedback.render, &feedback.scanout, ) }, ); } if let CursorImageStatus::Surface(surface) = &self.cursor_manager.cursor_image() { send_dmabuf_feedback_surface_tree( surface, output, surface_primary_scanout_output, |surface, _| { select_dmabuf_feedback( surface, render_element_states, &feedback.render, &feedback.scanout, ) }, ); } } pub fn send_frame_callbacks(&self, output: &Output) { let _span = tracy_client::span!("Niri::send_frame_callbacks"); let state = self.output_state.get(output).unwrap(); let sequence = state.frame_callback_sequence; let should_send = |surface: &WlSurface, states: &SurfaceData| { // Do the standard primary scanout output check. For pointer surfaces it deduplicates // the frame callbacks across potentially multiple outputs, and for regular windows and // layer-shell surfaces it avoids sending frame callbacks to invisible surfaces. let current_primary_output = surface_primary_scanout_output(surface, states); if current_primary_output.as_ref() != Some(output) { return None; } // Next, check the throttling status. let frame_throttling_state = states .data_map .get_or_insert(SurfaceFrameThrottlingState::default); let mut last_sent_at = frame_throttling_state.last_sent_at.borrow_mut(); let mut send = true; // If we already sent a frame callback to this surface this output refresh // cycle, don't send one again to prevent empty-damage commit busy loops. if let Some((last_output, last_sequence)) = &*last_sent_at { if last_output == output && *last_sequence == sequence { send = false; } } if send { *last_sent_at = Some((output.clone(), sequence)); Some(output.clone()) } else { None } }; let frame_callback_time = get_monotonic_time(); for mapped in self.layout.windows_for_output(output) { mapped.window.send_frame( output, frame_callback_time, FRAME_CALLBACK_THROTTLE, should_send, ); } for surface in layer_map_for_output(output).layers() { surface.send_frame( output, frame_callback_time, FRAME_CALLBACK_THROTTLE, should_send, ); } if let Some(surface) = &self.output_state[output].lock_surface { send_frames_surface_tree( surface.wl_surface(), output, frame_callback_time, FRAME_CALLBACK_THROTTLE, should_send, ); } if let Some(surface) = &self.dnd_icon { send_frames_surface_tree( surface, output, frame_callback_time, FRAME_CALLBACK_THROTTLE, should_send, ); } if let CursorImageStatus::Surface(surface) = self.cursor_manager.cursor_image() { send_frames_surface_tree( surface, output, frame_callback_time, FRAME_CALLBACK_THROTTLE, should_send, ); } } pub fn send_frame_callbacks_on_fallback_timer(&self) { let _span = tracy_client::span!("Niri::send_frame_callbacks_on_fallback_timer"); // Make up a bogus output; we don't care about it here anyway, just the throttling timer. let output = Output::new( String::new(), PhysicalProperties { size: Size::from((0, 0)), subpixel: Subpixel::Unknown, make: String::new(), model: String::new(), }, ); let output = &output; let frame_callback_time = get_monotonic_time(); self.layout.with_windows(|mapped, _| { mapped.window.send_frame( output, frame_callback_time, FRAME_CALLBACK_THROTTLE, |_, _| None, ); }); for (output, state) in self.output_state.iter() { for surface in layer_map_for_output(output).layers() { surface.send_frame( output, frame_callback_time, FRAME_CALLBACK_THROTTLE, |_, _| None, ); } if let Some(surface) = &state.lock_surface { send_frames_surface_tree( surface.wl_surface(), output, frame_callback_time, FRAME_CALLBACK_THROTTLE, |_, _| None, ); } } if let Some(surface) = &self.dnd_icon { send_frames_surface_tree( surface, output, frame_callback_time, FRAME_CALLBACK_THROTTLE, |_, _| None, ); } if let CursorImageStatus::Surface(surface) = self.cursor_manager.cursor_image() { send_frames_surface_tree( surface, output, frame_callback_time, FRAME_CALLBACK_THROTTLE, |_, _| None, ); } } pub fn take_presentation_feedbacks( &mut self, output: &Output, render_element_states: &RenderElementStates, ) -> OutputPresentationFeedback { let mut feedback = OutputPresentationFeedback::new(output); if let CursorImageStatus::Surface(surface) = &self.cursor_manager.cursor_image() { take_presentation_feedback_surface_tree( surface, &mut feedback, surface_primary_scanout_output, |surface, _| { surface_presentation_feedback_flags_from_states(surface, render_element_states) }, ); } if let Some(surface) = &self.dnd_icon { take_presentation_feedback_surface_tree( surface, &mut feedback, surface_primary_scanout_output, |surface, _| { surface_presentation_feedback_flags_from_states(surface, render_element_states) }, ); } for mapped in self.layout.windows_for_output(output) { mapped.window.take_presentation_feedback( &mut feedback, surface_primary_scanout_output, |surface, _| { surface_presentation_feedback_flags_from_states(surface, render_element_states) }, ) } for surface in layer_map_for_output(output).layers() { surface.take_presentation_feedback( &mut feedback, surface_primary_scanout_output, |surface, _| { surface_presentation_feedback_flags_from_states(surface, render_element_states) }, ); } if let Some(surface) = &self.output_state[output].lock_surface { take_presentation_feedback_surface_tree( surface.wl_surface(), &mut feedback, surface_primary_scanout_output, |surface, _| { surface_presentation_feedback_flags_from_states(surface, render_element_states) }, ); } feedback } #[cfg(feature = "xdp-gnome-screencast")] fn render_for_screen_cast( &mut self, renderer: &mut GlesRenderer, output: &Output, target_presentation_time: Duration, ) { let _span = tracy_client::span!("Niri::render_for_screen_cast"); let target = CastTarget::Output(output.downgrade()); let size = output.current_mode().unwrap().size; let transform = output.current_transform(); let size = transform.transform_size(size); let scale = Scale::from(output.current_scale().fractional_scale()); let mut elements = None; let mut casts_to_stop = vec![]; let mut casts = mem::take(&mut self.casts); for cast in &mut casts { if !cast.is_active.get() { continue; } if cast.target != target { continue; } match cast.ensure_size(size) { Ok(CastSizeChange::Ready) => (), Ok(CastSizeChange::Pending) => continue, Err(err) => { warn!("error updating stream size, stopping screencast: {err:?}"); casts_to_stop.push(cast.session_id); } } if cast.should_skip_frame(target_presentation_time) { continue; } // FIXME: Hidden / embedded / metadata cursor let elements = elements.get_or_insert_with(|| { self.render(renderer, output, true, RenderTarget::Screencast) }); let elements = elements.iter().rev(); if cast.dequeue_buffer_and_render(renderer, elements, size, scale) { cast.last_frame_time = target_presentation_time; } } self.casts = casts; for id in casts_to_stop { self.stop_cast(id); } } #[cfg(feature = "xdp-gnome-screencast")] fn render_windows_for_screen_cast( &mut self, renderer: &mut GlesRenderer, output: &Output, target_presentation_time: Duration, ) { let _span = tracy_client::span!("Niri::render_windows_for_screen_cast"); let scale = Scale::from(output.current_scale().fractional_scale()); let mut casts_to_stop = vec![]; let mut casts = mem::take(&mut self.casts); for cast in &mut casts { if !cast.is_active.get() { continue; } let CastTarget::Window { id } = cast.target else { continue; }; let mut windows = self.layout.windows_for_output(output); let Some(mapped) = windows.find(|win| u64::from(win.id().get()) == id) else { continue; }; let bbox = mapped .window .bbox_with_popups() .to_physical_precise_up(scale); match cast.ensure_size(bbox.size) { Ok(CastSizeChange::Ready) => (), Ok(CastSizeChange::Pending) => continue, Err(err) => { warn!("error updating stream size, stopping screencast: {err:?}"); casts_to_stop.push(cast.session_id); } } if cast.should_skip_frame(target_presentation_time) { continue; } // FIXME: pointer. let elements = mapped.render_for_screen_cast(renderer, scale).rev(); if cast.dequeue_buffer_and_render(renderer, elements, bbox.size, scale) { cast.last_frame_time = target_presentation_time; } } self.casts = casts; for id in casts_to_stop { self.stop_cast(id); } } #[cfg(feature = "xdp-gnome-screencast")] fn render_window_for_screen_cast( &mut self, renderer: &mut GlesRenderer, window_id: u64, target_presentation_time: Duration, ) { let _span = tracy_client::span!("Niri::render_window_for_screen_cast"); let mut window = None; self.layout.with_windows(|mapped, _| { if u64::from(mapped.id().get()) != window_id { return; } window = Some(mapped.window.clone()); }); let Some(window) = window else { return; }; // Use the cached output since it will be present even if the output was // currently disconnected. let Some(output) = self.mapped_cast_output.get(&window) else { return; }; let mut windows = self.layout.windows_for_output(output); let mapped = windows .find(|mapped| u64::from(mapped.id().get()) == window_id) .unwrap(); let scale = Scale::from(output.current_scale().fractional_scale()); let bbox = mapped .window .bbox_with_popups() .to_physical_precise_up(scale); let mut elements = None; let mut casts_to_stop = vec![]; let mut casts = mem::take(&mut self.casts); for cast in &mut casts { if !cast.is_active.get() { continue; } if cast.target != (CastTarget::Window { id: window_id }) { continue; } match cast.ensure_size(bbox.size) { Ok(CastSizeChange::Ready) => (), Ok(CastSizeChange::Pending) => continue, Err(err) => { warn!("error updating stream size, stopping screencast: {err:?}"); casts_to_stop.push(cast.session_id); } } if cast.should_skip_frame(target_presentation_time) { continue; } let elements = elements.get_or_insert_with(|| { // FIXME: pointer. mapped .render_for_screen_cast(renderer, scale) .rev() .collect::>() }); let elements = elements.iter(); if cast.dequeue_buffer_and_render(renderer, elements, bbox.size, scale) { cast.last_frame_time = target_presentation_time; } } self.casts = casts; drop(windows); for id in casts_to_stop { self.stop_cast(id); } } pub fn render_for_screencopy_with_damage( &mut self, renderer: &mut GlesRenderer, output: &Output, ) { let _span = tracy_client::span!("Niri::render_for_screencopy_with_damage"); let mut screencopy_state = mem::take(&mut self.screencopy_state); let elements = OnceCell::new(); for queue in screencopy_state.queues_mut() { let (damage_tracker, screencopy) = queue.split(); if let Some(screencopy) = screencopy { if screencopy.output() == output { let elements = elements.get_or_init(|| { self.render(renderer, output, true, RenderTarget::ScreenCapture) }); // FIXME: skip elements if not including pointers let render_result = Self::render_for_screencopy_internal( renderer, output, elements, true, damage_tracker, screencopy, ); match render_result { Ok((sync, damages)) => { if let Some(damages) = damages { // Convert from Physical coordinates back to Buffer coordinates. let transform = output.current_transform(); let physical_size = transform.transform_size(screencopy.buffer_size()); let damages = damages.iter().map(|dmg| { dmg.to_logical(1).to_buffer( 1, transform.invert(), &physical_size.to_logical(1), ) }); screencopy.damage(damages); queue.pop().submit_after_sync(false, sync, &self.event_loop); } else { trace!("no damage found, waiting till next redraw"); } } Err(err) => { // Recreate damage tracker to report full damage next check. *damage_tracker = OutputDamageTracker::new((0, 0), 1.0, Transform::Normal); queue.pop(); warn!("error rendering for screencopy: {err:?}"); } } }; } } self.screencopy_state = screencopy_state; } pub fn render_for_screencopy_without_damage( &mut self, renderer: &mut GlesRenderer, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy, ) -> anyhow::Result<()> { let _span = tracy_client::span!("Niri::render_for_screencopy"); let output = screencopy.output(); ensure!( self.output_state.contains_key(output), "screencopy output missing" ); self.layout.update_render_elements(output); let elements = self.render( renderer, output, screencopy.overlay_cursor(), RenderTarget::ScreenCapture, ); let Some(queue) = self.screencopy_state.get_queue_mut(manager) else { bail!("screencopy manager destroyed already"); }; let damage_tracker = queue.split().0; let render_result = Self::render_for_screencopy_internal( renderer, output, &elements, false, damage_tracker, &screencopy, ); let res = render_result .map(|(sync, _damage)| screencopy.submit_after_sync(false, sync, &self.event_loop)); if res.is_err() { // Recreate damage tracker to report full damage next check. *damage_tracker = OutputDamageTracker::new((0, 0), 1.0, Transform::Normal); } res } #[allow(clippy::type_complexity)] fn render_for_screencopy_internal<'a>( renderer: &mut GlesRenderer, output: &Output, elements: &[OutputRenderElements], with_damage: bool, damage_tracker: &'a mut OutputDamageTracker, screencopy: &Screencopy, ) -> anyhow::Result<(Option, Option<&'a Vec>>)> { let OutputModeSource::Static { size: last_size, scale: last_scale, transform: last_transform, } = damage_tracker.mode().clone() else { unreachable!("damage tracker must have static mode"); }; let size = screencopy.buffer_size(); let scale: Scale = output.current_scale().fractional_scale().into(); let transform = output.current_transform(); if size != last_size || scale != last_scale || transform != last_transform { *damage_tracker = OutputDamageTracker::new(size, scale, transform); } let region_loc = screencopy.region_loc(); let elements = elements .iter() .map(|element| { RelocateRenderElement::from_element( element, region_loc.upscale(-1), Relocate::Relative, ) }) .collect::>(); // Just checked damage tracker has static mode let damages = damage_tracker.damage_output(1, &elements).unwrap().0; if with_damage && damages.is_none() { return Ok((None, None)); } let elements = elements.iter().rev(); let sync = match screencopy.buffer() { ScreencopyBuffer::Dmabuf(dmabuf) => { let sync = render_to_dmabuf(renderer, dmabuf.clone(), size, scale, transform, elements) .context("error rendering to screencopy dmabuf")?; Some(sync) } ScreencopyBuffer::Shm(wl_buffer) => { render_to_shm(renderer, wl_buffer, size, scale, transform, elements) .context("error rendering to screencopy shm buffer")?; None } }; if let Err(err) = renderer.unbind() { warn!("error unbinding after rendering for screencopy: {err:?}"); } Ok((sync, damages)) } #[cfg(feature = "xdp-gnome-screencast")] fn stop_cast(&mut self, session_id: usize) { let _span = tracy_client::span!("Niri::stop_cast"); debug!(session_id, "StopCast"); for i in (0..self.casts.len()).rev() { let cast = &self.casts[i]; if cast.session_id != session_id { continue; } let cast = self.casts.swap_remove(i); if let Err(err) = cast.stream.disconnect() { warn!("error disconnecting stream: {err:?}"); } } let dbus = &self.dbus.as_ref().unwrap(); let server = dbus.conn_screen_cast.as_ref().unwrap().object_server(); let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id); if let Ok(iface) = server.interface::<_, mutter_screen_cast::Session>(path) { let _span = tracy_client::span!("invoking Session::stop"); async_io::block_on(async move { iface .get() .stop(&server, iface.signal_context().clone()) .await }); } } #[cfg(feature = "xdp-gnome-screencast")] pub fn stop_casts_for_target(&mut self, target: CastTarget) { let _span = tracy_client::span!("Niri::stop_casts_for_target"); // This is O(N^2) but it shouldn't be a problem I think. let ids: Vec<_> = self .casts .iter() .filter(|cast| cast.target == target) .map(|cast| cast.session_id) .collect(); for id in ids { self.stop_cast(id); } } pub fn remove_screencopy_output(&mut self, output: &Output) { let _span = tracy_client::span!("Niri::remove_screencopy_output"); for queue in self.screencopy_state.queues_mut() { queue.remove_output(output); } } pub fn debug_toggle_damage(&mut self) { self.debug_draw_damage = !self.debug_draw_damage; if self.debug_draw_damage { for (output, state) in &mut self.output_state { state.debug_damage_tracker = OutputDamageTracker::from_output(output); } } self.queue_redraw_all(); } pub fn capture_screenshots<'a>( &'a self, renderer: &'a mut GlesRenderer, ) -> impl Iterator + 'a { self.global_space.outputs().cloned().filter_map(|output| { let size = output.current_mode().unwrap().size; let transform = output.current_transform(); let size = transform.transform_size(size); let scale = Scale::from(output.current_scale().fractional_scale()); let targets = [ RenderTarget::Output, RenderTarget::Screencast, RenderTarget::ScreenCapture, ]; let screenshot = targets.map(|target| { let elements = self.render::(renderer, &output, false, target); let elements = elements.iter().rev(); let res = render_to_texture( renderer, size, scale, Transform::Normal, Fourcc::Abgr8888, elements, ); if let Err(err) = &res { warn!("error rendering output {}: {err:?}", output.name()); } let res_output = res.ok(); let pointer = self.pointer_element(renderer, &output); let res_pointer = if pointer.is_empty() { None } else { let res = render_to_encompassing_texture( renderer, scale, Transform::Normal, Fourcc::Abgr8888, &pointer, ); if let Err(err) = &res { warn!("error rendering pointer for {}: {err:?}", output.name()); } res.ok() }; res_output.map(|(texture, _)| { OutputScreenshot::from_textures( renderer, scale, texture, res_pointer.map(|(texture, _, geo)| (texture, geo)), ) }) }); if screenshot.iter().any(|res| res.is_none()) { return None; } let screenshot = screenshot.map(|res| res.unwrap()); Some((output, screenshot)) }) } pub fn screenshot( &mut self, renderer: &mut GlesRenderer, output: &Output, ) -> anyhow::Result<()> { let _span = tracy_client::span!("Niri::screenshot"); self.layout.update_render_elements(output); let size = output.current_mode().unwrap().size; let transform = output.current_transform(); let size = transform.transform_size(size); let scale = Scale::from(output.current_scale().fractional_scale()); let elements = self.render::(renderer, output, true, RenderTarget::ScreenCapture); let elements = elements.iter().rev(); let pixels = render_to_vec( renderer, size, scale, Transform::Normal, Fourcc::Abgr8888, elements, )?; self.save_screenshot(size, pixels) .context("error saving screenshot") } pub fn screenshot_window( &self, renderer: &mut GlesRenderer, output: &Output, mapped: &Mapped, ) -> anyhow::Result<()> { let _span = tracy_client::span!("Niri::screenshot_window"); let scale = Scale::from(output.current_scale().fractional_scale()); let alpha = if mapped.is_fullscreen() { 1. } else { mapped.rules().opacity.unwrap_or(1.).clamp(0., 1.) }; // FIXME: pointer. let elements = mapped.render( renderer, mapped.window.geometry().loc.to_f64(), scale, alpha, RenderTarget::ScreenCapture, ); let geo = elements .iter() .map(|ele| ele.geometry(scale)) .reduce(|a, b| a.merge(b)) .unwrap_or_default(); let elements = elements.iter().rev().map(|elem| { RelocateRenderElement::from_element(elem, geo.loc.upscale(-1), Relocate::Relative) }); let pixels = render_to_vec( renderer, geo.size, scale, Transform::Normal, Fourcc::Abgr8888, elements, )?; self.save_screenshot(geo.size, pixels) .context("error saving screenshot") } pub fn save_screenshot( &self, size: Size, pixels: Vec, ) -> anyhow::Result<()> { let path = match make_screenshot_path(&self.config.borrow()) { Ok(path) => path, Err(err) => { warn!("error making screenshot path: {err:?}"); None } }; // Prepare to set the encoded image as our clipboard selection. This must be done from the // main thread. let (tx, rx) = calloop::channel::sync_channel::>(1); self.event_loop .insert_source(rx, move |event, _, state| match event { calloop::channel::Event::Msg(buf) => { set_data_device_selection( &state.niri.display_handle, &state.niri.seat, vec![String::from("image/png")], buf.clone(), ); } calloop::channel::Event::Closed => (), }) .unwrap(); // Encode and save the image in a thread as it's slow. thread::spawn(move || { let mut buf = vec![]; let w = std::io::Cursor::new(&mut buf); if let Err(err) = write_png_rgba8(w, size.w as u32, size.h as u32, &pixels) { warn!("error encoding screenshot image: {err:?}"); return; } let buf: Arc<[u8]> = Arc::from(buf.into_boxed_slice()); let _ = tx.send(buf.clone()); let mut image_path = None; if let Some(path) = path { debug!("saving screenshot to {path:?}"); if let Some(parent) = path.parent() { if let Err(err) = std::fs::create_dir(parent) { if err.kind() != std::io::ErrorKind::AlreadyExists { warn!("error creating screenshot directory: {err:?}"); } } } match std::fs::write(&path, buf) { Ok(()) => image_path = Some(path), Err(err) => { warn!("error saving screenshot image: {err:?}"); } } } else { debug!("not saving screenshot to disk"); } #[cfg(feature = "dbus")] crate::utils::show_screenshot_notification(image_path); #[cfg(not(feature = "dbus"))] drop(image_path); }); Ok(()) } #[cfg(feature = "dbus")] pub fn screenshot_all_outputs( &mut self, renderer: &mut GlesRenderer, include_pointer: bool, on_done: impl FnOnce(PathBuf) + Send + 'static, ) -> anyhow::Result<()> { let _span = tracy_client::span!("Niri::screenshot_all_outputs"); self.layout.update_render_elements_all(); let outputs: Vec<_> = self.global_space.outputs().cloned().collect(); // FIXME: support multiple outputs, needs fixing multi-scale handling and cropping. anyhow::ensure!(outputs.len() == 1); let output = outputs.into_iter().next().unwrap(); let geom = self.global_space.output_geometry(&output).unwrap(); let output_scale = output.current_scale().integer_scale(); let geom = geom.to_physical(output_scale); let size = geom.size; let transform = output.current_transform(); let size = transform.transform_size(size); let elements = self.render::( renderer, &output, include_pointer, RenderTarget::ScreenCapture, ); let elements = elements.iter().rev(); let pixels = render_to_vec( renderer, size, Scale::from(f64::from(output_scale)), Transform::Normal, Fourcc::Abgr8888, elements, )?; let path = make_screenshot_path(&self.config.borrow()) .ok() .flatten() .unwrap_or_else(|| { let mut path = env::temp_dir(); path.push("screenshot.png"); path }); debug!("saving screenshot to {path:?}"); thread::spawn(move || { let file = match std::fs::File::create(&path) { Ok(file) => file, Err(err) => { warn!("error creating file: {err:?}"); return; } }; let w = std::io::BufWriter::new(file); if let Err(err) = write_png_rgba8(w, size.w as u32, size.h as u32, &pixels) { warn!("error encoding screenshot image: {err:?}"); return; } on_done(path); }); Ok(()) } pub fn is_locked(&self) -> bool { !matches!(self.lock_state, LockState::Unlocked) } pub fn lock(&mut self, confirmation: SessionLocker) { // Check if another client is in the process of locking. if matches!(self.lock_state, LockState::Locking(_)) { info!("refusing lock as another client is currently locking"); return; } // Check if we're already locked with an active client. if let LockState::Locked(lock) = &self.lock_state { if lock.is_alive() { info!("refusing lock as already locked with an active client"); return; } // If the client had died, continue with the new lock. } info!("locking session"); self.screenshot_ui.close(); self.cursor_manager .set_cursor_image(CursorImageStatus::default_named()); self.lock_state = LockState::Locking(confirmation); self.queue_redraw_all(); } pub fn unlock(&mut self) { info!("unlocking session"); self.lock_state = LockState::Unlocked; for output_state in self.output_state.values_mut() { output_state.lock_surface = None; } self.queue_redraw_all(); } pub fn new_lock_surface(&mut self, surface: LockSurface, output: &Output) { if !self.is_locked() { error!("tried to add a lock surface on an unlocked session"); return; } let Some(output_state) = self.output_state.get_mut(output) else { error!("missing output state"); return; }; output_state.lock_surface = Some(surface); } pub fn maybe_activate_pointer_constraint( &self, new_pos: Point, new_under: &PointerFocus, ) { let Some((surface, surface_loc)) = &new_under.surface else { return; }; if self.pointer_grab_ongoing { return; } let pointer = &self.seat.get_pointer().unwrap(); with_pointer_constraint(surface, pointer, |constraint| { let Some(constraint) = constraint else { return }; if constraint.is_active() { return; } // Constraint does not apply if not within region. if let Some(region) = constraint.region() { let new_pos_within_surface = new_pos - *surface_loc; if !region.contains(new_pos_within_surface.to_i32_round()) { return; } } constraint.activate(); }); } pub fn focus_layer_surface_if_on_demand(&mut self, surface: Option) { if let Some(surface) = surface { if surface.cached_state().keyboard_interactivity == wlr_layer::KeyboardInteractivity::OnDemand { if self.layer_shell_on_demand_focus.as_ref() != Some(&surface) { self.layer_shell_on_demand_focus = Some(surface); // FIXME: granular. self.queue_redraw_all(); } return; } } // Something else got clicked, clear on-demand layer-shell focus. if self.layer_shell_on_demand_focus.is_some() { self.layer_shell_on_demand_focus = None; // FIXME: granular. self.queue_redraw_all(); } } #[cfg(feature = "dbus")] pub fn on_ipc_outputs_changed(&self) { let _span = tracy_client::span!("Niri::on_ipc_outputs_changed"); let Some(dbus) = &self.dbus else { return }; let Some(conn_display_config) = dbus.conn_display_config.clone() else { return; }; let res = thread::Builder::new() .name("DisplayConfig MonitorsChanged Emitter".to_owned()) .spawn(move || { use crate::dbus::mutter_display_config::DisplayConfig; let _span = tracy_client::span!("MonitorsChanged"); let iface = match conn_display_config .object_server() .interface::<_, DisplayConfig>("/org/gnome/Mutter/DisplayConfig") { Ok(iface) => iface, Err(err) => { warn!("error getting DisplayConfig interface: {err:?}"); return; } }; async_io::block_on(async move { if let Err(err) = DisplayConfig::monitors_changed(iface.signal_context()).await { warn!("error emitting MonitorsChanged: {err:?}"); } }); }); if let Err(err) = res { warn!("error spawning a thread to send MonitorsChanged: {err:?}"); } } pub fn handle_focus_follows_mouse(&mut self, new_focus: &PointerFocus) { let Some(ffm) = self.config.borrow().input.focus_follows_mouse else { return; }; let pointer = &self.seat.get_pointer().unwrap(); if pointer.is_grabbed() { return; } // Recompute the current pointer focus because we don't update it during animations. let current_focus = self.surface_under_and_global_space(pointer.current_location()); if let Some(output) = &new_focus.output { if current_focus.output.as_ref() != Some(output) { self.layout.focus_output(output); } } if let Some(window) = &new_focus.window { if current_focus.window.as_ref() != Some(window) { if let Some(threshold) = ffm.max_scroll_amount { if self.layout.scroll_amount_to_activate(window) > threshold.0 { return; } } self.layout.activate_window(window); self.layer_shell_on_demand_focus = None; } } if let Some(layer) = &new_focus.layer { if current_focus.layer.as_ref() != Some(layer) { self.layer_shell_on_demand_focus = Some(layer.clone()); } } } pub fn do_screen_transition(&mut self, renderer: &mut GlesRenderer, delay_ms: Option) { let _span = tracy_client::span!("Niri::do_screen_transition"); self.layout.update_render_elements_all(); let textures: Vec<_> = self .output_state .keys() .cloned() .filter_map(|output| { let size = output.current_mode().unwrap().size; let transform = output.current_transform(); let size = transform.transform_size(size); let scale = Scale::from(output.current_scale().fractional_scale()); let targets = [ RenderTarget::Output, RenderTarget::Screencast, RenderTarget::ScreenCapture, ]; let textures = targets.map(|target| { let elements = self.render::(renderer, &output, false, target); let elements = elements.iter().rev(); let res = render_to_texture( renderer, size, scale, Transform::Normal, Fourcc::Abgr8888, elements, ); if let Err(err) = &res { warn!("error rendering output {}: {err:?}", output.name()); } res }); if textures.iter().any(|res| res.is_err()) { return None; } let textures = textures.map(|res| { let texture = res.unwrap().0; TextureBuffer::from_texture( renderer, texture, output.current_scale().fractional_scale(), Transform::Normal, Vec::new(), // We want windows below to get frame callbacks. ) }); Some((output, textures)) }) .collect(); let delay = delay_ms.map_or(screen_transition::DELAY, |d| { Duration::from_millis(u64::from(d)) }); let start_at = get_monotonic_time() + delay; for (output, from_texture) in textures { let state = self.output_state.get_mut(&output).unwrap(); state.screen_transition = Some(ScreenTransition::new(from_texture, start_at)); } // We don't actually need to queue a redraw because the point is to freeze the screen for a // bit, and even if the delay was zero, we're drawing the same contents anyway. } pub fn recompute_window_rules(&mut self) { let _span = tracy_client::span!("Niri::recompute_window_rules"); let changed = { let window_rules = &self.config.borrow().window_rules; for unmapped in self.unmapped_windows.values_mut() { let new_rules = ResolvedWindowRules::compute( window_rules, WindowRef::Unmapped(unmapped), self.is_at_startup, ); if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state { *rules = new_rules; } } let mut windows = vec![]; self.layout.with_windows_mut(|mapped, _| { if mapped.recompute_window_rules(window_rules, self.is_at_startup) { windows.push(mapped.window.clone()); } }); let changed = !windows.is_empty(); for win in windows { self.layout.update_window(&win, None); } changed }; if changed { // FIXME: granular. self.queue_redraw_all(); } } pub fn ipc_workspaces(&self) -> Vec { self.layout.ipc_workspaces() } } pub struct ClientState { pub compositor_state: CompositorClientState, pub can_view_decoration_globals: bool, /// Whether this client is denied from the restricted protocols such as security-context. pub restricted: bool, } impl ClientData for ClientState { fn initialized(&self, _client_id: ClientId) {} fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} } niri_render_elements! { OutputRenderElements => { Monitor = MonitorRenderElement, Wayland = WaylandSurfaceRenderElement, NamedPointer = MemoryRenderBufferRenderElement, SolidColor = SolidColorRenderElement, ScreenshotUi = ScreenshotUiRenderElement, Texture = PrimaryGpuTextureRenderElement, // Used for the CPU-rendered panels. RelocatedMemoryBuffer = RelocateRenderElement>, } }