<feed xmlns='http://www.w3.org/2005/Atom'>
<title>srdwm/crates/wayland/src/state/lifecycle.rs, branch main</title>
<subtitle>Cross-platform window manager written in Rust.
</subtitle>
<id>https://srdusr.com/git/srdwm/atom?h=main</id>
<link rel='self' href='https://srdusr.com/git/srdwm/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/'/>
<updated>2026-07-26T23:25:00+00:00</updated>
<entry>
<title>Hide a window only when srdwm knows it has not drawn, not when a lookup says so</title>
<updated>2026-07-26T23:25:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-07-26T23:25:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=e20d49b0ee3dbd83499445d61eb2d65904d74311'/>
<id>urn:sha1:e20d49b0ee3dbd83499445d61eb2d65904d74311</id>
<content type='text'>
Regression I introduced two commits ago, reported live: "I can click close
where the button would normally be and it does close, but it is still
invisible."

The gate that stops an empty frame being drawn before a client paints asked
the renderer, from inside the render loop, whether a window's surface had a
buffer attached right now - and treated "no" as "do not draw". That
question is only meaningful for a native xdg-shell toplevel. An XWayland
window's surface state does not describe it the same way, so the answer came
back no on every frame and the window was never drawn again, while srdwm's
own hit-testing carried on working perfectly: an invisible window that still
takes clicks, which is a worse failure than the empty frame it was meant to
prevent.

Inverted to the fail-safe direction. `new_managed_window` - the one path
that creates a native toplevel - puts the window into
`awaiting_first_buffer`, and `commit` takes it out on the first commit that
carries a buffer. The render and capture paths test that set and nothing
else. A window is now hidden only when srdwm itself put it there, so no
window whose plumbing works differently can be hidden by a lookup that did
not apply to it: the XWayland map path never touches the set, and neither
can anything else.

The buffer question still gets asked, but only in `commit`, about a surface
it was just handed, where it is the right question.

Verified both halves: an ordinary spawn still shows no frame before content
(26 captured frames with content, 0 without), and the only way into the set
is one line in one function.
</content>
</entry>
<entry>
<title>Do not draw a window before its client has painted anything</title>
<updated>2026-07-23T20:56:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-07-23T20:56:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=8ed79c21a53e10c29ea35b9a605250ea18f191cf'/>
<id>urn:sha1:8ed79c21a53e10c29ea35b9a605250ea18f191cf</id>
<content type='text'>
Reported as "before a window spawns, the border corners look funny".

A toplevel is placed, sized and decorated the moment its role is created,
which is well before the client draws. srdwm was rendering it from that
moment, so what appeared first was an empty frame: border, titlebar and
shadow standing around bare desktop, at the guessed 800x600 placeholder
size, with nothing inside. When the real buffer arrived the frame snapped
to the real size.

Measured in a nested session, capturing a cold terminal's spawn with grim:
four consecutive captured frames spanning 540ms showed a complete red
border with zero client content inside it, at 642px outer height, which
then settled at 610 - a jump of exactly one TITLEBAR_HEIGHT. After this
change the same capture has no such frame at all: every frame that shows a
border shows content in it, and the height does not change afterward.

Two parts:

- Nothing is drawn for a window that has never committed a buffer. All
  five paths that draw a frame agree on this - both udev render loops
  (Pixman and GPU), the winit render loop, and both screencopy paths, so a
  screenshot cannot show a frame the screen does not.
- The open-slide starts at the first commit that carries a buffer rather
  than at role creation. A cold terminal took ~800ms to paint, long enough
  for the whole tween to finish against the empty frame, so the window
  simply appeared, already at rest, with no animation at all. It now
  animates where it can actually be seen.

The answer latches once true (windows_shown_once), so a window that has
legitimately shown something is never hidden again by this however its
buffer state changes. A window that cannot be resolved to a surface counts
as drawable, deliberately: this hides a window only on positive evidence
that it has never drawn, so nothing whose surface plumbing works
differently - an XWayland window - can be hidden by a lookup that did
not apply to it.

Same shape, and the same reason, as sync_layer_visibility's own has_buffer
branch, which layer surfaces have had all along.

