use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::error::Error; use std::ffi::OsString; use std::sync::Arc; use std::time::{Duration, Instant}; use calloop::LoopHandle; 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 ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::drm::{DrmEventTime, DrmNode}; use smithay::backend::input::Keycode; use smithay::backend::renderer::{ErasedContextId, ImportDma}; use smithay::desktop::space::SpaceElement; use smithay::desktop::utils::send_frames_surface_tree; use smithay::desktop::{LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output}; use smithay::input::keyboard::XkbConfig; use smithay::input::pointer::{CursorIcon, CursorImageStatus}; use smithay::input::{Seat, SeatState}; use smithay::output::Output; use smithay::reexports::calloop::generic::Generic; use smithay::reexports::calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction}; use smithay::reexports::drm::control::crtc; use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason, ObjectId}; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Display, DisplayHandle, Weak}; use smithay::utils::{Logical, Point, Rectangle, Size}; use smithay::wayland::alpha_modifier::AlphaModifierState; use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_states}; use smithay::wayland::cursor_shape::CursorShapeManagerState; use smithay::wayland::dmabuf::{DmabufState, ImportNotifier}; use smithay::wayland::fractional_scale::FractionalScaleManagerState; use smithay::wayland::idle_inhibit::IdleInhibitManagerState; use smithay::wayland::idle_notify::IdleNotifierState; use smithay::wayland::input_method::InputMethodManagerState; use smithay::wayland::keyboard_shortcuts_inhibit::{KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor}; use smithay::wayland::output::OutputManagerState; use smithay::wayland::pointer_constraints::PointerConstraintsState; use smithay::wayland::presentation::PresentationState; use smithay::wayland::relative_pointer::RelativePointerManagerState; use smithay::wayland::selection::data_device::DataDeviceState; use smithay::wayland::selection::primary_selection::PrimarySelectionState; use smithay::wayland::session_lock::SessionLockManagerState; use smithay::wayland::shell::wlr_layer::{Layer, WlrLayerShellState}; use smithay::wayland::shell::xdg::decoration::XdgDecorationState; use smithay::wayland::shell::xdg::{SurfaceCachedState, XdgShellState, XdgToplevelSurfaceData}; use smithay::wayland::shm::ShmState; use smithay::wayland::single_pixel_buffer::SinglePixelBufferState; use smithay::wayland::socket::ListeningSocketSource; use smithay::wayland::text_input::TextInputManagerState; use smithay::wayland::viewporter::ViewporterState; use smithay::wayland::xdg_activation::XdgActivationState; use smithay::wayland::xdg_foreign::XdgForeignState; use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager; use crate::backends::direct::DirectBackendState; use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings}; use crate::cursor::{NamedCursor, cursor_theme, load_named_cursor}; use crate::dimming::DimAnimation; use crate::gestures::{Swipe, SwipeDirection}; use crate::handlers::screencopy::PendingScreencopy; use crate::handlers::screenshot::{Coordinator, PartSender}; use crate::handlers::screenshot_worker::Worker; use crate::input::LockedPointerHint; use crate::input_capture::InputCapture; use crate::ipc::IpcSocketGuard; use crate::overview::OverviewState; use crate::portal_session::PortalSession; use crate::portal_shortcuts::PortalShortcuts; use crate::resize_transaction::ResizeTransaction; use crate::session_lock::{IdleSettings, Lock}; use crate::shell_control::ShellSnapshot; use crate::stacking::WindowStack; use crate::wallpaper::{WallpaperConfig, WallpaperState}; use crate::window_rules::{WindowRule, resolve as resolve_window_rules}; use crate::winit::{ BlurProgram, MaterialBuffers, MaterialProgram, NestedBackend, OverviewScrim, ResizeSnapshot, RoundedClipPrograms, WindowBorderBuffers, WindowShadowBuffers, }; const CLOSE_ANIMATION_DURATION: Duration = Duration::from_millis(140); const WORKSPACE_SLIDE_DURATION: Duration = Duration::from_millis(220); const WORKSPACE_SLIDE_FIRST_FRAME: Duration = Duration::from_millis(8); #[derive(Clone, Copy, Debug, Default, PartialEq)] struct SlideOffset { x: f64, y: f64, } impl SlideOffset { fn for_swipe(direction: SwipeDirection) -> Self { match direction { SwipeDirection::Left => Self { x: -1.0, y: 0.0 }, SwipeDirection::Right => Self { x: 1.0, y: 0.0 }, SwipeDirection::Up => Self { x: 0.0, y: -1.0 }, SwipeDirection::Down => Self { x: 0.0, y: 1.0 }, } } fn opposite(self) -> Self { Self { x: -self.x, y: -self.y } } fn between(self, target: Self, progress: f64) -> Self { Self { x: self.x + (target.x - self.x) * progress, y: self.y + (target.y - self.y) * progress, } } } struct WorkspaceSlideItem { workspace: WorkspaceId, start: SlideOffset, target: SlideOffset, } struct WorkspaceSlide { items: Vec, elapsed: Duration, } impl WorkspaceSlide { fn new(previous: Option, from: WorkspaceId, to: WorkspaceId, direction: SwipeDirection) -> Self { let movement = SlideOffset::for_swipe(direction); let mut items = previous .map(|slide| { let progress = slide.progress(); slide .items .into_iter() .map(|mut item| { item.start = item.start.between(item.target, progress); item }) .collect::>() }) .unwrap_or_default(); if let Some(item) = items.iter_mut().find(|item| item.workspace == from) { item.target = movement; } else { items.push(WorkspaceSlideItem { workspace: from, start: SlideOffset::default(), target: movement, }); } if let Some(item) = items.iter_mut().find(|item| item.workspace == to) { item.target = SlideOffset::default(); } else { items.push(WorkspaceSlideItem { workspace: to, start: movement.opposite(), target: SlideOffset::default(), }); } Self { items, elapsed: WORKSPACE_SLIDE_FIRST_FRAME, } } fn progress(&self) -> f64 { smoothstep((self.elapsed.as_secs_f64() / WORKSPACE_SLIDE_DURATION.as_secs_f64()).min(1.0)) } fn advance(&mut self, delta: Duration) -> bool { self.elapsed = (self.elapsed + delta).min(WORKSPACE_SLIDE_DURATION); self.elapsed < WORKSPACE_SLIDE_DURATION } fn contains(&self, workspace: WorkspaceId) -> bool { self.items.iter().any(|item| item.workspace == workspace) } fn offset(&self, workspace: WorkspaceId, width: f64, height: f64) -> (f64, f64) { let progress = self.progress(); let position = self .items .iter() .find(|item| item.workspace == workspace) .map(|item| item.start.between(item.target, progress)) .unwrap_or_default(); (position.x * width, position.y * height) } } #[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: SessionLockManagerState, pub(crate) session_lock: Lock, pub(crate) lock_idle: IdleSettings, pub(crate) config_source: Option, pub(crate) theme_engine: crate::theme::Engine, pub start_time: Instant, pub socket_name: OsString, pub display_handle: DisplayHandle, pub loop_signal: LoopSignal, pub(crate) loop_handle: LoopHandle<'static, Self>, pub(crate) input_capture: InputCapture, pub(crate) portal_session: PortalSession, 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: WallpaperState, maximized_windows: HashSet, maximized_column_widths: HashMap, window_stack: WindowStack, floating_above_fullscreen: HashMap, 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) portal_shortcuts: PortalShortcuts, pub(crate) pending_logout: Option, pub(crate) logout_query: Option, pub(crate) logout_owner: Option, pub(crate) blur_programs: HashMap, pub(crate) backdrop_generation: u64, closing_windows: HashMap, viewport_animations: HashMap, workspace_slides: 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(crate) autostart: 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) locked_pointer_hint: Option, pub(crate) last_pointer_time: u32, pub(crate) cursor_theme: xcursor::CursorTheme, pub(crate) named_cursors: HashMap, pub intercepted_keys: HashSet, pub(crate) swipe: Swipe, 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, // Screenshot requests outlive the readback that filled them: the // coordinator owns them until encoding finishes and the caller is // answered, or the request is terminated. pub(crate) screenshot: Coordinator, pub(crate) screenshot_parts: Option, pub(crate) screenshot_worker: Option, pub(crate) shell_resources: Vec>, pub(crate) shell_snapshot_serial: u32, pub(crate) last_shell_snapshot: Option, 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, PopupKind, 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) lock_idle: IdleSettings, pub(crate) autostart: Vec, pub(crate) overview_font_family: String, pub(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 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::(&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 = cursor_theme(); let default_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(), lock_idle: config.lock_idle, config_source: None, theme_engine: Default::default(), start_time, socket_name, display_handle, loop_signal: event_loop.get_signal(), loop_handle: event_loop.handle(), portal_session: Default::default(), input_capture: Default::default(), 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: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), maximized_windows: HashSet::new(), maximized_column_widths: HashMap::new(), window_stack: WindowStack::default(), floating_above_fullscreen: HashMap::new(), 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(), portal_shortcuts: PortalShortcuts::default(), pending_logout: None, logout_query: None, logout_owner: None, blur_programs: HashMap::new(), backdrop_generation: 0, closing_windows: HashMap::new(), viewport_animations: HashMap::new(), workspace_slides: 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, autostart: config.autostart, cursor_status: CursorImageStatus::default_named(), cursor_redraw_pending: false, locked_pointer_hint: None, last_pointer_time: 0, cursor_theme, named_cursors, intercepted_keys: HashSet::new(), swipe: Swipe::default(), idle_inhibitors: HashMap::new(), active_shortcuts_inhibitor: None, direct_backend: None, _ipc_socket: None, pending_dmabuf_imports: Vec::new(), pending_screencopies: Vec::new(), screenshot: Coordinator::new(), screenshot_parts: None, screenshot_worker: None, shell_resources: Vec::new(), shell_snapshot_serial: 0, last_shell_snapshot: None, 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, }; let screenshot = crate::ipc::init(event_loop)?; state.screenshot_parts = Some(screenshot.parts); state.screenshot_worker = Some(screenshot.worker); state._ipc_socket = Some(screenshot._guard); Ok(state) } pub(crate) fn apply_runtime_config(&mut self, config: RuntimeConfig) -> Result<(), String> { if self.output_profiles != config.output_profiles { crate::backends::direct::validate_live_outputs(self, &config.output_profiles)?; } 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 { self.input_capture.close_all(); if self.input_capture.restore_focus { self.restore_input_capture_focus(); } 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.lock_idle = config.lock_idle; self.advance_animations(Instant::now()); let touchpad_changed = self.input_settings.touchpad != config.input_settings.touchpad; let outputs_changed = self.output_profiles != config.output_profiles; let bounds = self .workspaces .iter() .map(|workspace| { let output = self.output_workspaces.output_for_workspace(workspace.id); let rect = self .output_ids .iter() .find(|(_, id)| Some(**id) == output) .and_then(|(output, _)| self.output_bounds_for(output)) .or_else(|| self.output_bounds()) .unwrap_or(Rect::new(0., 0., 1920., 1080.)); (workspace.id, rect) }) .collect(); self.workspaces .reconfigure_live( config.layout_mode, config.default_column_width, config.scrolling_focus_strategy, &bounds, ) .map_err(|error| error.to_string())?; self.gap_config = config.gap_config; self.input_settings = config.input_settings; self.bindings = config.bindings; self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings); let old_rules = std::mem::replace(&mut 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; if !self.animations_enabled { self.workspace_slides.clear(); } 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.autostart = config.autostart; if touchpad_changed { crate::backends::direct::reload_input_devices(self); } if outputs_changed { crate::backends::direct::reload_outputs(self); } if old_rules != self.window_rules { self.reapply_window_rules(&old_rules); } 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) { if self.session_lock.active { self.session_lock.output_added(output); self.lock_input_activity(); } 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.workspace_slides.remove(&output_id); self.overview_scrims.remove(&output_id); self.pending_screencopies.retain(|capture| { if capture.output == *output { capture.fail(); false } else { true } }); self.space.unmap_output(output); self.session_lock.surfaces.remove(output); self.session_lock.backgrounds.remove(output); self.session_lock.output_removed(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 reposition_output_floats(&mut self, output: OutputId, old: Rect, new: Rect) { let floats = self .window_ids .values() .filter_map(|id| { let workspace = self.workspaces.workspace_for_window(*id)?; if self.output_workspaces.output_for_workspace(workspace) != Some(output) { return None; } match self.workspaces.placement(*id)? { WindowPlacement::Floating { rect } => Some((*id, moved_floating_rect(rect, old, new))), _ => None, } }) .collect::>(); for (id, rect) in floats { let _ = self.workspaces.set_floating_rect(id, rect); } } 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(); } } } 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| { let mut client_state = ClientState::default(); if crate::handlers::window_capture::is_portal(&client_stream) { client_state .capabilities .insert(crate::private_client::ClientCapabilities::WINDOW_CAPTURE); } if let Err(error) = state .display_handle .insert_client(client_stream, Arc::new(client_state)) { 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); } if self.input_capture.active() { return None; } 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(); let surface = window.surface_under(source_point, WindowSurfaceType::ALL); let (surface, surface_location) = match surface { Some(hit) => hit, None if self.window_fills_visual_bounds(*id, workspace) => { (window.toplevel()?.wl_surface().clone(), Point::from((0, 0))) } None => return None, }; let surface_point = source_point - surface_location.to_f64(); Some((surface, position - surface_point)) }) } fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool { self.maximized_windows.contains(&id) || self .workspaces .workspace(workspace) .is_some_and(|workspace| workspace.fullscreen == Some(id)) } 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; }; let Some(pointer) = self.seat.get_pointer() else { return; }; if self.space.output_under(pointer.current_location()).next() != Some(output) { 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 overview_active = self.overview.is_active(); let workspace = self.workspace_under_pointer(position)?; let hit = |window: &Window| -> Option { if !self.window_content_ready(window) { return None; } 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 caption_height = if overview_active { 34.0 } else { 0.0 }; let inside_visual = position.x >= visual.x && position.y >= visual.y && position.x < visual.x + visual.width && position.y < visual.y + visual.height + caption_height; if !inside_visual { return None; } if !overview_active { 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(); if window.surface_under(source_point, WindowSurfaceType::ALL).is_none() && !self.window_fills_visual_bounds(*id, workspace) { return None; } } Some(window.clone()) }; if overview_active { let mut candidates = self.window_ids.keys().cloned().collect::>(); // Match Overview's front-to-back render order during overlapping motion. candidates.sort_by_key(|window| std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0))); candidates.iter().find_map(hit) } else { self.space.elements().rev().find_map(hit) } } 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 Some(output_id) = self.output_id(output) else { return false; }; let workspace = self.workspaces.workspace_for_window(window); workspace == self.output_workspaces.active_workspace(output_id) || self .workspace_slides .get(&output_id) .is_some_and(|slide| workspace.is_some_and(|workspace| slide.contains(workspace))) } pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) { let Some(workspace) = self.workspaces.workspace_for_window(window) else { return (0.0, 0.0); }; let Some(output_id) = self.output_workspaces.output_for_workspace(workspace) else { return (0.0, 0.0); }; let Some(slide) = self.workspace_slides.get(&output_id) else { return (0.0, 0.0); }; let Some(size) = self .output_ids .iter() .find(|(_, id)| **id == output_id) .and_then(|(output, _)| self.space.output_geometry(output)) .map(|geometry| geometry.size) else { return (0.0, 0.0); }; slide.offset(workspace, f64::from(size.w), f64::from(size.h)) } pub(crate) fn cancel_workspace_slides(&mut self) -> bool { let active = !self.workspace_slides.is_empty(); self.workspace_slides.clear(); active } 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()), floating && id.is_some_and(|id| { self.floating_above_fullscreen.get(&id).is_some_and(|parent| { self.workspaces .workspace_for_window(id) .and_then(|workspace| self.workspaces.workspace(workspace)) .is_some_and(|workspace| workspace.fullscreen == Some(*parent)) }) }), ); (priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id))) }); if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) { return; } 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(crate) fn window_content_ready(&self, window: &Window) -> bool { window_has_buffer(window) && self .window_ids .get(window) .and_then(|id| self.window_geometry.get(id)) .is_some_and(|geometry| geometry.client.committed_size.is_some()) } pub(crate) fn add_rule_placed_window( &mut self, window: Window, app_id: Option<&str>, title: Option<&str>, parent: Option, ) { let rule = resolve_window_rules(&self.window_rules, app_id, title, parent.is_some()); let floating = rule .floating .unwrap_or(parent.is_some() || rule.width.is_some() || rule.height.is_some()); if !floating { self.add_tiled_window(window); return; } if let Some(parent) = parent { self.add_transient_window(window, parent); return; } if let Some(pointer) = self.seat.get_pointer() { self.focus_output_at(pointer.current_location()); } let Some(bounds) = self.output_bounds() else { self.add_tiled_window(window); return; }; let id = WindowId(self.next_window_id); self.next_window_id += 1; let rect = client_size(&window) .map(|size| natural_floating_rect(bounds, size)) .unwrap_or_else(|| centered_floating_rect(bounds)); let fullscreen = self.workspaces.active().fullscreen; let focus = true; if let Err(error) = self .workspaces .insert_floating_window(id, self.workspaces.active_id(), rect, focus) { tracing::error!(%error, ?id, "failed to insert rule-placed floating window"); return; } if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() { self.natural_floating_pending.insert(id); } self.window_ids.insert(window.clone(), id); self.window_stack.insert(id); if let Some(fullscreen) = fullscreen { self.floating_above_fullscreen.insert(id, fullscreen); } if focus { self.focused_window = Some(id); } self.space.map_element(window, (0, 0), focus); // apply_initial_window_rules performs the first relayout/configure. } 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; let focus = workspace == self.workspaces.active_id() && (self.focused_window == Some(parent) || self.workspaces.active().fullscreen == Some(parent)); if let Err(error) = self.workspaces.insert_floating_window(id, workspace, rect, focus) { tracing::error!(%error, ?id, ?parent, "failed to insert transient window"); return; } self.window_ids.insert(window.clone(), id); self.window_stack.insert(id); if self .workspaces .workspace(workspace) .is_some_and(|workspace| workspace.fullscreen == Some(parent)) { self.floating_above_fullscreen.insert(id, parent); } if focus { self.focused_window = Some(id); } self.space.map_element(window, (0, 0), focus); self.relayout(); if focus { self.restore_keyboard_focus(); } } 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; }; if !self.window_rules_applied.insert(id) { return; } let rule = resolve_window_rules(&self.window_rules, app_id, title, transient); self.apply_window_rule_result(window, rule); } fn reapply_window_rules(&mut self, old_rules: &[WindowRule]) { let windows = self.window_ids.keys().cloned().collect::>(); for window in windows { let Some(toplevel) = window.toplevel() else { continue; }; let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| { let attributes = states.data_map.get::().unwrap().lock().unwrap(); ( attributes.app_id.clone(), attributes.title.clone(), attributes.parent.is_some(), ) }); let old = resolve_window_rules(old_rules, app_id.as_deref(), title.as_deref(), transient); let new = resolve_window_rules(&self.window_rules, app_id.as_deref(), title.as_deref(), transient); if let Some(new) = crate::window_rules::live_result(old, new, transient) { self.apply_window_rule_result(&window, new); } } } fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) { let Some(id) = self.window_ids.get(window).copied() else { return; }; let Some(bounds) = self.floating_bounds_for_window(id) else { return; }; 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 bounds = self.floating_bounds_for_window(id).unwrap_or(bounds); 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; } if self .workspaces .workspace_for_window(parent) .and_then(|workspace| self.workspaces.workspace(workspace)) .is_some_and(|workspace| workspace.fullscreen == Some(parent)) { self.floating_above_fullscreen.insert(id, parent); } if self.focused_window == Some(parent) { if let Err(error) = self.workspaces.focus_window(id) { tracing::warn!(%error, ?id, "failed to focus transient window"); } else { self.focused_window = Some(id); self.window_stack.raise(id); } } self.relayout(); self.restore_keyboard_focus(); } pub fn remove_tiled_window(&mut self, window: &Window) { // Uncommitted toplevels have not entered a workspace yet. self.space.unmap_elem(window); let Some(id) = self.window_ids.remove(window) else { return; }; 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.floating_above_fullscreen.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(); } fn visible_workspace_ids(&self) -> HashSet { let mut visible = self .output_workspaces .connected_outputs() .filter_map(|output| self.output_workspaces.active_workspace(output)) .collect::>(); visible.extend( self.workspace_slides .values() .flat_map(|slide| slide.items.iter().map(|item| item.workspace)), ); visible } fn prune_empty_workspaces(&mut self, visible: &HashSet) { for workspace in self.workspaces.prune_empty(visible) { self.output_workspaces.forget_workspace(workspace); self.viewport_animations.remove(&workspace); } } fn unmap_invisible_windows(&mut self, visible: &HashSet) -> bool { let mut changed = false; for (window, id) in &self.window_ids { if visible.contains(id) { continue; } let was_mapped = self.space.element_location(window).is_some(); changed |= was_mapped; if was_mapped && let Some(geometry) = self.window_geometry.get_mut(id) { geometry.settle_presentation(); } self.space.unmap_elem(window); } changed } pub fn relayout(&mut self) { self.refresh_input_capture_zones(); let visible_workspaces = self.visible_workspace_ids(); self.prune_empty_workspaces(&visible_workspaces); 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(); let mut visible_workspace_pairs = Vec::new(); for output in outputs { let Some(output_id) = self.output_ids.get(&output).copied() else { continue; }; let Some(active_workspace) = self.output_workspaces.active_workspace(output_id) else { continue; }; visible_workspace_pairs.push((output.clone(), active_workspace)); if let Some(slide) = self.workspace_slides.get(&output_id) { for item in &slide.items { if item.workspace != active_workspace { visible_workspace_pairs.push((output.clone(), item.workspace)); } } } } for (output, workspace_id) in visible_workspace_pairs { let Some(bounds) = self.output_bounds_for(&output) else { continue; }; let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds); let scale = output.current_scale().fractional_scale(); for layer in layer_map_for_output(&output).layers() { crate::handlers::set_surface_tree_scale(layer.wl_surface(), scale); } 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; } if let Some(toplevel) = window.toplevel() { crate::handlers::set_surface_tree_scale(toplevel.wl_surface(), scale); } 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 }) => { let constrained = constrained_floating_rect(rect, constraints.get(id).copied().unwrap_or_default()); if constrained != rect { let _ = self.workspaces.set_floating_rect(*id, constrained); } constrained } 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 mut layout_changed = self.unmap_invisible_windows(&visible); 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 { let was_mapped = self.space.element_location(&window).is_some(); // A viewport-coupled width must never override fullscreen/floating geometry. if scrolling.is_none() { self.viewport_coupled_widths.remove(&id); } let (geometry, had_geometry) = match self.window_geometry.entry(id) { Entry::Occupied(entry) => (entry.into_mut(), true), Entry::Vacant(entry) => (entry.insert(WindowGeometry::new(rect, client_size(&window))), false), }; 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 !was_mapped && (had_geometry || is_fullscreen) { geometry.settle_presentation(); } 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) && 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) { if let Some(backend) = self.direct_backend.as_mut() { backend.record_presentation(node, crtc, time, sequence); } } pub(crate) fn animation_fallback_deadline(&self, interval: std::time::Duration) -> Instant { self.last_animation_tick + interval.saturating_mul(2) } pub(crate) fn reset_animation_clock(&mut self) { self.last_animation_tick = Instant::now(); } fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool { let delta = delta.mul_f64(self.animation_speed); let visible_workspaces = if self.viewport_animations.is_empty() { HashSet::new() } else { 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; let mut completed_slides = Vec::new(); for (output, slide) in &mut self.workspace_slides { if slide.advance(delta) { active_animation = true; } else { completed_slides.push(*output); } } 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. // Read selection once so these immutable fields can be borrowed alongside // window_focus without allocating a temporary window-ID vector. let selected_window = if self.overview.is_active() { self.overview.selected() } else { self.focused_window }; for id in self.window_ids.values().copied() { let selected = selected_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(now) { 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(now) && !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); } if !completed_slides.is_empty() { for output in completed_slides { self.workspace_slides.remove(&output); } let visible = self.visible_workspace_ids(); let visible_windows = self .window_ids .values() .copied() .filter(|id| { self.workspaces .workspace_for_window(*id) .is_none_or(|workspace| visible.contains(&workspace)) }) .collect::>(); self.unmap_invisible_windows(&visible_windows); self.prune_empty_workspaces(&visible); self.send_shell_snapshots(); active_animation = true; } 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(); } } // A normal floating client may choose a different size (minimum sizes, // terminal cell grids, or a dialog changing its contents). Its committed // window geometry is authoritative once the latest configure is committed. // Never let an old buffer undo a newer resize or a fullscreen transition. let settled_configure = window.toplevel().is_some_and(|toplevel| { with_states(toplevel.wl_surface(), |states| { states.data_map.get::().is_some_and(|data| { let Ok(data) = data.lock() else { return false }; floating_commit_is_current( data.pending_configures().is_empty(), data.current_serial, data.configure_serial, data.current.states.contains(xdg_toplevel::State::Resizing), ) }) }) }); let fullscreen = self .workspaces .workspace_for_window(id) .and_then(|workspace| self.workspaces.workspace(workspace)) .is_some_and(|workspace| workspace.fullscreen == Some(id)); if settled_configure && !fullscreen && !self.maximized_windows.contains(&id) && self .window_geometry .get(&id) .is_some_and(|geometry| !geometry.is_zooming()) && let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) && ClientSize::from_rect(rect) != size { let rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height)); if self.workspaces.set_floating_rect(id, rect).is_ok() { self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size))); self.resize_transactions.remove(&id); self.resize_snapshots.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) { self.focus_direction_with_slide(direction, false); } pub(crate) fn focus_direction_from_swipe(&mut self, direction: Direction) { self.focus_direction_with_slide(direction, true); } fn focus_direction_with_slide(&mut self, direction: Direction, slide: bool) { if self.input_capture.captures(1) { return; } 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; } if slide && let WorkspaceLayout::Scrolling(layout) = &mut self.workspaces.active_mut().layout && let Err(error) = layout.slide_focus_from(current, next) { tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus"); } 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) && 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; }; if !activate_window_workspace(&mut self.workspaces, &mut self.output_workspaces, id) { return false; } let workspace = self.workspaces.active_id(); if let Some(output) = self.output_workspaces.output_for_workspace(workspace) { self.workspace_slides.remove(&output); } 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 { self.activate_managed_workspace_internal(workspace, None) } pub(crate) fn activate_managed_workspace_from_swipe( &mut self, workspace: WorkspaceId, direction: SwipeDirection, ) -> bool { self.activate_managed_workspace_internal(workspace, Some(direction)) } fn activate_managed_workspace_internal( &mut self, workspace: WorkspaceId, slide_direction: Option, ) -> bool { if self.workspaces.workspace(workspace).is_none() { return false; } let output = self .output_workspaces .focused_output() .or_else(|| self.output_workspaces.output_for_workspace(workspace)); let Some(output) = output else { return false; }; let previous = self.output_workspaces.active_workspace(output); let owner = match self.output_workspaces.switch_workspace(output, workspace) { Ok(ferese_core::WorkspaceSwitch::Activated(output)) | Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output, Err(_) => return false, }; self.update_workspace_slide(output, owner, previous, workspace, slide_direction); self.activate_output_workspace(owner, 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(); // Move/resize/cancel callbacks run with Smithay's pointer mutex held. // Cursor rendering reads current_location(), which would lock it again. // The event-loop epilogue coalesces this redraw after the grab returns. self.cursor_redraw_pending = true; } pub fn switch_workspace(&mut self, index: u32) { self.switch_workspace_internal(index, None); } pub(crate) fn switch_workspace_from_swipe(&mut self, index: u32, direction: SwipeDirection) { self.switch_workspace_internal(index, Some(direction)); } fn switch_workspace_internal(&mut self, index: u32, slide_direction: Option) { 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 previous = self.output_workspaces.active_workspace(output); 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.update_workspace_slide(output, owner, previous, workspace, slide_direction); self.activate_output_workspace(owner, workspace); self.relayout(); self.restore_keyboard_focus(); } fn update_workspace_slide( &mut self, requested_output: OutputId, owner: OutputId, previous: Option, workspace: WorkspaceId, slide_direction: Option, ) { let previous_slide = self.workspace_slides.remove(&owner); if owner == requested_output && let (Some(from), Some(direction)) = (previous, slide_direction) && from != workspace && self.animations_enabled { self.last_animation_tick = Instant::now(); self.workspace_slides .insert(owner, WorkspaceSlide::new(previous_slide, from, workspace, direction)); } } 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.input_capture.captures(1) { return; } 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 moved_floating_rect(rect: Rect, old: Rect, new: Rect) -> Rect { let width = rect.width.min(new.width).max(1.); let height = rect.height.min(new.height).max(1.); Rect::new( (rect.x + new.x - old.x).clamp(new.x, new.x + (new.width - width).max(0.)), (rect.y + new.y - old.y).clamp(new.y, new.y + (new.height - height).max(0.)), width, height, ) } 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 floating_commit_is_current( no_pending_configures: bool, committed: Option, acknowledged: Option, resizing: bool, ) -> bool { no_pending_configures && committed.is_some() && committed == acknowledged && !resizing } fn constrained_floating_rect(rect: Rect, constraints: SizeConstraints) -> Rect { let min_width = constraints.min_width.max(1.0); let min_height = constraints.min_height.max(1.0); Rect::new( rect.x, rect.y, rect.width .clamp(min_width, constraints.max_width.unwrap_or(f64::INFINITY).max(min_width)), rect.height.clamp( min_height, constraints.max_height.unwrap_or(f64::INFINITY).max(min_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 { // XDG geometry alone is metadata, not a painted application frame. // Initial empty commits must not make the placeholder presentation ready. if !window_has_buffer(window) { return None; } let size = window.geometry().size; (size.w > 0 && size.h > 0).then_some(ClientSize { width: size.w, height: size.h, }) } pub(crate) fn window_has_buffer(window: &Window) -> bool { window.toplevel().is_some_and(|toplevel| { smithay::backend::renderer::utils::with_renderer_surface_state(toplevel.wl_surface(), |state| { state.buffer().is_some() }) .unwrap_or(false) }) } fn activate_window_workspace( workspaces: &mut WorkspaceSet, outputs: &mut OutputWorkspaceMap, window: WindowId, ) -> bool { let Some(workspace) = workspaces.workspace_for_window(window) else { return false; }; if let Some(output) = outputs .focused_output() .or_else(|| outputs.output_for_workspace(workspace)) && outputs.switch_workspace(output, workspace).is_err() { return false; } if workspaces.activate(workspace).is_err() { return false; } if let Some(fullscreen) = workspaces.active().fullscreen && fullscreen != window && workspaces.placement(window) == Some(WindowPlacement::Tiled) && workspaces.set_fullscreen(fullscreen, false).is_err() { return false; } workspaces.focus_window(window).is_ok() } fn stacking_order_settled<'a, T, I>(current: I, target: &'a [T], z_index: impl Fn(&T) -> u8) -> bool where I: IntoIterator, T: PartialEq, { let Some((first, rest)) = target.split_first() else { return current.into_iter().next().is_none(); }; let expected = z_index(first); rest.iter().all(|window| z_index(window) == expected) && current.into_iter().eq(target) } #[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 { #[test] fn selecting_a_window_activates_its_workspace_on_its_owner() { for assignment in [None, Some(OutputId(1)), Some(OutputId(2))] { let mut workspaces = WorkspaceSet::default(); let first = workspaces.active_id(); workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); let second = workspaces.ensure_numeric(2).unwrap(); workspaces.activate(second).unwrap(); workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); workspaces.insert_window(WindowId(3), Axis::Horizontal, 0.5).unwrap(); workspaces.activate(first).unwrap(); let mut outputs = OutputWorkspaceMap::default(); outputs .connect(OutputId(1), OutputGeometry::new(0, 0, 1920, 1080), first) .unwrap(); let owner = if assignment == Some(OutputId(2)) { let third = workspaces.ensure_numeric(3).unwrap(); outputs .connect(OutputId(2), OutputGeometry::new(1920, 0, 1920, 1080), third) .unwrap(); outputs.assign_workspace(OutputId(2), second).unwrap(); OutputId(2) } else { if let Some(output) = assignment { outputs.assign_workspace(output, second).unwrap(); } OutputId(1) }; outputs.focus_output(OutputId(1)).unwrap(); assert!(activate_window_workspace(&mut workspaces, &mut outputs, WindowId(2))); assert_eq!(outputs.active_workspace(owner), Some(second)); assert_eq!(outputs.focused_output(), Some(owner)); assert_eq!(outputs.output_for_workspace(second), Some(owner)); assert_eq!(workspaces.active_id(), second); assert_eq!(workspaces.active().last_focused, Some(WindowId(2))); if assignment == Some(OutputId(2)) { assert_eq!(outputs.active_workspace(OutputId(1)), Some(first)); } } } #[test] fn selecting_a_tiled_window_reveals_it_past_another_fullscreen_window() { let mut workspaces = WorkspaceSet::default(); workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); workspaces.set_fullscreen(WindowId(1), true).unwrap(); let mut outputs = OutputWorkspaceMap::default(); outputs .connect( OutputId(1), OutputGeometry::new(0, 0, 1920, 1080), workspaces.active_id(), ) .unwrap(); assert!(activate_window_workspace(&mut workspaces, &mut outputs, WindowId(2))); assert_eq!(workspaces.active().fullscreen, None); assert_eq!(workspaces.active().last_focused, Some(WindowId(2))); } #[test] fn workspace_slide_moves_both_workspaces_without_a_gap() { let width = 1600.0; let height = 900.0; for (direction, outgoing_target) in [ (SwipeDirection::Left, (-width, 0.0)), (SwipeDirection::Right, (width, 0.0)), (SwipeDirection::Up, (0.0, -height)), (SwipeDirection::Down, (0.0, height)), ] { let from = WorkspaceId(1); let to = WorkspaceId(2); let mut slide = WorkspaceSlide::new(None, from, to, direction); assert!(slide.contains(from)); assert!(slide.contains(to)); assert!(slide.advance(WORKSPACE_SLIDE_DURATION / 2)); let outgoing = slide.offset(from, width, height); let incoming = slide.offset(to, width, height); assert!((incoming.0 - outgoing.0).abs() == outgoing_target.0.abs()); assert!((incoming.1 - outgoing.1).abs() == outgoing_target.1.abs()); assert!(!slide.advance(WORKSPACE_SLIDE_DURATION)); assert_eq!(slide.offset(from, width, height), outgoing_target); assert_eq!(slide.offset(to, width, height), (0.0, 0.0)); } } #[test] fn chained_workspace_slides_keep_all_visible_workspaces() { let first = WorkspaceId(1); let second = WorkspaceId(2); let third = WorkspaceId(3); let mut slide = WorkspaceSlide::new(None, first, second, SwipeDirection::Up); slide.advance(WORKSPACE_SLIDE_DURATION / 3); let first_position = slide.offset(first, 1600.0, 900.0); let second_position = slide.offset(second, 1600.0, 900.0); let chained = WorkspaceSlide::new(Some(slide), second, third, SwipeDirection::Left); assert!(chained.contains(first)); assert!(chained.contains(second)); assert!(chained.contains(third)); let first_start = chained.items.iter().find(|item| item.workspace == first).unwrap().start; let second_start = chained .items .iter() .find(|item| item.workspace == second) .unwrap() .start; assert_eq!((first_start.x * 1600.0, first_start.y * 900.0), first_position); assert_eq!((second_start.x * 1600.0, second_start.y * 900.0), second_position); let reversed = WorkspaceSlide::new(Some(chained), third, first, SwipeDirection::Down); assert_eq!(reversed.items.len(), 3); assert!(reversed.contains(second)); assert_eq!( reversed .items .iter() .find(|item| item.workspace == first) .unwrap() .target, SlideOffset::default() ); } #[test] fn floating_sizes_respect_client_limits_without_moving_the_window() { let rect = Rect::new(40., 60., 300., 200.); let constraints = SizeConstraints { min_width: 640., min_height: 480., max_width: Some(900.), max_height: Some(700.), }; assert_eq!( constrained_floating_rect(rect, constraints), Rect::new(40., 60., 640., 480.) ); assert_eq!( constrained_floating_rect(Rect::new(40., 60., 1000., 800.), constraints), Rect::new(40., 60., 900., 700.) ); assert_eq!(constrained_floating_rect(rect, SizeConstraints::default()), rect); } #[test] fn floating_commits_cannot_undo_pending_or_interactive_resizes() { let serial = Some(12.into()); assert!(floating_commit_is_current(true, serial, serial, false)); assert!(!floating_commit_is_current(false, serial, serial, false)); assert!(!floating_commit_is_current(true, Some(11.into()), serial, false)); assert!(!floating_commit_is_current(true, serial, serial, true)); assert!(!floating_commit_is_current(true, None, None, false)); } #[test] fn display_reposition_moves_floats_with_their_output_and_clamps_after_shrinking() { let old = ferese_layout::Rect::new(0., 0., 1920., 1080.); let new = ferese_layout::Rect::new(2000., 0., 1000., 700.); let rect = super::moved_floating_rect(ferese_layout::Rect::new(1500., 800., 400., 300.), old, new); assert_eq!(rect, ferese_layout::Rect::new(2600., 400., 400., 300.)); } 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); } #[derive(Clone, Copy, PartialEq, Debug)] struct StackedWindow { id: u32, z_index: u8, } fn stacked(id: u32, z_index: u8) -> StackedWindow { StackedWindow { id, z_index } } #[test] fn stacking_is_settled_when_order_matches_and_z_index_is_uniform() { let current = [stacked(1, 30), stacked(2, 30), stacked(3, 30)]; assert!(stacking_order_settled(current.iter(), ¤t, |window| { window.z_index })); } #[test] fn stacking_is_not_settled_when_order_differs() { let current = [stacked(2, 30), stacked(1, 30)]; let target = [stacked(1, 30), stacked(2, 30)]; assert!(!stacking_order_settled(current.iter(), &target, |window| { window.z_index })); } #[test] fn stacking_is_not_settled_when_z_index_is_mixed_even_with_matching_order() { let current = [stacked(1, 30), stacked(2, 40)]; assert!(!stacking_order_settled(current.iter(), ¤t, |window| window.z_index)); } #[test] fn stacking_is_settled_for_an_empty_space() { let empty: [StackedWindow; 0] = []; assert!(stacking_order_settled(empty.iter(), &empty, |window| { window.z_index })); } }