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 /docs/ARCHITECTURE.md | |
| 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 'docs/ARCHITECTURE.md')
| -rw-r--r-- | docs/ARCHITECTURE.md | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..61d036c --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,118 @@ +# Architecture + +## Crate layout + +``` +crates/ + core/ srdwm-core window/workspace/monitor state, layout engine, + smart placement, hit-testing - pure logic, + no I/O, no platform dependency + config/ srdwm-config Lua (`srd`) scripting engine, wraps mlua + platform/ srdwm-platform the `Platform` trait + PlatformKind detection + x11/ srdwm-x11 X11 backend (x11rb) + wayland/ srdwm-wayland Wayland backend (smithay) + windows/ srdwm-windows Windows backend (windows-rs, cfg-gated) + macos/ srdwm-macos macOS backend (core-graphics/accessibility-sys, cfg-gated) + srdwm/ srdwm (bin) wires config + core + platform together +``` + +Dependency direction is strictly one-way: `core` depends on nothing else in +the workspace; `platform` depends only on `core`; each backend depends on +`core` + `platform`; `config` depends only on `core` (it never talks to a +platform directly - see below); the `srdwm` binary is the only crate that +depends on everything. + +## Why config never touches a platform directly + +`srdwm-config`'s `srd.window.close()` (etc.) mutates a +`Rc<RefCell<WindowManager>>` shared with the running backend - it does not +call into `srdwm-x11` or `srdwm-wayland` itself. The backend is the thing +that notices the `WindowManager`'s state changed (on its next `poll_events` +tick, via `main.rs`'s `sync()` helper) and pushes the resulting geometry to +the real X11/Wayland surface. + +This indirection is deliberate: it's what let the exact same `srd` API +implementation, with the exact same test suite, work correctly against a +`WindowManager` in isolation (10 config tests never touch a display server) +and then, unmodified, drive a real X11 session and a real Wayland +compositor. If `srd.window.close()` called `platform.close()` directly, the +config crate would need a generic `Platform` handle and every test would +need a fake one. + +## The `Platform` trait + +```rust +trait Platform { + fn kind(&self) -> PlatformKind; + fn poll_events(&mut self) -> Result<Vec<srdwm_core::Event>>; + fn monitors(&mut self) -> Result<Vec<Monitor>>; + fn apply_geometry(&mut self, window: WindowId, geometry: Rect) -> Result<()>; + fn set_title(&mut self, ...) / focus / minimize / restore / close (...); + fn set_decorated / set_border_color / set_border_width / redraw_decoration (...); + fn grab_keyboard(&mut self) / ungrab_keyboard(&mut self); +} +``` + +`poll_events` is the one place each backend bridges its native event model +(X11's blocking `XNextEvent`, Wayland's callback-driven `wl_display` +dispatch) into the common `srdwm_core::Event` queue; everything downstream +of that - layout, placement, focus, drag/resize - is platform-independent. +This mirrors the legacy C++ `Platform` interface's shape (see +`docs/PRIOR_ART.md`), which was one of the few architectural decisions in +that codebase that held up. + +Both `X11Platform` and `WaylandPlatform` additionally hold their own +`Rc<RefCell<WindowManager>>` clone (shared with `srdwm-config`), so that +when a backend detects a new window (X11 `MapRequest`, Wayland +`new_toplevel`), it can call `wm.alloc_window_id()` + `wm.add_window(...)` +directly rather than needing a separate "please allocate an ID for me" +round-trip through `main.rs`. + +## Decoration strategy per platform + +Windows can't be decorated the same way on every platform, so each backend +takes the approach that's actually available to it (informed by the prior +art in `docs/PRIOR_ART.md`): + +- **X11**: classic reparenting WM. srdwm creates a frame window, reparents + the client into it below a drawn titlebar band, and owns all decoration + pixels directly (Xlib/xcb core drawing). Full control, which is why "full + title bar support" (buttons, drag, resize, matching Windows/macOS) is most + complete here. +- **Wayland**: srdwm *is* the compositor, so it negotiates + `zxdg_decoration_manager_v1` server-side mode and renders a decoration + band itself via `smithay`'s `SolidColorRenderElement`, composited above + each client surface. Same `ResizeEdge::hit_test` as X11; see + `docs/IMPLEMENTATION_STATUS.md` for what's not finished (text, precise + global-keybinding routing). +- **Windows**: DWM will not give you a custom-width or custom-drawn frame + without disabling the native one entirely, so the design (not yet built -- + see status doc) keeps DWM's frame and controls it (`DWMWA_BORDER_COLOR`), + matching how komorebi/glazewm operate rather than fighting DWM. +- **macOS**: there is no public API to draw on another process's window at + all. The design (also not yet built) is a separate, click-through overlay + window that tracks the target window's position via the Accessibility + API, matching AeroSpace's AX-only approach rather than yabai's + SIP-disabling private APIs. + +## Why `srdwm_core::window::ResizeEdge::hit_test` is shared, not duplicated + +Titlebar hit-testing (which pixel band is "drag", which is the close +button, which edge is a resize grab) is pure geometry - it doesn't need to +know anything about X11 or Wayland. Putting it in `srdwm-core` means the +X11 and Wayland backends *cannot* drift into behaving differently for the +same click, which was worth the small indirection cost (both backends pass +`(frame_rect, x, y)` in and get back a `TitlebarHit` enum to act on). + +## Config loading + +`Engine::new(wm, config_dir)` seeds every key documented in +`docs/DEFAULTS.md` before any user script runs (so `srd.get(...)` never +returns `nil` for a documented key), then `Engine::load_init()` executes +`config_dir/init.lua`, which in the shipped example +(`config/srd/init.lua`) calls `srd.load("keybindings")` etc. to pull in the +rest - `srd.load(name)` reads and executes `config_dir/{name}.lua` in the +same Lua state, so later files can see earlier `srd.bind()`/`srd.set()` +calls. Config directory resolution order: `$SRDWM_CONFIG_PATH`, then +`$XDG_CONFIG_HOME/srdwm/srd`, then `~/.config/srdwm/srd` (matching +`docs/DEFAULTS.md`'s documented location). |