Files
feresaria/compositor/src/state.rs
T
Samuel Onoja 73f21e20ce general improvement and bug fixes, cursor scaling, DMA-BUF routing and nested backend recovery
ignore performance-baseline.md

more improvement and code quality fixes
2026-10-04 18:33:05 +01:00

1646 lines
65 KiB
Rust

mod animation;
mod capture_privacy;
mod floating;
mod focus;
mod hit_testing;
mod layout;
mod navigation;
mod outputs;
mod reconciliation;
#[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 window_registry;
mod windows;
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::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::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,
}
}
}
struct FocusSwipe {
workspace: WorkspaceId,
from: WindowId,
to: WindowId,
direction: Direction,
gesture_direction: SwipeDirection,
start: f64,
destination: f64,
progress: f64,
layout: Option<ferese_layout::ScrollingLayout>,
dependencies: Option<presentation_dependencies::ViewportDependencies>,
}
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<WorkspaceSlideItem>,
spring: SpringConfig,
held_progress: Option<f64>,
gesture: Option<(WorkspaceId, WorkspaceId, SwipeDirection)>,
}
impl WorkspaceSlide {
fn new(previous: Option<Self>, 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::<Vec<_>>()
})
.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) session_lock_state: SessionLockManagerState,
pub(crate) session_lock: Lock,
pub(crate) lock_idle: IdleSettings,
pub(crate) config_source: Option<String>,
pub(crate) config_sections: Option<serde_json::Value>,
pub(crate) config_worker: Option<crate::reload::Worker>,
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<Window>,
pub workspaces: WorkspaceSet,
pub output_workspaces: OutputWorkspaceMap,
pub(crate) desktop_transition: Option<DesktopTransition>,
pub(crate) output_ids: HashMap<Output, OutputId>,
outputs_by_id: HashMap<OutputId, Output>,
pub(crate) output_names: HashMap<OutputId, String>,
output_identity_ids: HashMap<String, OutputId>,
pub(crate) windows: WindowRegistry<Window>,
pub(crate) render: crate::render::RenderResources,
pub(crate) capture_render: crate::render::RenderResources,
pub(crate) capture_epoch: u64,
capture_protected_windows: HashSet<WindowId>,
capture_protected_cursor: Option<WlSurface>,
pub(crate) daemons: Option<std::rc::Rc<std::cell::RefCell<crate::daemon::Runner>>>,
pub(crate) display_presentation: crate::display_presentation::DisplayPresentation,
pub(crate) nested_backend: Option<NestedBackend>,
pub(crate) wallpaper: WallpaperState,
window_stack: WindowStack,
stacking_cache: StackingCache<Window>,
floating_above_fullscreen: HashMap<WindowId, WindowId>,
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<u32>,
pub(crate) logout_query: Option<u32>,
pub(crate) logout_owner: Option<ObjectId>,
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
presentation_dependencies: presentation_dependencies::PresentationDependencies,
#[cfg(feature = "resize-metrics")]
pub(crate) resize_metrics: crate::resize_metrics::ResizeMetrics,
focus_swipe: Option<FocusSwipe>,
workspace_slides: HashMap<OutputId, WorkspaceSlide>,
workspace_slide_offsets: HashMap<WorkspaceId, (f64, f64)>,
pub focused_window: Option<WindowId>,
pub(crate) focus_history: ferese_core::FocusHistory,
pub(crate) focus_cycle: Option<ferese_core::FocusCycle>,
column_width_presets: Vec<ColumnWidth>,
gap_config: GapConfig,
pub(crate) input_settings: InputSettings,
pub(crate) bindings: BindingSet,
workspace_auto_back_and_forth: bool,
pub(crate) window_rules: Vec<WindowRule>,
pub(crate) theme_settings: ThemeSettings,
pub(crate) inactive_dim: crate::config::InactiveDimSettings,
animations_enabled: bool,
animation_speed: f64,
pub(crate) spring_config: SpringConfig,
viewport_spring_config: SpringConfig,
pub(crate) output_profiles: Vec<OutputProfile>,
pub(crate) autostart: Vec<DaemonConfig>,
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<Output>,
pub(crate) locked_pointer_hint: Option<LockedPointerHint>,
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<CursorIcon, NamedCursor>,
pub intercepted_keys: HashSet<Keycode>,
pub(crate) swipe: Swipe,
pub idle_inhibitors: HashMap<WlSurface, usize>,
pub(crate) idle_inhibit: crate::idle_inhibition::Settings,
pub(crate) media_players: Vec<crate::idle_inhibition::media::Player>,
pub(crate) automatic_idle_inhibited: bool,
pub active_shortcuts_inhibitor: Option<KeyboardShortcutsInhibitor>,
pub direct_backend: Option<DirectBackendState>,
_ipc_socket: Option<IpcSocketGuard>,
pub(crate) dmabuf_imports: crate::dmabuf_imports::ImportManager,
pub(crate) activation_inputs: crate::handlers::activation::InputHistory,
pub(crate) pending_screencopies: Vec<PendingScreencopy>,
// 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<PartSender>,
pub(crate) screenshot_worker: Option<Worker>,
pub(crate) shell_resources: Vec<Weak<FereseShellV1>>,
pub(crate) shell_snapshot_serial: u32,
pub(crate) last_shell_snapshot: Option<ShellSnapshot>,
pub(crate) overview: OverviewState,
next_output_id: u64,
last_animation_tick: Instant,
#[cfg(test)]
animation_test_time: Option<Duration>,
pub popups: PopupManager,
pub(crate) dismissing_popups: Vec<(WlSurface, PopupKind, DimAnimation)>,
pub seat: Seat<Self>,
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<Self>,
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<Self>,
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) lock_idle: IdleSettings,
pub(crate) idle_inhibit: crate::idle_inhibition::Settings,
pub(crate) autostart: Vec<DaemonConfig>,
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<Binding>,
pub workspace_auto_back_and_forth: bool,
pub window_rules: Vec<WindowRule>,
pub theme_settings: ThemeSettings,
pub inactive_dim: crate::config::InactiveDimSettings,
pub default_column_width: ColumnWidth,
pub scrolling_focus_strategy: ViewportFocusStrategy,
pub column_width_presets: Vec<ColumnWidth>,
pub animations_enabled: bool,
pub animation_speed: f64,
pub spring_config: SpringConfig,
pub viewport_spring_config: SpringConfig,
pub output_profiles: Vec<OutputProfile>,
}
impl Ferese {
pub fn new(
event_loop: &mut EventLoop<'static, Self>,
display: Display<Self>,
config: RuntimeConfig,
) -> Result<Self, Box<dyn Error>> {
let display_handle = display.handle();
crate::handlers::screencopy::init_global(&display_handle, &event_loop.handle());
crate::effects::init_global(&display_handle);
crate::shell_control::init_global(&display_handle);
let alpha_modifier_state = AlphaModifierState::new::<Self>(&display_handle);
let compositor_state = CompositorState::new_v6::<Self>(&display_handle);
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
let xdg_shell_state = XdgShellState::new::<Self>(&display_handle);
let decoration_state = XdgDecorationState::new::<Self>(&display_handle);
let dmabuf_state = DmabufState::new();
let fractional_scale_state = FractionalScaleManagerState::new::<Self>(&display_handle);
let idle_inhibit_state = IdleInhibitManagerState::new::<Self>(&display_handle);
let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.handle());
let session_lock_state =
smithay::wayland::session_lock::SessionLockManagerState::new::<Self, _>(&display_handle, |_| true);
let input_method_enabled = std::env::var_os("FERESE_ENABLE_INPUT_METHOD").is_some_and(|value| value == "1");
let input_method_manager_state =
InputMethodManagerState::new::<Self, _>(&display_handle, move |_| input_method_enabled);
let keyboard_shortcuts_inhibit_state = KeyboardShortcutsInhibitState::new::<Self>(&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::<Self>(keyboard_shortcuts_inhibit_state.global());
}
let shm_state = ShmState::new::<Self>(&display_handle, Vec::new());
let single_pixel_buffer_state = SinglePixelBufferState::new::<Self>(&display_handle);
let text_input_manager_state = TextInputManagerState::new::<Self>(&display_handle);
let output_manager_state = OutputManagerState::new_with_xdg_output::<Self>(&display_handle);
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle);
let presentation_state = PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);
let data_device_state = DataDeviceState::new::<Self>(&display_handle);
let primary_selection_state = PrimarySelectionState::new::<Self>(&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::<Self, _>(&display_handle, Some(&primary_selection_state), |_| {
true
});
let ext_data_control_state =
ext_data_control::DataControlState::new::<Self, _>(&display_handle, Some(&primary_selection_state), |_| {
true
});
let relative_pointer_state = RelativePointerManagerState::new::<Self>(&display_handle);
let viewporter_state = ViewporterState::new::<Self>(&display_handle);
let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
let xdg_activation_state = XdgActivationState::new::<Self>(&display_handle);
let xdg_foreign_state = XdgForeignState::new::<Self>(&display_handle);
let xdg_toplevel_icon_manager = XdgToplevelIconManager::new::<Self>(&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 state = Self {
session_lock_state,
session_lock: crate::session_lock::Lock::default(),
lock_idle: config.lock_idle,
config_source: None,
config_sections: None,
config_worker: 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(),
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,
viewport_animations: HashMap::new(),
presentation_dependencies: Default::default(),
#[cfg(feature = "resize-metrics")]
resize_metrics: crate::resize_metrics::ResizeMetrics::default(),
focus_swipe: None,
workspace_slides: HashMap::new(),
workspace_slide_offsets: HashMap::new(),
focused_window: None,
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(),
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
window_rules: config.window_rules,
theme_settings: config.theme_settings,
inactive_dim: config.inactive_dim,
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(),
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,
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)?;
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);
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 layout_changed = changed(&["layout", "scrolling", "workspaces"]);
let bindings_changed = changed(&["bindings", "commands"]);
let input_changed = changed(&["input"]);
let daemons_changed = changed(&["autostart"]);
let lock_changed = changed(&["lock_screen"]);
let non_output_scene_changed = 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)?;
}
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();
}
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 layout_changed {
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;
if bindings_changed {
self.bindings = config.bindings.into();
}
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.inactive_dim = config.inactive_dim;
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<Self>,
event_loop: &mut EventLoop<'static, Self>,
) -> Result<OsString, Box<dyn Error>> {
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<smithay::utils::Serial>,
acknowledged: Option<smithay::utils::Serial>,
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<ClientSize> {
// 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<Item = &'a T>,
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(), &current, |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(), &current, |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
}));
}
}