| Age | Commit message (Collapse) | Author | Files | Lines |
|
srdwm never read xdg_surface.set_window_geometry anywhere. A CSD
client (GTK4/Firefox) declares its real visible content as a sub-rect
inset within a larger buffer that also reserves an invisible shadow
margin - that margin stays reserved in the buffer even once the
tiled-state hint tells the client to stop drawing the shadow itself.
Every render path was positioning content at the client's raw buffer
origin instead of subtracting that declared offset, leaving the
margin's width/height as a gap with wallpaper visible through it at
the window's top-left corner. Confirmed by pixel-diffing the gap
against the real wallpaper at that exact screen position: an exact
match, ruling out "just the client's own dark theme."
Fixed in three places that have to move together: udev/render.rs and
winit/render.rs's content-positioning code, and sync_geometry's
space.map_element call. That last one matters as much as the other
two - it's what smithay's Space (and therefore click hit-testing)
reads, so a render-only fix would have traded a visible gap for an
invisible, same-size hit-test offset in the other direction. Also
applied to the new capture-workspace off-screen render for the same
reason.
This likely also explains real dropdown/context-menu misplacement (and
the clicks landing on the wrong spot) in CSD apps: popup positioning
anchors against the same window position this fix corrects.
|
|
focus_window marked the target focused without clearing minimized, so
a dock icon's Activate (or the plain "focus" IPC command) on a
minimized window left it focused but still excluded from
visible_windows/rendering - reads exactly like the click did nothing,
since the window never actually reappears. Every focus_window caller
gets the restore for free now. Added a Super+n keybind for minimize
too, since none existed at all before this.
|
|
Remaining files from the backlog: Lua config engine
additions (general/register/support/window), workspace Cargo.toml/
Cargo.lock churn from the new dependencies added elsewhere in this
sweep, srdwm/main.rs wiring, and docs (DEFAULTS.md plus a new
SESSION_HANDOFF.md written mid-session for continuity across a restart
- see that file's own header for what it is and isn't).
|
|
resume, plumbing
Bundles the remaining wayland-crate changes built up here,
touching both backends (udev and winit) and the shared input/rendering
code:
- udev/capture.rs: off-screen Pixman render of an arbitrary (not
necessarily on-screen) workspace's window content to a PPM file --
what crates/core's capture-request queue drives, for a workspace
switcher's thumbnail previews. wlr-screencopy structurally can't do
this (it can only see what an output is presenting), which is why
this exists as a separate render path rather than reusing it.
- input::focus_window now also raises the window in smithay's own
Space, not just core's stacking order - Space is what actually
renders on top and what pointer hit-testing reads, so any focus path
that skipped this (an IPC "focus" dispatch, concretely) left a
window genuinely focused while still rendering, and receiving
clicks, underneath whatever was already topmost. Both backends'
poll loops now re-sync this after any IPC mutation.
- udev/session.rs's VT-switch resume fix (drains a stale pending page
flip before reasserting CRTCs) already has its own earlier, cleanly
isolated commit - not duplicated here.
- Assorted decoration/cursor/rounded-corners/output-management/
screencopy/XWayland changes and their cross-backend wiring.
Coarser than the repo's usual one-purpose-per-commit convention,
deliberately - see the core-crate sweep commit's own message for why.
|
|
CLI verbs
Bundles related IPC-surface and CLI additions built up over this
session:
- capture_workspace IPC command + srd capture workspace CLI verb (see
the wayland-crate commit for the off-screen render this drives).
- Expanded IPC response payloads (monitors, client events, global menu
info) and their matching CLI plumbing.
Same reasoning as the core-crate sweep commit for why this is coarser
than the repo's usual convention: too entangled to split safely without
a dedicated review pass.
|
|
test coverage
Bundles several related changes to crates/core built up over this
session rather than committed incrementally:
- WindowManager::request_capture_workspace/drain_capture_requests (new
manager/capture.rs) - backend-agnostic queuing for an off-screen
workspace render, see the wayland-side commit for why this exists.
- focus_window now switches workspace as a side effect when the target
isn't on the current one, matching Hyprland's focuswindow convention
(manager/focus.rs).
- Assorted window/rules/theme field additions and their test coverage.
Left less granular than the repo's usual one-purpose-per-commit
convention deliberately: these accumulated across a long session
without being committed as they landed, and are too entangled
line-by-line to safely split apart now without risking mis-attributing
changes to the wrong commit message.
|
|
toggle_maximize previously targeted full_geometry outright (past every
reserved zone), on an earlier request specifically about the dock -
which also silently let it extend behind a top bar's zone, reported
back as its own bug once live-tested. maximize_geometry is a third
rect distinct from geometry (every zone) and full_geometry (none):
full_geometry with only a top-anchored bar's exclusive zone subtracted
back out. New test locks in dock-covered/bar-respected together.
|
|
Right-clicking (or hovering) the maximize button opens a flyout of
fixed half/quarter positions (snap_flyout.rs renders it); picking one
applies that zone via WindowManager::apply_snap_zone, the click-driven
equivalent of dragging a window to that edge and releasing.
|
|
Exposes each window's application menu (Firefox/GTK's dbusmenu export,
X11's _GTK_APPLICATION_OBJECT_PATH-style menus via global_menu.rs) so
an external panel can render it as a system menu bar rather than each
window drawing its own, the same convention appmenu.rs/gtk_shell.rs
and appmenu_registrar.rs wire up across both backends.
|
|
A real ext-session-lock-v1 implementation drawn by the compositor
itself, not a hand-off to an external locker process: a live-blurred
background, PAM authentication on a background thread, and its own
config surface (lock_config.rs) rather than hardcoded appearance.
|
|
A flip issued right before a VT switch away could still be undelivered
when the session resumed - the kernel refuses a new page flip on a CRTC
with one already outstanding, which showed up live as a black screen
that never recovered across two switch attempts, with a rapid repeating
"Device or resource busy" loop in the log. Drain and apply any pending
DRM events before reasserting CRTCs and rendering again on resume, so a
stale flip from before the switch can't collide with the fresh one.
|
|
read_global_menu unconditionally preferred MenuSource::Gtk whenever a
GTK menubar path resolved - but appmenu-gtk-module exports a plain
Gtk.Window's menu (no GtkApplication, so no _GTK_APPLICATION_OBJECT_
PATH/_GTK_WINDOW_OBJECT_PATH) through its Unity-compatibility shim,
unity.-prefixed actions and all, while still setting _GTK_MENUBAR_
OBJECT_PATH. Labeling that Gtk meant a consumer inserted app/win
action groups the app never populated instead of a unity one it did --
every menu item rendered, but permanently insensitive, since none of
them resolved against a group that existed. This was flagged as a
known risk in this function's own doc comment when `source` was first
added, but the priority logic itself never got the fix.
Root-caused live by an AGS peer session: read the actual exported menu
content off the bus for a real appmenu-gtk-module app and found
unity.-prefixed actions at the GTK atom's own path, with app_path/
window_path both empty - confirming that emptiness is the reliable
tell, not which atom happened to resolve.
Fixed by preferring Unity whenever app_path/window_path are both
absent, even if a GTK menubar path resolved - the path itself doesn't
change, only the label. Pulled the decision out into a standalone
classify_menu_source function so it's unit-testable without a real X
connection, with the exact live case as a regression test.
|
|
Auditing "clicking behavior and basics": general.focus_follows_mouse,
general.mouse_follows_focus, general.auto_raise, general.auto_focus,
the entire window.* namespace (8 more keys, a full duplicate of the
same four plus remember_position/size/state), and general.
smart_placement/border_width were all seeded into default_config() and
documented in DEFAULTS.md, but none were read anywhere - srd.set()/
srd.get() on any of them silently succeeded while doing nothing.
focus_follows_mouse is real, well-defined, and directly relevant to
clicking basics - implemented it plus auto_raise (raise, not just
focus, on hover) rather than just deleting the promise like the
others. WindowManager gained focus_follows_mouse/auto_raise bools,
wired from apply_general_settings the same way every other general.*
flag is. handle_pointer_position now tracks whichever window (content
or decoration) is under the pointer and, when the setting is on and
that differs from the currently-focused window, focuses it through the
same focus_window() free function every click-driven focus change
already uses (real keyboard focus, not just core state) - skipped
entirely while dragging/resizing or over a layer-shell surface, so the
pointer sweeping over other windows mid-drag or hovering a bar can't
steal focus from what's actually being manipulated.
mouse_follows_focus (pointer warp on keybinding-driven focus change)
and auto_focus (no clear distinct meaning beyond click-to-focus) stay
unimplemented and are now undocumented rather than promised.
|
|
MISSING.md listed the border frame's bottom/left/right strips as
staying square while the top one (and the titlebar above it) rounds --
"unrelated to client content rounding... not attempted." The left/
right strips genuinely can't participate (border_strips' geometry has
them span only the height between the top and bottom strips, no
corner to round), but the bottom strip is exactly the same shape as
the top one and had no reason left to stay square.
decoration::render_border_bottom mirrors render_border_top exactly
(round_bottom_corners mirrors round_top_corners), cached the same way
in a new border_bottom_decorations map, and drawn in both render loops
via the same all-or-nothing occlusion check the top strip already
uses - pulled out of the left/right strips' per-fragment occlusion
splitting into its own dedicated bitmap path, matching top's existing
trade-off (cropping a rounded bitmap's source rect per fragment is
real extra work for a strip this thin) rather than inventing a new one.
One real bug caught before it shipped: round_bottom_corners' corner-
centre math (height - r - 1) panics on unsigned underflow whenever the
radius clamp lands on the strip's own full height (a real, common case
- a 2px-thick test strip hits it immediately). Fixed by computing the
centre as a signed offset instead, mirroring how the existing dx/dy
distance math already avoids the same class of issue.
|
|
MISSING.md had listed this as needing to touch srdwm_core::window::
hit_test's signature at every call site, including the honest-stub
Windows/macOS backends - overstated on closer inspection: that shared
function already takes a plain resize_margin: i32 parameter, agnostic
to where the value comes from. The only real change needed was reading
Window.resize_margin.unwrap_or(wm-wide default) instead of always the
WM-wide value, at the single call site inside WindowManager::hit_test
in core - no backend touched at all.
Window.resize_margin: Option<i32>, WindowRuleActions.resize_margin to
match, applied in add_window/reapply_rules_if_pending the same way
opacity already is. Settable via a rule action or
srd.window.set_resize_margin(n) on the focused window.
|
|
Same shape as the existing toggle_visibility/focus/close dispatch
actions - lets an external script (or a live diagnostic check)
drive either without a keybinding already existing to trigger it.
Immediately useful for verifying the maximize/fullscreen contract on
a live session without synthesizing any input at all.
|
|
No configure this compositor ever sent set any xdg_toplevel::State bit
at all - confirmed by grepping the whole crate, zero hits before this.
GTK4 (Firefox concretely) reads the tiled bits to decide whether to
reserve an invisible client-side shadow margin around its own content,
independent of server- vs client-side decoration; with none ever sent
it always assumed "floating, might need a shadow" and kept reserving
one. That margin sits inside the committed buffer but is functionally
invisible, so this compositor's own border - drawn at the full
geometry, margin included, since nothing here knew the margin existed
- ended up visibly offset from where the client's real chrome began.
Root-caused from a live screenshot: Firefox's srdwm-drawn border sat
clearly up-and-left of its actual toolbar, not framing it. Reported as
"border is not with the window at start" and, more generally, borders
never feeling like part of the window they're drawn around - which
this is: decoration and content genuinely disagreeing about where the
window's edge is.
Sets all four Tiled* bits unconditionally on every xdg_toplevel
configure - the same technique river/dwl use, telling every window
it's flush against something and should skip its own shadow regardless
of whether it's in a literal tiling layout, which is the outcome
actually wanted here: this compositor draws the frame, nothing else
should also be reserving room for one.
Not visually verified against a live client yet - this needs an
actual GTK app rendering under a restarted session to confirm the
shadow margin is really gone, which no offline test can substitute
for.
|
|
Reported live, repeatedly: corners felt like they had no priority over
sides. They didn't, structurally - a corner only ever registered in
the exact pixel square where both edges' own resize_margin zones
happened to overlap (6x6px at the default 6px margin), nothing wider.
A click a little further along either axis, still clearly aiming for
the corner, fell back to a single straight-edge resize instead.
resize_edge_at now checks a separate, wider corner zone
(CORNER_MARGIN * resize_margin) first, so a corner claims a real,
deliberately larger target the way GNOME/KDE already do - diagonal
resize is a harder grab than a straight edge and deserves more room,
not the same or less.
Also fixed a related, previously-unreachable case: a *decorated*
window's whole titlebar band returned Drag/Close/Maximize/Minimize
unconditionally, so top-left/top-right diagonal resize never had a
code path at all, even at the titlebar's own corner pixels. Added
top-left resize as a small, genuine corner square (both axes, not just
one) checked before drag/buttons. Deliberately did NOT add the
matching top-right case: that corner is the close button on every
mainstream desktop, and trading a well-known, expected target for a
rarely-wanted one at exactly the spot a miss costs the most isn't a
trade worth making.
Four new regression tests cover: reaching a corner past the old tight
margin, falling back to a plain edge just past the new wider one, the
newly-reachable decorated top-left corner, and confirming top-right
still closes rather than competing with resize.
|
|
A peer session pointed out srd clients reports a decorated window's
rect 30px taller than X11 reports the client's own content window --
correct (Window.geometry is the frame rect, TITLEBAR_HEIGHT included
on top, the same convention hit-testing/rendering already use
internally), but genuinely undocumented from an external IPC
consumer's point of view. Made the frame-vs-content distinction
explicit on the field itself rather than leaving it to be reverse-
engineered from a pixel diff.
|
|
map_window_request only ever read window.title()/.class() once, at
MapRequest - for a client whose managed window doesn't carry
WM_NAME/WM_CLASS at that exact moment (or the properties simply arrive
later), Window.title/app_id stayed permanently empty. That reaches
srd.rule's class matching, this compositor's own titlebar text, and
every wlr-foreign-toplevel-management listener (a dock's running
indicator, an app switcher, icon lookup) - confirmed live via a peer
session's AGS instance rendering blank rows for Spotify and OpenSnitch.
Implements XwmHandler::property_notify (previously unhandled, a
no-op default) to re-read title/class on WmWindowProperty::Title/Class
and update Window plus notify rule/decoration/foreign-toplevel
listeners on an actual change - the XWayland-side mirror of
sync_toplevel_metadata, which already exists for exactly this problem
on the native xdg-shell path (see its own doc comment).
Not fully verified against the specific live case that surfaced this:
whether XWayland's X11Wm delivers property_notify for a *managed*
window whose real WM_NAME/WM_CLASS live only on an unmanaged sibling/
child (rather than arriving late on the same window) is still an open
question - this fixes the well-documented "arrives late" case with
certainty, and may or may not cover that harder case too.
|
|
Platform::close only ever called w.toplevel(), which is None for an
XWayland window - closing one (the WM's own close binding, or `srd
dispatch close`) silently did nothing at all, on both udev and winit.
Found live: a leftover untitled fullscreen window wouldn't close via
srd dispatch close even after several seconds, tracing back to this.
Fixed by falling back to X11Surface::close() when there's no xdg
toplevel - it already handles both cases smithay-side (a polite
WM_DELETE_WINDOW for a cooperating client, outright destroy_window for
one that doesn't support it), so no new logic was needed, just calling
it.
|
|
visible_windows' doc comment claimed windows show "on the active
workspace of whichever monitor they're assigned to" - the code never
reads w.monitor at all; current_workspace is one flat value shared by
every monitor, not per-output. Documented that explicitly on both the
field and the method, since this is a real behavioral difference from
Hyprland worth a reader actually seeing, not just an inaccurate comment
to fix quietly.
monitor.primary_workspace/monitor.workspace_count describe a per-
monitor-workspace design that doesn't exist; workspace.auto_switch/
workspace.persistent were never wired to any behavior. All four were
seeded into default_config() and documented in DEFAULTS.md, so
srd.set()/srd.get() on them silently succeeded while doing nothing --
removed from both, matching the precedent already set by general.
rounded_corners' deliberate absence from default_config for a
different reason (backend-dependent default rather than unbuilt).
|
|
WindowManager::hit_test/window_at filtered only by `!w.minimized`,
never by workspace - but a window on a workspace that isn't current
is not minimized, it's just not shown. Rendering (visible_windows/
visible_windows_front_to_back) already restricted to the current
workspace; hit-testing didn't, so a click landing on where an
invisible window's stale on-screen geometry happened to sit routed to
that window instead of whatever was actually visible underneath.
Reported live. Fixed by adding the same workspace check rendering
already uses, plus a regression test with two identically-positioned
windows on different workspaces.
|
|
Named Pointer/Crosshair/Move/text/four resize-direction shapes all
have dedicated art now, but the comment picking Pointer/Crosshair as
its examples of "shapes we don't draw" predates that - misleading
about this file's own current behavior. Swapped in shapes that
genuinely still fall back to the arrow (Grab, Wait, Help, NotAllowed).
|
|
~17 comments across the codebase still pointed at udev.rs/winit.rs/
state.rs by their old flat-file names after those became udev/,
winit/, state/ directories - found while auditing what this work
rushed, since the split verification (function/struct-name diffing,
full test suite) checked structural correctness but never comment
accuracy. Updated each to either the specific new file (e.g. "see
state.rs's REPEAT_DELAY" -> "see state/mod.rs's REPEAT_DELAY",
"udev.rs's monitors()" -> "udev/platform.rs's monitors()") or the bare
module name where the reference was already generic ("the udev/winit
backends", not a specific location).
|
|
Pure reorganization, no behavior change - verified by diffing the
function-name and struct-name sets before/after (both identical) plus
a full cargo test pass. mod.rs keeps the module doc comment, imports,
WaylandPlatform's struct definition, and TARGET_FRAME_TIME, plus mod
declarations. The rest splits by concern:
- connect.rs: connect(), the ~200-line setup/init function.
- run.rs: accept_clients, pump_winit - the small per-poll pair.
- render.rs: render_frame, the per-frame render loop (left as one
intact ~330-line function, same reasoning as udev's render.rs: its
structure is deliberate and already documented inline, not a target
for further decomposition in a pure reorganization pass).
- capture.rs: capture_offscreen, the screencopy path.
- events.rs: handle_winit_event.
- platform.rs: `impl Platform for WaylandPlatform`.
No tests module existed in the original file, so none was split out.
A handful of methods/functions (accept_clients, pump_winit,
render_frame, capture_offscreen, handle_winit_event) went from private
to pub(super): called across what are now sibling submodules, which
Rust's privacy model doesn't let see each other's private items.
|
|
Pure reorganization, no behavior change - verified by diffing the
function-name and struct/trait-name sets before/after (both identical)
plus a full cargo test pass. lib.rs is now a thin shim (mod
declaration + pub use), same pattern crates/config used, since a crate
root can't itself become a directory. platform/mod.rs keeps the atom
table, Frame/X11Platform's struct definitions, the small free-function
helpers (err, modmask_for_keycode_in_mod_slots, rgb_to_pixel), and the
ClonedForRender trait+impl. The rest splits by concern:
- connect.rs: connect, keymap/modifier helpers, grab_keybindings.
- window.rs: manage_new_window and the other per-client lifecycle
methods (window_title/class, supports_wm_delete, unmanage,
frame_for).
- events.rs: handle_event, the X11 event-dispatch loop.
- actions.rs: raise_and_focus/request_close/sync_geometry/
redraw_all_decorations.
- trait_impl.rs: `impl Platform for X11Platform` - named to avoid
clippy's module_inception lint, since the containing directory is
already named `platform`.
- tests.rs: unsplit, same reasoning as every other split this pass.
A handful of X11Platform methods (frame_for, manage_new_window,
unmanage, raise_and_focus, request_close, sync_geometry,
keycode_to_keysym, modifiers_from_state, handle_event) went from
private to pub(super): called across what are now sibling submodules,
which Rust's privacy model doesn't let see each other's private items.
|
|
Pure reorganization, no behavior change - verified by diffing the
function-name and struct-name sets before/after (both identical) plus
a full cargo test pass. mod.rs keeps ClientState/OutputEntry/CompState/
WindowAnim/RepeatState's definitions, the key-repeat impl, and the
output-lookup impl (all small and tightly coupled to the type
definitions), plus mod declarations. The one large impl CompState
block (previously ~600 lines) splits by concern:
- lifecycle.rs: new_managed_window, set_decorated_from_mode,
redraw_decoration_buffer, remove_window.
- layers.rs: ensure_layer_initial_configure.
- focus.rs: set_keyboard_focus, set_window_activated.
- menu.rs: open/close/run_context_menu_action, is_double_click.
- geometry.rs: raise_pinned, sync_geometry.
- tick.rs: tick_dirty_broadcasts, tick_animations,
resync_stacking_order.
- toplevel.rs: the with_toplevel_title/app_id/sync_toplevel_metadata
free functions.
- tests.rs: unsplit, same reasoning as every other split this pass.
CompState's fields were already pub(crate) (this crate's existing
convention, unlike core's/config's plain-private), so no field-
visibility changes were needed - only resync_stacking_order (called
from geometry.rs, defined in tick.rs) needed bumping from private to
pub(crate), matching that same convention.
|
|
Pure reorganization, no behavior change - verified by diffing the
function-name and struct/enum-name sets before/after (both identical)
plus a full cargo test pass. lib.rs is now a thin shim (mod
declarations + pub use) since a crate root can't itself become a
directory; all the actual content moved into engine/, split along the
Lua API's own srd.*/srd.window.*/srd.layout.*/srd.workspace.*/
srd.theme.* namespace groupings the file's own section comments
already used:
- mod.rs: SharedState, Engine, ConfigError, and Engine's core methods
(new/get/set/dispatch/reload/...).
- register.rs: register_srd_module, which wires every fn_* builder
from every other file into the srd Lua table - the one place that
genuinely needs to see all of them.
- general.rs/window.rs/layout.rs/workspace.rs/theme.rs: the fn_*
builder methods themselves, one file per srd.* sub-namespace.
- support.rs: free functions shared across those (do_reload,
parse_direction, flatten_table_into, validate, default_config) and
the WindowAction enum.
- tests.rs: the ~300-line test module, left unsplit for the same
shared-helper reason manager/tests.rs and udev's tests were.
~40 fn_* methods and the support.rs free functions/enum went from
private to pub(super): called across what are now sibling submodules,
which Rust's privacy model doesn't let see each other's private items.
|
|
Pure reorganization, no behavior change - verified by diffing the
function-name and struct-name sets before/after (both identical) plus
a full cargo test pass. Struct definitions (Card, DrmBuffer, UdevHead,
UdevState) and their small impls stay in mod.rs, since default (crate-
scoped) privacy there is visible to every descendant submodule without
further changes. The rest splits by concern:
- render.rs: the per-frame impl CompState block (render_udev_frame and
the gamma/output-power methods) - still one ~520-line function,
left intact rather than decomposed, given how much of its structure
(the self.udev.as_mut() disjoint-borrow pattern threaded through it)
is deliberate and already documented inline.
- outputs.rs: hotplug reprobe/relayout (impl CompState).
- platform.rs: UdevPlatform's struct/connect logic and its `impl
Platform for UdevPlatform`, previously split apart in the flat file
by ~250 lines of unrelated DRM/session code sitting between them.
- drm.rs: mode/CRTC/framebuffer probing and setup.
- session.rs: libseat/libinput/udev-monitor calloop registration and
the libinput event handler.
A few free functions and one struct (ConnectorProbe, bring_up_head,
probe_connected, pick_crtc, the register_* functions) went from
module-private to pub(crate): called across what are now sibling
submodules, which - unlike a defining module's own descendants --
Rust's privacy model doesn't let see each other's private items.
Matches this crate's existing pub(crate) convention rather than
introducing pub(super), which crates/core's manager/ split used
instead to match *that* crate's plain-private convention.
|
|
canonicalize_key_combo already reordered multi-modifier combos into
dispatch's canonical Ctrl/Shift/Alt/Mod4 order, but passed the key
name through verbatim. keysyms::keysym_to_name capitalizes every
named key ("Space", "Return", "Escape", "BackSpace", ...) while
leaving letters/digits alone, so srd.bind("Super+space", ...) stored
"Mod4+space" while a real Space keypress dispatches as "Mod4+Space" --
never matching. Accepted silently at config-load time, so the only
live symptom was the bind's own callback never running at all.
Root-caused live: keybindings.lua's Super+space bind had a temporary
diagnostic added (logs to /tmp/superspace.log before spawning ags) to
tell "key never fired" apart from "key fired but ags failed" - the
log file never existed, meaning the callback itself never ran.
Fix: round-trip the key name through name_to_keysym (already case-
insensitive) and back through keysym_to_name before storing, so any
case the config writes normalizes to dispatch's canonical form.
|
|
Pure reorganization, no behavior change - verified by diffing the
function-name set before/after (identical 130 functions) plus a full
cargo test pass. WindowManager's struct/field definitions, Default,
new(), and the three trivial constructors (add_rule/register_layout/
available_layouts) stay in mod.rs; the rest of the single ~950-line
impl block is split into one file per the section comments the file
already had (monitors, windows, focus, winops, hittest, dragresize,
workspaces, layout). Three methods called across section boundaries
(monitor_for, windows_on_workspace, cycle_focus) went from private to
pub(super) - Rust's privacy model doesn't let sibling submodules see
each other's private items, only a defining module's own descendants.
The ~1000-line test module moves to manager/tests.rs unsplit: its
helpers (wm_with_monitor, two_monitors, monitor_with_dock) are shared
across tests for every section, so splitting further would mean
duplicating them or adding another shared-support file for little
benefit.
|
|
- ipc.rs: drop a u64 -> u64 no-op cast (WindowId is a plain u64 alias).
- decoration.rs: draw_line took 8 args; bundle the two endpoints into
(i32, i32) tuples instead of four loose coordinates.
- output_management.rs: collapse a WEnum::Value if-let directly into
the outer SetTransform match arm's pattern.
|
|
CPU-side rounded corners for the software-only udev/Pixman renderer,
which has no shader stage to hook the existing GLES version into.
Reads a window's own committed wl_shm buffer, punches premultiplied-
alpha holes into the four corner regions, and hands the masked copy
to MemoryRenderBuffer - the same path already used for titlebar/
border/shadow bitmaps, so it composites through the ordinary unmasked
path and the corners genuinely disappear rather than being painted
over.
Cached per window, invalidated by a per-commit content_epoch counter
rather than rebuilt every frame, so an idle window costs nothing once
masked. general.rounded_corners now defaults per backend instead of
one global true: on for GLES/winit (a real GPU shader, no measurable
cost), off for udev/Pixman (an untested-on-real-hardware CPU cost for
constantly-repainting clients) - WindowManager.rounded_corners_enabled
is Option<bool> so the backend can tell "unset" from "explicitly off".
|
|
decoration.rs's round_top_corners only ever clipped the compositor's own
titlebar/border bitmap - its own doc comment already said why nothing more
had been done: clipping arbitrary client content needs a real per-pixel
mask, "a much bigger change than this cosmetic pass". That's this change,
for the one backend that can do it cheaply: the udev backend's
PixmanRenderer is software-only with no shader stage at all, but GlesRenderer
(winit) has a real custom-shader path (`compile_custom_texture_shader`,
`TextureShaderElement`) that a first look at smithay's higher-level
convenience APIs missed entirely.
crates/wayland/src/rounded_corners.rs: a GLSL fragment shader masking a
window's texture against a rounded-rect signed-distance field while
sampling it (the same technique cosmic-comp/niri use for GPU-side rounded
corners) - built by hand from the surface's own committed texture/view/
damage state (`RendererSurfaceState`'s public accessors), since no smithay
convenience wrapper builds a masked element at all (`CropRenderElement`
only crops to a rectangle). A decorated window rounds only its bottom two
corners - the top two are already rounded, on the titlebar's own CPU
bitmap, by decoration.rs, at the exact same `CORNER_RADIUS` (now
`pub(crate)`, shared between the two so the curve reads as one continuous
radius, not two different ones meeting at a seam) - an undecorated/CSD
window rounds all four, since its content is the window's whole visible
extent. Falls back to plain unrounded content on any failure (shader
didn't compile, no committed buffer yet, a single-pixel-buffer surface),
same "always show something over a prettier maybe-nothing" reasoning
cursor.rs's built-in-arrow fallback already uses. Deliberately scoped to a
window's *main* surface only, not subsurfaces - documented as a real, if
narrow, follow-up rather than attempted here.
`TextureShaderElement` only implements `RenderElement<GlesRenderer>`, not
the generic `RenderElement<R>` every `OverlayElement<R>` variant needs, so
it can't be added to that shared enum without breaking `OverlayElement<
PixmanRenderer>` (used identically by udev.rs) the moment a GLES-only
variant showed up in it. `WinitElement<=GlesRenderer>` (new, winit.rs-only)
wraps the existing enum as one variant instead of touching it - this is
the same lesson as the fullscreen-hiding investigation earlier this
session, just resolved cleanly this time: nesting a *foreign* generic
type inside your own hits real bound-resolution walls; wrapping your own
already-working type inside a new concrete-renderer enum doesn't, because
smithay's own `render_elements!` macro documents exactly this
`<=ConcreteRenderer>` form.
Config: `general.rounded_corners` (default `true`), `srd.window` unaffected
- this is a `general.*` compositor-behavior knob, not a per-window rule
action like `opacity`.
Verified live: shader compiles without error on this machine's real Mesa/
llvmpipe GL driver, and a decorated wezterm window's bottom-left and
bottom-right corners both show a real, smoothly anti-aliased curve on the
actual client-rendered pixels (not a compositor bitmap) in a host-session
screenshot - qualitatively sharper than `round_top_corners`' deliberate
hard cutoff, since a GPU shader can afford a ~2px smoothstep a CPU bitmap
pass isn't worth adding for. cargo build --workspace (all 9 crates), cargo
clippy --workspace (0 new warnings), cargo test --workspace (197 tests,
0 failed).
|
|
opacity
The user asked for per-window opacity (MISSING.md's `windowrule = opacity`
gap) and pushed back on treating smithay's convenience wrappers as a hard
ceiling: "don't rely on smithay, it won't have everything we need." Looked
again at why opacity was ruled out earlier - `render_output`/
`space_render_elements` take one `alpha` for the whole frame's `self.space`
content, no per-element control - and found a path that doesn't need
nesting smithay's internal `SpaceRenderElements` type (the approach that
hit an unresolvable generic-bounds wall investigating fullscreen-hiding
earlier): call `render_elements_from_surface_tree` directly,
once per window and once per layer-shell surface, each with its own alpha,
wrapping the result in the *existing* `OverlayElement::Surface` variant.
That primitive was already proven safe in this codebase (cursor.rs's
client-image path, this file's own popup rendering) - reusing it here for
a window's main content is the same call, not a new one.
Both `udev.rs` and `winit.rs`'s render loops now build window content and
layer-shell surfaces themselves (`elements.rs`: `surface_content_elements`,
`output_layer_elements`, `window_wl_surface` for the Wayland/XWayland
split), in the correct front-to-back order, then call
`OutputDamageTracker::render_output` directly instead of the
`space::render_output`/`space_render_elements` convenience wrappers. Content
itself needs no occlusion clipping against `occluders` (unlike border/
titlebar bitmaps) - pushed in the same front-to-back order as everything
else, ordinary painter's-algorithm draw order already occludes it correctly,
the same property it had via `self.space`'s own order before. `self.space`
stays mapped and `resync_stacking_order`-maintained exactly as before; only
the render step stopped reading from it.
A comment in winit.rs's render loop warned that a near-identical earlier
attempt was reverted for a real ordering bug (whichever window was created
first always painted in front, regardless of focus). That bug's actual root
cause, identified and fixed since, was `Space::map_element` silently
re-stacking on every geometry sync, independent of which render path was
used - see `resync_stacking_order`. This rewrite never reads `Space`'s
internal order for rendering at all (`ids` comes from `WindowManager.order`
directly, the same source `hit_test` already trusts), so that specific bug
class can't recur here regardless of whether `resync_stacking_order` ever
drifts again.
Bonus from the same infrastructure: the bar/dock now genuinely don't render
at all (not just get covered) for a fullscreen window - `output_layer_elements`
skips `Layer::Top`/`Overlay` entirely when any visible window is fullscreen,
the hardening this work backed away from earlier for being too risky to
build via the nested-SpaceRenderElements approach. `capture_offscreen`
(winit.rs's screencopy path) picked up opacity-aware content and layer-shell
inclusion too, though not full parity with the on-screen loop (still no
border/shadow strips there - a pre-existing, separately-flagged gap).
Opacity itself: `Window.opacity` (core), `WindowRuleActions.opacity` /
`srd.rule(..., { opacity = 0.9 })`, `srd.window.set_opacity()`. Caught live,
before commit: opacity was wired into `add_window`'s own rule match but not
`reapply_rules_if_pending` - the *only* path a class-based rule actually
takes effect through for a native Wayland client, since `add_window`'s own
attempt always runs against a still-empty `app_id` (see the regression test
next to the existing one covering the identical historical bug for
`decorated`). Found by setting an isolated `SRDWM_CONFIG_PATH` test config
with `srd.rule({ class = "Alacritty" }, { opacity = 0.4 })` against a nested
instance and pixel-sampling a real screenshot: predicted blend (242,230,53)
at 0.4 over (10,10,15) is (103,98,30); measured (105,100,33).
Verified live in a nested session, screenshotting the *host* compositor
(shows the real on-screen render, unlike grim against the nested socket,
which - separately discovered this work - routes through
`capture_offscreen`): stacking order correct with two overlapping windows
(topmost fully occludes the one behind it, the exact scenario the reverted
attempt got wrong), opacity blend matches prediction. Also confirmed, by
testing the previous commit against the same scene, that upside-down
content on this backend is a pre-existing bug unrelated to this change --
noted, not fixed here.
cargo build --workspace (all 9 crates), cargo clippy --workspace (0 new
warnings), cargo test --workspace (197 tests, 0 failed, includes 2 new
regression tests).
|
|
clicks
Two independent daily-driving gaps closed in one pass, both from MISSING.md
and live user feedback:
Drop shadows (general.shadows, default true). Reuses the exact "bitmap
drawn outside geometry, cached like the border" technique border_strips/
render_border_top already established - decoration::shadow_bitmap rasterizes
a linear alpha falloff (Chebyshev/square-ring distance, not a true blur --
no blur primitive exists without a GPU shader, and the udev backend's
PixmanRenderer is software-only) from SHADOW_MAX_ALPHA (90/255, deliberately
subtle) at the window's own edge down to fully transparent SHADOW_SIZE (12px)
out. Cached in CompState::shadow_buffers, rebuilt at the same trigger points
as border_top_decorations (redraw_decoration_buffer), for the identical
damage-tracking reason: a fresh Id every frame means OutputDamageTracker
never finds a previous-frame match. No shadow for a maximized or fullscreen
window, matching the Hyprland/GNOME convention MISSING.md measures against.
Resize grab margin: 10px -> 6px (general.resize_margin, now configurable,
same call-site-count-preserving change as threading a new parameter through
one indirection point: ResizeEdge::hit_test's only production caller is
WindowManager::hit_test, so this didn't need touching every backend despite
hit_test being shared verbatim across X11/Wayland/Windows/macOS). Reported
live: ordinary clicks near any window edge - a link near a browser's edge,
a button near a panel's edge - regularly registered as a resize-edge grab
instead of reaching the client, not just an occasional near-miss, because
the 10px band was measured inward from the client's own content rect. 6px
stays comfortably grabbable while giving content back most of its edge.
Verified: cargo build --workspace (all 9 crates including the windows/macos
stub backends), cargo clippy --workspace (0 new warnings), cargo test across
core/wayland/config/x11 (188 tests, 0 failed). Shadow rendering verified at
the render-element level live in a nested session (correct geometry, alpha,
buffer contents) - grim/screencopy itself turned out to route through
winit.rs's separate capture_offscreen path, which only ever drew
`decorations` (titlebars), never borders or shadows, so screenshots taken
this way have never shown either; a real gap, not fixed in this pass.
|
|
built-in bitmap
The compositor's own pointer - over decorations and the desktop, and as
the fallback for any named shape with no dedicated art - was always the
hand-rasterized 24px ARROW bitmap, regardless of what theme the rest of the
session was using. Confirmed live (by the AGS peer session) that this
machine's actual GTK cursor theme is Sweet-cursors at size 24 (both
gtk-3.0 and gtk-4.0 settings.ini, and gsettings, agree), installed and
present, but nothing read it - XCURSOR_THEME/XCURSOR_SIZE aren't set on
this work either, so even naive env-based theme loading would have
found nothing.
load_theme_arrow (cursor.rs) now resolves a theme name and size --
XCURSOR_THEME/XCURSOR_SIZE first, falling back to GTK's own
gtk-cursor-theme-name/-size out of settings.ini, since that's what's
actually authoritative in practice here - and loads left_ptr via the
`xcursor` crate (the same one anvil and other smithay compositors use),
picking whichever nominal size the theme ships is closest to the target.
XCursor pixel data comes back as straight RGBA off disk; only the channel
order needs converting to the BGRA every other buffer in this file uses for
Fourcc::Argb8888 (see arrow_bitmap's own byte order) - the data is already
premultiplied alpha per the file format spec, same as everything else built
here, so no premultiplication step. Falls back to the existing built-in
bitmap arrow on any failure (theme/icon missing, corrupt file, a pixel
count that doesn't match the declared dimensions) - same "always present
beats prettier but sometimes absent" reasoning the built-in arrow's own doc
comment already gave for not doing this at all, kept intact as the
fallback rather than replaced.
The built-in arrow's hotspot was implicitly (0, 0) - its tip, baked into
where render_elements positioned it. A real theme's hotspot is data
(CursorBuffers::arrow_hotspot), not necessarily the bitmap's corner, so
both `_ =>` arrow arms in render_elements now subtract it like every other
named shape already does.
Verified live on this machine: resolves theme="Sweet-cursors" size=24 from
settings.ini, loads a real 30x30 left_ptr image with hotspot (4,4) --
checked with a temporary probe test, removed before committing. Two
lightweight unit tests (rgba_to_bgra_*) cover the channel-reorder byte math
without needing a theme installed, so they run everywhere. cargo build
--workspace, cargo clippy -p srdwm-wayland (0 new warnings), cargo test -p
srdwm-wayland cursor (13/13), cargo test -p srdwm-core (111/111) all pass.
Known remaining gap, not addressed here: the loaded size still doesn't
track output scale (a HiDPI output gets whatever size settings.ini says
regardless of scale factor) - MemoryRenderBuffer's own scale param is
hardcoded to 1 throughout this file already, a pre-existing limitation
this change doesn't touch.
|
|
commit
ensure_layer_initial_configure (state.rs) already recomputes the usable
monitor rect when a layer surface's exclusive zone changes, but only from
the commit pre-hook - a surface that goes away without a final commit
(zwlr_layer_surface_v1's destroy path, layer_destroyed here) never ran that
check. unmap_layer's own zone change went unnoticed.
Found live by the AGS peer session: unmapping the bar for fullscreen logged
visible=false immediately, but `srd monitors` kept reporting the bar's old
reserved_top for as long as fullscreen lasted. Harmless there only because
toggle_fullscreen targets full_geometry, which ignores the reservation
outright - but wrong for anything that reads the reserved/usable rect while
a bar is unmapped without a clean exit (a crash, not just AGS's cooperative
fullscreen hide). Same zone_before/zone_after diff ensure_layer_initial_
configure already uses, run around unmap_layer instead of arrange().
Verified: cargo build --workspace, cargo clippy -p srdwm-wayland (0 new
warnings), cargo test -p srdwm-core (111/111).
|
|
IPC/global-menu/output-management work
Border/titlebar decoration was built from Window.geometry (the animation's
final target) in both wayland backends' render loops, while sync_geometry
already draws a window's actual content at window_anims' interpolated rect
during any maximize/fullscreen/open-slide tween. Border and content read two
different rectangles for the whole transition, so the border visibly
detached from the window it was outlining - reported as "borders aren't
flush." Both udev.rs and winit.rs now read the same animated rect for
titlebar placement, border-strip placement, and the occlusion test against
later windows in stacking order. Verified: cargo build --workspace, cargo
clippy (0 new warnings), cargo test -p srdwm-core (111/111).
Also checkpoints substantial protocol/IPC work from prior sessions that had
accumulated uncommitted: gtk-shell1 support (gtk_shell.rs, the vendored
gtk-shell.xml, xwayland.rs's X11-side mirror) backing the global app menu;
zwlr_foreign_toplevel_manager_v1 (foreign_toplevel.rs) broadcasting distinct
maximized/minimized/fullscreen/activated state per window; output_management
(ext-output-management + layer-shell exclusive-zone reservation tracking);
workspace.rs and context_menu.rs; a Unix-socket IPC crate (platform/src/
ipc.rs) and a `srd` control-CLI crate (crates/ctl); xkb_config.rs; and a
theme module (core/src/theme.rs). A peer session working the AGS shell
concurrently verified several of these live against a running srdwm: the
global menu rendering a real app's File/Edit menu over gtk-shell1, and
foreign-toplevel correctly reporting maximized and fullscreen as independent,
non-simultaneous states with the geometry each implies (maximize stops at a
reserved top bar and past a dock; fullscreen reaches the true monitor edge).
|
|
Config path drops a level: ~/.config/srd, not ~/.config/srdwm/srd, which
said the same thing twice. No other user-facing path had the same problem --
srdwm reads the config dir and writes nothing else.
Cursor shapes. A client's own cursor surface is now rendered with the
hotspot it declared, so an I-beam over text or a hand over a link shows the
app's image instead of srdwm's arrow. The built-in arrow stays as the
fallback when no client has set one, over decorations and the desktop.
Named shapes still fall back to the arrow; most toolkits set a surface.
Decorations and cursors now share one OverlayElement type, since
render_output takes a single custom-element slice.
Key repeat (srd.bind_repeat, Hyprland's binde). Held volume, brightness and
switcher keys repeat at the seat's own rate rather than firing once. Driven
from the poll loop, not a timer source: the winit backend has no calloop
loop of its own, and poll_events already runs continuously in both backends.
Repeat stops when *that* key is released, not when any key is.
Always-on-top / pin, for the picture-in-picture and HUD rules that used it.
Window::always_on_top was another declared-but-never-read field. Enforced in
WindowManager's stacking order rather than at render time, so every consumer
of stacking_order gets it and none can forget to honour it.
Mouse-only window management, checked end to end: drag the titlebar to move,
drag any edge or corner to resize, titlebar buttons to close/maximise/
minimise, click to focus, drag to a screen edge to snap, and now
double-click the titlebar to maximise. The resize grab band went from 6px to
10px - a hairline is genuinely hard to hit with a mouse, which is why
Hyprland ships extend_border_grab_area.
Also removed the emoji status markers from docs/IMPLEMENTATION_STATUS.md.
|
|
Groundwork for actually daily-driving this: porting the user's Hyprland
config exposed what srdwm couldn't yet express, and testing on a bare TTY
exposed something worse.
A visible mouse cursor. Nothing drew a pointer at all - on a bare TTY the
mouse was simply invisible. It hid because the nested backend runs inside
another compositor, which draws a cursor over srdwm's window; only the DRM
backend, i.e. the actual session path, was affected. A built-in arrow is now
composited above everything on the output the pointer is on. It's a
reviewable ASCII bitmap rather than an XCursor theme: a cursor that is always
present beats a prettier one that sometimes isn't, the same reasoning as
decoration.rs's font fallback. Client-set cursor surfaces and named shapes
are still not rendered, so an app asking for an I-beam gets the arrow.
Lid switch. libinput switch events are handled and surfaced to config as
srd.on("lid_closed"/"lid_open", fn), so closing the lid can lock and suspend
instead of doing nothing.
Config-driven additions, each needed by a binding in the ported config and
none of which existed: fullscreen (Window.fullscreen was a dead field --
declared, never read or written), directional window move that swaps with
the neighbour and reorders the stack so tiling follows, focus cycling,
modifier+drag to move/resize anywhere in a window rather than only by the
titlebar, modifier+scroll to change workspace, and 8 XF86 media/power
keysyms taken from the system's own XF86keysym.h. The keysym tables are
hand-maintained in both directions and a key missing from either fails
silently, so a round-trip test now covers every one the configs bind.
Verified in the QEMU VM: a bare-TTY screendump shows a recognisable arrow at
the pointer position (113 white fill + 58 black outline pixels at screen
centre, where the pointer starts).
|
|
Monitors were probed once at startup, so plugging or unplugging one while
srdwm was running went unnoticed. A UdevBackend event source now watches for
the kernel's `change` uevent and reconciles the head list against a fresh
connector probe - forcing a re-probe rather than trusting cached status,
since on a hotplug the cache is exactly what has gone stale.
Removing a head tears down everything it owned: the wl_output global, its
place in the Space, its DRM framebuffers and dumb buffers (dropping the Rust
structs alone leaks the kernel-side objects, which matters when a cable is
plugged repeatedly), and any lock surface for it - otherwise
confirm_lock_if_presented would wait forever on a monitor that no longer
exists. New connectors go through the same bring_up_head path as startup, so
a monitor plugged in later is set up identically to one present at boot.
Heads are then repositioned left-to-right, since removing one shifts the
rest, and layer maps re-arranged so bars follow their moved output.
set_monitors rehomes windows stranded by the change, and main.rs re-queries
the whole monitor list on MonitorAdded/MonitorRemoved rather than applying
the single monitor in the event, because the others' positions move too.
The rehoming had a bug that unit tests missed and live testing caught.
It originally keyed off Window::monitor, but that field records the monitor
a window was *assigned* at creation, not where it is: add_window always sets
it from the primary monitor, so a window placed on the second monitor by a
rule - or dragged there - still reads monitor == 0. The field-only check
saw a valid id, skipped the window, and left it at coordinates that no
longer existed: invisible and unreachable. Found by unplugging a monitor out
from under a real xterm and watching it vanish from both heads. It now keys
off geometry, with a regression test that fails against the old logic.
Verified in the QEMU VM, booting with one connector and toggling the second
at runtime: plug in -> head added and rendering at its own resolution;
unplug -> head removed cleanly; and an xterm at global x=1500 survived its
monitor being unplugged, reappearing at x=680 (= min(1500, 1280-600)) with
its size intact. Writing to /sys/class/drm/<connector>/status changes the
connector but emits no uevent on this kernel, so the signal the kernel would
send is synthesized with `udevadm trigger`; the whole reaction path is
genuinely exercised.
|
|
Implements the protocols that were blocking srdwm-wayland from being a real
session, plus multi-monitor, and splits the backend into modules. Everything
here was verified by running it against real clients, not just compiled.
Protocols
- wlr-layer-shell + xdg-output: bars/launchers/notifications. xdg-output is
not optional in practice - without it wofi segfaults rather than
degrading, since it calls get_xdg_output without null-checking.
- Clipboard: wl_data_device_manager, primary selection, and wlr-data-control.
Data-control is what `wl-paste --watch cliphist store` needs, as it reads
the selection without holding focus. Selection focus now follows keyboard
focus, without which a focused window can neither copy nor paste.
- ext-session-lock: `locked` gates rendering and input. No key is treated as
a WM binding while locked - the config binds Mod4+Return to spawn a
terminal, so honouring bindings at a locked screen would defeat the lock.
The lock is confirmed only after a client-content-free frame has actually
been presented, never at request time.
- wlr-screencopy (hand-written; smithay ships no helper) for grim/slurp.
Multi-monitor
Every connected connector becomes a head with its own buffers, damage
tracker and page-flip state, matched by CRTC so differing refresh rates
don't gate each other. Outputs are reached through primary_output/output_at/
output_for_wl rather than a single field, which kept the change to ~11 call
sites. Session lock creates one lock surface per output and waits for all of
them, so a second monitor can't still show the desktop when the locker is
told the session is safe. Modes are picked by the PREFERRED flag, not list
order, and CRTCs are never double-assigned.
Bugs found by testing, not review
- Nothing gave a newly-created window Wayland focus: a freshly-opened app
received no keystrokes and could not paste until clicked.
- Opening a window at a locked screen stole keyboard focus. Caught by
counting wl_keyboard.enter delivered to a client launched while locked:
1 before the guard, 0 after. A killed locker correctly leaves it locked.
- Reading back the winit EGL window surface destroyed the GL context on the
first screencopy capture, taking the compositor down. Root-caused by
A/B-ing the same build with only the readback removed; capture now renders
an offscreen pass.
- Output mode was resent every frame at 60Hz, flooding any client bound to
wl_output with duplicate mode/done events.
- general.default_layout was defaulted and validated but never read, so
setting it did nothing. srdwm is dynamic-first (Windows/macOS style, with
drag-to-edge snapping); tiling is one opt-in layout, and that stays true.
- .gitignore's unanchored `srdwm` matched any path component of that name,
silently excluding the whole crates/srdwm source crate - the binary crate
the workspace lists as a member, so a fresh clone could not build.
Modularization
lib.rs went from ~1260 lines to 78: state, protocols, input, lock,
screencopy, winit and udev now each own one responsibility. lock is grouped
by feature rather than kind on purpose, since its security invariant spans
state, protocol handling and rendering at once.
Multi-monitor was verified in the QEMU VM with a two-output virtio-gpu: both
heads screendumped at their own resolution showing srdwm's clear colour, and
a window forced to global x=1500 landed on head 1 at head-local x=220 while
head 0 stayed empty. Known limitation, now documented: the nested winit
backend stalls while its window is occluded, because the host stops
scheduling frames and eglSwapBuffers blocks.
|
|
layer-shell, clipboard (wl_data_device_manager), session-lock, and
udev-backend multi-monitor support are the gaps identified when deciding
srdwm isn't ready to add to a session picker yet - recorded in
docs/IMPLEMENTATION_STATUS.md's "Not implemented anywhere yet" section
and as inline TODOs at the relevant code (CompState's delegate_*! list,
udev.rs's find_connected_output).
|
|
Three real bugs found and fixed via WAYLAND_DEBUG=1 protocol tracing in
the QEMU VM, plus a fourth found along the way:
- XWayland tried glamor (GBM rendering) first, which fails against this
deliberately software-only compositor; its post-failure fallback path
never used the xwayland_shell_v1 protocol at all, so X11Surface::
wl_surface() never resolved. Fixed by shadowing `Xwayland` on PATH with
a wrapper script that always re-execs it with -shm (smithay's
XWayland::spawn hardcodes its own argv and can't be bypassed either,
since XWaylandClientData's fields are private).
- Even with -shm, set_mapped(true) was only called after wl_surface()
already resolved, deadlocking XWayland (it never advances a window past
surface creation until the map is granted). Fixed by calling
set_mapped(true) unconditionally in map_window_request.
- The window then rendered as a ~1px sliver: initial geometry was seeded
from X11Surface::geometry(), which can still be a tiny default at
MapRequest time. Fixed by using the same 800x600 default the xdg-shell
path already uses.
- Typing didn't reach the window until a broader, XWayland-independent
bug was fixed: nothing in the Wayland backend ever called
KeyboardHandle::set_focus, so no window (native or X11) could ever
receive keyboard input. Fixed in handle_pointer_button, along with
TitlebarHit::Close being X11-surface-blind.
Verified live: xterm launched via XWayland renders correctly sized and
decorated, and a synthetic keypress sequence (ls + Enter) executed in its
shell, screendump-confirmed.
|
|
- srd.rule(): match windows by title/class, apply floating/maximized/
workspace/geometry/decoration actions on creation (crates/core/src/rules.rs)
- srd.validate_config()/srd.debug.*: real range/format checks and
status/profiling helpers, replacing the always-true stub
- Wayland titlebar text rendering via fontdue, unit-tested without a
display (crates/wayland/src/decoration.rs)
- Wayland precise keybinding matching, replacing the "any Super-held key"
heuristic, sharing the keysym table with X11 (moved to
crates/core/src/keysyms.rs)
- Wayland DRM/udev backend (crates/wayland/src/udev.rs): runs as the real
compositor on a bare TTY via libseat/libinput/KMS, software rendering
via Pixman + dumb buffers (no GBM/EGL required)
- srdwm_platform::detect() fix, found via VM testing: a bare TTY with no
DISPLAY/WAYLAND_DISPLAY now correctly resolves to Wayland instead of an
X11 backend that can never work there
- XWayland integration groundwork (crates/wayland/src/xwayland.rs): spawn,
X11Wm, and full XwmHandler event routing into the same WindowManager/
Space pipeline as native clients. Windows don't render yet - a real
glamor-vs-software-renderer conflict in XWayland's own fallback path,
root-caused via WAYLAND_DEBUG tracing and documented in
docs/IMPLEMENTATION_STATUS.md rather than worked around blind.
All verified live in an isolated QEMU VM: X11 backend shows two
decorated, correctly-tiled xterms with real title text; the DRM/udev
Wayland backend opens the GPU, initializes input, and scans out a
rendered frame via KMS page-flip.
|
|
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.
|