srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/window.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-04-02 00:58:00 +0200
committersrdusr <[email protected]>2024-04-02 00:58:00 +0200
commit8110bb2773b6c841029a51eca7971f42a36f480c (patch)
treeb3b364a6c0231fe408229e94dc9e25ce4562ad02 /crates/core/src/window.rs
parentd1f856edc516047575cf46f12fad2b59006363c8 (diff)
downloadsrdwm-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.rs221
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);
+ }
+}