window management library
git clone https://git.lucas.co/cce-window-manager.git
src/api.rs (16.3K)
1 // The Policy / Compositor trait boundary — snapshot-style.
2 //
3 // `Policy` is implemented policy-side (`actions::DefaultPolicy`): its methods
4 // take a plain-data snapshot the mechanism captured at dispatch time and
5 // return `Command`s — the same snapshot → plan convention as the arrange
6 // pass, with the mechanism owning all state. `Compositor` is implemented by
7 // the mechanism (`window_manager.rs`): it applies one `Command` at a time
8 // against the wlroots/scenefx world.
9 //
10 // Migration status: `Policy::action` is live — the compositor routes user
11 // actions through it and falls back to its legacy arms only for actions the
12 // policy doesn't claim. Further flows (window lifecycle, the animation tick)
13 // grow new snapshot-taking methods here as they migrate; don't add
14 // speculative signatures ahead of a real mechanism caller. Effects are
15 // declarative on purpose: new scenefx capabilities extend `EffectSpec`
16 // without changing either trait.
17
18 use crate::camera::Camera;
19 use crate::tiling::TilingMode;
20
21 /// Opaque handle to a window. Wraps the `SlotMap` key that the mechanism side
22 /// uses internally; policy code never sees a pointer.
23 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24 pub struct WindowId(pub crate::slotmap::Key);
25
26 /// What a surface is for. Assigned once at map time, this replaces scattered
27 /// app_id string-matching (`"cce-wallpaper"`, `"cce-status*"`) in mechanism code.
28 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29 pub enum WindowRole {
30 Normal,
31 StatusBar,
32 Background,
33 Overlay,
34 /// The desktop-grid layer: a client surface world-anchored to a patch of
35 /// the virtual desktop (`WindowSnapshot::grid_patch`). The compositor
36 /// pans/zooms it per frame exactly like window content — the client is
37 /// never in the frame loop; it re-renders only when handed a new patch.
38 /// Input-transparent, stacked above the wallpaper and below everything
39 /// else.
40 Grid,
41 }
42
43 impl WindowRole {
44 /// The single place the special app_id conventions are interpreted.
45 /// `Overlay` is never derived from an app_id — it comes from tiling mode.
46 /// `Grid` also has a protocol declaration (`set_grid`); the app_id match
47 /// makes the fallback swap and placement correct from map time.
48 pub fn from_app_id(app_id: Option<&str>) -> Self {
49 match app_id {
50 Some("cce-wallpaper") => WindowRole::Background,
51 Some("cce-grid") => WindowRole::Grid,
52 Some(id) if id.starts_with("cce-status") => WindowRole::StatusBar,
53 _ => WindowRole::Normal,
54 }
55 }
56 }
57
58 /// User-triggered window-management actions, bound to keys/pointers/gestures
59 /// by the compositor's config and dispatched into policy code.
60 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
61 pub enum Action {
62 None,
63 Spawn,
64 Toggle,
65 Close,
66 FocusNext,
67 FocusPrev,
68 FocusUp,
69 FocusDown,
70 FocusLeft,
71 FocusRight,
72 WindowSwitcher,
73 WindowSwitcherPrev,
74 Move,
75 Resize,
76 MoveWindowLeft,
77 MoveWindowRight,
78 MoveWindowUp,
79 MoveWindowDown,
80 Exit,
81 Reload,
82 Fullscreen,
83 ModeNext,
84 ModeNextShared,
85 Overview,
86 OverviewEnter,
87 OverviewExit,
88 Minimize,
89 OverlayLeft,
90 OverlayRight,
91 ZoomIn,
92 ZoomOut,
93 ZoomReset,
94 PanLeft,
95 PanRight,
96 PanUp,
97 PanDown,
98 Screenshot,
99 VolumeUp,
100 VolumeDown,
101 VolumeMute,
102 MicMute,
103 BrightnessUp,
104 BrightnessDown,
105 }
106
107 impl Action {
108 /// Canonical snake_case name — what users write in the
109 /// `cce-window-manager` domain of `input.kdl`.
110 pub fn name(&self) -> &'static str {
111 match self {
112 Action::None => "none",
113 Action::Spawn => "spawn",
114 Action::Toggle => "toggle",
115 Action::Close => "close_window",
116 Action::FocusNext => "focus_next",
117 Action::FocusPrev => "focus_prev",
118 Action::FocusUp => "focus_up",
119 Action::FocusDown => "focus_down",
120 Action::FocusLeft => "focus_left",
121 Action::FocusRight => "focus_right",
122 Action::WindowSwitcher => "window_switcher",
123 Action::WindowSwitcherPrev => "window_switcher_prev",
124 Action::Move => "move",
125 Action::Resize => "resize",
126 Action::MoveWindowLeft => "move_window_left",
127 Action::MoveWindowRight => "move_window_right",
128 Action::MoveWindowUp => "move_window_up",
129 Action::MoveWindowDown => "move_window_down",
130 Action::Exit => "exit",
131 Action::Reload => "reload",
132 Action::Fullscreen => "toggle_fullscreen",
133 Action::ModeNext => "mode_next",
134 Action::ModeNextShared => "mode_next_shared",
135 Action::Overview => "overview",
136 Action::OverviewEnter => "overview_enter",
137 Action::OverviewExit => "overview_exit",
138 Action::Minimize => "minimize",
139 Action::OverlayLeft => "overlay_left",
140 Action::OverlayRight => "overlay_right",
141 Action::ZoomIn => "zoom_in",
142 Action::ZoomOut => "zoom_out",
143 Action::ZoomReset => "zoom_reset",
144 Action::PanLeft => "pan_left",
145 Action::PanRight => "pan_right",
146 Action::PanUp => "pan_up",
147 Action::PanDown => "pan_down",
148 Action::Screenshot => "screenshot",
149 Action::VolumeUp => "volume_up",
150 Action::VolumeDown => "volume_down",
151 Action::VolumeMute => "volume_mute",
152 Action::MicMute => "mic_mute",
153 Action::BrightnessUp => "brightness_up",
154 Action::BrightnessDown => "brightness_down",
155 }
156 }
157
158 /// Inverse of `name()`, plus aliases from the old `config.kdl`
159 /// vocabulary (`close`, `fullscreen`, `expose`, `toggle_overview`).
160 pub fn from_name(name: &str) -> Option<Action> {
161 Some(match name.trim() {
162 "none" => Action::None,
163 "spawn" => Action::Spawn,
164 "toggle" => Action::Toggle,
165 "close_window" | "close" => Action::Close,
166 "focus_next" => Action::FocusNext,
167 "focus_prev" => Action::FocusPrev,
168 "focus_up" => Action::FocusUp,
169 "focus_down" => Action::FocusDown,
170 "focus_left" => Action::FocusLeft,
171 "focus_right" => Action::FocusRight,
172 "window_switcher" => Action::WindowSwitcher,
173 "window_switcher_prev" => Action::WindowSwitcherPrev,
174 "move" => Action::Move,
175 "resize" => Action::Resize,
176 "move_window_left" => Action::MoveWindowLeft,
177 "move_window_right" => Action::MoveWindowRight,
178 "move_window_up" => Action::MoveWindowUp,
179 "move_window_down" => Action::MoveWindowDown,
180 "exit" => Action::Exit,
181 "reload" => Action::Reload,
182 "toggle_fullscreen" | "fullscreen" => Action::Fullscreen,
183 "mode_next" => Action::ModeNext,
184 "mode_next_shared" => Action::ModeNextShared,
185 "overview" | "expose" | "toggle_overview" => Action::Overview,
186 "overview_enter" => Action::OverviewEnter,
187 "overview_exit" => Action::OverviewExit,
188 "minimize" => Action::Minimize,
189 "overlay_left" => Action::OverlayLeft,
190 "overlay_right" => Action::OverlayRight,
191 "zoom_in" => Action::ZoomIn,
192 "zoom_out" => Action::ZoomOut,
193 "zoom_reset" => Action::ZoomReset,
194 "pan_left" => Action::PanLeft,
195 "pan_right" => Action::PanRight,
196 "pan_up" => Action::PanUp,
197 "pan_down" => Action::PanDown,
198 "screenshot" => Action::Screenshot,
199 "volume_up" => Action::VolumeUp,
200 "volume_down" => Action::VolumeDown,
201 "volume_mute" | "mute" => Action::VolumeMute,
202 "mic_mute" => Action::MicMute,
203 "brightness_up" => Action::BrightnessUp,
204 "brightness_down" => Action::BrightnessDown,
205 _ => return None,
206 })
207 }
208 }
209
210 #[derive(Debug, Clone, Copy, PartialEq)]
211 pub struct Rect {
212 pub x: i32,
213 pub y: i32,
214 pub width: i32,
215 pub height: i32,
216 }
217
218 /// The world-anchored patch a grid client's buffer covers: virtual origin and
219 /// size, plus the buffer resolution. The compositor issues patches
220 /// (grid_patch events) and latches one when the client's rendered buffer
221 /// arrives; arrange places the surface at `(x, y)` with display scale
222 /// `zoom / scale`, so the buffer pans and zooms in lockstep with windows.
223 #[derive(Debug, Clone, Copy, PartialEq)]
224 pub struct GridPatch {
225 pub x: f64,
226 pub y: f64,
227 pub w: f64,
228 pub h: f64,
229 /// Buffer px per virtual unit.
230 pub scale: f64,
231 }
232
233 /// Premultiplied-alpha RGBA, 0.0–1.0 per channel (scenefx convention).
234 #[derive(Debug, Clone, Copy, PartialEq)]
235 pub struct Rgba(pub [f32; 4]);
236
237 /// Server-side decoration for one window: borders now, titlebars later.
238 #[derive(Debug, Clone, Copy, PartialEq)]
239 pub struct DecorationSpec {
240 pub border_width: i32,
241 pub border_color: Rgba,
242 pub corner_radius: i32,
243 }
244
245 /// Declarative per-window scenefx effects.
246 #[derive(Debug, Clone, PartialEq)]
247 pub struct EffectSpec {
248 pub opacity: f32,
249 pub blur: bool,
250 pub shadow: Option<ShadowSpec>,
251 }
252
253 #[derive(Debug, Clone, Copy, PartialEq)]
254 pub struct ShadowSpec {
255 pub color: Rgba,
256 pub blur_sigma: f32,
257 }
258
259 /// What the background layer shows. The desktop is normally Grid; Solid is
260 /// the degenerate single-color background.
261 #[derive(Debug, Clone, PartialEq)]
262 pub enum BackgroundSpec {
263 Solid(Rgba),
264 Grid(GridSpec),
265 }
266
267 /// The desktop grid, as configured (unzoomed virtual units). The per-frame
268 /// geometry — pan/zoom offsets, density fade, cell counts — is derived by
269 /// `background::grid_frame`.
270 #[derive(Debug, Clone, PartialEq)]
271 pub struct GridSpec {
272 /// Backdrop color behind and between the cells (premultiplied).
273 pub gap_color: Rgba,
274 pub cell_color: Rgba,
275 /// Cell width (x axis); the column period is `cell_w + gap_width`.
276 pub cell_w: f64,
277 /// Cell height (y axis); the row period is `cell_h + gap_width`.
278 pub cell_h: f64,
279 pub gap_width: f64,
280 pub cell_corner_radius: i32,
281 /// Cells fade inward by this many virtual px.
282 pub cell_fade_inset: i32,
283 pub fade_mode: GridFadeMode,
284 }
285
286 /// Which side of the viewport the overlay column docks on.
287 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
288 pub enum OverlaySide {
289 Left,
290 Right,
291 }
292
293 /// Shape of a cell's inward edge fade.
294 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
295 pub enum GridFadeMode {
296 Linear,
297 Smoothstep,
298 Quadratic,
299 Cosine,
300 Gaussian,
301 }
302
303 impl GridFadeMode {
304 /// Config-string names; anything unrecognized is Linear.
305 pub fn from_name(name: &str) -> Self {
306 match name {
307 "smoothstep" => GridFadeMode::Smoothstep,
308 "quadratic" => GridFadeMode::Quadratic,
309 "cosine" => GridFadeMode::Cosine,
310 "gaussian" => GridFadeMode::Gaussian,
311 _ => GridFadeMode::Linear,
312 }
313 }
314 }
315
316 /// Everything `Policy::action` may consult, captured by the mechanism at
317 /// dispatch time. Seat- and scene-dependent answers (cursor output, hovered
318 /// window, focus) are resolved into the snapshot up front — the arrange-pass
319 /// convention; policy never queries back mid-decision.
320 #[derive(Debug, Clone)]
321 pub struct ActionCtx {
322 pub camera: Camera,
323 /// The mechanism is in overview mode. Set by fiat on Overview enter, so
324 /// this is NOT always `camera::is_overview(zoom)` — an overview fit can
325 /// land at zoom 1.
326 pub overview: bool,
327 /// Pending pan-animation targets, if the camera is mid-ease.
328 pub pan_target_x: Option<f64>,
329 pub pan_target_y: Option<f64>,
330 /// First enabled output's extent — the legacy "viewport" for keyed zooms
331 /// and View jumps (the multi-output quirk, preserved by construction).
332 pub viewport_w: f64,
333 pub viewport_h: f64,
334 /// Output box under the cursor, falling back to the first enabled
335 /// output: the viewport Overview enters/exits in.
336 pub cursor_viewport: Rect,
337 /// False when there is no seat; the cursor fields then hold zeros.
338 pub has_cursor: bool,
339 pub cursor_x: f64,
340 pub cursor_y: f64,
341 /// Non-status, non-background window under the cursor.
342 pub hovered: Option<WindowId>,
343 pub focused: Option<WindowId>,
344 /// Desktop grid periods (cell size + gap width, per axis) for
345 /// cell-aligned panning: keyed horizontal pans step `grid_period_x`,
346 /// vertical pans `grid_period_y`.
347 pub grid_period_x: f64,
348 pub grid_period_y: f64,
349 pub windows: Vec<ActionWindow>,
350 }
351
352 /// A window as `Policy::action` sees it.
353 #[derive(Debug, Clone)]
354 pub struct ActionWindow {
355 pub id: WindowId,
356 pub app_id: Option<String>,
357 pub title: Option<String>,
358 /// state == Mapped (narrower than `visible`, which also spans teardown).
359 pub mapped: bool,
360 /// Virtual-space position. `w`/`h` are the mechanism's working extent in
361 /// output px (box_geom, defaulted to 800x600 while unmapped).
362 pub x: f64,
363 pub y: f64,
364 pub w: f64,
365 pub h: f64,
366 /// Per-window output scale; the virtual footprint is `w * scale`.
367 pub scale: f64,
368 /// The window's own tiling mode (as set, before viewport resolution).
369 pub mode: TilingMode,
370 /// The mode the window resolves to through the viewport-mode rules
371 /// (mechanism's `get_mode_for_window`) — what leaving Fullscreen
372 /// falls back to.
373 pub resolved_mode: TilingMode,
374 /// The mode and lock the window had when a `SetWindowMode` sent it
375 /// Fullscreen — what leaving Fullscreen restores, so a Tiled window
376 /// comes back Tiled. `None` when it is not Fullscreen, or got there by
377 /// another route (a client request, a rule); the exit then falls back
378 /// to `resolved_mode`.
379 pub pre_fullscreen: Option<(TilingMode, bool)>,
380 /// Mapped and not in Closing/Init teardown/startup.
381 pub visible: bool,
382 /// In the focus-cycling set: currently rendered, not minimized, not a
383 /// status bar.
384 pub focus_cyclable: bool,
385 /// Participates in the overview fit: mapped, not minimized, not
386 /// status/background, not popup/overlay.
387 pub overview_eligible: bool,
388 }
389
390 /// One mechanism write, returned by policy decisions and applied in order —
391 /// the command-stream counterpart of the arrange plan.
392 #[derive(Debug, Clone, PartialEq)]
393 pub enum Command {
394 /// Spawn a command line (the mechanism forks `sh -c`).
395 Spawn(String),
396 /// Write the camera. `overview: None` leaves the mode untouched.
397 /// `animate: true` eases pan and zoom toward the target (the overview
398 /// enter/exit transition); `false` snaps and cancels any easing in
399 /// flight. The mode flip itself always applies immediately.
400 SetCamera { camera: Camera, overview: Option<bool>, animate: bool },
401 /// Set pan-animation targets (a `None` axis is left alone) and start
402 /// easing toward them.
403 PanTo { x: Option<f64>, y: Option<f64> },
404 StopPanAnimation,
405 Focus(WindowId),
406 /// Focus whichever visible window the mechanism's next-visible rule
407 /// picks — the refocus step after closing/minimizing the focused window.
408 FocusNextVisible,
409 /// Raise a window to the top of the stacking order.
410 Raise(WindowId),
411 /// Ask the window to close (and let teardown proceed).
412 CloseWindow(WindowId),
413 SetMinimized { id: WindowId, minimized: bool },
414 /// Set a window's tiling mode; `locked` pins it against viewport-mode
415 /// resolution.
416 SetWindowMode { id: WindowId, mode: TilingMode, locked: bool },
417 /// Dock the overlay column on the given side.
418 SetOverlayPosition(OverlaySide),
419 /// Reposition a window in virtual space.
420 MoveWindow { id: WindowId, x: f64, y: f64 },
421 /// Full re-arrange (the mechanism's `dirty_windowing`).
422 Relayout,
423 /// Camera-only refresh: the fast viewport path when the WM is idle.
424 RefreshCamera,
425 }
426
427 /// Decisions, policy-side. Implemented by `actions::DefaultPolicy`.
428 pub trait Policy {
429 /// Decide a user action against the snapshot; `arg` is the binding's
430 /// command string (Spawn/Toggle carry one; media-key actions may carry
431 /// an override of their stock command). An empty vec means "not
432 /// mine" — the mechanism falls through to its remaining legacy arms.
433 fn action(&mut self, ctx: &ActionCtx, action: Action, arg: Option<&str>) -> Vec<Command>;
434 }
435
436 /// Execution, mechanism-side. Implemented by the compositor's WindowManager.
437 pub trait Compositor {
438 fn apply(&mut self, cmd: &Command);
439 }