use std::{ collections::{HashMap, HashSet}, error::Error, ffi::OsString, sync::Arc, time::{Duration, Instant}, }; use ferese_animation::{AnimatedValue, ClientSize, PresentationMode, SpringConfig, WindowGeometry}; use ferese_core::{ LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId, WorkspaceLayout, WorkspaceSet, }; use ferese_layout::{ Axis, ColumnWidth, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, ViewportFocusStrategy, WindowId, }; use smithay::{ backend::{ allocator::dmabuf::Dmabuf, drm::{DrmEventTime, DrmNode}, renderer::{ErasedContextId, ImportDma}, }, desktop::{ LayerSurface, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output, utils::send_frames_surface_tree, }, input::{ Seat, SeatState, keyboard::XkbConfig, pointer::{CursorIcon, CursorImageStatus}, }, output::Output, reexports::{ calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic}, drm::control::crtc, wayland_protocols::xdg::{ decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode, shell::server::xdg_toplevel, }, wayland_server::{ Display, DisplayHandle, backend::{ClientData, ClientId, DisconnectReason}, protocol::wl_surface::WlSurface, }, }, utils::{Logical, Point, Rectangle, Size}, wayland::{ alpha_modifier::AlphaModifierState, compositor::{CompositorClientState, CompositorState, with_states}, cursor_shape::CursorShapeManagerState, dmabuf::{DmabufState, ImportNotifier}, fractional_scale::FractionalScaleManagerState, idle_inhibit::IdleInhibitManagerState, idle_notify::IdleNotifierState, input_method::InputMethodManagerState, keyboard_shortcuts_inhibit::{KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor}, output::OutputManagerState, pointer_constraints::PointerConstraintsState, presentation::PresentationState, relative_pointer::RelativePointerManagerState, selection::{data_device::DataDeviceState, primary_selection::PrimarySelectionState}, shell::wlr_layer::Layer, shell::wlr_layer::WlrLayerShellState, shell::xdg::decoration::XdgDecorationState, shell::xdg::{SurfaceCachedState, XdgShellState, XdgToplevelSurfaceData}, shm::ShmState, single_pixel_buffer::SinglePixelBufferState, socket::ListeningSocketSource, text_input::TextInputManagerState, viewporter::ViewporterState, xdg_activation::XdgActivationState, xdg_foreign::XdgForeignState, xdg_toplevel_icon::XdgToplevelIconManager, }, }; use crate::{ config::{Binding, InputSettings, OutputProfile, ThemeSettings}, window_rules::{WindowRule, resolve as resolve_window_rules}, }; const CLOSE_ANIMATION_DURATION: Duration = Duration::from_millis(140); #[derive(Clone, Copy, Debug, Default)] struct ClosingAnimation { progress: f64, close_sent: bool, } impl ClosingAnimation { fn advance(&mut self, delta: Duration) -> bool { if self.close_sent { return false; } self.progress = (self.progress + delta.as_secs_f64() / CLOSE_ANIMATION_DURATION.as_secs_f64()).min(1.0); if self.progress < 1.0 { return false; } self.close_sent = true; true } } pub struct Ferese { pub(crate) session_lock_state: smithay::wayland::session_lock::SessionLockManagerState, pub(crate) session_lock: crate::session_lock::Lock, pub(crate) config_source: Option, pub start_time: Instant, pub socket_name: OsString, pub display_handle: DisplayHandle, pub loop_signal: LoopSignal, pub space: Space, pub workspaces: WorkspaceSet, pub output_workspaces: OutputWorkspaceMap, pub(crate) output_ids: HashMap, output_identity_ids: HashMap, pub window_ids: HashMap, pub window_geometry: HashMap, resize_transactions: HashMap, pub(crate) resize_snapshots: HashMap, pub(crate) nested_backend: Option, pub(crate) wallpaper: crate::wallpaper::WallpaperState, maximized_windows: HashSet, maximized_column_widths: HashMap, window_stack: crate::stacking::WindowStack, natural_floating_pending: HashSet, pub(crate) window_borders: HashMap, pub(crate) window_dims: HashMap, pub(crate) window_resize_fills: HashMap, pub(crate) window_dimming: HashMap, pub(crate) window_focus: HashMap, pub(crate) window_shadows: HashMap, pub(crate) rounded_clip_programs: HashMap, pub(crate) overview_scrims: HashMap, pub(crate) material_programs: HashMap, pub(crate) material_buffers: HashMap, pub(crate) blur_programs: HashMap, pub(crate) backdrop_generation: u64, closing_windows: HashMap, viewport_animations: HashMap, scrolling_world_x: HashMap, viewport_coupled_widths: HashMap, pending_column_width_cycles: HashSet, pub focused_window: Option, column_width_presets: Vec, gap_config: GapConfig, pub(crate) input_settings: InputSettings, pub(crate) bindings: Vec, window_rules: Vec, window_rules_applied: HashSet, pub(crate) theme_settings: ThemeSettings, animations_enabled: bool, animation_speed: f64, spring_config: SpringConfig, viewport_spring_config: SpringConfig, pub(crate) output_profiles: Vec, pub cursor_status: CursorImageStatus, // Cursor callbacks may run with Smithay's pointer mutex held. Rendering // reads the pointer position, so defer it until event dispatch returns. pub(crate) cursor_redraw_pending: bool, pub(crate) cursor_theme: xcursor::CursorTheme, pub(crate) named_cursors: HashMap, pub intercepted_keys: HashSet, pub idle_inhibitors: HashMap, pub active_shortcuts_inhibitor: Option, pub direct_backend: Option, _ipc_socket: Option, pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>, pub(crate) pending_screencopies: Vec, pub(crate) shell_resources: Vec< smithay::reexports::wayland_server::Weak< ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1, >, >, pub(crate) shell_snapshot_serial: u32, pub(crate) overview: crate::overview::OverviewState, next_window_id: u64, next_output_id: u64, last_animation_tick: Instant, pub popups: PopupManager, pub(crate) dismissing_popups: Vec<( WlSurface, smithay::desktop::PopupKind, crate::dimming::DimAnimation, )>, pub seat: Seat, pub alpha_modifier_state: AlphaModifierState, pub compositor_state: CompositorState, pub cursor_shape_state: CursorShapeManagerState, pub data_device_state: DataDeviceState, pub decoration_state: XdgDecorationState, pub dmabuf_state: DmabufState, pub fractional_scale_state: FractionalScaleManagerState, pub idle_inhibit_state: IdleInhibitManagerState, pub idle_notifier_state: IdleNotifierState, pub input_method_manager_state: InputMethodManagerState, pub keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState, pub output_manager_state: OutputManagerState, pub pointer_constraints_state: PointerConstraintsState, pub presentation_state: PresentationState, pub primary_selection_state: PrimarySelectionState, pub relative_pointer_state: RelativePointerManagerState, pub seat_state: SeatState, pub shm_state: ShmState, pub single_pixel_buffer_state: SinglePixelBufferState, pub text_input_manager_state: TextInputManagerState, pub viewporter_state: ViewporterState, pub layer_shell_state: WlrLayerShellState, pub xdg_activation_state: XdgActivationState, pub xdg_foreign_state: XdgForeignState, pub xdg_shell_state: XdgShellState, pub xdg_toplevel_icon_manager: XdgToplevelIconManager, } pub struct RuntimeConfig { pub(crate) overview_font_family: String, pub(crate) wallpaper: crate::wallpaper::WallpaperConfig, pub layout_mode: LayoutMode, pub gap_config: GapConfig, pub input_settings: InputSettings, pub bindings: Vec, pub window_rules: Vec, pub theme_settings: ThemeSettings, pub default_column_width: ColumnWidth, pub scrolling_focus_strategy: ViewportFocusStrategy, pub column_width_presets: Vec, pub animations_enabled: bool, pub animation_speed: f64, pub spring_config: SpringConfig, pub viewport_spring_config: SpringConfig, pub output_profiles: Vec, } impl Ferese { pub(crate) fn apply_runtime_config(&mut self, config: RuntimeConfig) -> Result<(), String> { let keyboard_changed = self.input_settings.xkb_layout != config.input_settings.xkb_layout || self.input_settings.xkb_variant != config.input_settings.xkb_variant || self.input_settings.xkb_options != config.input_settings.xkb_options; if let Some(keyboard) = self.seat.get_keyboard() { if keyboard_changed { let options = (!config.input_settings.xkb_options.is_empty()) .then(|| config.input_settings.xkb_options.join(",")); keyboard .set_xkb_config( self, XkbConfig { layout: &config.input_settings.xkb_layout, variant: &config.input_settings.xkb_variant, options, ..XkbConfig::default() }, ) .map_err(|e| format!("keymap reload failed: {e}"))?; } keyboard.change_repeat_info( config.input_settings.repeat_rate, config.input_settings.repeat_delay_ms, ); } self.advance_animations(Instant::now()); if self.input_settings.touchpad != config.input_settings.touchpad { tracing::warn!( "touchpad changes apply when the device reconnects or the session restarts" ); } if self.output_profiles != config.output_profiles { tracing::warn!( "output profile changes apply on output reconnect; current modes are preserved" ); } self.workspaces.reconfigure_defaults( config.layout_mode, config.default_column_width, config.scrolling_focus_strategy, ); self.gap_config = config.gap_config; self.input_settings = config.input_settings; self.bindings = config.bindings; self.window_rules = config.window_rules; self.theme_settings = config.theme_settings; self.column_width_presets = config.column_width_presets; self.animations_enabled = config.animations_enabled; self.animation_speed = config.animation_speed; self.spring_config = config.spring_config; self.viewport_spring_config = config.viewport_spring_config; self.output_profiles = config.output_profiles; self.overview.set_font_family(config.overview_font_family); self.wallpaper.reload(config.wallpaper); self.backdrop_generation = self.backdrop_generation.wrapping_add(1); self.relayout(); crate::backends::direct::render_all(self); Ok(()) } pub fn register_output(&mut self, output: &Output, identity: String) { let Some(geometry) = self.space.output_geometry(output) else { tracing::error!(output = %output.name(), "cannot register an unmapped output"); return; }; let output_id = *self.output_identity_ids.entry(identity).or_insert_with(|| { let id = OutputId(self.next_output_id); self.next_output_id = self.next_output_id.saturating_add(1); id }); let fallback_workspace = (1..) .find_map(|index| { let workspace = self.workspaces.ensure_numeric(index).ok()?; self.output_workspaces .output_for_workspace(workspace) .is_none() .then_some(workspace) }) .expect("numeric workspaces are inexhaustible"); let geometry = OutputGeometry::new( geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h, ); let registration = self .output_workspaces .connect(output_id, geometry, fallback_workspace); match registration { Ok(workspace) => { self.output_ids.insert(output.clone(), output_id); if self.output_workspaces.focused_output() == Some(output_id) { self.activate_output_workspace(output_id, workspace); } self.send_shell_snapshots(); } Err(error) => { tracing::error!(%error, output = %output.name(), "failed to register output"); } } } pub fn unregister_output(&mut self, output: &Output) { let Some(output_id) = self.output_ids.remove(output) else { return; }; self.overview_scrims.remove(&output_id); self.pending_screencopies.retain(|capture| { if capture.output == *output { capture.frame.failed(); false } else { true } }); self.space.unmap_output(output); self.session_lock.surfaces.remove(output); self.session_lock.backgrounds.remove(output); match self.output_workspaces.disconnect(output_id) { Ok(Some(target)) => { if let Some(workspace) = self.output_workspaces.active_workspace(target) { self.activate_output_workspace(target, workspace); } } Ok(None) => { self.focused_window = None; self.restore_keyboard_focus(); } Err(error) => { tracing::error!(%error, output = %output.name(), "failed to unregister output") } } self.relayout(); } pub(crate) fn is_focused_output(&self, output: &Output) -> bool { self.output_ids.get(output).copied() == self.output_workspaces.focused_output() } fn activate_output_workspace(&mut self, output: OutputId, workspace: ferese_core::WorkspaceId) { if let Err(error) = self.output_workspaces.focus_output(output) { tracing::error!(%error, "failed to focus output"); return; } match self.workspaces.activate(workspace) { Ok(focus) => self.focused_window = focus, Err(error) => tracing::error!(%error, "failed to activate output workspace"), } } pub(crate) fn focused_output(&self) -> Option<&Output> { let focused = self.output_workspaces.focused_output()?; self.output_ids .iter() .find_map(|(output, id)| (*id == focused).then_some(output)) } pub(crate) fn restore_output_focus(&mut self) { if let Some(output) = self.output_workspaces.focused_output() && let Some(workspace) = self.output_workspaces.active_workspace(output) { self.activate_output_workspace(output, workspace); self.restore_keyboard_focus(); } } pub(crate) fn output_id(&self, output: &Output) -> Option { self.output_ids.get(output).copied() } pub(crate) fn focus_output_at(&mut self, position: Point) { let output = self.space.outputs().find_map(|output| { let geometry = self.space.output_geometry(output)?; (position.x >= f64::from(geometry.loc.x) && position.y >= f64::from(geometry.loc.y) && position.x < f64::from(geometry.loc.x + geometry.size.w) && position.y < f64::from(geometry.loc.y + geometry.size.h)) .then(|| output.clone()) }); let Some(output) = output else { return; }; let Some(output_id) = self.output_ids.get(&output).copied() else { return; }; let Some(workspace) = self.output_workspaces.active_workspace(output_id) else { return; }; if self.output_workspaces.focused_output() == Some(output_id) && self.workspaces.active_id() == workspace { return; } self.activate_output_workspace(output_id, workspace); self.relayout(); } pub(crate) fn queue_dmabuf_import(&mut self, dmabuf: Dmabuf, notifier: ImportNotifier) { self.pending_dmabuf_imports.push((dmabuf, notifier)); } pub(crate) fn process_dmabuf_imports(&mut self, renderer: &mut R) where R: ImportDma, { for (dmabuf, notifier) in self.pending_dmabuf_imports.drain(..) { if renderer.import_dmabuf(&dmabuf, None).is_ok() { if let Err(error) = notifier.successful::() { tracing::debug!(?error, "dma-buf client disappeared before import completed"); } } else { notifier.failed(); } } } pub fn new( event_loop: &mut EventLoop<'static, Self>, display: Display, config: RuntimeConfig, ) -> Result> { let display_handle = display.handle(); crate::handlers::screencopy::init_global(&display_handle); crate::effects::init_global(&display_handle); crate::shell_control::init_global(&display_handle); let alpha_modifier_state = AlphaModifierState::new::(&display_handle); let compositor_state = CompositorState::new::(&display_handle); let cursor_shape_state = CursorShapeManagerState::new::(&display_handle); let xdg_shell_state = XdgShellState::new::(&display_handle); let decoration_state = XdgDecorationState::new::(&display_handle); let dmabuf_state = DmabufState::new(); let fractional_scale_state = FractionalScaleManagerState::new::(&display_handle); let idle_inhibit_state = IdleInhibitManagerState::new::(&display_handle); let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.handle()); let session_lock_state = smithay::wayland::session_lock::SessionLockManagerState::new::< Self, _, >(&display_handle, |_| true); let input_method_enabled = std::env::var_os("FERESE_ENABLE_INPUT_METHOD").is_some_and(|value| value == "1"); let input_method_manager_state = InputMethodManagerState::new::(&display_handle, move |_| input_method_enabled); let keyboard_shortcuts_inhibit_state = KeyboardShortcutsInhibitState::new::(&display_handle); let shortcut_inhibit_enabled = std::env::var_os("FERESE_ENABLE_SHORTCUT_INHIBIT").is_some_and(|value| value == "1"); if !shortcut_inhibit_enabled { display_handle.disable_global::(keyboard_shortcuts_inhibit_state.global()); } let shm_state = ShmState::new::(&display_handle, Vec::new()); let single_pixel_buffer_state = SinglePixelBufferState::new::(&display_handle); let text_input_manager_state = TextInputManagerState::new::(&display_handle); let output_manager_state = OutputManagerState::new_with_xdg_output::(&display_handle); let pointer_constraints_state = PointerConstraintsState::new::(&display_handle); let presentation_state = PresentationState::new::(&display_handle, libc::CLOCK_MONOTONIC as u32); let data_device_state = DataDeviceState::new::(&display_handle); let primary_selection_state = PrimarySelectionState::new::(&display_handle); let relative_pointer_state = RelativePointerManagerState::new::(&display_handle); let viewporter_state = ViewporterState::new::(&display_handle); let layer_shell_state = WlrLayerShellState::new::(&display_handle); let xdg_activation_state = XdgActivationState::new::(&display_handle); let xdg_foreign_state = XdgForeignState::new::(&display_handle); let xdg_toplevel_icon_manager = XdgToplevelIconManager::new::(&display_handle); let mut seat_state = SeatState::new(); let mut seat = seat_state.new_wl_seat(&display_handle, "ferese-winit"); let xkb_options = (!config.input_settings.xkb_options.is_empty()) .then(|| config.input_settings.xkb_options.join(",")); seat.add_keyboard( XkbConfig { layout: &config.input_settings.xkb_layout, variant: &config.input_settings.xkb_variant, options: xkb_options, ..XkbConfig::default() }, config.input_settings.repeat_delay_ms, config.input_settings.repeat_rate, )?; seat.add_pointer(); seat.add_touch(); let socket_name = Self::init_wayland_listener(display, event_loop)?; let start_time = Instant::now(); let cursor_theme = crate::cursor::cursor_theme(); let default_cursor = crate::cursor::load_named_cursor(&cursor_theme, CursorIcon::Default); let named_cursors = HashMap::from([(CursorIcon::Default, default_cursor)]); let mut state = Self { session_lock_state, session_lock: crate::session_lock::Lock::default(), config_source: None, start_time, socket_name, display_handle, loop_signal: event_loop.get_signal(), space: Space::default(), workspaces: WorkspaceSet::new( config.layout_mode, config.default_column_width, config.scrolling_focus_strategy, ), output_workspaces: OutputWorkspaceMap::default(), output_ids: HashMap::new(), output_identity_ids: HashMap::new(), window_ids: HashMap::new(), window_geometry: HashMap::new(), resize_transactions: HashMap::new(), resize_snapshots: HashMap::new(), nested_backend: None, wallpaper: crate::wallpaper::WallpaperState::new(config.wallpaper), maximized_windows: HashSet::new(), maximized_column_widths: HashMap::new(), window_stack: crate::stacking::WindowStack::default(), natural_floating_pending: HashSet::new(), window_borders: HashMap::new(), window_dims: HashMap::new(), window_resize_fills: HashMap::new(), window_dimming: HashMap::new(), window_focus: HashMap::new(), window_shadows: HashMap::new(), rounded_clip_programs: HashMap::new(), overview_scrims: HashMap::new(), material_programs: HashMap::new(), material_buffers: HashMap::new(), blur_programs: HashMap::new(), backdrop_generation: 0, closing_windows: HashMap::new(), viewport_animations: HashMap::new(), scrolling_world_x: HashMap::new(), viewport_coupled_widths: HashMap::new(), pending_column_width_cycles: HashSet::new(), focused_window: None, column_width_presets: config.column_width_presets, gap_config: config.gap_config, input_settings: config.input_settings, bindings: config.bindings, window_rules: config.window_rules, window_rules_applied: HashSet::new(), theme_settings: config.theme_settings, animations_enabled: config.animations_enabled, animation_speed: config.animation_speed, spring_config: config.spring_config, viewport_spring_config: config.viewport_spring_config, output_profiles: config.output_profiles, cursor_status: CursorImageStatus::default_named(), cursor_redraw_pending: false, cursor_theme, named_cursors, intercepted_keys: HashSet::new(), idle_inhibitors: HashMap::new(), active_shortcuts_inhibitor: None, direct_backend: None, _ipc_socket: None, pending_dmabuf_imports: Vec::new(), pending_screencopies: Vec::new(), shell_resources: Vec::new(), shell_snapshot_serial: 0, overview: crate::overview::OverviewState::with_font_family(config.overview_font_family), next_window_id: 1, next_output_id: 1, last_animation_tick: start_time, popups: PopupManager::default(), dismissing_popups: Vec::new(), seat, alpha_modifier_state, compositor_state, cursor_shape_state, data_device_state, decoration_state, dmabuf_state, fractional_scale_state, idle_inhibit_state, idle_notifier_state, input_method_manager_state, keyboard_shortcuts_inhibit_state, output_manager_state, pointer_constraints_state, presentation_state, primary_selection_state, relative_pointer_state, seat_state, shm_state, single_pixel_buffer_state, text_input_manager_state, viewporter_state, layer_shell_state, xdg_activation_state, xdg_foreign_state, xdg_shell_state, xdg_toplevel_icon_manager, }; state._ipc_socket = Some(crate::ipc::init(event_loop)?); Ok(state) } fn init_wayland_listener( display: Display, event_loop: &mut EventLoop<'static, Self>, ) -> Result> { let listening_socket = ListeningSocketSource::new_auto()?; let socket_name = listening_socket.socket_name().to_os_string(); let loop_handle = event_loop.handle(); loop_handle.insert_source(listening_socket, |client_stream, _, state| { if let Err(error) = state .display_handle .insert_client(client_stream, Arc::new(ClientState::default())) { tracing::warn!(%error, "failed to register Wayland client"); } })?; loop_handle.insert_source( Generic::new(display, Interest::READ, Mode::Level), |_, display, state| { // SAFETY: this event source owns the display for the loop lifetime. unsafe { display.get_mut().dispatch_clients(state)? }; Ok(PostAction::Continue) }, )?; Ok(socket_name) } pub fn surface_under( &self, position: Point, ) -> Option<(WlSurface, Point)> { if self.session_lock.active { return self.lock_surface_under(position); } self.layer_surface_under(position, &[Layer::Overlay, Layer::Top]) .map(|(_, surface, origin)| (surface, origin)) .or_else(|| self.window_surface_under(position)) .or_else(|| { self.layer_surface_under(position, &[Layer::Bottom, Layer::Background]) .map(|(_, surface, origin)| (surface, origin)) }) } pub fn layer_under( &self, position: Point, ) -> Option<(LayerSurface, WlSurface, Point)> { if self.session_lock.active { return None; } if let Some(layer) = self.layer_surface_under(position, &[Layer::Overlay, Layer::Top]) { return Some(layer); } if self.window_surface_under(position).is_some() { return None; } self.layer_surface_under(position, &[Layer::Bottom, Layer::Background]) } fn window_surface_under( &self, position: Point, ) -> Option<(WlSurface, Point)> { if self.overview.is_active() { return None; } let workspace = self.workspace_under_pointer(position)?; self.space.elements().rev().find_map(|window| { let id = self.window_ids.get(window)?; if self.workspaces.workspace_for_window(*id) != Some(workspace) { return None; } let visual = self.presented_window_rect(*id)?; let inside_visual = position.x >= visual.x && position.y >= visual.y && position.x < visual.x + visual.width && position.y < visual.y + visual.height; if !inside_visual { return None; } let (source_x, source_y) = self.inverse_presented_window_point(*id, position.x, position.y)?; let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); window .surface_under(source_point, WindowSurfaceType::ALL) .map(|(surface, surface_location)| { let surface_point = source_point - surface_location.to_f64(); (surface, position - surface_point) }) }) } fn layer_surface_under( &self, position: Point, layers: &[Layer], ) -> Option<(LayerSurface, WlSurface, Point)> { let output = self.space.output_under(position).next()?; let output_geometry = self.space.output_geometry(output)?; let output_position = position - output_geometry.loc.to_f64(); let map = layer_map_for_output(output); for requested in layers { if *requested == Layer::Top && self.output_has_fullscreen(output) { continue; } for layer in map.layers_on(*requested).rev() { let geometry = map.layer_geometry(layer)?; let layer_position = output_position - geometry.loc.to_f64(); let Some((surface, surface_location)) = layer.surface_under(layer_position, WindowSurfaceType::ALL) else { continue; }; let origin = output_geometry.loc + geometry.loc + surface_location; return Some((layer.clone(), surface, origin.to_f64())); } } None } pub fn send_cursor_frame(&self, output: &Output) { let CursorImageStatus::Surface(surface) = &self.cursor_status else { return; }; send_frames_surface_tree( surface, output, self.start_time.elapsed(), Some(Duration::ZERO), |_, _| Some(output.clone()), ); } pub fn window_under_visual(&self, position: Point) -> Option { let workspace = self.workspace_under_pointer(position)?; let candidates = if self.overview.is_active() { self.window_ids.keys().cloned().collect::>() } else { self.space.elements().rev().cloned().collect::>() }; candidates.iter().find_map(|window| { let id = self.window_ids.get(window)?; if self.workspaces.workspace_for_window(*id) != Some(workspace) { return None; } let visual = self.presented_window_rect(*id)?; (position.x >= visual.x && position.y >= visual.y && position.x < visual.x + visual.width && position.y < visual.y + visual.height) .then(|| window.clone()) }) } fn workspace_under_pointer(&self, position: Point) -> Option { let output = self.space.output_under(position).next()?; self.output_workspaces .active_workspace(self.output_id(output)?) } pub(crate) fn window_belongs_to_output(&self, window: WindowId, output: &Output) -> bool { let workspace = self .output_id(output) .and_then(|output| self.output_workspaces.active_workspace(output)); workspace.is_some() && self.workspaces.workspace_for_window(window) == workspace } pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) { if let Some(id) = self.window_ids.get(window) { self.window_stack.raise(*id); } self.space.raise_element(window, activate); self.sync_window_stacking(); } pub(crate) fn sync_window_stacking(&mut self) { let mut windows = self.space.elements().cloned().collect::>(); windows.sort_by_key(|window| { let id = self.window_ids.get(window).copied(); let geometry = id.and_then(|id| self.window_geometry.get(&id)); let floating = id.is_some_and(|id| { matches!( self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }) ) }); let priority = crate::stacking::layer_priority( floating, geometry.is_some_and(|geometry| geometry.is_zooming()), geometry.is_some_and(|geometry| geometry.is_fullscreen()), ); ( priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id)), ) }); for window in windows { self.space.raise_element(&window, false); } } pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> { if !self.overview.is_presenting() { return Some((1.0, 1.0)); } let id = self.window_ids.get(window)?; let geometry = self.window_geometry.get(id)?; let source = geometry.client.committed_size?; let presented = self.presented_window_rect(*id)?; if source.width <= 0 || source.height <= 0 { return None; } Some(( presented.width / f64::from(source.width), presented.height / f64::from(source.height), )) } pub(crate) fn visual_rect_for_window( &self, window: &Window, ) -> Option> { let id = self.window_ids.get(window)?; let rect = self.presented_window_rect(*id)?; let size = ClientSize::from_rect(rect); Some(Rectangle::new( (rect.x.round() as i32, rect.y.round() as i32).into(), (size.width, size.height).into(), )) } pub fn add_tiled_window(&mut self, window: Window) { let id = WindowId(self.next_window_id); self.next_window_id += 1; let focus_new_window = self.workspaces.active().fullscreen.is_none(); let axis = self .output_bounds() .and_then(|bounds| { self.workspaces .active() .layout .automatic_axis(self.focused_window, bounds) .ok() }) .unwrap_or(Axis::Horizontal); if let Err(error) = self.workspaces.insert_window(id, axis, 0.5) { tracing::error!(%error, ?id, "failed to insert window into layout"); return; } self.window_ids.insert(window.clone(), id); self.window_stack.insert(id); if focus_new_window { self.focused_window = Some(id); } self.space.map_element(window, (0, 0), focus_new_window); self.relayout(); if focus_new_window { self.restore_keyboard_focus(); } } pub fn add_transient_window(&mut self, window: Window, parent: WindowId) { let Some(workspace) = self.workspaces.workspace_for_window(parent) else { self.add_tiled_window(window); return; }; let Some(bounds) = self.output_bounds() else { self.add_tiled_window(window); return; }; let parent_rect = self.logical_window_rect(parent, bounds).unwrap_or(bounds); let rect = centered_transient_rect(parent_rect); let id = WindowId(self.next_window_id); self.next_window_id += 1; if let Err(error) = self .workspaces .insert_floating_window(id, workspace, rect, false) { tracing::error!(%error, ?id, ?parent, "failed to insert transient window"); return; } self.window_ids.insert(window.clone(), id); self.window_stack.insert(id); self.space.map_element(window, (0, 0), false); self.relayout(); } pub(crate) fn apply_initial_window_rules( &mut self, window: &Window, app_id: Option<&str>, title: Option<&str>, transient: bool, ) { let Some(id) = self.window_ids.get(window).copied() else { return; }; let Some(bounds) = self.output_bounds() else { return; }; if !self.window_rules_applied.insert(id) { return; } let rule = resolve_window_rules(&self.window_rules, app_id, title, transient); if rule == Default::default() { return; } let axis = self .workspaces .active() .layout .automatic_axis(self.focused_window, bounds) .unwrap_or(Axis::Horizontal); if let Some(workspace) = rule.workspace && let Err(error) = self .workspaces .move_window_to_numeric(id, workspace, axis, 0.5) { tracing::warn!(%error, ?id, workspace, "failed to apply window workspace rule"); } let should_float = rule .floating .or((rule.width.is_some() || rule.height.is_some()).then_some(true)); if let Some(should_float) = should_float { let is_floating = matches!( self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }) ); let mut rect = centered_floating_rect(bounds); if should_float && rule.width.is_none() && rule.height.is_none() { if let Some(size) = client_size(window) { rect = natural_floating_rect(bounds, size); } else { self.natural_floating_pending.insert(id); } } rect.width = rule.width.unwrap_or(rect.width).min(bounds.width); rect.height = rule.height.unwrap_or(rect.height).min(bounds.height); rect.x = bounds.x + (bounds.width - rect.width) / 2.0; rect.y = bounds.y + (bounds.height - rect.height) / 2.0; let result = if should_float != is_floating { self.workspaces .toggle_floating(id, rect, axis, 0.5) .map(|_| ()) } else if should_float && (rule.width.is_some() || rule.height.is_some()) { self.workspaces.set_floating_rect(id, rect) } else { Ok(()) }; if let Err(error) = result { tracing::warn!(%error, ?id, "failed to apply floating window rule"); } } if let Some(fullscreen) = rule.fullscreen && let Err(error) = self.workspaces.set_fullscreen(id, fullscreen) { tracing::warn!(%error, ?id, fullscreen, "failed to apply fullscreen window rule"); } if self.workspaces.workspace_for_window(id) != Some(self.workspaces.active_id()) && self.focused_window == Some(id) { self.focused_window = self.workspaces.active().last_focused; } self.relayout(); self.restore_keyboard_focus(); } pub fn make_window_transient(&mut self, window: &Window, parent: WindowId) { let Some(id) = self.window_ids.get(window).copied() else { return; }; if self.workspaces.workspace_for_window(id) != self.workspaces.workspace_for_window(parent) { tracing::warn!( ?id, ?parent, "ignored transient parent on another workspace" ); return; } let Some(bounds) = self.output_bounds() else { return; }; let parent_rect = self.logical_window_rect(parent, bounds).unwrap_or(bounds); let rect = centered_transient_rect(parent_rect); let result = match self.workspaces.placement(id) { Some(WindowPlacement::Tiled) => self .workspaces .toggle_floating(id, rect, Axis::Horizontal, 0.5) .map(|_| ()), Some(WindowPlacement::Floating { .. }) => self.workspaces.set_floating_rect(id, rect), None => return, }; if let Err(error) = result { tracing::warn!(%error, ?id, ?parent, "failed to apply transient placement"); return; } self.relayout(); } pub fn remove_tiled_window(&mut self, window: &Window) { let Some(id) = self.window_ids.remove(window) else { return; }; self.space.unmap_elem(window); self.window_geometry.remove(&id); self.resize_transactions.remove(&id); self.resize_snapshots.remove(&id); self.window_resize_fills.remove(&id); self.maximized_windows.remove(&id); self.maximized_column_widths.remove(&id); self.window_stack.remove(id); self.natural_floating_pending.remove(&id); self.window_borders.remove(&id); self.window_dims.remove(&id); self.window_dimming.remove(&id); self.window_focus.remove(&id); self.window_shadows.remove(&id); self.closing_windows.remove(&id); self.window_rules_applied.remove(&id); self.scrolling_world_x.remove(&id); self.viewport_coupled_widths.remove(&id); self.pending_column_width_cycles.remove(&id); if let Err(error) = self.workspaces.remove_window(id) { tracing::error!(%error, ?id, "failed to remove window from layout"); } if self.focused_window == Some(id) { self.focused_window = self.workspaces.active().last_focused; } self.relayout(); } pub fn relayout(&mut self) { if self.session_lock.active { self.configure_lock_surfaces(); } // Consume idle time before setting new targets; it must not become a // large first animation step after a keypress on an idle desktop. self.advance_animations(Instant::now()); self.arrange_layers(); let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x); let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles); let outputs = self.space.outputs().cloned().collect::>(); let constraints = self.window_constraints(); let mut visible = HashSet::new(); let mut placements = Vec::new(); for output in outputs { let Some(output_id) = self.output_ids.get(&output).copied() else { continue; }; let Some(workspace_id) = self.output_workspaces.active_workspace(output_id) else { continue; }; let Some(bounds) = self.output_bounds_for(&output) else { continue; }; let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds); let Some(workspace) = self.workspaces.workspace(workspace_id) else { continue; }; let workspace_fullscreen = workspace.fullscreen; let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_)); let workspace_focus = workspace.last_focused; let focused = self .focused_window .filter(|window| { self.workspaces.workspace_for_window(*window) == Some(workspace_id) }) .or(workspace_focus); let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else { continue; }; let layout = match workspace.layout.geometry_with_constraints( bounds, self.gap_config, &constraints, focused, ) { Ok(layout) => layout, Err(error) => { tracing::error!(%error, ?workspace_id, "failed to compute tiled geometry"); continue; } }; let viewport_target = workspace.layout.viewport_x(); let viewport_motion = viewport_target.map(|target| { let viewport = self .viewport_animations .entry(workspace_id) .or_insert_with(|| AnimatedValue::new(target)); let target_changed = (viewport.target - target).abs() > 0.001; viewport.retarget_preserving_motion(target); if !self.animations_enabled { viewport.snap(); } (viewport.current, target_changed) }); let viewport_current = viewport_motion.map(|(current, _)| current); let viewport_target_changed = viewport_motion.is_some_and(|(_, changed)| changed); for warning in layout.warnings { tracing::warn!( window = ?warning.window, kind = ?warning.kind, requested = warning.requested, assigned = warning.assigned, "window size constraint could not be satisfied exactly" ); } for (window, id) in &self.window_ids { if self.workspaces.workspace_for_window(*id) != Some(workspace_id) { continue; } let is_maximized = self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id); let rect = if workspace_fullscreen == Some(*id) { fullscreen_bounds } else if is_maximized && !is_scrolling_layout { maximized_rect(bounds, self.gap_config.outer) } else { match self.workspaces.placement(*id) { Some(WindowPlacement::Tiled) => { let Some(rect) = layout.geometry.get(id).copied() else { continue; }; rect } Some(WindowPlacement::Floating { rect }) => rect, None => continue, } }; let is_fullscreen = workspace_fullscreen == Some(*id); let is_floating = matches!( self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. }) ); visible.insert(*id); let scrolling = if !is_fullscreen && !is_floating && (!is_maximized || is_scrolling_layout) { viewport_target .zip(viewport_current) .map(|(target, current)| (workspace_id, rect.x + target, current)) } else { None }; placements.push(( window.clone(), *id, rect, is_fullscreen, is_maximized, is_floating, scrolling, pending_column_width_cycles.contains(id) && viewport_target_changed, )); } } let hidden = self .window_ids .iter() .filter(|(_, id)| !visible.contains(id)) .map(|(window, _)| window.clone()) .collect::>(); let mut layout_changed = hidden .iter() .any(|window| self.space.element_location(window).is_some()); for window in hidden { self.space.unmap_elem(&window); } let now = self.start_time.elapsed(); let mut scrolling_world_x = HashMap::new(); placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id)); for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in placements { // A viewport-coupled width must never override fullscreen/floating geometry. if scrolling.is_none() { self.viewport_coupled_widths.remove(&id); } let committed_size = client_size(&window); let had_geometry = self.window_geometry.contains_key(&id); let geometry = self .window_geometry .entry(id) .or_insert_with(|| WindowGeometry::new(rect, committed_size)); let mode = if is_fullscreen { PresentationMode::Fullscreen } else if is_maximized { PresentationMode::Maximized } else { PresentationMode::Normal }; layout_changed |= !had_geometry || geometry.logical != rect; let mut requested_size = geometry.set_presentation_mode(rect, mode, now); if !self.animations_enabled { geometry.advance(Duration::ZERO, self.spring_config, false); requested_size = geometry.presentation_size_request(now).or(requested_size); } if geometry.is_zooming() { self.viewport_coupled_widths.remove(&id); } if let Some((workspace, world_x, viewport_x)) = scrolling { let restored_world_x = restored_scrolling_world_x( had_geometry, geometry.visual.current.x, viewport_x, world_x, ); let mut animated_world_x = previous_scrolling_world_x .remove(&id) .filter(|(previous_workspace, _)| *previous_workspace == workspace) .map(|(_, world_x)| world_x) .unwrap_or_else(|| AnimatedValue::new(restored_world_x)); animated_world_x.set_target(world_x); if !self.animations_enabled { animated_world_x.snap(); } if !geometry.is_zooming() { geometry.visual.current.x = animated_world_x.current - viewport_x; geometry.visual.velocity.x = animated_world_x.velocity; } scrolling_world_x.insert(id, (workspace, animated_world_x)); let coupled = !geometry.is_zooming() && (couple_width || self.viewport_coupled_widths.get(&id).is_some_and( |(previous_workspace, _)| *previous_workspace == workspace, )); if coupled { let width = self.viewport_coupled_widths.entry(id).or_insert_with(|| { (workspace, AnimatedValue::new(geometry.visual.current.width)) }); if width.0 != workspace { *width = (workspace, AnimatedValue::new(geometry.visual.current.width)); } width.1.retarget_preserving_motion(rect.width); if !self.animations_enabled { width.1.snap(); } geometry.visual.current.width = width.1.current; geometry.visual.velocity.width = width.1.velocity; } else if pending_column_width_cycles.contains(&id) { self.viewport_coupled_widths.remove(&id); } } let visual = geometry.visual.current; let natural_pending = self.natural_floating_pending.contains(&id) && is_floating && !is_fullscreen && !is_maximized; if natural_pending { requested_size = None; } let location = (visual.x.round() as i32, visual.y.round() as i32); self.space.map_element(window.clone(), location, false); if let Some(toplevel) = window.toplevel() { let state_changed = toplevel.with_pending_state(|state| { if natural_pending { state.size = None; } else if let Some(size) = requested_size { state.size = Some((size.width, size.height).into()); } let fullscreen_changed = if is_fullscreen { state.states.set(xdg_toplevel::State::Fullscreen) } else { state.states.unset(xdg_toplevel::State::Fullscreen) }; let maximized_changed = if is_maximized { state.states.set(xdg_toplevel::State::Maximized) } else { state.states.unset(xdg_toplevel::State::Maximized) }; let tiled = !is_floating && !is_fullscreen; let tiled_changed = if tiled { state.states.set(xdg_toplevel::State::TiledLeft) | state.states.set(xdg_toplevel::State::TiledRight) | state.states.set(xdg_toplevel::State::TiledTop) | state.states.set(xdg_toplevel::State::TiledBottom) } else { state.states.unset(xdg_toplevel::State::TiledLeft) | state.states.unset(xdg_toplevel::State::TiledRight) | state.states.unset(xdg_toplevel::State::TiledTop) | state.states.unset(xdg_toplevel::State::TiledBottom) }; let decoration_mode = if is_floating && !is_fullscreen && !is_maximized { DecorationMode::ClientSide } else { DecorationMode::ServerSide }; let decoration_changed = state.decoration_mode != Some(decoration_mode); state.decoration_mode = Some(decoration_mode); fullscreen_changed || maximized_changed || tiled_changed || decoration_changed }); if natural_pending || requested_size.is_some() || state_changed { if let Some(serial) = toplevel.send_pending_configure() && requested_size.is_some() && had_geometry && self.animations_enabled { self.resize_transactions.insert( id, crate::resize_transaction::ResizeTransaction::new(serial, now) .with_source_geometry(window.geometry()), ); } } } } self.scrolling_world_x = scrolling_world_x; if layout_changed { self.backdrop_generation = self.backdrop_generation.wrapping_add(1); } self.sync_window_stacking(); self.retarget_overview(); self.send_shell_snapshots(); crate::backends::direct::render_all(self); } pub fn advance_animations(&mut self, now: Instant) -> bool { let delta = crate::presentation::frame_delta(&mut self.last_animation_tick, now); self.advance_animations_by(delta) } pub fn record_drm_presentation( &mut self, node: DrmNode, crtc: crtc::Handle, time: DrmEventTime, sequence: u32, ) { let delta = self .direct_backend .as_mut() .and_then(|backend| backend.record_presentation(node, crtc, time, sequence)); if delta.is_some() { // All outputs animate the same scene. Summing each CRTC's frame // interval makes motion run faster with two monitors (and uneven // with mixed 60/120 Hz). Advance once by actual elapsed time. self.advance_animations(Instant::now()); } } fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool { let delta = delta.mul_f64(self.animation_speed); let visible_workspaces = self .space .outputs() .filter_map(|output| self.output_ids.get(output)) .filter_map(|output| self.output_workspaces.active_workspace(*output)) .collect::>(); let windows = self .space .elements() .filter_map(|window| { self.window_ids .get(window) .copied() .map(|id| (window.clone(), id)) }) .collect::>(); let mut active_animation = false; self.dismissing_popups.retain_mut(|(root, popup, motion)| { if !smithay::utils::IsAlive::alive(popup.wl_surface()) { return false; } let duration = if self.animations_enabled { 140.0 } else { 0.0 }; let active = motion.advance_visual(0.0, delta, duration); crate::effects::fade_dismissed_surface(popup.wl_surface(), motion.current); active_animation |= active; if !active { let _ = PopupManager::dismiss_popup(root, popup); } active }); let dim_settings = self.theme_settings.inactive_dim; let duration = if self.animations_enabled { dim_settings.duration_ms } else { 0.0 }; let mut dim_changed = false; // Include hidden workspace windows: overview can present them too. for id in self.window_ids.values().copied().collect::>() { let selected = if self.overview.is_active() { self.overview_selected(id) } else { self.focused_window == Some(id) }; let target = if selected { 1.0 } else { 0.0 }; let focus = self .window_focus .entry(id) .or_insert_with(|| crate::dimming::DimAnimation::new(target)); let previous = focus.current; active_animation |= focus.advance_visual(target, delta, duration); dim_changed |= previous != focus.current; } for (_, id) in &windows { let target = crate::dimming::target( dim_settings, self.focused_window, *id, self.overview.is_presenting(), ); let dim = self .window_dimming .entry(*id) .or_insert_with(|| crate::dimming::DimAnimation::new(target)); let previous = dim.current; active_animation |= dim.advance_visual(target, delta, duration); dim_changed |= previous != dim.current; } let mut ready_to_close = Vec::new(); for (id, animation) in &mut self.closing_windows { if animation.advance(delta) { ready_to_close.push(*id); } else if !animation.close_sent { active_animation = true; } } for id in ready_to_close { self.send_window_close(id); } let now = self.start_time.elapsed(); self.resize_transactions.retain(|id, transaction| { if transaction.expired(now) { tracing::warn!(?id, "resize presentation deadline reached"); false } else { true } }); let blocked_workspaces = self .resize_transactions .keys() .filter_map(|id| self.workspaces.workspace_for_window(*id)) .collect::>(); let animations_enabled = self.animations_enabled; let animation_speed = self.animation_speed; self.resize_snapshots.retain(|id, snapshot| { if !animations_enabled { return false; } let waiting_for_client = self .workspaces .workspace_for_window(*id) .is_some_and(|workspace| blocked_workspaces.contains(&workspace)); // A shrinking client's destination buffer arrives before the // animated bounds reach it. Keep the old native pixels covering // that strip rather than fading them into the neutral resize fill. let uncovered = self.window_geometry.get(id).is_some_and(|geometry| { geometry.client.committed_size.is_some_and(|size| { crate::presentation::resize_needs_old_frame( geometry.visual.current, geometry.logical, size.width, size.height, ) }) }); let blocked = waiting_for_client || uncovered; let active = crate::presentation::advance_handoff( &mut snapshot.elapsed, &mut snapshot.last_tick, now, blocked, animation_speed, ); if !blocked { snapshot.commit.increment(); } if !active { tracing::debug!( ?id, bytes = snapshot.bytes(), "released resize handoff snapshot" ); } active }); active_animation |= !self.resize_snapshots.is_empty(); // Keep scheduling frames while waiting, so the deadline cannot stall. active_animation |= !blocked_workspaces.is_empty(); for (workspace, viewport) in &mut self.viewport_animations { if !visible_workspaces.contains(workspace) { continue; } if blocked_workspaces.contains(workspace) { continue; } if self.animations_enabled { active_animation |= viewport.advance(delta, self.viewport_spring_config); } else { viewport.snap(); } } let mut settled_coupled_widths = Vec::new(); for (window, id) in windows { if self .workspaces .workspace_for_window(id) .is_some_and(|workspace| blocked_workspaces.contains(&workspace)) { continue; } let Some(geometry) = self.window_geometry.get_mut(&id) else { continue; }; let zooming = geometry.is_zooming(); let coupled_target = self .viewport_coupled_widths .get(&id) .map(|(_, width)| width.target); if coupled_target.is_some() { geometry.visual.target.width = geometry.visual.current.width; geometry.visual.velocity.width = 0.0; } active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled); if let Some(target) = coupled_target { geometry.visual.target.width = target; } if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id) && let Some(viewport) = self.viewport_animations.get(workspace) { if zooming { world_x.current = geometry.visual.current.x + viewport.current; world_x.velocity = geometry.visual.velocity.x + viewport.velocity; } else if self.animations_enabled { active_animation |= world_x.advance(delta, self.spring_config); } else { world_x.snap(); } if !zooming { geometry.visual.current.x = world_x.current - viewport.current; geometry.visual.velocity.x = world_x.velocity - viewport.velocity; } } if let Some((_, width)) = self.viewport_coupled_widths.get_mut(&id) { let width_active = if self.animations_enabled { width.advance(delta, self.viewport_spring_config) } else { width.snap(); false }; active_animation |= width_active; geometry.visual.current.width = width.current; geometry.visual.velocity.width = width.velocity; if !width_active { settled_coupled_widths.push(id); } } if let Some(size) = geometry.client.expire_wait(self.start_time.elapsed()) { tracing::warn!( ?id, configured_width = size.width, configured_height = size.height, committed = ?geometry.client.committed_size, "client did not commit the final configured size within 500 ms" ); } if let Some(size) = geometry.presentation_size_request(self.start_time.elapsed()) && !self.natural_floating_pending.contains(&id) && let Some(toplevel) = window.toplevel() { toplevel.with_pending_state(|state| { state.size = Some((size.width, size.height).into()); }); toplevel.send_pending_configure(); } let visual = geometry.visual.current; self.space.map_element( window, (visual.x.round() as i32, visual.y.round() as i32), false, ); } for id in settled_coupled_widths { self.viewport_coupled_widths.remove(&id); } active_animation |= self.overview .advance(delta, self.spring_config, self.animations_enabled); self.sync_window_stacking(); if active_animation || dim_changed { self.backdrop_generation = self.backdrop_generation.wrapping_add(1); } active_animation } pub(crate) fn animations_enabled(&self) -> bool { self.animations_enabled } pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) { let Some((window, id)) = self .window_ids .iter() .find(|(window, _)| { window .toplevel() .is_some_and(|toplevel| toplevel.wl_surface() == surface) }) .map(|(window, id)| (window.clone(), *id)) else { return; }; let Some(transaction) = self.resize_transactions.get(&id) else { return; }; let serial = with_states(surface, |states| { states .data_map .get::() .and_then(|data| data.lock().ok().and_then(|data| data.current_serial)) }); if !transaction.accepts(serial) { return; } let Some(output) = self .space .outputs() .find(|output| self.window_belongs_to_output(id, output)) .cloned() else { return; }; let source_geometry = transaction .source_geometry() .unwrap_or_else(|| window.geometry()); let scale = output.current_scale().fractional_scale(); let source_size = source_geometry.size.to_physical_precise_round(scale); if self.resize_snapshots.get(&id).is_some_and(|snapshot| { crate::presentation::snapshot_covers_source( smithay::backend::renderer::Texture::size(&snapshot.texture), source_size, snapshot.scale, scale, ) }) { return; } let used: usize = self .resize_snapshots .values() .map(|snapshot| snapshot.bytes()) .sum(); let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used); let result = if let Some(backend) = &self.nested_backend { // Commit dispatch does not run inside the Winit event callback; // still never risk reentrant renderer access or a compositor panic. match backend.try_borrow_mut() { Ok(mut backend) => crate::winit::capture_resize_snapshot( backend.renderer(), &window, source_geometry, output.current_scale().fractional_scale(), remaining, ), Err(_) => return, } } else if let Some(backend) = &mut self.direct_backend { backend.capture_resize_snapshot(&window, source_geometry, &output, remaining) } else { return; }; match result { Ok(Some(snapshot)) => { tracing::debug!( ?id, bytes = snapshot.bytes(), "captured native resize handoff" ); self.resize_snapshots.insert(id, snapshot); } Ok(None) => {} Err(error) => { tracing::debug!(%error, ?id, "resize snapshot unavailable; using native crop") } } } pub fn record_client_commit(&mut self, window: &Window) { let Some(&id) = self.window_ids.get(window) else { return; }; // current_serial is promoted by Smithay on commit, unlike // configure_serial, which only records an acknowledgement. let committed_serial = window.toplevel().and_then(|toplevel| { with_states(toplevel.wl_surface(), |states| { states .data_map .get::() .and_then(|data| data.lock().ok().and_then(|data| data.current_serial)) }) }); if self .resize_transactions .get(&id) .is_some_and(|transaction| transaction.accepts(committed_serial)) { self.resize_transactions.remove(&id); } let Some(size) = client_size(window) else { return; }; if self.natural_floating_pending.remove(&id) && matches!( self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }) ) && let Some(bounds) = self.floating_bounds_for_window(id) { let rect = natural_floating_rect(bounds, size); if self.workspaces.set_floating_rect(id, rect).is_ok() { // This is the first mapped client buffer, not a user resize. // Do not animate from the temporary placement box. self.window_geometry .insert(id, WindowGeometry::new(rect, Some(size))); self.resize_transactions.remove(&id); self.relayout(); } } let Some(geometry) = self.window_geometry.get_mut(&id) else { return; }; let matches_target = geometry.client.commit(size); tracing::debug!( ?id, ?size, matches_target, "recorded client geometry commit" ); } pub fn focus_direction(&mut self, direction: Direction) { if self.focus_overview_direction(direction) { return; } let Some(current) = self.focused_window else { return; }; let Some(bounds) = self.output_bounds() else { return; }; let Ok(Some(next)) = self .workspaces .active() .layout .directional_neighbor(current, direction, bounds) else { return; }; if let Err(error) = self.workspaces.focus_window(next) { tracing::error!(%error, ?next, "failed to update workspace focus"); return; } let Some(window) = self .window_ids .iter() .find_map(|(window, id)| (*id == next).then(|| window.clone())) else { return; }; let Some(surface) = window .toplevel() .map(|toplevel| toplevel.wl_surface().clone()) else { return; }; self.focused_window = Some(next); self.raise_window(&window, true); self.seat .get_keyboard() .expect("seat has a keyboard") .set_focus( self, Some(surface), smithay::utils::SERIAL_COUNTER.next_serial(), ); for window in self.space.elements() { if let Some(toplevel) = window.toplevel() { toplevel.send_pending_configure(); } } self.relayout(); } pub fn move_direction(&mut self, direction: Direction) { let Some(current) = self.focused_window else { return; }; let Some(bounds) = self.output_bounds() else { return; }; match self .workspaces .active_mut() .layout .move_window(current, direction, bounds) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => tracing::error!(%error, ?current, "failed to move tiled window"), } } pub fn resize_direction(&mut self, direction: Direction) { const RESIZE_STEP: f64 = 0.05; let Some(current) = self.focused_window else { return; }; match self .workspaces .active_mut() .layout .resize_window(current, direction, RESIZE_STEP) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => tracing::error!(%error, ?current, "failed to resize tiled window"), } } pub fn toggle_layout_mode(&mut self) { let Some(bounds) = self.output_bounds() else { return; }; let mode = match self.workspaces.active().layout.mode() { LayoutMode::Scrolling => LayoutMode::Tree, LayoutMode::Tree => LayoutMode::Scrolling, }; match self.workspaces.set_active_layout_mode(mode, bounds) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => tracing::error!(%error, ?mode, "failed to change layout mode"), } } pub fn consume_focused_window(&mut self) { let Some(window) = self.focused_window else { return; }; let Some(bounds) = self.output_bounds() else { return; }; if self.workspaces.active().layout.mode() != LayoutMode::Scrolling { return; } let target = self .workspaces .active() .layout .directional_neighbor(window, Direction::Left, bounds) .ok() .flatten() .or_else(|| { self.workspaces .active() .layout .directional_neighbor(window, Direction::Right, bounds) .ok() .flatten() }); let Some(target) = target else { return; }; if let Err(error) = self.workspaces.stack_window(window, target) { tracing::error!(%error, ?window, ?target, "failed to consume window into column"); return; } self.relayout(); } pub fn expel_focused_window(&mut self) { let Some(window) = self.focused_window else { return; }; match self.workspaces.extract_window(window) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => tracing::error!(%error, ?window, "failed to expel window from column"), } } pub fn cycle_focused_column_width(&mut self) { let Some(window) = self.focused_window else { return; }; match self .workspaces .cycle_column_width(window, &self.column_width_presets) { Ok(true) => { self.pending_column_width_cycles.insert(window); self.relayout(); } Ok(false) => {} Err(error) => tracing::error!(%error, ?window, "failed to cycle column width"), } } pub fn center_focused_column(&mut self) { let Some(window) = self.focused_window else { return; }; let Some(bounds) = self.output_bounds() else { return; }; let constraints = self.window_constraints(); match self .workspaces .center_window(window, bounds, self.gap_config, &constraints) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => tracing::error!(%error, ?window, "failed to center column"), } } pub fn toggle_focused_floating(&mut self) { let Some(window) = self.focused_window else { return; }; let Some(bounds) = self.output_bounds() else { return; }; let floating_rect = match self.workspaces.placement(window) { Some(WindowPlacement::Tiled) => self .tiled_layout(bounds) .ok() .and_then(|layout| layout.geometry.get(&window).copied()) .unwrap_or_else(|| centered_floating_rect(bounds)), Some(WindowPlacement::Floating { rect }) => rect, None => return, }; let axis = self .workspaces .active() .layout .automatic_axis(Some(window), bounds) .unwrap_or(Axis::Horizontal); if let Err(error) = self .workspaces .toggle_floating(window, floating_rect, axis, 0.5) { tracing::error!(%error, ?window, "failed to toggle floating window"); return; } self.relayout(); } pub fn toggle_focused_fullscreen(&mut self) { let Some(window) = self.focused_window else { return; }; if let Err(error) = self.workspaces.toggle_fullscreen(window) { tracing::error!(%error, ?window, "failed to toggle fullscreen window"); return; } self.relayout(); } pub fn toggle_focused_maximized(&mut self) { let Some(window) = self.focused_window else { return; }; // Super+F from true fullscreen enters decorated maximization. let fullscreen = self .workspaces .workspace_for_window(window) .and_then(|workspace| self.workspaces.workspace(workspace)) .is_some_and(|workspace| workspace.fullscreen == Some(window)); let enabled = fullscreen || !self.maximized_windows.contains(&window); self.set_window_maximized(window, enabled); } pub fn set_window_maximized(&mut self, window: WindowId, enabled: bool) { if self.workspaces.workspace_for_window(window).is_none() { return; } if enabled { let _ = self.workspaces.set_fullscreen(window, false); self.maximized_windows.insert(window); } else { self.maximized_windows.remove(&window); } if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) { let workspace_id = self.workspaces.workspace_for_window(window).unwrap(); if let Some(workspace) = self.workspaces.workspace_mut(workspace_id) { if let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout { if enabled { if let Some(column) = layout .columns() .iter() .find(|column| column.windows.contains(&window)) { self.maximized_column_widths .entry(window) .or_insert(column.width); } let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0)); } else if let Some(width) = self.maximized_column_widths.remove(&window) { let _ = layout.set_column_width(window, width); } } } } self.relayout(); } pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool { !self.overview.is_presenting() && self .output_id(output) .and_then(|id| self.output_workspaces.active_workspace(id)) .and_then(|workspace| self.workspaces.workspace(workspace)) .is_some_and(|workspace| workspace.fullscreen.is_some()) } pub fn close_focused_window(&mut self) { let Some(focused) = self.focused_window else { return; }; if !self.animations_enabled { self.send_window_close(focused); return; } self.closing_windows.entry(focused).or_default(); crate::backends::direct::render_all(self); } pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool { if !self.window_ids.values().any(|window_id| *window_id == id) { return false; } if self.animations_enabled { self.closing_windows.entry(id).or_default(); crate::backends::direct::render_all(self); } else { self.send_window_close(id); } true } pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool { let Some(window) = self .window_ids .iter() .find_map(|(window, window_id)| (*window_id == id).then(|| window.clone())) else { return false; }; let Some(workspace) = self.workspaces.workspace_for_window(id) else { return false; }; if let Some(output) = self.output_workspaces.output_for_workspace(workspace) { self.activate_output_workspace(output, workspace); } else if let Some(output) = self.output_workspaces.focused_output() { if self .output_workspaces .assign_workspace(output, workspace) .is_err() { return false; } self.activate_output_workspace(output, workspace); } else if self.workspaces.activate(workspace).is_err() { return false; } if self.workspaces.focus_window(id).is_err() { return false; } self.focused_window = Some(id); self.raise_window(&window, true); self.relayout(); self.restore_keyboard_focus(); true } pub(crate) fn activate_managed_workspace(&mut self, workspace: WorkspaceId) -> bool { if self.workspaces.workspace(workspace).is_none() { return false; } let output = self .output_workspaces .output_for_workspace(workspace) .or_else(|| self.output_workspaces.focused_output()); let Some(output) = output else { return false; }; if self .output_workspaces .output_for_workspace(workspace) .is_none() && self .output_workspaces .assign_workspace(output, workspace) .is_err() { return false; } self.activate_output_workspace(output, workspace); self.relayout(); self.restore_keyboard_focus(); true } pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) { let Some(animation) = self.closing_windows.get(&id) else { return (1.0, 1.0); }; let eased = smoothstep(animation.progress); (1.0 - eased * 0.02, (1.0 - eased) as f32) } fn send_window_close(&self, id: WindowId) { let Some(toplevel) = self.window_ids.iter().find_map(|(window, window_id)| { (*window_id == id).then(|| window.toplevel()).flatten() }) else { return; }; toplevel.send_close(); } pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) { match self.workspaces.set_fullscreen(window, enabled) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => { tracing::error!(%error, ?window, enabled, "failed to set fullscreen window") } } } pub fn is_floating_window(&self, window: &Window) -> bool { self.window_ids .get(window) .and_then(|id| self.workspaces.placement(*id)) .is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. })) } pub fn set_floating_window_geometry( &mut self, window: &Window, location: Point, size: Size, ) { let Some(id) = self.window_ids.get(window).copied() else { return; }; let rect = Rect::new( location.x as f64, location.y as f64, size.w.max(1) as f64, size.h.max(1) as f64, ); if let Err(error) = self.workspaces.set_floating_rect(id, rect) { tracing::error!(%error, ?id, "failed to update floating window geometry"); return; } // Direct manipulation follows the pointer, including while the client is // still drawing its next buffer. Rendering and hit testing share this rect. self.resize_transactions.remove(&id); self.resize_snapshots.remove(&id); let geometry = self .window_geometry .entry(id) .or_insert_with(|| WindowGeometry::new(rect, client_size(window))); if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed()) && let Some(toplevel) = window.toplevel() { toplevel.with_pending_state(|state| { state.size = Some((size.width, size.height).into()); }); toplevel.send_pending_configure(); } self.scrolling_world_x.remove(&id); self.viewport_coupled_widths.remove(&id); self.space.map_element(window.clone(), location, false); self.sync_window_stacking(); crate::backends::direct::render_all(self); } pub fn switch_workspace(&mut self, index: u32) { let workspace = match self.workspaces.ensure_numeric(index) { Ok(workspace) => workspace, Err(error) => { tracing::error!(%error, index, "failed to switch workspace"); return; } }; let Some(output) = self.output_workspaces.focused_output() else { return; }; let owner = match self.output_workspaces.switch_workspace(output, workspace) { Ok(ferese_core::WorkspaceSwitch::Activated(output)) | Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output, Err(error) => { tracing::error!(%error, index, "failed to assign workspace to output"); return; } }; self.activate_output_workspace(owner, workspace); self.relayout(); self.restore_keyboard_focus(); } pub fn move_focused_to_workspace(&mut self, index: u32) { let Some(window) = self.focused_window else { return; }; let axis = self .output_bounds() .and_then(|bounds| { let workspace = self.workspaces.ensure_numeric(index).ok()?; let target = self.workspaces.workspace(workspace)?; target .layout .automatic_axis(target.last_focused, bounds) .ok() }) .unwrap_or(Axis::Horizontal); let destination = match self .workspaces .move_window_to_numeric(window, index, axis, 0.5) { Ok(destination) => destination, Err(error) => { tracing::error!(%error, ?window, index, "failed to move window to workspace"); return; } }; if let Some(output) = self.output_workspaces.focused_output() && let Err(error) = self.output_workspaces.assign_workspace(output, destination) { tracing::error!(%error, ?destination, "failed to assign destination workspace"); } self.focused_window = self.workspaces.active().last_focused; self.relayout(); self.restore_keyboard_focus(); } pub(crate) fn restore_keyboard_focus(&mut self) { if self.session_lock.active { self.focus_lock_surface(); return; } let surface = self.focused_window.and_then(|focused| { self.window_ids.iter().find_map(|(window, id)| { (*id == focused) .then(|| window.toplevel()) .flatten() .map(|toplevel| toplevel.wl_surface().clone()) }) }); self.seat .get_keyboard() .expect("seat has a keyboard") .set_focus(self, surface, smithay::utils::SERIAL_COUNTER.next_serial()); } fn output_bounds(&self) -> Option { let output = self .focused_output() .or_else(|| self.space.outputs().next())?; self.output_bounds_for(output) } fn floating_bounds_for_window(&self, window: WindowId) -> Option { let output_id = self .workspaces .workspace_for_window(window) .and_then(|workspace| self.output_workspaces.output_for_workspace(workspace)); self.output_ids .iter() .find(|(_, id)| Some(**id) == output_id) .and_then(|(output, _)| self.output_bounds_for(output)) .or_else(|| self.output_bounds()) } pub(crate) fn output_bounds_for(&self, output: &Output) -> Option { let geometry = self.space.output_geometry(output)?; let zone = layer_map_for_output(output).non_exclusive_zone(); Some(Rect::new( (geometry.loc.x + zone.loc.x) as f64, (geometry.loc.y + zone.loc.y) as f64, zone.size.w.max(0) as f64, zone.size.h.max(0) as f64, )) } fn full_output_bounds_for(&self, output: &Output) -> Option { let geometry = self.space.output_geometry(output)?; Some(Rect::new( geometry.loc.x as f64, geometry.loc.y as f64, geometry.size.w as f64, geometry.size.h as f64, )) } fn arrange_layers(&self) { let outputs = self.space.outputs().cloned().collect::>(); for output in outputs { layer_map_for_output(&output).arrange(); } } fn logical_window_rect(&mut self, window: WindowId, bounds: Rect) -> Option { let workspace = self.workspaces.workspace_for_window(window)?; let fullscreen = self.workspaces.workspace(workspace)?.fullscreen; if fullscreen == Some(window) { return Some(bounds); } let constraints = self.window_constraints(); let focused = self.focused_window; match self.workspaces.placement(window)? { WindowPlacement::Tiled => self .workspaces .workspace_mut(workspace)? .layout .geometry_with_constraints(bounds, self.gap_config, &constraints, focused) .ok()? .geometry .get(&window) .copied(), WindowPlacement::Floating { rect } => Some(rect), } } fn tiled_layout(&mut self, bounds: Rect) -> Result { let constraints = self.window_constraints(); let focused = self.focused_window; self.workspaces .active_mut() .layout .geometry_with_constraints(bounds, self.gap_config, &constraints, focused) } fn window_constraints(&self) -> HashMap { self.window_ids .iter() .filter_map(|(window, id)| { let toplevel = window.toplevel()?; let (minimum, maximum) = with_states(toplevel.wl_surface(), |states| { let mut cached = states.cached_state.get::(); let state = cached.current(); (state.min_size, state.max_size) }); Some(( *id, SizeConstraints { min_width: minimum.w.max(1) as f64, min_height: minimum.h.max(1) as f64, max_width: (maximum.w > 0).then_some(maximum.w as f64), max_height: (maximum.h > 0).then_some(maximum.h as f64), }, )) }) .collect() } } fn maximized_rect(bounds: Rect, gap: f64) -> Rect { let gap = gap .max(0.0) .min(((bounds.width.min(bounds.height) - 1.0) / 2.0).max(0.0)); Rect::new( bounds.x + gap, bounds.y + gap, (bounds.width - 2.0 * gap).max(1.0), (bounds.height - 2.0 * gap).max(1.0), ) } fn centered_floating_rect(bounds: Rect) -> Rect { let width = (bounds.width * 0.6).max(1.0); let height = (bounds.height * 0.6).max(1.0); Rect::new( bounds.x + (bounds.width - width) / 2.0, bounds.y + (bounds.height - height) / 2.0, width, height, ) } fn natural_floating_rect(bounds: Rect, size: ClientSize) -> Rect { let width = f64::from(size.width).clamp(1.0, bounds.width.max(1.0)); let height = f64::from(size.height).clamp(1.0, bounds.height.max(1.0)); Rect::new( bounds.x + (bounds.width - width) / 2.0, bounds.y + (bounds.height - height) / 2.0, width, height, ) } fn smoothstep(progress: f64) -> f64 { let progress = progress.clamp(0.0, 1.0); progress * progress * (3.0 - 2.0 * progress) } fn restored_scrolling_world_x( had_geometry: bool, visual_x: f64, viewport_x: f64, target_world_x: f64, ) -> f64 { if had_geometry { visual_x + viewport_x } else { target_world_x } } fn centered_transient_rect(parent: Rect) -> Rect { let width = (parent.width * 0.75).clamp(1.0, 640.0); let height = (parent.height * 0.75).clamp(1.0, 480.0); Rect::new( parent.x + (parent.width - width) / 2.0, parent.y + (parent.height - height) / 2.0, width, height, ) } fn client_size(window: &Window) -> Option { let size = window.geometry().size; (size.w > 0 && size.h > 0).then_some(ClientSize { width: size.w, height: size.h, }) } #[derive(Default)] pub struct ClientState { pub compositor_state: CompositorClientState, pub(crate) capabilities: crate::private_client::ClientCapabilities, } impl ClientData for ClientState { fn initialized(&self, _client_id: ClientId) {} fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} } #[cfg(test)] mod tests { use super::*; #[test] fn floating_dialog_keeps_its_client_size_not_workspace_proportion() { assert_eq!( natural_floating_rect( Rect::new(100.0, 48.0, 1600.0, 900.0), ClientSize { width: 400, height: 200 } ), Rect::new(700.0, 398.0, 400.0, 200.0) ); } #[test] fn maximization_respects_layer_exclusion_and_outer_gaps() { assert_eq!( maximized_rect(Rect::new(0.0, 48.0, 1280.0, 752.0), 10.0), Rect::new(10.0, 58.0, 1260.0, 732.0) ); assert_eq!( maximized_rect(Rect::new(1600.0, 36.0, 1280.0, 764.0), 6.0), Rect::new(1606.0, 42.0, 1268.0, 752.0) ); let tiny = maximized_rect(Rect::new(0.0, 0.0, 5.0, 3.0), 10.0); assert!(tiny.width >= 1.0 && tiny.height >= 1.0); } #[test] fn transient_geometry_is_centered_and_bounded_by_its_parent() { let parent = Rect::new(100.0, 50.0, 1_000.0, 800.0); let transient = centered_transient_rect(parent); assert_eq!(transient, Rect::new(280.0, 210.0, 640.0, 480.0)); } #[test] fn close_easing_is_bounded_and_symmetric() { assert_eq!(smoothstep(-1.0), 0.0); assert_eq!(smoothstep(0.5), 0.5); assert_eq!(smoothstep(2.0), 1.0); } #[test] fn close_animation_delays_the_protocol_close_until_it_finishes() { let mut animation = ClosingAnimation::default(); assert!(!animation.advance(Duration::from_millis(70))); assert_eq!(animation.progress, 0.5); assert!(animation.advance(Duration::from_millis(70))); assert!(animation.close_sent); assert!(!animation.advance(Duration::from_secs(1))); } #[test] fn coupled_right_column_width_keeps_its_right_edge_stable() { let spring = SpringConfig { mass: 1.0, stiffness: 320.0, damping: 2.0 * 320.0_f64.sqrt(), position_tolerance: 0.1, velocity_tolerance: 0.1, }; let mut width = AnimatedValue::new(980.0); let mut viewport = AnimatedValue::new(495.0); width.retarget_preserving_motion(485.0); viewport.retarget_preserving_motion(0.0); for _ in 0..120 { width.advance(Duration::from_secs_f64(1.0 / 120.0), spring); viewport.advance(Duration::from_secs_f64(1.0 / 120.0), spring); assert!((width.current - viewport.current - 485.0).abs() < 0.001); } } #[test] fn fullscreen_exit_restores_world_x_from_presented_position() { assert_eq!(restored_scrolling_world_x(true, 0.0, 0.0, 505.0), 0.0); assert_eq!(restored_scrolling_world_x(true, 25.0, 480.0, 505.0), 505.0); assert_eq!( restored_scrolling_world_x(false, 10.0, 495.0, 1_000.0), 1_000.0 ); } }