mod animation; mod capture_privacy; #[cfg(test)] pub(crate) use capture_privacy::tests::{ dispatch as dispatch_test_client, request as test_client_request, window as test_window, }; mod floating; mod focus; mod hit_testing; mod layout; mod navigation; mod outputs; mod reconciliation; pub(crate) mod tab_indicator; mod tabs; #[cfg(test)] mod window_lifecycle_tests; pub(crate) use reconciliation::{DesktopChanges, DesktopOutput, DesktopTransition}; mod prediction; mod presentation_dependencies; pub(crate) use prediction::FrameScene; mod lifecycle; mod viewport; mod window_registry; mod windows; use viewport::ViewportPresentation; 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::drm::{DrmEventTime, DrmNode}; use smithay::backend::input::Keycode; 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::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; 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::selection::{ext_data_control, wlr_data_control}; 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 window_registry::WindowRegistry; use crate::backends::direct::DirectBackendState; use crate::config::{Binding, BindingSet, DaemonConfig, InputSettings, OutputProfile, ThemeSettings}; use crate::cursor::NamedCursor; use crate::focus_effect::BoundedFade; 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::session_lock::{IdleSettings, Lock}; use crate::shell_control::ShellSnapshot; use crate::stacking::{StackingCache, WindowStack}; use crate::wallpaper::{WallpaperConfig, WallpaperState}; use crate::window_rules::{WindowRule, resolve as resolve_window_rules}; use crate::winit::NestedBackend; #[cfg(test)] use ferese_animation::CrossingPolicy; #[cfg(test)] const SLIDE_SETTLE_TEST_BUDGET: Duration = Duration::from_secs(2); fn workspace_swipe_is_current( outputs: &OutputWorkspaceMap, output: OutputId, from: WorkspaceId, to: WorkspaceId, ) -> bool { outputs.focused_output() == Some(output) && outputs.active_workspace(output) == Some(from) && outputs.output_for_workspace(to).is_none_or(|owner| owner == output) } #[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, } } } #[derive(Clone, PartialEq)] struct FocusSwipe { workspace: WorkspaceId, from: WindowId, to: WindowId, direction: Direction, gesture_direction: SwipeDirection, start: f64, destination: f64, progress: f64, layout: Option, dependencies: Option, } impl FocusSwipe { fn distance(&self) -> f64 { let distance = self.destination - self.start; // When both columns already fit, give bounded drag feedback and settle // back to the unchanged viewport after selecting the next window. if distance.abs() < 0.001 { if self.direction == Direction::Right { 64.0 } else { -64.0 } } else { distance } } fn position(&self) -> f64 { let progress = if self.to == self.from { ferese_animation::gesture::rubber_band(self.progress.max(0.0), 1.0, 0.55).0 } else { self.progress.clamp(0.0, 1.0) }; self.start + self.distance() * progress } fn release_velocity(&self, bounded: f64, unbounded: f64) -> f64 { let velocity = if self.to == self.from { if self.progress <= 0.0 { 0.0 } else { unbounded * ferese_animation::gesture::rubber_band(self.progress, 1.0, 0.55).1 } } else { bounded }; self.distance() * velocity } } #[derive(Clone)] struct WorkspaceSlideItem { workspace: WorkspaceId, start: SlideOffset, target: SlideOffset, velocity: SlideOffset, } #[derive(Clone)] struct WorkspaceSlide { speed: f64, items: Vec, spring: SpringConfig, held_progress: Option, gesture: Option<(WorkspaceId, WorkspaceId, SwipeDirection)>, } 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.held_progress; slide .items .into_iter() .map(|mut item| { if let Some(progress) = progress { 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, velocity: SlideOffset::default(), }); } 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(), velocity: SlideOffset::default(), }); } Self { speed: 1.0, items, spring: SpringConfig { stiffness: 320.0, damping: 2.0 * 320.0_f64.sqrt(), position_tolerance: 0.00001, velocity_tolerance: 0.00001, ..SpringConfig::default() }, held_progress: None, gesture: None, } } fn position(&self, item: &WorkspaceSlideItem, delta: Duration) -> SlideOffset { self.sample(item, delta).0 } fn sample(&self, item: &WorkspaceSlideItem, delta: Duration) -> (SlideOffset, SlideOffset) { let delta = delta.mul_f64(self.speed); if let Some(progress) = self.held_progress { return (item.start.between(item.target, progress), item.velocity); } let mut x = AnimatedValue { current: item.start.x, target: item.target.x, velocity: item.velocity.x, }; let mut y = AnimatedValue { current: item.start.y, target: item.target.y, velocity: item.velocity.y, }; x.advance(delta, self.spring); y.advance(delta, self.spring); ( SlideOffset { x: x.current, y: y.current, }, SlideOffset { x: x.velocity * self.speed, y: y.velocity * self.speed, }, ) } fn moving(&self) -> bool { self.items .iter() .any(|item| item.start != item.target || item.velocity != SlideOffset::default()) } fn release_with_velocity(&mut self, committed: bool, velocity: f64) { for item in &mut self.items { item.velocity = SlideOffset { x: (item.target.x - item.start.x) * velocity, y: (item.target.y - item.start.y) * velocity, }; } self.release(committed); } fn release(&mut self, committed: bool) { let progress = self.held_progress.unwrap_or(0.0); for item in &mut self.items { let position = item.start.between(item.target, progress); if !committed && let Some((from, to, direction)) = self.gesture { if item.workspace == from { item.target = SlideOffset::default(); } else if item.workspace == to { item.target = SlideOffset::for_swipe(direction).opposite(); } } item.start = position; } self.held_progress = None; self.gesture = None; } fn advance(&mut self, delta: Duration) -> bool { let delta = delta.mul_f64(self.speed); if self.held_progress.is_some() { return true; } let mut moving = false; for item in &mut self.items { let mut x = AnimatedValue { current: item.start.x, target: item.target.x, velocity: item.velocity.x, }; let mut y = AnimatedValue { current: item.start.y, target: item.target.y, velocity: item.velocity.y, }; moving |= x.advance(delta, self.spring); moving |= y.advance(delta, self.spring); item.start = SlideOffset { x: x.current, y: y.current, }; item.velocity = SlideOffset { x: x.velocity, y: y.velocity, }; } moving } fn contains(&self, workspace: WorkspaceId) -> bool { self.items.iter().any(|item| item.workspace == workspace) } #[cfg(test)] fn offset(&self, workspace: WorkspaceId, width: f64, height: f64) -> (f64, f64) { let position = self .items .iter() .find(|item| item.workspace == workspace) .map(|item| self.position(item, Duration::ZERO)) .unwrap_or_default(); (position.x * width, position.y * height) } } fn scaled_animation_duration(duration: Duration, enabled: bool, speed: f64) -> Duration { if enabled { duration.div_f64(speed) } else { Duration::ZERO } } pub struct Ferese { pub(crate) gamma_dirty: bool, pub(crate) gamma_timer_pending: bool, pub(crate) night_light: ferese_config::night_light::Settings, pub(crate) session_lock_state: SessionLockManagerState, pub(crate) session_lock: Lock, pub(crate) lock_idle: IdleSettings, pub(crate) config_source: Option, pub(crate) config_sections: Option, pub(crate) ipc_events: crate::ipc_events::Subscribers, pub(crate) config_worker: Option, pub(crate) theme_engine: crate::theme::Engine, pub start_time: Instant, pub socket_name: OsString, pub(crate) session_environment: crate::SessionEnvironment, pub(crate) xwayland: Option, pub(crate) xwayland_config: crate::config::XwaylandConfig, pub(crate) x11_diagnostic: Option, 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) desktop_transition: Option, pub(crate) output_ids: HashMap, outputs_by_id: HashMap, pub(crate) output_names: HashMap, output_identity_ids: HashMap, pub(crate) windows: WindowRegistry, pub(crate) render: crate::render::RenderResources, pub(crate) capture_render: crate::render::RenderResources, pub(crate) capture_epoch: u64, capture_protected_windows: HashSet, capture_protected_cursor: Option, pub(crate) daemons: Option>>, pub(crate) display_presentation: crate::display_presentation::DisplayPresentation, pub(crate) nested_backend: Option, pub(crate) wallpaper: WallpaperState, window_stack: WindowStack, stacking_cache: StackingCache, floating_above_fullscreen: HashMap, floating_memory: crate::floating::Memory, floating_save_worker: crate::floating::SaveWorker, floating_cascade: crate::floating::Cascade, pub(crate) portal_shortcuts: PortalShortcuts, pub(crate) pending_logout: Option, pub(crate) logout_query: Option, pub(crate) logout_owner: Option, viewports: HashMap, presentation_dependencies: presentation_dependencies::PresentationDependencies, #[cfg(feature = "resize-metrics")] pub(crate) resize_metrics: crate::resize_metrics::ResizeMetrics, workspace_slides: HashMap, workspace_slide_offsets: HashMap, pub focused_window: Option, pub(crate) tab_indicator: tab_indicator::TabIndicatorState, pub(crate) tab_indicator_settings: crate::config::TabIndicatorSettings, pub(crate) tab_bars: HashMap>, pub(crate) tab_buttons: HashSet, pub(crate) focus_history: ferese_core::FocusHistory, pub(crate) focus_cycle: Option, column_width_presets: Vec, gap_config: GapConfig, pub(crate) input_settings: InputSettings, pub(crate) bindings: BindingSet, pub(crate) binding_modes: crate::config::BindingModes, pub(crate) binding_mode_pressed: HashSet, pub(crate) binding_mode_repeats: HashMap, workspace_auto_back_and_forth: bool, pub(crate) window_rules: Vec, pub(crate) theme_settings: ThemeSettings, pub(crate) panel_corner_radius: Option, pub(crate) panel_background_opacity: Option, pub(crate) panel_style: ferese_panel::PanelStyle, pub(crate) focus_effect: crate::config::FocusEffectSettings, animations_enabled: bool, animation_speed: f64, pub(crate) 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) output_redraw_pending: Vec, pub(crate) locked_pointer_hint: Option, pub(crate) last_pointer_time: u32, pub(crate) cursor_loader: crate::cursor::Loader, pub(crate) cursor_animation: crate::cursor::Animation, pub(crate) named_cursors: HashMap, pub intercepted_keys: HashSet, pub(crate) swipe: Swipe, pub idle_inhibitors: HashMap, pub(crate) idle_inhibit: crate::idle_inhibition::Settings, pub(crate) media_players: Vec, pub(crate) media_engine: crate::idle_inhibition::media::Engine, pub(crate) activities: crate::activities::Engine, pub(crate) automatic_idle_inhibited: bool, pub active_shortcuts_inhibitor: Option, pub direct_backend: Option, _ipc_socket: Option, pub(crate) dmabuf_imports: crate::dmabuf_imports::ImportManager, pub(crate) activation_inputs: crate::handlers::activation::InputHistory, 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) workspace_preview_revision: u64, workspace_previews: std::cell::RefCell, pub(crate) overview: OverviewState, next_output_id: u64, last_animation_tick: Instant, #[cfg(test)] animation_test_time: Option, pub popups: PopupManager, pub(crate) dismissing_popups: Vec<(WlSurface, PopupKind, BoundedFade)>, 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 wlr_data_control_state: wlr_data_control::DataControlState, pub ext_data_control_state: ext_data_control::DataControlState, 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, } #[derive(Clone)] pub struct RuntimeConfig { pub(crate) night_light: ferese_config::night_light::Settings, pub(crate) activity_settings: ferese_config::activities::Settings, pub(crate) activity_matcher: ferese_config::activities::Matcher, pub(crate) lock_idle: IdleSettings, pub(crate) idle_inhibit: crate::idle_inhibition::Settings, 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(crate) binding_modes: crate::config::BindingModes, pub workspace_auto_back_and_forth: bool, pub window_rules: Vec, pub theme_settings: ThemeSettings, pub(crate) panel_corner_radius: Option, pub(crate) panel_background_opacity: Option, pub(crate) panel_style: ferese_panel::PanelStyle, pub focus_effect: crate::config::FocusEffectSettings, pub default_column_width: ColumnWidth, pub default_column_display: ferese_layout::ColumnDisplay, pub(crate) tab_indicator: crate::config::TabIndicatorSettings, 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, pub(crate) xwayland: crate::config::XwaylandConfig, } impl Ferese { #[cfg(test)] pub fn new( event_loop: &mut EventLoop<'static, Self>, display: Display, config: RuntimeConfig, ) -> Result> { Self::new_with_session( event_loop, display, config, crate::ipc::Endpoint::desktop()?, crate::SessionPolicy::Desktop, ) } pub(crate) fn new_with_session( event_loop: &mut EventLoop<'static, Self>, display: Display, config: RuntimeConfig, ipc_endpoint: crate::ipc::Endpoint, session_policy: crate::SessionPolicy, ) -> Result> { let display_handle = display.handle(); crate::handlers::screencopy::init_global(&display_handle, &event_loop.handle()); crate::handlers::gamma_control::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_v6::(&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); // Clipboard tools need direct selection access. Without data control, // wl-copy maps a temporary window to obtain keyboard focus. let wlr_data_control_state = wlr_data_control::DataControlState::new::(&display_handle, Some(&primary_selection_state), |_| { true }); let ext_data_control_state = ext_data_control::DataControlState::new::(&display_handle, Some(&primary_selection_state), |_| { true }); 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_loader = crate::cursor::Loader::new(event_loop.get_signal())?; let default_cursor = crate::cursor::fallback_cursor(); let named_cursors = HashMap::from([(CursorIcon::Default, default_cursor)]); let mut activities = crate::activities::Engine::default(); activities.configure(&config.activity_settings, config.activity_matcher.clone()); let mut state = Self { session_lock_state, session_lock: crate::session_lock::Lock::default(), ipc_events: crate::ipc_events::Subscribers::default(), lock_idle: config.lock_idle, night_light: config.night_light, gamma_dirty: true, gamma_timer_pending: false, config_source: None, config_sections: None, config_worker: None, theme_engine: Default::default(), start_time, session_environment: crate::SessionEnvironment { wayland_display: socket_name.clone(), control_socket: ipc_endpoint.path.clone(), policy: session_policy, x11: None, }, socket_name, xwayland: None, xwayland_config: config.xwayland.clone(), x11_diagnostic: None, 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(), desktop_transition: None, output_ids: HashMap::new(), outputs_by_id: HashMap::new(), output_names: HashMap::new(), output_identity_ids: HashMap::new(), windows: WindowRegistry::default(), render: Default::default(), capture_render: Default::default(), capture_epoch: 0, capture_protected_windows: HashSet::new(), capture_protected_cursor: None, display_presentation: Default::default(), daemons: None, nested_backend: None, wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), window_stack: WindowStack::default(), stacking_cache: StackingCache::default(), floating_above_fullscreen: HashMap::new(), floating_memory: crate::floating::Memory::path() .map(|p| crate::floating::Memory::load(&p)) .unwrap_or_default(), floating_save_worker: Default::default(), floating_cascade: Default::default(), portal_shortcuts: PortalShortcuts::default(), pending_logout: None, logout_query: None, logout_owner: None, viewports: HashMap::new(), presentation_dependencies: Default::default(), #[cfg(feature = "resize-metrics")] resize_metrics: crate::resize_metrics::ResizeMetrics::default(), workspace_slides: HashMap::new(), workspace_slide_offsets: HashMap::new(), focused_window: None, tab_indicator: tab_indicator::TabIndicatorState::default(), tab_indicator_settings: config.tab_indicator, tab_bars: HashMap::new(), tab_buttons: HashSet::new(), focus_history: Default::default(), focus_cycle: None, column_width_presets: config.column_width_presets, gap_config: config.gap_config, input_settings: config.input_settings, bindings: config.bindings.into(), binding_modes: config.binding_modes, binding_mode_pressed: HashSet::new(), binding_mode_repeats: HashMap::new(), workspace_auto_back_and_forth: config.workspace_auto_back_and_forth, window_rules: config.window_rules, theme_settings: config.theme_settings, panel_corner_radius: config.panel_corner_radius, panel_background_opacity: config.panel_background_opacity, panel_style: config.panel_style, focus_effect: config.focus_effect, 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, output_redraw_pending: Vec::new(), locked_pointer_hint: None, last_pointer_time: 0, cursor_loader, cursor_animation: crate::cursor::Animation::default(), named_cursors, intercepted_keys: HashSet::new(), swipe: Swipe::default(), idle_inhibitors: HashMap::new(), idle_inhibit: config.idle_inhibit, media_players: Vec::new(), media_engine: Default::default(), activities, automatic_idle_inhibited: false, active_shortcuts_inhibitor: None, direct_backend: None, _ipc_socket: None, dmabuf_imports: crate::dmabuf_imports::ImportManager::default(), activation_inputs: Default::default(), 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, workspace_preview_revision: 0, workspace_previews: Default::default(), overview: OverviewState::with_font_family(config.overview_font_family), next_output_id: 1, last_animation_tick: start_time, #[cfg(test)] animation_test_time: None, 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, wlr_data_control_state, ext_data_control_state, relative_pointer_state, seat_state, shm_state, single_pixel_buffer_state, text_input_manager_state, viewporter_state, layer_shell_state, xdg_activation_state, xdg_foreign_state, xdg_shell_state, xdg_toplevel_icon_manager, }; state.config_worker = Some(crate::reload::Worker::new(event_loop)?); let screenshot = crate::ipc::init(event_loop, ipc_endpoint)?; state .screenshot .set_deadline_observer(move |deadline| screenshot.deadline_timer.update(deadline)); state.screenshot_parts = Some(screenshot.parts); state.screenshot_worker = Some(screenshot.worker); state._ipc_socket = Some(screenshot._guard); state .workspaces .set_default_column_display(config.default_column_display); Ok(state) } pub(crate) fn apply_runtime_config( &mut self, config: RuntimeConfig, sections: &serde_json::Value, ) -> Result<(), String> { let changed = |keys: &[&str]| { self.config_sections .as_ref() .is_none_or(|previous| keys.iter().any(|key| previous.get(*key) != sections.get(*key))) }; let activities_changed = changed(&["activities"]); let layout_changed = changed(&["layout", "scrolling", "workspaces"]); let bindings_changed = changed(&["bindings", "binding_modes", "commands"]); let input_changed = changed(&["input"]); let daemons_changed = changed(&["autostart"]); let lock_changed = changed(&["lock_screen"]); let panel_layout_changed = self.panel_style != config.panel_style; let non_output_scene_changed = layout_changed || panel_layout_changed || changed(&["animations", "appearance", "window_rules"]); let scene_changed = non_output_scene_changed || changed(&["output_profiles"]); if self.output_profiles != config.output_profiles { crate::backends::direct::validate_live_outputs(self, &config.output_profiles)?; } // Stage layout changes before replacing the keymap: both can fail. // Everything below the successful keymap replacement must be infallible. let workspaces = if layout_changed { let mut workspaces = self.workspaces.clone(); 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(); workspaces .reconfigure_live( config.layout_mode, config.default_column_width, config.scrolling_focus_strategy, &bounds, ) .map_err(|error| error.to_string())?; Some(workspaces) } else { None }; 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 input_changed && let Some(keyboard) = self.seat.get_keyboard() { if keyboard_changed { let options = (!config.input_settings.xkb_options.is_empty()) .then(|| config.input_settings.xkb_options.join(",")); keyboard .set_xkb_config( self, XkbConfig { layout: &config.input_settings.xkb_layout, variant: &config.input_settings.xkb_variant, options, ..XkbConfig::default() }, ) .map_err(|e| format!("keymap reload failed: {e}"))?; self.input_capture.close_all(); if self.input_capture.restore_focus { self.restore_input_capture_focus(); } } keyboard.change_repeat_info(config.input_settings.repeat_rate, config.input_settings.repeat_delay_ms); } if lock_changed { self.lock_idle = config.lock_idle; self.refresh_lock_idle_policy(); } self.gamma_dirty |= self.night_light != config.night_light; self.night_light = config.night_light; if scene_changed { 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; if let Some(workspaces) = workspaces { self.workspaces = workspaces; } // Desired configuration: reloads update it while endpoints stay // session-scoped, so a difference from the running service is reported // as `restart_required` rather than applied behind the user's back. self.xwayland_config = config.xwayland.clone(); if activities_changed { self.activities .configure(&config.activity_settings, config.activity_matcher.clone()); } if layout_changed || self.gap_config != config.gap_config || panel_layout_changed { // Hidden layouts are resolved independently of presented geometry. self.workspace_preview_revision = self.workspace_preview_revision.wrapping_add(1); } self.workspaces .set_default_column_display(config.default_column_display); self.gap_config = config.gap_config; self.input_settings = config.input_settings; if bindings_changed || input_changed { self.binding_mode_repeats.clear(); self.bindings = config.bindings.into(); self.binding_modes = config.binding_modes; self.cancel_focus_cycle(); self.send_shell_snapshots(); } self.workspace_auto_back_and_forth = config.workspace_auto_back_and_forth; if bindings_changed || input_changed { self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings); } let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules); let idle_policy_changed = self.idle_inhibit != config.idle_inhibit || old_rules != self.window_rules; self.idle_inhibit = config.idle_inhibit; self.theme_settings = config.theme_settings; self.panel_corner_radius = config.panel_corner_radius; self.tab_indicator_settings = config.tab_indicator; self.clear_tab_indicator(); self.panel_background_opacity = config.panel_background_opacity; self.panel_style = config.panel_style; self.focus_effect = config.focus_effect; self.column_width_presets = config.column_width_presets; self.animations_enabled = config.animations_enabled; if !self.animations_enabled { self.workspace_slides.clear(); self.workspace_slide_offsets.clear(); } self.animation_speed = config.animation_speed; self.spring_config = config.spring_config; self.viewport_spring_config = config.viewport_spring_config; for slide in self.workspace_slides.values_mut() { slide.spring = SpringConfig { position_tolerance: 0.00001, velocity_tolerance: 0.00001, ..self.viewport_spring_config }; } self.output_profiles = config.output_profiles; self.autostart = config.autostart; if daemons_changed && let Some(runner) = self.daemons.clone() { crate::daemon::Runner::refresh(&runner, self); } if touchpad_changed { crate::backends::direct::reload_input_devices(self); } if old_rules != self.window_rules { self.refresh_capture_privacy(); self.reapply_window_rules(&old_rules); } if idle_policy_changed { self.refresh_idle_inhibition(); } self.overview.set_font_family(config.overview_font_family); if self.wallpaper.configuration() != &config.wallpaper { self.wallpaper.reload(config.wallpaper); } if outputs_changed && self.direct_backend.is_some() { // Output publication also finalizes any accompanying scene changes. crate::backends::direct::reload_outputs(self, non_output_scene_changed); } else if scene_changed { self.relayout(); } Ok(()) } 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) } } 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 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 overview_slide_prediction_uses_the_same_clock_as_live_motion() { let mut slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left); slide.speed = crate::overview::OVERVIEW_MOTION_SPEED; for delta in [ Duration::from_millis(8), Duration::from_millis(100), Duration::from_secs(2), ] { let mut advanced = slide.clone(); advanced.advance(delta); for (item, after) in slide.items.iter().zip(&advanced.items) { let (position, velocity) = slide.sample(item, delta); assert_eq!(position, after.start); assert_eq!(velocity.x, after.velocity.x * slide.speed); assert_eq!(velocity.y, after.velocity.y * slide.speed); } } } #[test] fn slide_item_sampling_matches_advance_without_mutating_the_slide() { let mut slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Up); slide.advance(Duration::from_millis(30)); for held in [None, Some(0.35)] { slide.held_progress = held; for delta in [Duration::ZERO, Duration::from_millis(8), Duration::from_millis(400)] { let before = (slide.items[0].start, slide.items[0].velocity); let mut reference = slide.clone(); reference.advance(delta); for (item, advanced) in slide.items.iter().zip(&reference.items) { let (position, velocity) = slide.sample(item, delta); let expected = held.map_or(advanced.start, |p| advanced.start.between(advanced.target, p)); assert_eq!(position, expected); assert_eq!(velocity, advanced.velocity); } assert_eq!((slide.items[0].start, slide.items[0].velocity), before); } } } #[test] fn workspace_reversal_keeps_position_and_velocity() { let from = WorkspaceId(1); let to = WorkspaceId(2); let mut slide = WorkspaceSlide::new(None, from, to, SwipeDirection::Up); slide.advance(Duration::from_millis(40)); let before: Vec<_> = slide .items .iter() .map(|item| (item.workspace, item.start, item.velocity)) .collect(); let reversed = WorkspaceSlide::new(Some(slide), to, from, SwipeDirection::Down); for (id, position, velocity) in before { let item = reversed.items.iter().find(|item| item.workspace == id).unwrap(); assert_eq!(item.start, position); assert_eq!(item.velocity, velocity); assert!(velocity.y < 0.0); } } #[test] fn viewport_limit_uses_resisted_position_and_release_velocity() { let swipe = FocusSwipe { workspace: WorkspaceId(1), from: WindowId(1), to: WindowId(1), direction: Direction::Right, gesture_direction: SwipeDirection::Left, start: 20.0, destination: 20.0, progress: 2.0, layout: None, dependencies: None, }; assert!(swipe.position() > 20.0 && swipe.position() < 84.0); assert!(swipe.release_velocity(0.0, 3.0) > 0.0); let mut released = AnimatedValue { current: swipe.position(), target: swipe.start, velocity: swipe.release_velocity(0.0, 3.0), }; let initial = released; released.advance(Duration::from_micros(1), SpringConfig::default()); assert!((released.current - initial.current - initial.velocity * 1e-6).abs() < 1e-6); released.advance(Duration::from_secs(2), SpringConfig::default()); assert_eq!(released.current, swipe.start); } #[test] fn sampling_slide_progress_matches_advancing_without_mutating_the_slide() { let slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left); for delta in [Duration::ZERO, Duration::from_millis(16), Duration::from_millis(100)] { let mut advanced = slide.clone(); advanced.advance(delta); for (before, after) in slide.items.iter().zip(&advanced.items) { assert_eq!(slide.position(before, delta), after.start); } } } #[test] fn configured_motion_policy_scales_durations_and_reduced_motion_wins() { for (source, milliseconds) in [ ("animations { speed 2; }", 100), ("animations { speed 0.5; }", 400), ("animations { speed 2; reduced-motion #true; }", 0), ("animations { speed 0.5; enabled #false; }", 0), ] { let config = crate::config::Config::parse_source(source).unwrap(); assert_eq!( scaled_animation_duration( Duration::from_millis(200), config.animations_enabled(), config.animation_speed().unwrap() ), Duration::from_millis(milliseconds), ); } } #[test] fn workspace_drag_cancels_when_focus_or_target_ownership_changes() { let mut outputs = OutputWorkspaceMap::default(); let left = OutputId(1); let right = OutputId(2); let from = WorkspaceId(1); let to = WorkspaceId(3); outputs .connect(left, OutputGeometry::new(0, 0, 1000, 1000), from) .unwrap(); outputs .connect(right, OutputGeometry::new(1000, 0, 1000, 1000), WorkspaceId(2)) .unwrap(); assert!(workspace_swipe_is_current(&outputs, left, from, to)); outputs.focus_output(right).unwrap(); assert!(!workspace_swipe_is_current(&outputs, left, from, to)); outputs.focus_output(left).unwrap(); outputs.assign_workspace(right, to).unwrap(); assert!(!workspace_swipe_is_current(&outputs, left, from, to)); assert_eq!(outputs.active_workspace(left), Some(from)); assert_eq!(outputs.output_for_workspace(to), Some(right)); outputs.switch_workspace(left, WorkspaceId(4)).unwrap(); assert!(!workspace_swipe_is_current(&outputs, left, from, WorkspaceId(5))); } #[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.create_workspace(); 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.create_workspace(); 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 focus_drag_reverses_and_settles_from_the_release_position() { for (direction, gesture_direction, destination) in [ (Direction::Right, SwipeDirection::Left, 900.0), (Direction::Left, SwipeDirection::Right, 100.0), ] { for committed in [false, true] { let mut swipe = FocusSwipe { workspace: WorkspaceId(1), from: WindowId(1), to: WindowId(2), direction, gesture_direction, start: 500.0, destination, progress: 0.75, layout: None, dependencies: None, }; let far = swipe.position(); swipe.progress = 0.25; let near = swipe.position(); assert!((near - swipe.start).abs() < (far - swipe.start).abs()); let mut viewport = AnimatedValue::new(near); viewport.set_target(if committed { destination } else { swipe.start }); assert_eq!(viewport.current, near); for _ in 0..300 { viewport.advance(Duration::from_millis(16), SpringConfig::default()); } assert_eq!(viewport.current, viewport.target); } } } #[test] fn fully_visible_columns_still_give_bounded_horizontal_drag_feedback() { for (direction, sign) in [(Direction::Right, 1.0), (Direction::Left, -1.0)] { let swipe = FocusSwipe { workspace: WorkspaceId(1), from: WindowId(1), to: WindowId(2), direction, gesture_direction: SwipeDirection::Left, start: 0.0, destination: 0.0, progress: 0.5, layout: None, dependencies: None, }; assert_eq!(swipe.position(), sign * 32.0); assert_eq!(swipe.destination, 0.0); } } #[test] fn workspace_drag_tracks_progress_and_releases_without_a_jump() { for committed in [false, true] { let from = WorkspaceId(1); let to = WorkspaceId(2); let mut slide = WorkspaceSlide::new(None, from, to, SwipeDirection::Up); slide.gesture = Some((from, to, SwipeDirection::Up)); slide.held_progress = Some(0.4); slide.advance(Duration::from_secs(10)); assert_eq!(slide.offset(from, 1000.0, 1000.0), (0.0, -400.0)); assert_eq!(slide.offset(to, 1000.0, 1000.0), (0.0, 600.0)); slide.release(committed); assert_eq!(slide.offset(from, 1000.0, 1000.0), (0.0, -400.0)); assert_eq!(slide.offset(to, 1000.0, 1000.0), (0.0, 600.0)); assert!(!slide.advance(SLIDE_SETTLE_TEST_BUDGET)); let active = if committed { to } else { from }; assert_eq!(slide.offset(active, 1000.0, 1000.0), (0.0, 0.0)); } } #[test] fn cancelling_a_chained_drag_returns_to_the_active_workspace() { let first = WorkspaceId(1); let second = WorkspaceId(2); let third = WorkspaceId(3); let mut previous = WorkspaceSlide::new(None, first, second, SwipeDirection::Up); previous.advance(Duration::from_millis(40)); let mut slide = WorkspaceSlide::new(Some(previous), second, third, SwipeDirection::Up); slide.gesture = Some((second, third, SwipeDirection::Up)); slide.held_progress = Some(0.25); let position = slide.offset(second, 1000.0, 1000.0); slide.release(false); assert_eq!(slide.offset(second, 1000.0, 1000.0), position); slide.advance(SLIDE_SETTLE_TEST_BUDGET); assert_eq!(slide.offset(second, 1000.0, 1000.0), (0.0, 0.0)); } #[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(Duration::from_millis(50))); 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(SLIDE_SETTLE_TEST_BUDGET)); 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(Duration::from_millis(40)); 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 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, ..SpringConfig::default() }; 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 })); } }