533 tests pass, clippy clean.
</content>
</entry>
<entry>
<title>Fix spawn placement under the top bar, add per-window minimum sizes, and clean up maximize</title>
<updated>2026-05-11T14:57:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-05-11T14:57:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=646b37e7e3aa5931079c6b9e804f420bb43c74d5'/>
<id>urn:sha1:646b37e7e3aa5931079c6b9e804f420bb43c74d5</id>
<content type='text'>
Four reports after restarting into today's build, with a screenshot. The
screenshot was measured, not eyeballed: top bar y=0..29, a 4px accent border
at y=30..33, titlebar from y=34, bottom border at y=1027..1030, and 49px of
bare desktop below it.

Windows spawning too close to the top bar. A remembered position was
validated only by asking whether it landed on some monitor's full_geometry,
which includes the strip a top bar reserves, so an app whose remembered y was
small reopened with its titlebar under the bar. That is why it was
"sometimes": it depended on the stored value, and the live store holds
wezterm at y=44 and firefox at y=69 against a 30px bar. Remembered positions
are now clamped into the monitor's usable area.

Placement not surviving a logout. Window memory does persist, but five of the
eleven entries in the live store were saved with a second monitor attached,
at x &gt;= 2000. Those points match no current monitor and were discarded
outright, falling back to a fresh cascade, so those apps appeared to remember
nothing. Such a position is now clamped onto a monitor that exists instead.

Per-window minimum sizes. One global floor is wrong in both directions.
Three sources now, in increasing precedence: the global floor, the client's
own declared minimum (xdg_toplevel.set_min_size or ICCCM hints), and a
min_width/min_height window rule overriding both. A rule wins permanently --
the backend refreshes the client's declared minimum on every decoration
redraw and must not undo a deliberate override.

Maximize, three faults in one report. A maximized window now draws no
border: its edges are the screen's edges, and the only place maximize stops
short is the bar strip, which is exactly where the measured line was.
maximize_geometry_for no longer subtracts a bottom-anchored dock's exclusive
zone, so maximize runs to the bottom of the screen and the dock floats over
it; top, left and right are still honoured.
general.maximize_covers_dock = false restores the old behaviour. With the
border gone the window sits flush under the bar instead of with an accent
line crowding it.

