2023-08-29 13:49:41 -07:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
2025-10-16 18:53:57 +02:00
|
|
|
use cosmic_config::{CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry};
|
2023-08-29 13:49:41 -07:00
|
|
|
use serde::{Deserialize, Serialize};
|
2023-12-20 16:52:44 -08:00
|
|
|
use std::collections::HashMap;
|
2026-09-12 08:59:31 +02:00
|
|
|
use std::time::Duration;
|
2023-08-29 13:49:41 -07:00
|
|
|
|
2025-08-20 16:49:08 +02:00
|
|
|
use crate::input::TouchpadOverride;
|
|
|
|
|
|
2023-08-29 13:49:41 -07:00
|
|
|
pub mod input;
|
2025-08-19 16:51:37 -04:00
|
|
|
#[cfg(feature = "output")]
|
2025-01-31 14:13:33 -08:00
|
|
|
pub mod output;
|
2023-09-07 13:28:08 -07:00
|
|
|
pub mod workspace;
|
2023-08-29 13:49:41 -07:00
|
|
|
|
2025-08-26 19:32:35 -04:00
|
|
|
#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
|
|
|
pub struct EdidProduct {
|
|
|
|
|
pub manufacturer: [char; 3],
|
|
|
|
|
pub product: u16,
|
|
|
|
|
pub serial: Option<u32>,
|
|
|
|
|
pub manufacture_week: i32,
|
|
|
|
|
pub manufacture_year: i32,
|
|
|
|
|
pub model_year: Option<i32>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "libdisplay-info")]
|
|
|
|
|
impl From<libdisplay_info::edid::VendorProduct> for EdidProduct {
|
|
|
|
|
fn from(vp: libdisplay_info::edid::VendorProduct) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
manufacturer: vp.manufacturer,
|
|
|
|
|
product: vp.product,
|
|
|
|
|
serial: vp.serial,
|
|
|
|
|
manufacture_week: vp.manufacture_week,
|
|
|
|
|
manufacture_year: vp.manufacture_year,
|
|
|
|
|
model_year: vp.model_year,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 05:35:22 -08:00
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
pub struct KeyboardConfig {
|
|
|
|
|
/// Boot state for numlock
|
|
|
|
|
pub numlock_state: NumlockState,
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-17 08:30:47 -08:00
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
2025-02-12 05:35:22 -08:00
|
|
|
pub enum NumlockState {
|
|
|
|
|
BootOn,
|
|
|
|
|
#[default]
|
|
|
|
|
BootOff,
|
|
|
|
|
LastBoot,
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 16:43:19 +01:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
2025-12-08 18:21:58 +01:00
|
|
|
pub struct AppearanceConfig {
|
|
|
|
|
pub clip_floating_windows: bool,
|
|
|
|
|
pub clip_tiled_windows: bool,
|
|
|
|
|
pub shadow_tiled_windows: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 16:43:19 +01:00
|
|
|
impl Default for AppearanceConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
AppearanceConfig {
|
|
|
|
|
clip_floating_windows: true,
|
|
|
|
|
clip_tiled_windows: true,
|
|
|
|
|
shadow_tiled_windows: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 18:25:41 +02:00
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum DecorationPreference {
|
|
|
|
|
#[default]
|
|
|
|
|
ClientSide,
|
|
|
|
|
ServerSide,
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-08 14:25:18 -05:00
|
|
|
#[derive(Clone, Debug, PartialEq, CosmicConfigEntry)]
|
|
|
|
|
#[version = 1]
|
2023-12-20 16:52:44 -08:00
|
|
|
pub struct CosmicCompConfig {
|
|
|
|
|
pub workspaces: workspace::WorkspaceConfig,
|
2025-01-31 14:13:33 -08:00
|
|
|
pub pinned_workspaces: Vec<workspace::PinnedWorkspace>,
|
2023-12-20 16:52:44 -08:00
|
|
|
pub input_default: input::InputConfig,
|
|
|
|
|
pub input_touchpad: input::InputConfig,
|
2025-08-20 16:49:08 +02:00
|
|
|
pub input_touchpad_override: TouchpadOverride,
|
2023-12-20 16:52:44 -08:00
|
|
|
pub input_devices: HashMap<String, input::InputConfig>,
|
|
|
|
|
pub xkb_config: XkbConfig,
|
2025-02-12 05:35:22 -08:00
|
|
|
pub keyboard_config: KeyboardConfig,
|
2024-02-08 14:25:18 -05:00
|
|
|
/// Autotiling enabled
|
|
|
|
|
pub autotile: bool,
|
|
|
|
|
/// Determines the behavior of the autotile variable
|
|
|
|
|
/// If set to Global, autotile applies to all windows in all workspaces
|
|
|
|
|
/// If set to PerWorkspace, autotile only applies to new windows, and new workspaces
|
|
|
|
|
pub autotile_behavior: TileBehavior,
|
|
|
|
|
/// Active hint enabled
|
|
|
|
|
pub active_hint: bool,
|
2024-09-04 11:13:59 -05:00
|
|
|
/// Enables changing keyboard focus to windows when the cursor passes into them
|
|
|
|
|
pub focus_follows_cursor: bool,
|
|
|
|
|
/// Enables warping the cursor to the focused window when focus changes due to keyboard input
|
|
|
|
|
pub cursor_follows_focus: bool,
|
|
|
|
|
/// The delay in milliseconds before focus follows mouse (if enabled)
|
|
|
|
|
pub focus_follows_cursor_delay: u64,
|
2024-08-23 18:26:08 +02:00
|
|
|
/// Let X11 applications scale themselves
|
2025-04-04 18:49:45 +02:00
|
|
|
pub descale_xwayland: XwaylandDescaling,
|
2025-03-28 17:45:28 +01:00
|
|
|
/// Let X11 applications snoop on certain key-presses to allow for global shortcuts
|
|
|
|
|
pub xwayland_eavesdropping: XwaylandEavesdropping,
|
2025-02-14 21:58:09 +11:00
|
|
|
/// The threshold before windows snap themselves to output edges
|
|
|
|
|
pub edge_snap_threshold: u32,
|
2025-01-24 17:49:13 +01:00
|
|
|
pub accessibility_zoom: ZoomConfig,
|
2025-12-08 18:21:58 +01:00
|
|
|
pub appearance_settings: AppearanceConfig,
|
2026-09-12 08:59:31 +02:00
|
|
|
/// When the cursor hides itself: idle, fullscreen idle, typing, touch
|
|
|
|
|
pub cursor_hide: CursorHideConfig,
|
2026-07-20 16:11:13 -06:00
|
|
|
/// Briefly magnify the cursor when the pointer is shaken, to help locate it
|
|
|
|
|
pub cursor_shake_to_find: bool,
|
2026-05-22 17:06:49 +02:00
|
|
|
pub activation_policy: ActivationPolicy,
|
2026-08-14 18:25:41 +02:00
|
|
|
pub decoration_preference: DecorationPreference,
|
2024-02-08 14:25:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for CosmicCompConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
workspaces: Default::default(),
|
2025-01-31 14:13:33 -08:00
|
|
|
pinned_workspaces: Vec::new(),
|
2024-02-08 14:25:18 -05:00
|
|
|
input_default: Default::default(),
|
2024-07-31 06:04:08 +02:00
|
|
|
// By default, enable tap-to-click and disable-while-typing.
|
|
|
|
|
input_touchpad: input::InputConfig {
|
|
|
|
|
state: input::DeviceState::Enabled,
|
|
|
|
|
click_method: Some(input::ClickMethod::Clickfinger),
|
|
|
|
|
disable_while_typing: Some(true),
|
|
|
|
|
tap_config: Some(input::TapConfig {
|
|
|
|
|
enabled: true,
|
|
|
|
|
button_map: Some(input::TapButtonMap::LeftRightMiddle),
|
|
|
|
|
drag: true,
|
|
|
|
|
drag_lock: false,
|
|
|
|
|
}),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
2025-08-20 16:49:08 +02:00
|
|
|
input_touchpad_override: Default::default(),
|
2024-02-08 14:25:18 -05:00
|
|
|
input_devices: Default::default(),
|
|
|
|
|
xkb_config: Default::default(),
|
2025-02-12 05:35:22 -08:00
|
|
|
keyboard_config: Default::default(),
|
2024-02-08 14:25:18 -05:00
|
|
|
autotile: Default::default(),
|
|
|
|
|
autotile_behavior: Default::default(),
|
|
|
|
|
active_hint: true,
|
2024-09-04 11:13:59 -05:00
|
|
|
focus_follows_cursor: false,
|
|
|
|
|
cursor_follows_focus: false,
|
|
|
|
|
focus_follows_cursor_delay: 250,
|
2025-04-04 18:49:45 +02:00
|
|
|
descale_xwayland: XwaylandDescaling::Fractional,
|
2025-03-28 17:45:28 +01:00
|
|
|
xwayland_eavesdropping: XwaylandEavesdropping::default(),
|
2025-02-14 21:58:09 +11:00
|
|
|
edge_snap_threshold: 0,
|
2025-01-24 17:49:13 +01:00
|
|
|
accessibility_zoom: ZoomConfig::default(),
|
2025-12-08 18:21:58 +01:00
|
|
|
appearance_settings: AppearanceConfig::default(),
|
2026-09-12 08:59:31 +02:00
|
|
|
cursor_hide: CursorHideConfig::default(),
|
2026-07-20 16:11:13 -06:00
|
|
|
cursor_shake_to_find: true,
|
2026-05-22 17:06:49 +02:00
|
|
|
activation_policy: ActivationPolicy::default(),
|
2026-08-14 18:25:41 +02:00
|
|
|
decoration_preference: DecorationPreference::default(),
|
2024-02-08 14:25:18 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize)]
|
|
|
|
|
pub enum TileBehavior {
|
|
|
|
|
#[default]
|
|
|
|
|
Global,
|
|
|
|
|
PerWorkspace,
|
2023-12-20 16:52:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
2023-08-29 13:49:41 -07:00
|
|
|
pub struct XkbConfig {
|
|
|
|
|
pub rules: String,
|
|
|
|
|
pub model: String,
|
|
|
|
|
pub layout: String,
|
|
|
|
|
pub variant: String,
|
|
|
|
|
pub options: Option<String>,
|
2024-05-19 18:19:43 +02:00
|
|
|
#[serde(default = "default_repeat_delay")]
|
|
|
|
|
pub repeat_delay: u32,
|
|
|
|
|
#[serde(default = "default_repeat_rate")]
|
|
|
|
|
pub repeat_rate: u32,
|
2023-08-29 13:49:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for XkbConfig {
|
|
|
|
|
fn default() -> XkbConfig {
|
|
|
|
|
XkbConfig {
|
|
|
|
|
rules: String::new(),
|
|
|
|
|
model: String::new(),
|
|
|
|
|
layout: String::new(),
|
|
|
|
|
variant: String::new(),
|
|
|
|
|
options: None,
|
2024-05-19 18:19:43 +02:00
|
|
|
repeat_delay: default_repeat_delay(),
|
|
|
|
|
repeat_rate: default_repeat_rate(),
|
2023-08-29 13:49:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-19 18:19:43 +02:00
|
|
|
|
|
|
|
|
fn default_repeat_rate() -> u32 {
|
|
|
|
|
25
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_repeat_delay() -> u32 {
|
|
|
|
|
600
|
|
|
|
|
}
|
2025-01-24 17:49:13 +01:00
|
|
|
|
2026-09-12 08:59:31 +02:00
|
|
|
/// What the cursor idle timer should do when it fires.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum HideDecision {
|
|
|
|
|
/// Nothing applies in this context; drop the timer until the next activity.
|
|
|
|
|
Drop,
|
|
|
|
|
/// Hide the cursor now.
|
|
|
|
|
Hide,
|
|
|
|
|
/// Not yet — re-arm for this long.
|
|
|
|
|
RearmAfter(Duration),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// When the cursor hides itself. Every trigger is revealed by pointer input, so
|
|
|
|
|
/// these differ only in what arms the hide and after how long.
|
2026-09-12 09:47:19 +02:00
|
|
|
// `default`: a trigger added later must not make every existing file fail to
|
|
|
|
|
// deserialize, which `get_entry` would swallow as "reset everyone to defaults".
|
2026-09-12 08:59:31 +02:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
2026-09-12 09:47:19 +02:00
|
|
|
#[serde(default)]
|
2026-09-12 08:59:31 +02:00
|
|
|
pub struct CursorHideConfig {
|
|
|
|
|
/// Seconds of pointer inactivity before hiding, anywhere. `None` disables.
|
|
|
|
|
pub idle_timeout: Option<u32>,
|
|
|
|
|
/// Seconds of pointer inactivity before hiding over a fullscreen window.
|
|
|
|
|
/// Shortens `idle_timeout` in fullscreen; it never lengthens it.
|
|
|
|
|
pub fullscreen_idle_timeout: Option<u32>,
|
|
|
|
|
/// Hide as soon as a key is pressed.
|
|
|
|
|
pub while_typing: bool,
|
|
|
|
|
/// Hide on touch input, until the pointer next moves.
|
|
|
|
|
pub after_touch: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CursorHideConfig {
|
|
|
|
|
/// The shortest timeout that applies right now, if any.
|
|
|
|
|
pub fn effective_timeout(&self, fullscreen: bool) -> Option<Duration> {
|
|
|
|
|
let secs = if fullscreen {
|
|
|
|
|
match (self.idle_timeout, self.fullscreen_idle_timeout) {
|
|
|
|
|
(Some(a), Some(b)) => Some(a.min(b)),
|
|
|
|
|
(a, b) => a.or(b),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.idle_timeout
|
|
|
|
|
}?;
|
|
|
|
|
Some(Duration::from_secs(secs as u64))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What to do when the idle timer fires after `elapsed` without pointer input.
|
|
|
|
|
pub fn resolve(&self, elapsed: Duration, fullscreen: bool) -> HideDecision {
|
|
|
|
|
match self.effective_timeout(fullscreen) {
|
|
|
|
|
None => HideDecision::Drop,
|
|
|
|
|
Some(timeout) if elapsed >= timeout => HideDecision::Hide,
|
|
|
|
|
Some(timeout) => HideDecision::RearmAfter(timeout - elapsed),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-12 09:48:09 +02:00
|
|
|
/// How long to wait before the idle timer should next fire, given how long
|
|
|
|
|
/// it has been since the last pointer event. Pessimistic: fullscreen only
|
|
|
|
|
/// ever shortens a timeout, so arming as if fullscreen cannot fire too
|
|
|
|
|
/// late, and `resolve` corrects an early fire once the context is known.
|
|
|
|
|
pub fn arm_delay(&self, since_pointer_activity: Duration) -> Option<Duration> {
|
|
|
|
|
Some(
|
|
|
|
|
self.effective_timeout(true)?
|
|
|
|
|
.saturating_sub(since_pointer_activity),
|
|
|
|
|
)
|
2026-09-12 08:59:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for CursorHideConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
CursorHideConfig {
|
|
|
|
|
idle_timeout: None,
|
|
|
|
|
fullscreen_idle_timeout: Some(3),
|
|
|
|
|
while_typing: false,
|
|
|
|
|
after_touch: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 17:49:13 +01:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
|
|
|
|
pub struct ZoomConfig {
|
|
|
|
|
pub start_on_login: bool,
|
2025-03-25 14:38:35 +01:00
|
|
|
pub show_overlay: bool,
|
2025-01-24 17:49:13 +01:00
|
|
|
pub increment: u32,
|
|
|
|
|
pub view_moves: ZoomMovement,
|
2025-03-25 14:42:20 +01:00
|
|
|
pub enable_mouse_zoom_shortcuts: bool,
|
2025-01-24 17:49:13 +01:00
|
|
|
}
|
|
|
|
|
|
a11y: magnifier: change zoom stepping to multiplicative, bound max, add presets
1. Change magnifier zoom stepping from additive to multiplicative,
because zoom is a ratio, not a difference, and human perception is
logarithmic (browser zoom steps, map zoom levels, microscope powers).
2. Add `pub const ZoomConfig::ZOOM_INCREMENT_PRESETS` with two
additional presets: 10% (macOS, Cinnamon) and 75% (for users at
70-90x zoom).
3. Bound the zoom level to a generous maximum, beyond what anyone is expected
to need, to prevent runaway / unrecoverable levels (#1230).
Full background and rationale at #2615.
I used AI to assist with research and issue writeup. Code is 100% mine.
- Closes #2615.
- Fixes #1239.
- Partially addresses #1230 and #1237.
2026-07-22 00:50:14 -06:00
|
|
|
impl ZoomConfig {
|
|
|
|
|
pub const ZOOM_INCREMENT_PRESETS: &[u32] = &[10, 25, 50, 75, 100, 150, 200];
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 17:49:13 +01:00
|
|
|
impl Default for ZoomConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
ZoomConfig {
|
|
|
|
|
start_on_login: false,
|
2025-03-25 14:38:35 +01:00
|
|
|
show_overlay: true,
|
2025-02-14 19:17:36 +01:00
|
|
|
increment: 50,
|
|
|
|
|
view_moves: ZoomMovement::Continuously,
|
2025-03-25 14:42:20 +01:00
|
|
|
enable_mouse_zoom_shortcuts: true,
|
2025-01-24 17:49:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
|
|
|
|
pub enum ZoomMovement {
|
|
|
|
|
OnEdge,
|
|
|
|
|
Centered,
|
|
|
|
|
Continuously,
|
|
|
|
|
}
|
2025-03-28 17:45:28 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
|
|
|
|
pub struct XwaylandEavesdropping {
|
|
|
|
|
pub keyboard: EavesdroppingKeyboardMode,
|
|
|
|
|
pub pointer: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
|
|
|
|
pub enum EavesdroppingKeyboardMode {
|
2025-04-04 14:29:29 -06:00
|
|
|
#[default]
|
2025-03-28 17:45:28 +01:00
|
|
|
None,
|
|
|
|
|
Modifiers,
|
|
|
|
|
Combinations,
|
|
|
|
|
All,
|
|
|
|
|
}
|
2025-04-04 18:49:45 +02:00
|
|
|
|
2026-05-22 17:06:49 +02:00
|
|
|
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
|
|
|
|
|
pub enum ActivationPolicy {
|
|
|
|
|
#[default]
|
|
|
|
|
Focus,
|
|
|
|
|
FocusIfActiveWorkspace,
|
|
|
|
|
Urgent,
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 18:49:45 +02:00
|
|
|
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum XwaylandDescaling {
|
|
|
|
|
#[serde(rename = "true")]
|
|
|
|
|
Enabled,
|
|
|
|
|
#[serde(rename = "false")]
|
|
|
|
|
Disabled,
|
|
|
|
|
#[default]
|
|
|
|
|
Fractional,
|
|
|
|
|
}
|
2026-09-12 08:59:31 +02:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test {
|
|
|
|
|
use super::{CursorHideConfig, HideDecision};
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
const OFF: CursorHideConfig = CursorHideConfig {
|
|
|
|
|
idle_timeout: None,
|
|
|
|
|
fullscreen_idle_timeout: None,
|
|
|
|
|
while_typing: false,
|
|
|
|
|
after_touch: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn secs(n: u64) -> Duration {
|
|
|
|
|
Duration::from_secs(n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn no_timeout_never_hides() {
|
|
|
|
|
assert_eq!(OFF.resolve(secs(600), false), HideDecision::Drop);
|
|
|
|
|
assert_eq!(OFF.resolve(secs(600), true), HideDecision::Drop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn global_timeout_applies_everywhere() {
|
|
|
|
|
let cfg = CursorHideConfig {
|
|
|
|
|
idle_timeout: Some(10),
|
|
|
|
|
..OFF
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
cfg.resolve(secs(4), false),
|
|
|
|
|
HideDecision::RearmAfter(secs(6))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(cfg.resolve(secs(10), false), HideDecision::Hide);
|
|
|
|
|
// A fullscreen window must not suppress an enabled global timeout.
|
|
|
|
|
assert_eq!(cfg.resolve(secs(10), true), HideDecision::Hide);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn fullscreen_timeout_only_applies_in_fullscreen() {
|
|
|
|
|
let cfg = CursorHideConfig {
|
|
|
|
|
fullscreen_idle_timeout: Some(3),
|
|
|
|
|
..OFF
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(cfg.resolve(secs(3), true), HideDecision::Hide);
|
|
|
|
|
assert_eq!(cfg.resolve(secs(600), false), HideDecision::Drop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn fullscreen_shortens_but_never_lengthens() {
|
|
|
|
|
let short_fs = CursorHideConfig {
|
|
|
|
|
idle_timeout: Some(10),
|
|
|
|
|
fullscreen_idle_timeout: Some(3),
|
|
|
|
|
..OFF
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(short_fs.resolve(secs(3), true), HideDecision::Hide);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
short_fs.resolve(secs(3), false),
|
|
|
|
|
HideDecision::RearmAfter(secs(7))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let long_fs = CursorHideConfig {
|
|
|
|
|
idle_timeout: Some(3),
|
|
|
|
|
fullscreen_idle_timeout: Some(30),
|
|
|
|
|
..OFF
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(long_fs.resolve(secs(3), true), HideDecision::Hide);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn boundary_is_inclusive() {
|
|
|
|
|
let cfg = CursorHideConfig {
|
|
|
|
|
idle_timeout: Some(5),
|
|
|
|
|
..OFF
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
cfg.resolve(Duration::from_millis(4999), false),
|
|
|
|
|
HideDecision::RearmAfter(Duration::from_millis(1))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(cfg.resolve(secs(5), false), HideDecision::Hide);
|
|
|
|
|
assert_eq!(cfg.resolve(secs(6), false), HideDecision::Hide);
|
|
|
|
|
}
|
2026-09-12 09:48:09 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn arm_delay_measures_from_the_last_pointer_event() {
|
|
|
|
|
let cfg = CursorHideConfig {
|
|
|
|
|
idle_timeout: Some(10),
|
|
|
|
|
..OFF
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(cfg.arm_delay(Duration::ZERO), Some(secs(10)));
|
|
|
|
|
assert_eq!(cfg.arm_delay(secs(4)), Some(secs(6)));
|
|
|
|
|
// Exactly due, and past due: fire immediately rather than deferring.
|
|
|
|
|
assert_eq!(cfg.arm_delay(secs(10)), Some(Duration::ZERO));
|
|
|
|
|
assert_eq!(cfg.arm_delay(secs(600)), Some(Duration::ZERO));
|
|
|
|
|
|
|
|
|
|
// Armed against the shortest timeout that any context could want.
|
|
|
|
|
let fs = CursorHideConfig {
|
|
|
|
|
idle_timeout: Some(10),
|
|
|
|
|
fullscreen_idle_timeout: Some(3),
|
|
|
|
|
..OFF
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(fs.arm_delay(secs(1)), Some(secs(2)));
|
|
|
|
|
|
|
|
|
|
assert_eq!(OFF.arm_delay(Duration::ZERO), None);
|
|
|
|
|
}
|
2026-09-12 08:59:31 +02:00
|
|
|
}
|