diff options
| author | srdusr <[email protected]> | 2024-04-02 00:58:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-04-02 00:58:00 +0200 |
| commit | 8110bb2773b6c841029a51eca7971f42a36f480c (patch) | |
| tree | b3b364a6c0231fe408229e94dc9e25ce4562ad02 /crates/core/src/window.rs | |
| parent | d1f856edc516047575cf46f12fad2b59006363c8 (diff) | |
| download | srdwm-8110bb2773b6c841029a51eca7971f42a36f480c.tar.gz srdwm-8110bb2773b6c841029a51eca7971f42a36f480c.zip | |
Rewrite srdwm in Rust: working X11 and Wayland backends, Lua config
The C++ prototype (moved to legacy-cpp/) was mostly a design skeleton:
X11 and Windows backends were partially real, Wayland created the
wlroots object graph but never wired a single event listener, macOS
was stub except monitor enumeration, and the Lua engine's srd.bind()
stored a key-combo string but never the actual closure. See
docs/PRIOR_ART.md for the full audit.
This replaces it with a Cargo workspace:
- srdwm-core: platform-independent window/workspace/monitor state,
a real master-stack tiling layout, and SmartPlacement grid/cascade/
snap-to-edge placement - fixing several bugs in the C++ version
(hardcoded 2-column grid, cascade that never cascaded, snap-to-edge
that always returned a fixed rect). 35 unit tests.
- srdwm-config: the srd Lua API via mlua, implementing the surface
docs/DEFAULTS.md always documented but the C++ engine never actually
built (srd.window.close()/focus(direction), srd.workspace.next(),
real keybinding closures, require("srd") support). 10 unit tests.
- srdwm-x11: a real reparenting WM with a drawn title bar (buttons,
drag, resize), verified live under Xephyr - frame placement and
client offset match srdwm-core's computed geometry exactly, and the
decoration renders correctly on screen.
- srdwm-wayland: a from-scratch smithay compositor (the C++ version
had nothing working to port from) - runs via the winit backend,
tracks xdg-shell toplevels through the same WindowManager and
hit-testing code X11 uses, verified to start/render/run without
crashing. Decorations are solid-color (no text yet); see
docs/IMPLEMENTATION_STATUS.md for exact scope.
- srdwm-windows / srdwm-macos: structured, cfg-gated designs informed
by komorebi/glazewm and yabai/AeroSpace respectively (see
docs/PRIOR_ART.md), honestly marked as unbuilt/unverified since this
sandbox has no Windows or macOS target.
Diffstat (limited to 'crates/core/src/window.rs')
| -rw-r--r-- | crates/core/src/window.rs | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs new file mode 100644 index 0000000..3970b02 --- /dev/null +++ b/crates/core/src/window.rs @@ -0,0 +1,221 @@ +use crate::geometry::Rect; + +pub type WindowId = u64; + +/// State of a single managed window. This is platform-independent: backends +/// (X11, Wayland, ...) own the real surface/client handle and keep a `Window` +/// in sync with it via `srdwm_core::WindowManager`. +#[derive(Debug, Clone)] +pub struct Window { + pub id: WindowId, + pub title: String, + pub app_id: String, + pub geometry: Rect, + /// Geometry to restore to when un-maximizing. + pub restore_geometry: Option<Rect>, + pub decorated: bool, + pub floating: bool, + pub minimized: bool, + pub maximized: bool, + pub fullscreen: bool, + pub always_on_top: bool, + pub border_color: (u8, u8, u8), + pub border_width: u32, + pub workspace: usize, + pub monitor: u32, +} + +impl Window { + pub fn new(id: WindowId, title: impl Into<String>) -> Self { + Self { + id, + title: title.into(), + app_id: String::new(), + geometry: Rect::new(0, 0, 640, 480), + restore_geometry: None, + decorated: true, + floating: false, + minimized: false, + maximized: false, + fullscreen: false, + always_on_top: false, + border_color: (136, 192, 208), // Nord accent, matches legacy theme default + border_width: 2, + workspace: 0, + monitor: 0, + } + } +} + +/// The height, in pixels, of the drawn title bar. Shared between backends so +/// hit-testing and rendering agree on the same band. +pub const TITLEBAR_HEIGHT: u32 = 30; +/// Width of a resize grab margin along each window edge. +pub const RESIZE_MARGIN: i32 = 6; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResizeEdge { + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +impl ResizeEdge { + /// Determine which titlebar button (if any) a point within the titlebar + /// band falls on. Buttons are laid out right-aligned: close, maximize, minimize. + pub fn hit_test(frame: Rect, x: i32, y: i32) -> Option<TitlebarHit> { + if !frame.contains_point(x, y) { + return None; + } + if y < frame.y + TITLEBAR_HEIGHT as i32 { + const BUTTON: i32 = TITLEBAR_HEIGHT as i32; + let right = frame.right(); + if x >= right - BUTTON { + return Some(TitlebarHit::Close); + } + if x >= right - BUTTON * 2 { + return Some(TitlebarHit::Maximize); + } + if x >= right - BUTTON * 3 { + return Some(TitlebarHit::Minimize); + } + return Some(TitlebarHit::Drag); + } + let edge = Self::resize_edge_at(frame, x, y)?; + Some(TitlebarHit::Resize(edge)) + } + + fn resize_edge_at(frame: Rect, x: i32, y: i32) -> Option<ResizeEdge> { + let m = RESIZE_MARGIN; + let near_left = x <= frame.x + m; + let near_right = x >= frame.right() - m; + let near_top = y <= frame.y + m; + let near_bottom = y >= frame.bottom() - m; + Some(match (near_left, near_right, near_top, near_bottom) { + (true, _, true, _) => ResizeEdge::TopLeft, + (_, true, true, _) => ResizeEdge::TopRight, + (true, _, _, true) => ResizeEdge::BottomLeft, + (_, true, _, true) => ResizeEdge::BottomRight, + (true, false, false, false) => ResizeEdge::Left, + (false, true, false, false) => ResizeEdge::Right, + (false, false, false, true) => ResizeEdge::Bottom, + _ => return None, + }) + } + + /// Apply a pointer delta to `original` geometry along this edge, honoring + /// the given minimum size. + pub fn apply_delta(self, original: Rect, dx: i32, dy: i32, min_w: u32, min_h: u32) -> Rect { + let mut r = original; + let min_w = min_w as i32; + let min_h = min_h as i32; + match self { + ResizeEdge::Left | ResizeEdge::TopLeft | ResizeEdge::BottomLeft => { + let new_w = (original.width as i32 - dx).max(min_w); + r.x = original.right() - new_w; + r.width = new_w as u32; + } + _ => {} + } + match self { + ResizeEdge::Right | ResizeEdge::TopRight | ResizeEdge::BottomRight => { + r.width = (original.width as i32 + dx).max(min_w) as u32; + } + _ => {} + } + match self { + ResizeEdge::Top | ResizeEdge::TopLeft | ResizeEdge::TopRight => { + let new_h = (original.height as i32 - dy).max(min_h); + r.y = original.bottom() - new_h; + r.height = new_h as u32; + } + _ => {} + } + match self { + ResizeEdge::Bottom | ResizeEdge::BottomLeft | ResizeEdge::BottomRight => { + r.height = (original.height as i32 + dy).max(min_h) as u32; + } + _ => {} + } + r + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TitlebarHit { + Drag, + Close, + Maximize, + Minimize, + Resize(ResizeEdge), +} + +#[cfg(test)] +mod tests { + use super::*; + + fn frame() -> Rect { + Rect::new(100, 100, 400, 300) + } + + #[test] + fn close_button_is_top_right_corner_of_titlebar() { + let f = frame(); + let hit = ResizeEdge::hit_test(f, f.right() - 5, f.y + 5); + assert_eq!(hit, Some(TitlebarHit::Close)); + } + + #[test] + fn maximize_is_left_of_close() { + let f = frame(); + let hit = ResizeEdge::hit_test(f, f.right() - TITLEBAR_HEIGHT as i32 - 5, f.y + 5); + assert_eq!(hit, Some(TitlebarHit::Maximize)); + } + + #[test] + fn middle_of_titlebar_is_drag() { + let f = frame(); + let (cx, _) = f.center(); + let hit = ResizeEdge::hit_test(f, cx, f.y + 5); + assert_eq!(hit, Some(TitlebarHit::Drag)); + } + + #[test] + fn bottom_right_corner_is_resize() { + let f = frame(); + let hit = ResizeEdge::hit_test(f, f.right() - 1, f.bottom() - 1); + assert_eq!(hit, Some(TitlebarHit::Resize(ResizeEdge::BottomRight))); + } + + #[test] + fn outside_frame_is_none() { + let f = frame(); + assert_eq!(ResizeEdge::hit_test(f, 0, 0), None); + } + + #[test] + fn resize_right_edge_grows_width_only() { + let r = Rect::new(0, 0, 200, 100); + let out = ResizeEdge::Right.apply_delta(r, 50, 999, 50, 50); + assert_eq!(out, Rect::new(0, 0, 250, 100)); + } + + #[test] + fn resize_left_edge_moves_x_and_shrinks_width() { + let r = Rect::new(100, 0, 200, 100); + let out = ResizeEdge::Left.apply_delta(r, 30, 0, 50, 50); + assert_eq!(out, Rect::new(130, 0, 170, 100)); + } + + #[test] + fn resize_respects_minimum_size() { + let r = Rect::new(0, 0, 100, 100); + let out = ResizeEdge::Right.apply_delta(r, -500, 0, 50, 50); + assert_eq!(out.width, 50); + } +} |