Verified: seven new tests on the real numbers from the live store, and
maximize geometry measured live in a nested instance (a window maximized on a
split half reports exactly that half's rect). NOT confirmed on screen: the
border removal and the dock behaviour - the nested backend has no bar or
dock to reserve a zone, and an attempt to check the border produced a failing
control, since srd set border_width only affects windows created after it.

515 tests pass, clippy clean.
</content>
</entry>
<entry>
<title>Build the eight asks recovered from the previous session's transcript</title>
<updated>2026-04-26T22:18:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-04-26T22:18:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=c4dc99cc3211c4dc1a461f228402be1593b883c9'/>
<id>urn:sha1:c4dc99cc3211c4dc1a461f228402be1593b883c9</id>
<content type='text'>
The punch list was not the whole record. These are the owner's own typed
requests, read back out of the previous session's transcript rather than
guessed at, then each checked against the code before being treated as open.
Three they suspected were already done really were: dialogs already had a
Close-only titlebar, inactive dimming already existed, and the corner resize
hitbox had already been tuned.

Snap layouts on drag, asked for twice. Edge snapping worked but committed
silently on release with nothing shown first, so there was no way to know it
would happen or where. A translucent drop-target preview now follows the
drag, and throwing the pointer at a monitor's top edge drops down the
existing six-cell grid to aim at. The preview calls the same snap_zone that
end_drag does, so the two cannot disagree. Two defects found by screenshot
before landing: moving down onto the flyout closed it, and its labels
overflowed at a fixed cell width - the same "text goes out of view" fault
already fixed once for the context menu.

New File now offers real types, chosen by extension, with the de-duplication
counter placed before the extension so the file stays what it says it is.

Refresh re-reads init.lua and fires a new srd.on("refresh") handler instead
of only re-scanning the icon grid. What refresh means beyond srdwm's own
config stays the config's decision.

general.config_reload_on_write (default on) applies an edited config on save,
via an mtime sweep rather than an inotify watch: no new dependency, same
behaviour on every target, and unaffected by editors that write through a
temp file.

A real bug behind "what happens when our config fails": you lost every
keybinding. do_reload cleared the binding, handler and repeat tables before
re-executing and never restored them, so a syntax error left neither the old
config nor the new one, and the only key still working was the reload combo
nobody thinks to press. The tables are now restored on any failure and
config errors reach notify-send, not just the log.

srd.lock() and a default Mod4+Ctrl+l binding: the built-in lock screen could
not be reached from Lua at all. Native rather than shelling out, because a
lock key that shells out fails silently when the binary is not on PATH.
Default bindings added for srd.window.move and a dynamic/tiling toggle, both
of which existed with no way to reach them, plus srd.layout.get() so the
toggle reads the live workspace rather than the configured default.

Dialogs open centred, and are excluded from remembered geometry in both
directions - that table is keyed by app_id, which a dialog shares with the
window that spawned it, so dialogs inherited an unrelated position and size
and then overwrote it with their own.

theme.decorations.title_bar.button_mode (dynamic by default, or fixed) drops
the Maximize button on a window whose client pinned min == max size, where
pressing it can do nothing. Maximize is removed from the slot list rather
than skipped in place on both the render and hit-test sides, so the remaining
buttons close the gap identically; three tests pin that agreement, which is
what fails silently when it drifts.

Also: the nested backend's screencopy pass now draws both menus, the flyout
and the drag preview. Four investigations in one day started from a
screenshot missing a tier, so that pass carries an explicit list of what it
still omits and the on-screen loop points at it.

512 tests pass, clippy clean.
</content>
</entry>
<entry>
<title>Fix a real regression: dynamic-mode windows lost their shadow via toggle_floating</title>
<updated>2026-01-30T07:39:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-01-30T07:39:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=2a51fb86e2a123ff9e810a0b4105c9fe5f85e3ae'/>
<id>urn:sha1:2a51fb86e2a123ff9e810a0b4105c9fe5f85e3ae</id>
<content type='text'>
The tiled-shadow-tint fix earlier today gated the shadow on Window::floating
alone. arrange_workspace only reads floating under the "tiling" layout, so
every window on this project's own default "dynamic" layout starts, and
stays, floating: false - the gate misread that as "tiled, no shadow"
regardless of which layout was actually running, so shadows silently
vanished under dynamic mode entirely, recoverable only by pressing Super+S
(toggle_floating), which then looked like that key toggles a tint rather
than floating. Fixed by checking the workspace's own layout name first:
a window is only "currently tiled" when its workspace runs "tiling" AND
it hasn't opted out via floating. DecorationSignature's floating field is
now currently_tiled, since a layout switch changes this for every window
on a workspace without touching any of their own floating fields.

Also disabled general.shadows in the user's own config per direct
request - never asked for, on by default, and a real problem for
color-accuracy work regardless of how correctly it renders otherwise.

Also fixed both context menus (titlebar and desktop) silently truncating
labels past a fixed 170px width with no indication - widened dynamically
to each menu's own real widest label via a new measure_text_width helper.
</content>
</entry>
<entry>
<title>Stop giving tiled windows a shadow that lands on their neighbour</title>
<updated>2025-12-03T20:52:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-12-03T20:52:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=393f886eb9e275c3b7a9e3ef396092ba71297dfb'/>
<id>urn:sha1:393f886eb9e275c3b7a9e3ef396092ba71297dfb</id>
<content type='text'>
Diagnosed by a peer session (dotfiles-1a): SHADOW_SIZE is 24px, and a
tiling layout with a small gap_inner (as little as 1px live) leaves the
shadow nowhere to fall except onto the adjacent tile, darkening it by up
to SHADOW_MAX_ALPHA (~35%) on whichever side is unfocused. Not a content
tint or an opacity rule - verified against the actual rasteriser and the
live rule set before accepting the diagnosis.

A drop shadow separates a window from what's behind it; tiled windows are
coplanar and adjacent by construction, with nothing behind them to
separate from. redraw_decoration_buffer's shadow gate now requires
w.floating in addition to the existing !maximized/!fullscreen checks.
DecorationSignature gained a floating field so toggling floating on its
own invalidates the decoration cache instead of waiting for an unrelated
field to force a rebuild.

Live-verified in a nested compositor: two tiled windows show a clean
shared edge with no gradient bleeding across; floating a window still
detaches it from the tile group with its shadow intact; shadows still
toggle globally both ways.
</content>
</entry>
<entry>
<title>Let a new window pick its own size instead of forcing a guessed placeholder</title>
<updated>2025-12-01T17:28:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-12-01T17:28:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=9e88d3e3f86a1248b72fdb9bab59d9936a3323a6'/>
<id>urn:sha1:9e88d3e3f86a1248b72fdb9bab59d9936a3323a6</id>
<content type='text'>
Root-caused "windows spawn small and square, not remembering placement or
size": new_managed_window hardcoded a fresh toplevel's geometry to 800x632
before the client had said anything about its own preferred size, and
sync_geometry forced that guess onto the client's very first
xdg_toplevel::configure unconditionally. Per xdg-shell, size: None on that
first configure is how every mainstream compositor lets a client pick its
own natural size instead; this one never did, so every app converged on
the same placeholder rectangle regardless of what it would have chosen.

Window::size_is_provisional marks a size that really was just the guess
(not a remembered geometry, a rule's explicit geometry action, or a
maximize/phone-mode fill, none of which are guesses). sync_geometry sends
size: None for such a window's first configure; a new adopt_provisional_size,
called from the commit handler, adopts the client's own real first size
into Window::geometry the moment it commits one, clamping only position so
a bigger-than-guessed window can't hang off its monitor's edge.

Live-verified in a nested compositor: a zenity dialog now renders at its
own compact natural size instead of being stretched to the old guess.
</content>
</entry>
<entry>
<title>Fix window memory never saving on close, and split-screen icon/primary bugs</title>
<updated>2025-11-04T09:55:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-11-04T09:55:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=a8991f65602abc5ecee740c443c58fa96ecd15e1'/>
<id>urn:sha1:a8991f65602abc5ecee740c443c58fa96ecd15e1</id>
<content type='text'>
Screenshotted the just-split display on request rather than guessing --
it showed why windows never seem to remember placement/size, plus two
real split-screen bugs.

Window memory (WindowManager::remembered_geometry) was correctly wired
on the read side, but the only writes came from end_drag/end_resize in
dragresize.rs - a real drag or resize. A window the user opens, looks
at, and closes without ever touching its edges had nothing recorded, so
reopening it always fell back to a fresh cascade placement, for what is
probably most ordinary window lifecycles. remove_window now also
snapshots geometry (same app_id-non-empty gate the drag/resize sites
use), persisted at both of its wayland-side call sites the same way the
drag/resize-release site already does.

desktop_icon_origins mirrored the full icon set onto every Monitor entry
when general.desktop_icons_all_monitors is on - which, after a
srd.monitor.split, is one entry per split part of the same physical
screen, not one per real monitor. Extracted into a separately-tested
icon_origins_for that collapses split parts of the same connector back
to one origin, keeping a genuinely separate monitor's own origin intact.

Found while fixing that: every split part also reported primary: true
(computed from the connector's name, which doesn't vary per part) --
fixed by gating on part == 0 too.
</content>
</entry>
<entry>
<title>Live-expose monitor split, clean up leftover debug diagnostics</title>
<updated>2025-10-26T20:58:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-10-26T20:58:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=304f3a374408bb6a5a04ebb7d4652c631395f5c6'/>
<id>urn:sha1:304f3a374408bb6a5a04ebb7d4652c631395f5c6</id>
<content type='text'>
srd.monitor.split only ever ran at Lua config load despite being a plain
WindowManager mutation that every backend's monitors() already reads
fresh on each call. Adds srd dispatch set output split &lt;name|id&gt; &lt;parts&gt;
[rows|columns] (IPC set_monitor_split), same id-resolves-to-name pattern
set_output_enabled already uses.

Also removes eight log::warn!("XXX-DIAG ...") lines left behind from live
debugging in the multi-session shift that landed in 3c41fc4 - the same
"temporary, never removed" pattern already fixed twice earlier this
session. Several fired on genuinely constant interaction (every title
change, every workspace switch, every layer-shell surface hide), not
just a one-off leftover. Left xdg_shell.rs's own POPUP-GEOM-DIAG/
POPUP-GRAB-DIAG alone - that one is a still-open, self-documented
investigation, not litter.

Also documents (docs/TODO.md, not a code change) a live incident where
creating a second fake monitor visibly corrupted the real monitor's
position and kept drifting with no further input - not root-caused
srdwm-side, flagged to the AGS peer session since a fake monitor's real
wl_output global is indistinguishable from a real hotplug to GDK/GTK.
And documents a deliberate decision not to blind-port window decoration
rendering onto the experimental, never-live-tested GPU render path.
</content>
</entry>
<entry>
<title>Wayland backend: desktop icons v2, menu rebuild, layer-shell/scale fixes,</title>
<updated>2025-09-10T09:24:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-09-10T09:24:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=5ee627928d0cb017e59d796c71793c35d5b65d33'/>
<id>urn:sha1:5ee627928d0cb017e59d796c71793c35d5b65d33</id>
<content type='text'>
XWayland stability, GPU rendering, and multi-cursor Phase 2

The bulk of a multi-session shift's real work landed in crates/wayland.
Full root-cause/verification narrative for every item below lives in
docs/TODO.md (each has its own dated entry); this is the summary:

Desktop shell:
- Real desktop icons v2 (state/desktop_icons.rs, desktop_icons.rs):
  fixed origin-baked-before-the-bar-connects, fixed-icon sort order, and
  a proper Rename/Delete-to-Trash menu (window_memory.rs backs the
  rename-persistence side). Rubber-band marquee multi-select.
- icon_theme.rs: real freedesktop icon-theme lookup (inherits chain,
  hicolor fallback) rendering actual theme SVGs via resvg/tiny-skia,
  replacing the hand-drawn placeholder glyphs.
- Context/desktop menus (decoration.rs, desktop_menu.rs, state/menu.rs)
  rebuilt to match the project's own AGS panel styling: rounded floating
  panel, tinted-fill row highlight, real separators, a much fuller
  titlebar window-menu action set.

Layer-shell / multi-monitor:
- Layer-shell hit-testing and render positioning (input/pointer.rs,
  udev/render.rs's element placement) now correctly convert LayerMap's
  logical geometry into physical pixels on a fractionally-scaled output
  - root cause of a bottom-anchored dock being unclickable and
  unpainted while a top-anchored bar on the same output worked.
  udev/outputs.rs's relayout_outputs gained the same physical/logical
  split for cross-output positioning, now backed by a real unit test
  (next_logical_x) built from the original measured incident numbers.
- state/geometry.rs: a window's border/decoration no longer briefly
  clips when moved between differently-scaled monitors mid-drag.

XWayland / stability:
- xwayland.rs, udev/session.rs, udev/platform.rs: fixed a 100%-
  reproducible cold-start XKEYBOARD crash-loop (XWayland's own stdin
  inherited a real, already-owned VT; env passthrough and idle-callback
  spawn timing were both real, independent gaps) that had silently taken
  down all X11-app support and the global-menu registrar every session.
- state/toplevel.rs, state/lifecycle.rs: XWayland dialog detection via
  WM_TRANSIENT_FOR, not just a native xdg_toplevel parent.

Rendering:
- udev/render.rs, decoration.rs: real GPU (GBM+EGL+DrmCompositor)
  window-content and cursor rendering on the udev backend, falling back
  to the untouched Pixman path automatically on any init failure.
- decoration/tests.rs, state/mod.rs: rounded-corner/border fixes for
  interactive resize lag and cross-monitor moves.

Multi-cursor Phase 2 (virtual_pointer.rs, new; state/mod.rs, udev/
platform.rs, winit/nested_platform.rs): pins a zwlr_virtual_pointer_
unstable_v1 object to a specific window, bypassing the shared seat/
focus/pointer_pos path entirely via hand-rolled wl_pointer.enter/motion/
button/frame/leave against every WlPointer the target client has bound
(PointerHandle::client_pointers). Lets an agent operate one window while
a human uses another, genuinely simultaneously, with zero client
cooperation and no second wl_seat (confirmed a dead end: real clients
only ever bind the first seat advertised).

Full workspace build/test/clippy clean.
</content>
</entry>
</feed>
