use crate::geometry::Rect; use crate::layout::{Layout, MasterStackLayout, NoOpLayout, TilingConfig}; use crate::monitor::{DisabledMonitor, Monitor, MonitorId, MonitorSplit}; use crate::placement::{PlacementConfig, SmartPlacement, SnapZoneKind, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH}; use crate::rules::WindowRule; use crate::lock_config::LockConfig; use crate::theme::ThemeConfig; use crate::window::{likely_draws_own_titlebar, ResizeEdge, TitlebarHit, Window, WindowId, RESIZE_MARGIN}; use crate::workspace::{Workspace, WorkspaceId}; use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Direction { Left, Right, Up, Down, } /// A whole-screen colour treatment, drawn by each Wayland backend as a /// translucent full-output overlay above every window but below the /// cursor - see `srdwm_wayland::color_filter` for the actual overlay /// colour/alpha each variant maps to, and why an alpha-blended overlay /// rather than a true per-pixel shader was chosen at all. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ColorFilter { #[default] None, /// Warm tint, reduces perceived blue light. Ported from a Hyprland /// `decoration:screen_shader` config that multiplied the framebuffer /// by `vec3(1.0, 0.82, 0.60)`. NightLight, /// Desaturating tint, for reduced visual noise during long-form /// reading. Ported from a Hyprland `decoration:screen_shader` config /// that replaced every pixel with its own luminance (flat grayscale). ReadingMode, } struct DragState { window: WindowId, start_x: i32, start_y: i32, orig: Rect, } struct ResizeState { window: WindowId, edge: ResizeEdge, start_x: i32, start_y: i32, orig: Rect, } /// The platform-independent core of srdwm: owns window/workspace/monitor /// state and layout policy. Backends (X11, Wayland, ...) drive this via /// `add_window`/`remove_window`/input events, and apply the `Rect`s it /// computes back onto real surfaces. pub struct WindowManager { windows: HashMap, order: Vec, focused: Option, monitors: Vec, /// Backend-agnostic "please move this output" requests, queued by /// `request_output_position` (an IPC `set_output_position` dispatch is /// the only caller today) and drained by whichever backend actually /// owns real output hardware (`drain_output_position_requests`) on its /// own next poll. Core has no way to reposition a real `Output` itself /// - monitor geometry flows one direction, backend into core, via /// `set_monitors` - so a request from an IPC caller (an AGS display- /// settings panel wanting to set up monitor mirroring, concretely) has /// to cross back over that boundary the same indirect way window /// geometry changes do in the other direction: queued here, applied by /// the backend, and `set_monitors` reports the result back on the /// backend's next monitor query, same as any other hotplug/reconfigure. output_position_requests: Vec<(MonitorId, i32, i32)>, /// Same cross-boundary-request pattern as `output_position_requests` /// just above, for enable/disable - see `request_output_enabled`'s /// own doc comment for why this is keyed by name, not `MonitorId`. output_enable_requests: Vec<(String, bool)>, /// The opposite direction of `output_enable_requests`: not a request /// *to* the backend, but the backend *reporting* an administratively- /// disabled-but-still-connected output's last-known state, purely for /// listing purposes - see `set_disabled_monitor`'s own doc comment /// for why this deliberately never touches `monitors`/real placement /// at all. disabled_monitors: HashMap, /// `srd.monitor.split(name, parts, direction)` requests, by connector /// name - read by a backend's own `monitors()` query to divide one /// real output's rectangle into several logical `Monitor` entries. See /// [`MonitorSplit`]'s own doc comment for what this deliberately does /// and does not give a client (no new `wl_output`). monitor_splits: HashMap, /// `srd.monitor.scale(name, factor)` requests, by connector name -- /// read once by a backend when it brings a head up (startup, hotplug, /// or re-enable), so a physically large, low-DPI monitor can run /// below `1.0` to show more logical desktop space instead of just /// larger text at the same pixel count. srdwm otherwise always drove /// every real output at a hardcoded `1.0`, with no way to change that /// short of a client speaking wlr-output-management itself. monitor_scales: HashMap, /// Same cross-boundary-request pattern as `output_position_requests` /// just above - core has no way to actually blank the screen and /// start drawing srdwm's own lock UI itself (that's real compositor /// rendering, backend-owned), so an IPC `"lock"` dispatch queues the /// intent here via `request_lock` and whichever backend is running /// drains it (`drain_lock_request`) on its own next poll. lock_requested: bool, /// Same cross-boundary-request pattern as `output_position_requests` /// again - see `capture::CaptureRequest`'s own doc comment for why /// this exists at all (a workspace switcher needing a thumbnail of a /// workspace that isn't the one currently presented, which no Wayland /// screencopy protocol can see). capture_requests: Vec, workspaces: Vec, /// The shared-mode value, used directly when `per_monitor_workspaces` /// is `false` (the default - unlike Hyprland, srdwm's original design /// has no notion of an independent workspace set per monitor; /// switching workspace changes what's visible on every screen at /// once). Still meaningful even when `per_monitor_workspaces` is `true` /// - it's the fallback `workspace_for_monitor` returns for a monitor /// that has never had its own workspace switched independently yet, /// and what a plain `current_workspace()` call reports either way. See /// `visible_windows`'s doc comment for the filter this actually drives. current_workspace: WorkspaceId, /// Whichever workspace was current immediately before the current one /// became current - see `switch_workspace`'s doc comment. previous_workspace: WorkspaceId, /// Read from `workspace.auto_back_and_forth`. When set, switching to /// the workspace that's already active switches to `previous_workspace` /// instead - sway's `workspace_auto_back_and_forth` behavior, a quick /// "jump back to whatever I was just on" toggle on a single keybinding. pub auto_back_and_forth: bool, /// Read from `workspace.per_monitor` - `false` (the default) keeps /// srdwm's original single-shared-workspace design exactly as it was; /// `true` switches to Hyprland/niri-style independent per-monitor /// workspace sets, where each monitor tracks and displays its own /// current workspace, switchable without affecting any other monitor. /// Explicitly requested as a configurable choice, not a hardcoded /// switch to one model or the other - see `workspace_for_monitor` and /// `switch_workspace_on_monitor` for what this actually gates. pub per_monitor_workspaces: bool, /// Read from `monitor.primary_layout`/`monitor.secondary_layout` -- /// validated/defaulted config keys that were never read anywhere /// before (same dead-config shape as `general.default_layout`'s own /// siblings). Empty string means "not set, no override". Applied by /// `set_monitors` to whichever workspace `workspace_for_monitor` /// resolves for each connected monitor - which only ever *differs* /// between monitors when `per_monitor_workspaces` is `true` (every /// monitor shares one workspace otherwise, so a primary/secondary /// split has nothing distinct to apply to and is skipped). pub primary_layout: String, pub secondary_layout: String, /// Each monitor's own current workspace, when `per_monitor_workspaces` /// is `true`. A monitor with no entry here yet (never had its /// workspace switched independently - e.g. right after the mode was /// turned on, or a newly connected monitor) falls back to /// `current_workspace`, the same shared value shared-mode always uses /// - see `workspace_for_monitor`. Unused, and left empty, whenever /// `per_monitor_workspaces` is `false`. monitor_workspaces: HashMap, next_workspace_id: WorkspaceId, next_window_id: WindowId, layouts: HashMap>, pub tiling: TilingConfig, pub placement: PlacementConfig, /// Whether geometry changes made via `toggle_maximize`/`toggle_fullscreen` /// should be animated. Read from `general.animations`; a backend's open /// animation is gated on this too, since core has no notion of "open". pub animations_enabled: bool, /// Tween duration in milliseconds, read from `general.animation_duration`. pub animation_duration_ms: u32, /// Whether windows get a drop shadow. Read from `general.shadows`. A /// maximized or fullscreen window never gets one regardless of this -- /// see the Wayland backend's shadow render call site - so this only /// ever turns it off entirely, not on for those. pub shadows_enabled: bool, /// Width, in pixels, of the resize grab band along a window's edges, /// read from `general.resize_margin`. See [`crate::window::RESIZE_MARGIN`]'s /// doc comment for the default and why it's what it is. pub resize_margin: i32, /// Whether a decorated window's content rounds its bottom two corners /// to match the titlebar's own curve (an undecorated/CSD window rounds /// all four). Read from `general.rounded_corners` - `None` when the /// user's config never touched that key at all (deliberately *not* /// defaulted in `crates/config`, unlike every other `general.*` key), /// so each backend can fall back to its own default rather than one /// baked in here: GLES/winit defaults on, udev/Pixman defaults off /// (an untested-on-real-hardware per-frame CPU cost for content that /// redraws constantly - see `crates/wayland/src/rounded_corners.rs`). /// `Some(_)` only when the user explicitly set it, and wins either way. pub rounded_corners_enabled: Option, /// The whole-screen colour treatment currently active (night light's /// warm tint or reading mode's desaturation), live-settable via `srd /// set night_light`/`srd set reading_mode` - see [`ColorFilter`]. Off /// by default; the two are mutually exclusive by construction (one /// enum, not two independent bools), matching the ported Hyprland /// scripts this replaces, which pointed the same single /// `screen_shader` slot at one file or the other. pub color_filter: ColorFilter, /// Whether hovering a window (no click needed) focuses it, read from /// `general.focus_follows_mouse`. Off by default - matches /// `general.focus_follows_mouse`'s own documented default, and every /// desktop's convention of click-to-focus unless a user explicitly /// opts into the classic X11 sloppy-focus behaviour. pub focus_follows_mouse: bool, /// Whether hover-driven focus (above) also raises the window, not just /// focuses it - read from `general.auto_raise`. Meaningless (never /// consulted) while `focus_follows_mouse` is off, since a plain click /// already raises unconditionally regardless of this. pub auto_raise: bool, /// Default decoration colours and border width, read from `theme.colors.*`/ /// `theme.decorations.*`. See `ThemeConfig`'s own doc comment. pub theme: ThemeConfig, /// Read from `theme.lock.*`. See `LockConfig`'s own doc comment for /// why this isn't just folded into `theme` above. pub lock: LockConfig, drag: Option, resize: Option, rules: Vec, /// Last floating size a user interactively resized each `app_id` to, /// applied to that app's *next* new window instead of the fixed /// 800x600 every backend otherwise hardcodes - see `end_resize` (where /// this is recorded) and `add_window` (where it's read). Keyed by /// `app_id` alone, not per-window: the ask is "my terminal should open /// at the size I last used a terminal at", not per-window-instance /// memory. Only an interactive drag-resize (`end_resize`) updates this /// - not a maximize/fullscreen toggle (that's a separate, temporary /// state with its own `restore_geometry`, not a new "size I want to /// keep using") and not a drag-to-edge snap (a deliberate one-off /// snap to a half/quarter of the screen isn't "the size I'll want my /// next terminal to open at" either). Session-lifetime only, not /// persisted to disk - a real per-app-size-memory feature that /// survives a restart would need a config-file-backed store, which is /// meaningfully more machinery than "remember it while running" asks /// for. remembered_sizes: HashMap, /// Windows a client-close was requested for, drained once per tick by /// `main.rs`'s event loop and forwarded to `Platform::close`. Needed /// because `WindowManager` is platform-agnostic and has no way to send /// a client its close request directly - see `close_window`. close_requests: Vec, /// The active XKB layout's own name (e.g. `"English (US)"`, whatever /// `xkb_keymap_layout_get_name` reports) - set by the platform once at /// startup and again after every `take_keyboard_layout_cycle_requests` /// is acted on. Empty until the platform has reported it at least once /// (a nested/test `WindowManager::new()` with no real keyboard, most /// of core's own tests). Read-only from an external caller's point of /// view (an AGS peer session's keyboard-layout badge, over `srd`); the /// only way to change it is a real layout cycle. pub keyboard_layout: String, /// How many `srd dispatch cycle_keyboard_layout` requests have arrived /// since the last drain - a count, not a flag, so two IPC requests in /// one tick both take effect rather than the second being silently /// swallowed. Same "core records the intent, `main.rs`'s `sync()` /// forwards it to the platform that can actually act on it" shape as /// `close_requests`, for the same reason: `WindowManager` has no real /// keyboard/seat handle of its own to cycle. keyboard_layout_cycle_requests: u32, /// Which monitor the pointer is currently over, as last reported by /// `set_pointer_monitor` - core has no pointer of its own (backend- /// agnostic, same reason `close_requests` exists instead of a direct /// client call), so a real backend's own pointer-motion handler is the /// only thing that can ever know this. `add_window`'s own target- /// monitor fallback chain reads it: a new window already preferred the /// *focused* window's monitor over the primary one (see that fix's own /// doc comment, `add_window`) - correct when something is focused on /// the monitor the user is actually at, but not when nothing is (an /// empty desktop there, or the last-focused window happens to sit on a /// *different* monitor than the one the user is currently pointing at /// while launching something new). Reported live: opening an /// application while on a non-primary monitor with nothing focused /// there still opened it on the primary one. `None` until the first /// real pointer-motion event arrives (matches `focused`'s own `None`- /// until-something-happens shape). pointer_monitor: Option, } impl Default for WindowManager { fn default() -> Self { Self::new() } } impl WindowManager { pub fn new() -> Self { let mut layouts: HashMap> = HashMap::new(); layouts.insert("tiling".into(), Box::new(MasterStackLayout)); layouts.insert("dynamic".into(), Box::new(NoOpLayout("dynamic"))); layouts.insert("floating".into(), Box::new(NoOpLayout("floating"))); Self { windows: HashMap::new(), order: Vec::new(), focused: None, monitors: Vec::new(), output_position_requests: Vec::new(), output_enable_requests: Vec::new(), disabled_monitors: HashMap::new(), monitor_splits: HashMap::new(), monitor_scales: HashMap::new(), lock_requested: false, capture_requests: Vec::new(), // 1-based, not 0-based: workspace ids match the human-visible // numbers (`workspace.names` defaults to "1".."9","0", // `apply_workspace_count` names workspace `i+1` "i+1") - an id // of `0` for the first workspace, with everything display-side // calling it "1", was a standing off-by-one between what a user // types/sees and the id `srd dispatch activate workspace ` // (and AGS's workspace switcher, which sends the same number it // shows) actually has to send. Matches how Hyprland's own // workspace ids already work (natively 1-based, no translation // layer needed) rather than niri's split id/idx or the // 0-based-plus-AGS-side-`+1` scheme this used to be - both // AGS integrations for those two compositors were checked // before choosing this, and neither needs hand-rolled offset // arithmetic the way srdwm's old 0-based ids forced `lib/ // srdwm.ts` to. // // Rolling this out requires `crates/config`'s shipped default, // this user's own `~/.config/srd/keybindings.lua`, and AGS's // `lib/srdwm.ts`/`service/wsPreview.ts` to all agree with core // at the same time - they cannot update atomically with a // single srdwm restart, so whichever of AGS/srdwm is running // the *other* scheme during that window will visibly // misbehave (confirmed live: AGS's Overview padding // `workspace.count` slots and matching real workspaces onto // them by id showed one extra/unmatched slot while AGS's own // code had already been updated to assume 1-based ids but the // live srdwm process was still 0-based). AGS's side is // deliberately reverted back to its old `+1` offset for now, // matching the still-running old build, and must be re-applied // in the same breath as the next real srdwm restart - not // before. workspaces: vec![Workspace::new(1, "1", "dynamic")], current_workspace: 1, previous_workspace: 1, auto_back_and_forth: false, per_monitor_workspaces: false, primary_layout: String::new(), secondary_layout: String::new(), monitor_workspaces: HashMap::new(), next_workspace_id: 2, next_window_id: 1, layouts, tiling: TilingConfig::default(), placement: PlacementConfig::default(), animations_enabled: true, animation_duration_ms: 200, shadows_enabled: true, resize_margin: RESIZE_MARGIN, rounded_corners_enabled: None, color_filter: ColorFilter::None, focus_follows_mouse: false, auto_raise: false, theme: ThemeConfig::default(), lock: LockConfig::default(), drag: None, resize: None, rules: Vec::new(), remembered_sizes: HashMap::new(), close_requests: Vec::new(), keyboard_layout: String::new(), keyboard_layout_cycle_requests: 0, pointer_monitor: None, } } /// Registers a window rule; on every subsequent `add_window`, the first /// rule whose matcher matches the new window has its actions applied. pub fn add_rule(&mut self, rule: WindowRule) { self.rules.push(rule); } pub fn register_layout(&mut self, name: impl Into, layout: Box) { self.layouts.insert(name.into(), layout); } pub fn available_layouts(&self) -> Vec<&str> { self.layouts.keys().map(String::as_str).collect() } } mod capture; mod dragresize; mod focus; mod hittest; mod layout; mod lock; mod monitors; mod windows; mod winops; mod workspaces; pub use capture::CaptureRequest; #[cfg(test)] mod tests;