| Age | Commit message (Collapse) | Author | Files | Lines |
|
An earlier cleanup deleted five of them from window-memory.json by hand and
they came back within minutes. The reason is that a running compositor holds
the whole table in memory and save_all writes all of it back on the next
window close, so a hand-edited file cannot survive a running session.
Filtering on load is the only point where the fix sticks.
remove_window already refuses to record a size the client never chose, so
nothing new is captured this way; this clears what was written before that
landed. Those entries are self-perpetuating - a remembered size makes the
next launch non-provisional, which forces the client to that size rather
than asking it to pick, which writes the same value back on close - so an
affected app can never escape on its own.
A window genuinely sized exactly 800x632 loses its remembered size once and
gets it back at the next real resize or close. Two tests cover the exact
match and that sharing only one dimension is not enough.
531 tests pass, clippy clean.
|
|
Reported as windows still being tinted. The tint is the drop shadow, and
init.lua sets general.shadows to false - loading that same config in a
fresh compositor reports false, while the running session reported true.
The reason it could not be corrected is a defect in the live-settings replay
added earlier today. That replay re-applies every srd set after a config
reload so the titlebar menu's Customize rows survive a save. The unintended
half is that a live override then outranked the config file permanently:
editing init.lua and saving put the override straight back, which is the
state the session was found in.
Live-always-wins and config-always-wins are both wrong. The rule is now that
the config wins for anything it states, and a live override survives only
where the config is silent. That distinction cannot come from `values`,
where defaults are seeded before any script runs so every key looks set, so
the config engine records which keys srd.set actually touched during the
load. That record is cleared and rebuilt on each load and restored along
with everything else when a reload fails.
Verified both directions: a config-stated key reverts to the file's value on
the next reload, and a key the config never mentions keeps its live
override.
529 tests pass, clippy clean.
|
|
character
Two gaps, both found by being asked about them.
~/.face was never read. The file exists here, a 300x300 JPEG, and a grep for
.face/AccountsService/avatar_path returned nothing anywhere in the codebase:
the lock screen drew a coloured circle with the user's initial
unconditionally. It now looks for ~/.face, ~/.face.icon, then
/var/lib/AccountsService/icons/$USER, which is where GNOME and KDE keep the
picture their settings UI sets. Scaled to cover the circle and
centre-cropped rather than letterboxed, and masked with a soft edge. Falls
back to the initial when nothing is set or the file will not decode.
That needed a raster decoder, since ~/.face is JPEG and the only image code
here was resvg, which is SVG-only. Added image with default features off and
only jpeg and png.
The on-screen keyboard could not type most passwords. It had letters,
digits, and the digits' own shifted symbols, and nothing else - no -, _, .,
/, =, [, ], ;, ', comma, backslash or backtick. For the case that keyboard
exists for, a session with no reachable physical keyboard, a password
containing any of those meant no way in at all. Every printable ASCII
character now has a key, with a test asserting the whole 0x20..0x7f range
rather than spot-checking.
The lock screen itself could not be screenshotted: locking a nested instance
hits the pre-existing EGL context-loss crash already recorded in docs/TODO.md,
confirmed again here. That is environmental and predates this change, so the
on-screen appearance still needs the real session. The avatar path is covered
by four tests including one that decodes the real ~/.face through the same
function the lock screen calls.
529 tests pass, clippy clean.
|
|
Asked for after the previous entry declined to write GTK CSS. The objection
was to clobbering a file full of hand-written work, not to the feature, and
splitting ownership solves both.
srdwm owns srdwm-buttons.css, generated from
theme.decorations.title_bar.button_style and rewritten on every start and
config reload, and adds exactly one @import line to the user's gtk.css if it
is missing. Nothing else in that file is ever touched. The import goes first
because CSS only permits @import ahead of other rules, which also leaves the
user's own rules last and therefore able to override the generated style.
The generated CSS sets a background per button rather than un-hiding a child
image, for the reason the earlier hand-written attempt found by screenshot:
WhiteSur paints the control as the button's own background-image from a
compiled gresource, so clearing that background leaves a blank button rather
than revealing a glyph.
Verified end to end on a real GTK app, both directions, through the
generated file: traffic_lights gives Nemo coloured dots, traditional gives a
dash, a square and an X, each matching srdwm's own titlebar directly above
it. Also verified the import is added once and not duplicated, that
switching style rewrites only the generated file, and that a home without
GTK config directories has nothing written to it.
527 tests pass, clippy clean.
|
|
Firefox and Nemo were reported as still showing traffic lights after the
button side was fixed. Neither was srdwm's doing.
Nemo, and every GTK app: ~/.config/gtk-3.0/gtk.css contained a deliberate
override from 2026-08-22 painting each titlebutton as a glossy macOS dot,
with the glyph hidden by opacity: 0. Its own comment records that it was
added when srdwm's own decoration drew traffic lights, so that every window
matched. srdwm's style has since changed to traditional and the stylesheet
was still enforcing the old look.
Firefox was already on its traditional variant, byte-identical to
userChrome-traditional.css with the legacy stylesheet pref enabled. It needs
only a Firefox restart.
A first attempt at the GTK fix produced invisible buttons, confirmed by
screenshot: clearing the coloured backgrounds and un-hiding the child image
left blank space, because WhiteSur paints the control as the button's own
background-image from its compiled gresource and there is no child image to
reveal. The working version supplies the icon explicitly via
-gtk-icontheme(). Verified by screenshot: Nemo's header now draws a dash, a
square and an X directly under srdwm's own titlebar drawing the same three.
Kept as a swappable pair, gtk-traditional.css and gtk-traffic-lights.css,
matching the convention Firefox's chrome directory already uses.
srdwm publishes the button layout itself but deliberately does not write
this CSS: the file is the user's, already held hand-written work, and a
compositor silently overwriting it would destroy customisation it cannot
understand.
|
|
Two reports, both traced to a cause other than the one being blamed.
"Windows still spawn as squares" was not placement. On the live session
firefox was 800x632 and so were four other apps, and 800x632 is exactly the
placeholder new_managed_window assigns before a client has chosen anything.
Firefox's remembered size earlier the same day was 1389x933.
The loop: a window closes while still carrying the placeholder, the
placeholder is remembered, the next launch therefore has a remembered size
and is no longer provisional, a non-provisional window is forced to its size
instead of being asked to pick, and on close the placeholder is written back.
Every app that ever closed early gets pinned to one identical box, and no
amount of placement work can touch it because the size never came from
placement.
remove_window now refuses to remember a size the client never chose. That
alone would have been wrong: adopt_provisional_size cleared its own tracking
set but never cleared Window::size_is_provisional, so nothing would ever have
been remembered again. Both halves are covered by tests. Five poisoned
entries were dropped from the live store and the six real ones kept, with a
backup alongside it.
"When user sets decorations should override all applications": the earlier
answer was true about the protocol and wrong about the outcome. GTK never
negotiates decoration, but it does read the desktop's button-layout
preference - GTK4 through xdg-desktop-portal, GTK3 through
gtk-decoration-layout. srdwm now publishes its own button_side there at
startup and after every reload, which is precisely the job kde-gtk-config
does for KWin. Verified in both directions from a neutral starting value;
testing the second direction is what exposed an ordering bug where the
publish ran before apply_general_settings and broadcast the default instead
of the configured side.
527 tests pass, clippy clean.
|
|
the GTK half
Asked to research how KDE and GNOME make decorations consistent, after being
told too quickly that srdwm could only control its own titlebar.
Measured against a nested srdwm, one client at a time: Qt/KDE creates a
decoration object and asks for server-side; winit creates one and asks for
CLIENT-side; GTK never creates one at all. The xdg-decoration spec says the
compositor "can decide not to use the client's mode and enforce a different
mode instead" and that the client "must obey" - so the first two are
srdwm's to decide, and it had simply been deferring. The same spec closes
the door on the third: a client that does not negotiate continues to
self-decorate, and GTK is not having the conversation.
New theme.decorations.force_server_side, default off, overrides the client's
requested mode. Verified: with it off Alacritty draws its own content to the
window's top edge, with it on the same window gets srdwm's titlebar --
(0,0,0) versus (46,52,64) sampled at three rows. Off by default because it
cannot move a GTK button and can stack srdwm's titlebar on a client that
draws its own regardless, which is the Firefox case already recorded here.
The GTK half needs no compositor code, only the desktop setting GTK actually
reads - xdg-desktop-portal's org.gnome.desktop.wm.preferences button-layout
for GTK4, gtk-decoration-layout for GTK3. This machine was serving
"close,minimize,maximize:" (left) while srdwm's own button_side was right,
which is the entire mismatch. Documented in DEFAULTS.md with the mapping
from button_side to layout string, and noted that this is exactly what
kde-gtk-config does for KWin.
525 tests pass, clippy clean.
|
|
The maximize border was still drawn because the earlier fix landed on the
wrong branch. udev/render.rs has three border blocks: the SRDWM_GPU=1 path
at the top and two Pixman ones below. The patch replaced the first match in
the file, which is the GPU branch a real DRM session never runs. All four
sites across both backends are now gated on !maximized.
The verification had failed twice for a separate reason: winit/capture.rs
did not draw border strips at all, so a screenshot could never answer "is
there a border here" and the control passed for the wrong reason. Border
strips are now drawn into that pass as solid fills - corner rounding is not
reproduced, so a capture is not pixel-exact at the corners, but presence,
position, thickness and colour are. With that closed the test has a real
control: unmaximized gives 6 accent pixels at x=800..805, exactly the
configured border_width, and maximized gives none at the right edge or along
the top row. That proves the winit path; the Pixman path is the same change
at two more sites and is not separately confirmed on screen.
Windows spawning as squares, partly off-screen, and always on the left were
all SmartPlacement::grid. It returned size.min(cell), shrinking every window
to its grid cell whatever size it asked for; it scanned cells in reading
order and took the first free one, which is the leftmost; and nothing clamped
the result, so a window larger than its cell could hang off the edge with its
border out of view. The cell now decides only where a window goes, the scan
starts from a rotating cell, and both grid and cascade clamp into the usable
area. Four tests, one per reported symptom.
525 tests pass, clippy clean.
|
|
clean up maximize
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 >= 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.
|
|
The seam bleed the owner reported as "windows show a bit in the other
monitor" was fixed and unit-tested but never seen. With monitor split now
working in a nested instance it can be, and it is.
Same window, same settings, same instance, same scanline. Control, right
edge at x=320 with no seam nearby: a real shadow, (9,9,13) two pixels out,
fading through (11,11,17) and (12,12,19) to the bare desktop (13,13,20) by
23px, a full 24px falloff. Seam, right edge exactly on the boundary at
x=640: (13,13,20) at every sample from two pixels out onward. Nothing
crosses.
Retracting the "SSD versus CSD" lead recorded in the previous commit. The
first attempt at this test looked like a pass until the negative control
failed - the same window off the seam had no shadow either. Narrowing it to
"Alacritty gets a shadow, Nemo does not" pointed at decoration, and that was
written down as a lead. It was neither: one temporary diagnostic in the build
path reported max=true. Nemo restores its own maximized state on startup and
the shadow gate correctly excludes a maximized window, which has no
neighbour to separate from. No bug, and decoration had nothing to do with
it. One log line beat two rounds of reasoning from symptoms; the lead is
retracted in the same file that stated it.
515 tests pass, clippy clean.
|
|
Earlier today I wrote that a nested compositor cannot produce a second
monitor because split and fake monitors "need real head machinery that only
the DRM backend has". That was inferred from both commands returning ok and
changing nothing, not read from the code, and the first half is false.
udev/platform.rs was simply the only backend draining those request queues.
The winit poll never took them off, so the request sat there forever and the
dispatch looked like it had worked. Nothing about split is DRM-bound:
MonitorSplit is bookkeeping in WindowManager and split_rect is pure geometry
in core. The winit poll now drains split requests the same way, and its
monitors() expands a split into one Monitor per part with its own
full_geometry and maximize_geometry, matching the udev expansion. Splitting
the nested output into two 640x800 monitors with a seam now works, which is
what a multi-monitor repro needs. Fake monitors stay udev-only; that half was
not re-checked and is not claimed either way.
Also corrected: the capture pass measured the shadow rect from w.geometry
while both on-screen loops measure it from effective_frame, the client's real
committed size. src indexes into a buffer rasterised at the frame's size, so
the two disagreeing reads the wrong region whenever a client settles on a
different size than it was asked for.
The seam check this was meant to unblock is still not done. With the split
working, the negative control failed: a floating window off the seam had no
shadow either. Running both clients in one instance showed Alacritty renders
a shadow in a capture and Nemo renders none, same settings, both floating,
either focus. Nemo is server-side decorated and Alacritty is not, which is a
lead and not a conclusion. Recorded in docs/TODO.md as open rather than
guessed at.
515 tests pass, clippy clean.
|
|
Reloading rebuilds the theme and general settings from the config file.
That is right for a file edit, but it also wiped every live `srd set` - and
the titlebar right-click menu's "Customize" rows are built entirely out of
live `srd set`s. Changing a button style from that menu and then saving
init.lua for any unrelated reason silently reverted it.
Survivable while reloads only happened on Mod4+Ctrl+r. The reload-on-write
support added in the previous commit makes a reload happen on every save,
which turns a rare surprise into a reliable one. A control that silently
reverts is worse than no control, so this is a defect rather than a
documented quirk. The AGS peer session reached the same conclusion from the
other side while deciding whether to build Settings controls against these
values, and would have had to label them session-only.
Every setting changed live is recorded on the WindowManager as key -> raw
JSON text, and replayed after each reload through the very same handle_set
that applied it, so a replayed setting cannot behave differently from a real
one. Recorded only on success, so a rejected value is never replayed, and
only for real client calls, so the replay cannot rewrite what it is reading.
Last write wins per key. Raw JSON text because core has no serde dependency
and no reason to gain one for this; the platform crate parses it back.
Verified live in a nested compositor: set button_side left and button_mode
fixed, saved an unrelated config edit, both survived, and the log reported
re-applying two live settings. Three tests cover the round trip, the
rejected-value case and the one-entry-per-key case.
A live value is still a session override rather than a persisted setting.
That distinction is now written down in DEFAULTS.md instead of being a trap.
515 tests pass, clippy clean.
|
|
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.
|
|
across a monitor seam
Nemo's right-click context menu was the last open punch-list item, parked
twice as untestable. It works: verified end to end in a throwaway nested
compositor, menu and submenu both, at the correct position and stacking.
The popup path itself needed no fix, so the POPUP-GEOM-DIAG/POPUP-GRAB-DIAG
diagnostics are removed.
Two real bugs turned up in the way of testing it.
zwlr_virtual_pointer was a silent no-op on the winit backend. Every
Motion/MotionAbsolute handler read UdevState::bounds() behind an early
return when state.udev was None, and that field is Some only for the DRM
backend. The protocol advertised its global, accepted create_virtual_pointer
and accepted every request, then discarded all motion with no error and no
log. That is the backend a nested instance runs on, so the only safe way to
drive a throwaway compositor - a Wayland client of that compositor, which
cannot reach any other session, unlike ydotool's /dev/uinput writes - did
not work at all. Bounds now come from WindowManager::monitors() when udev is
absent; both backends fill that list from Platform::monitors().
The winit backend's screencopy pass rendered no popups and no shadows. It
re-renders the scene offscreen, and that second scene was missing tiers, so
grim on a nested instance reported the opposite of the truth: a menu drawing
perfectly on screen photographed as absent. The DRM backend never had this,
since it serves screencopy from the on-screen frame it just drew. Border
strips are still missing from that pass, called out in the code rather than
left silent.
Also fixed, from the "windows show a bit in the other monitor" report:
shadow_rect expanded by SHADOW_SIZE on every side with no monitor-boundary
awareness, so a window flush against a seam put its 24px shadow strip on the
neighbouring screen. shadow_rect_clipped clips to the bounding box of the
monitors the window's geometry actually touches - not just its assigned
one, since a window straddling a seam really does occupy both and clipping
there would cut its shadow off mid-body. The bitmap's own extent stays
unclipped, because the src rectangle indexes into it; only the fragment list
is clipped. Six tests on the incident's own numbers. Not confirmed on
screen: the nested backend cannot produce a second monitor.
New tool: tools/virtual-pointer-click, a scriptable virtual-pointer driver
that acknowledges each command after its round-trip, so a test script can
put a screenshot between a move and the click that follows it.
489 tests pass, clippy clean.
|
|
shadow limit
Desktop icons stayed highlighted after clicking a window: select_desktop_icon(None)
was only ever called from start_desktop_marquee, never from the one place every
focus path (click, Alt-Tab, dock IPC, scratchpad show, snap flyout) already
funnels through. Added the deselect there instead of per-caller.
Closing a focused window could silently switch the user's active workspace:
remove_window's fallback picked self.order.last(), but that list is global,
not per-workspace, so it could land on a background window elsewhere - and
focus_window already switches workspace to match whatever it's given (a real,
separate feature for a deliberate srd dispatch focus). Fixed by preferring a
same-workspace window first. New general.close_focus_follows_workspace
(default false, live-settable) controls what happens only when nothing is
left on the current workspace at all: off leaves focus at nothing, matching
Windows/GNOME/macOS; on restores the old always-follow-the-global-fallback
behaviour. Three new tests.
Also documented, not fixed: shadows can still bleed onto a neighbouring
*monitor* near a multi-output seam (shadow_rect has no monitor-boundary
awareness), found via a live cross-monitor screenshot. Moot for this
session since general.shadows is already off in the live config, but a
real, open gap for anyone who re-enables shadows on a multi-monitor setup.
|
|
Reported live, found via a screenshot of the user's real open window:
srdwm's own server-side titlebar stacked on top of zathura's own
girara-drawn header, same class of bug Firefox/Nemo needed a fix for.
likely_draws_own_titlebar only matched org.gnome.* app ids; zathura's
app id (org.pwmt.zathura) fell through it entirely, with no rules.lua
entry to catch it either.
Broadened the heuristic to also match org.pwmt.* - PWMT's small
toolset (zathura the only one in common use) shares GNOME's own
"always draws its own header" property, on the same live evidence
that justified org.gnome.* in the first place. No rules.lua entry
needed; the heuristic catches it automatically now.
Reproduced and confirmed fixed in a disposable nested compositor
(built, tested double-decoration, then rebuilt with the fix and
retested clean) - never the live session itself.
|
|
toggle_floating
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.
|
|
customization
Reported live: "looks very ugly currently and some of it doesn't make
sense." Both were real. Every row, including a bare divider, took one
full TITLEBAR_HEIGHT slot, so a separator was a 1px hairline in the
middle of 32px of empty space; "Move to Workspace" faked a section
caption by embedding box-drawing characters directly in an ordinary
item's label, which rendered - and behaved, until the click-dispatch
site's own special case - exactly like a clickable row that did
nothing. Separately, "Floating" was always offered even though
Window::floating only affects the "tiling" layout: toggling it under
this project's own default "dynamic" layout visibly changes nothing,
reading as a broken control rather than an inapplicable one.
ContextMenu (crates/core/src/context_menu.rs) gained real Separator
(9px) and Header (22px, non-interactive, dimmed) row kinds with their
own small heights, replacing the label-hack outright. Both backends'
rendering now sum each row's own real height instead of assuming one
uniform value, so hit-testing and pixels can't disagree about where a
row is. Floating is omitted entirely outside the tiling layout.
New, in direct response to "allow customizing from there as well": a
Customize section with live Button Style / Button Side toggles. Each
flips the matching ThemeConfig field and immediately redraws every open
window's titlebar - not routed through srd set's own path, which is
scoped to windows created after the call for lack of a redraw hook it
can reach; a menu action that didn't visibly change the titlebar you
clicked would be its own "doesn't make sense" bug.
Full workspace build/test/clippy clean (242 core tests, +8; 152
wayland, net-even after rewriting the old label-hack tests).
|
|
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.
|
|
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.
|
|
Attempted to screenshot the redesigned lock screen in a disposable nested
compositor instance; the nested instance's own EGL context was lost the
moment the lock engaged, before any frame rendered. Reproduced the exact
same crash on the immediately prior commit, which never touches the lock
screen, confirming this is pre-existing sandbox/EGL flakiness (already
visible as transient BAD_ALLOC errors during ordinary nested startup, not
something this change introduced) rather than a lock-screen regression.
|
|
wrong-password shake
The native lock UI was a flat bordered rectangle with three left-aligned
text lines and no shadow, clock, or identity marker - reported directly
as looking unfinished. Splits the redesign across a new transparent-canvas
header (time, date, circular avatar, username) above a redesigned,
centered password box with a real drop shadow and a dimmed placeholder
prompt, plus a genuine on-screen QWERTY-shaped keyboard with working
Shift/Backspace/Return/Space and real click hit-testing shared with the
render path via one `lock_stack_layout` function, and a damped-sine shake
on a failed attempt.
LockConfig gains show_clock/show_keyboard/avatar_bg, each independently
srd.set-able and documented in a new theme.lock.* section in DEFAULTS.md.
native_lock_render_elements now takes one NativeLockFrame struct instead
of positional buffer arguments now that it composites five optional
layers instead of two.
Full workspace build/test/clippy clean (152 wayland tests, +6 new).
|
|
The GPU (SRDWM_GPU=1/general.gpu) path had real window content but
square corners and no border/titlebar. A prior pass investigated a full
port of the Pixman path's decoration rendering and deliberately did not
attempt it blind, given no working GPU-capable hardware on this machine
to verify a single pixel of it against. Asked directly, twice, to build
it anyway rather than leave it.
Scoped smaller than a full port: border top/bottom strips and the
titlebar bitmap now render, reusing the exact cached MemoryRenderBuffers
the Pixman path already builds (renderer-agnostic pixel buffers,
imported for GlesRenderer the same generic way cursor::render_elements
already does for either renderer). Left out on purpose: occlusion-
fragment clipping against overlapping windows, and the left/right border
side strips plus the drop shadow.
Full workspace build/test/clippy clean. Explicitly not visually
verified - same reason as before, no GPU-capable hardware on this
machine.
|
|
Chrome/Chromium: launched a real google-chrome-stable in a nested
compositor and confirmed no double-titlebar - Chrome negotiates
ClientSide decoration itself, srdwm correctly doesn't stack its own SSD
on top, and the Unity-style menu row it draws is Chrome's own chrome,
not evidence of a bug. likely_draws_own_titlebar needs no new entry for
it.
Nemo: confirmed the same no-double-decoration result, but could not
safely test the actual right-click-shows-a-popup symptom - ydotool is
a uinput-level daemon shared with the live session, not scoped to the
nested compositor, so a blind synthetic click there risks landing in
the user's real desktop. Parked rather than guessed at; the existing
POPUP-GEOM-DIAG/POPUP-GRAB-DIAG diagnostics stay since the underlying
bug's status is still genuinely unknown.
|
|
Closes the remaining "config-file only" gaps from the AGS capability
survey. workspace.per_monitor gets srd set per_monitor <bool> - safe to
flip live since a monitor with no per-monitor override already falls
back to current_workspace regardless of mode, so nothing visually jumps
on toggle.
theme.decorations.title_bar.button_style/button_side/button_order and
general.desktop_icons/desktop_icons_all_monitors all get the same
live-set + SettingsResponse readback treatment as everything else this
session. New srdwm_core::format_button_order is parse_button_order's
exact inverse, so button_order's readback matches the same string shape
srd set itself accepts.
|
|
question
Investigated a live srd.monitor.scale path, the obvious next candidate
after monitor.split's own live path. Unlike split (a pure
placement computation), scale is only ever read by disable_connector_by_
name/enable_connector_by_name - applying it live means a real disable-
then-re-enable cycle on the physical connector, the same screen blank a
genuine unplug/replug causes. Parked the "is that an acceptable cost"
question rather than deciding it alone; left monitor.scale as Lua-
config/restart-only for now.
|
|
Investigated the "tiling needs a lot of work" report directly. The
MasterStackLayout algorithm itself was already correct; the real gap was
that dragging or resizing a tiled window did nothing durable (raw
geometry that the next arrange_workspace silently discarded), and
master_ratio/master_count had no live path at all (config-file only).
A resize-drag on the shared master/stack boundary now live-adjusts
TilingConfig::master_ratio and re-arranges the group immediately; srd set
master_ratio/master_count do the same for a keybind or script. Found and
fixed a real bug while building this: start_resize's own focus_window
call re-stacks its target in self.order before the ratio-drag decision
used to be made, silently misclassifying real master-column grabs.
Fixed by deciding ratio-drag status (and freezing the membership
snapshot it depends on) before that raise happens, applying
MasterStackLayout directly against the frozen snapshot rather than
re-deriving membership from the by-then-reordered live order. Live-
verified in a nested compositor, not just unit-tested.
Also closes the readback gaps flagged directly by the AGS peer session:
border_width/border_color/corner_radius/decoration_mode/gap_inner/
gap_outer/master_ratio/master_count were all live-settable via srd set
with no way to read the current value back, and pin_input had no
readback at all. SettingsResponse now reports all of them; a new
pinned_inputs query (srd pinned inputs) lists every currently pinned
pid/window.
|
|
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.
|
|
Live-tested right after shipping it and caught immediately: srd dispatch
set output split returned ok, but srd monitors kept reporting the whole,
unsplit output. WindowManager::monitors is a passive cache, only
refreshed when a backend re-queries and calls set_monitors again - the
IPC handler mutated the split map directly but never triggered that
requery, unlike set_output_position's own drain site, which already
pushes a "just go recompute" event after applying.
Makes it a proper queued cross-boundary request instead, the same shape
as every other backend-owned effect on this socket: WindowManager::
request_monitor_split/drain_monitor_split_requests, dispatch queues
instead of mutating, the udev backend's poll drains it, applies via
set_monitor_split, and pushes the same recompute event. srd.monitor.
split's Lua config-time path is untouched - it runs before the very
first startup query, so it never had this problem.
|
|
Live incident, root-caused jointly with the AGS peer session: creating a
fake monitor visibly shrank the real primary output's usable area
(full_y stayed 0 throughout - its true position never moved) each time,
tracking almost exactly one bar height per fake monitor created.
create_virtual_head registered its new Output in udev.virtual_heads but
never in CompState::outputs, the list output_for_wl searches to resolve
a client-named wl_output back to anything. new_layer_surface's own
fallback for an output it can't resolve is landing on the primary output
- so AGS's own per-monitor bar, aimed at the fake monitor it reasonably
believed was a new real one, silently landed on the real primary output
instead, stacking its own exclusive-zone reservation on top of the real
bar already there. Two fake monitors, two misrouted bars, two zone
increments, matching the observed climb exactly.
Fixed by registering (and, on removal, deregistering) a virtual head's
Output in CompState::outputs the same way bring_up_head already does for
a real one. Also adds Monitor::is_virtual / MonitorInfo's "virtual" JSON
field, requested directly by the AGS peer session as the real
discriminator their own temporary FAKE- name-pattern match was standing
in for.
The X-position half of this same incident was AGS's own remembered-
layout restore treating a fake monitor's wl_output as a real hotplug --
already fixed on their side (readArrangeable() now filters split/virtual
outputs).
|
|
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 <name|id> <parts>
[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.
|
|
Live report: the real cursor sometimes leaves a brief ghost behind right
after moving between monitors. The bare-metal render loop already forces
a full repaint (ages = [0, 0]) on a workspace switch or any window move/
resize/open/close/restack, both added earlier for the same underlying gap:
the damage tracker's own element diffing doesn't always catch a vacated
region on its own. Neither reset noticed the pointer leaving one monitor
for another - no window moved, no workspace changed - so that head's own
vacated cursor-sized region was left entirely to the tracker's diffing,
intermittently.
Adds UdevState::last_cursor_head, compared each frame the same way the
other two resets are; only the head the pointer just left gets forced back
to ages = [0, 0] (the newly-entered head draws a genuinely new element
there and diffs correctly on its own).
|
|
Live report: a second cursor appeared uninvited and unusably (frozen,
uncontrollable) on screen. Multi-cursor Phase 1 rendered one sprite per
physical libinput pointer device that had ever reported a position, with
no way to turn it off and no expiry - so a phantom device (a real mouse's
side-button/scroll cluster enumerating as its own HID path is a common
case) that reports once and never moves again left a frozen ghost sprite
with nothing to control or dismiss it.
Adds general.multi_cursor (default false, live-settable via
`srd set multi_cursor <bool>`) and keys secondary_cursors to
(Point, Instant) so both the recording side (udev/session.rs) and the
render side (udev/render.rs) drop any entry older than
SECONDARY_CURSOR_TIMEOUT (1.5s). The "agent controls a window without
interrupting the user" use case this report also raised was never gated
on this flag - that's Multi-cursor Phase 2's pinned virtual-pointer
delivery, which never shows a visible cursor at all.
|
|
Codebase modularization, requested directly. Surveyed the whole
workspace first: at ~38k lines it's already organized by topic
(crates/core/src/manager/, crates/wayland/src/{state,udev,decoration}/
already split into small per-concern files) - crates/platform/src/
ipc.rs was the one real outlier, 1894 lines holding the socket
lifecycle, every payload type, both dispatch match statements, and its
own tests all in one file.
Split into ipc/{mod,types,dispatch,tests}.rs by concern, matching the
established pattern exactly - mod.rs keeps IpcServer itself, types.rs
the response/event structs and snapshot functions, dispatch.rs
handle_request/handle_set, tests.rs the existing suite moved verbatim.
Extracted via exact line-range copies against git's own HEAD content
(not retyped), specifically to rule out a transcription bug in a file
this central. Pure reorganization: build/test/clippy clean before and
after, exact same test count (29 in crates/platform) both times.
README.md separately corrected: it still linked to legacy-cpp/ (deleted
this shift) and described the Wayland backend as the smaller, less-done
one - backwards from current reality, where Wayland is the daily-driver
target and by far the more complete backend.
|
|
Real web research (KDE's own source tree, current as of Plasma
6.6.5/2026): com.canonical.AppMenu.Registrar + dbusmenu is still the
current, unreplaced global-menu mechanism in KDE Plasma 6, and generic
Qt apps still export via the same QGenericUnixTheme path since Qt 5.7 --
exactly what srdwm's own appmenu_registrar.rs/appmenu.rs already
implement. No newer protocol to catch up to, no code gap found. srdwm's
own scope (discovery/registration) is correctly split from AGS's
(rendering) - see the FEATURE_GAP.md entry from the previous commit.
|
|
Two research-only entries, no code changes:
- docs/FEATURE_GAP.md gains a "vs. full desktop environments" section
(KDE/GNOME/XFCE/macOS/Windows), requested directly and distinct from
the file's existing tiling-WM (niri/sway/Hyprland) comparison. Verified
rather than assumed: real app-to-app clipboard already works
(delegate_data_device!), drag-and-drop between real windows already
works; genuine gaps are compositor-level blur-behind, the
already-tracked fractional-scale wl_pointer bug's real-world cost, and
no PipeWire screencasting - with an explicit line drawn between
srdwm's own scope and AGS's (notifications, applets, alt-tab UI,
screenshot tooling are shell concerns, not compositor gaps).
- docs/TODO.md: researched "different titlebars, non-traffic-light,
right side, especially firefox/chrome" and found the requested system
already exists and is already documented (button_style, button_side/
order, glyph-always, and a real, already-correct xdg-decoration
negotiation with Firefox's own specific behavior already documented).
One real, unverified gap found via actual web research into
Chromium's own Wayland decoration history: likely_draws_own_titlebar
only matches org.gnome.* today, and Chromium's xdg-decoration support
has a documented history of inconsistency vs Firefox/GTK. Deliberately
not blind-fixed - forcing decorated=false for Chrome would be worse
than doing nothing if it already negotiates correctly; needs a live
screenshot check with a real Chrome/Chromium install first.
|
|
Reported live: "looks weird and unpolished... need a lot more items".
Compared directly against the exact AGS reference this project's own
menu rebuild already targets rather than guessing:
- Highlighted rows used a flat, fully-saturated fill instead of the
reference's subtle 22%-accent-into-background wash. New decoration::
color::mix_rgb (channel-wise linear blend, generalizing brighten/
darken's fixed-target blends to an arbitrary second colour/ratio) lets
render_context_menu reproduce that same ratio.
- Every separator row was a label string of Unicode box-drawing
characters rendered as text glyphs, which render inconsistently at
small sizes - a label that's entirely U+2500 now draws a real 1px
hairline instead; a label that mixes it with real text ("--- Move to
Workspace ---", a deliberate section-header convention) still renders
as text, unchanged.
- "Select All" added to the bare-desktop menu, the one action every
mainstream desktop's own menu offers that this one lacked.
New tests needed real care: the panel's own rounded-corner distance
field softens alpha within its radius of any canvas edge, not just the
visible corners, so a naive full-row pixel scan against bg picked that
up as a false positive on the first attempt - fixed by scanning only
rows/columns confirmed (via a throwaway debug dump) to sit inside the
panel's genuinely flat interior.
Full workspace build/test/clippy clean, built and installed. Real
submenus and per-row icons remain real, separate scope - this
project's floating-menu UI has no nested-panel concept yet.
|
|
Reported live: "try move desktop items all at once somewhere else"
didn't work. Two compounding bugs, both real: CompState::
desktop_icon_drag only ever tracked one icon id, and the click handler
that starts a drag unconditionally collapsed any existing multi-
selection down to just the grabbed icon before the drag even began.
desktop_icon_drag is now Option<DesktopIconDrag> (crates/wayland/src/
desktop_icons.rs, new type): a grab offset, the grabbed icon's own live
position, and a members list - every currently-selected icon (the
grabbed one included), each a fixed offset from the grabbed icon's own
top-left at drag start, so the group moves as one rigid unit.
input/pointer.rs's click handler now only resets to single-selection
when the grabbed icon isn't already part of the current selection --
grabbing one inside an existing multi-selection keeps the whole group
selected and dragging, matching Windows/GNOME/macOS/KDE convention.
end_desktop_icon_drag snaps every dragged icon to its own nearest free
grid cell independently, tracking newly-claimed cells across the group
so two icons landing near each other never claim the same one.
Full workspace build/test/clippy clean, built and installed. Not unit-
testable (this module has no CompState test fixture for its own
selection/drag logic, an already-documented, accepted gap) - needs a
live drag to confirm.
|
|
Two independent pieces landed together this pass - both real, both
scoped, see docs/TODO.md for the full narrative on each:
Fake monitors: a genuinely independent, additional wl_output with no DRM
connector/CRTC behind it at all - distinct from srd.monitor.split
(divides one real output's own placement rectangle). Researched niri's
own Headless backend first (cloned at ~/reference-wms/niri): its render()
never actually composites anything, a no-render stub for that project's
test suite only. This one is real: it renders whatever is placed on it,
on demand, whenever a zwlr_screencopy_manager_v1 client asks for a frame.
New crates/wayland/src/udev/virtual_heads.rs (create/remove a real Output
+ global, render-on-demand for screencopy, integrated into platform.rs's
monitors() as a genuine srdwm_core::Monitor so core placement/workspace
code needs zero special-casing). New IPC/CLI: srd dispatch create
fake-monitor <name> <w>x<h> / remove fake-monitor <name>. Core-side
request queue in crates/core/src/manager/fake_monitor.rs.
Placement bug, root-caused and fixed: every new window opened alone
landed in the exact same spot, "not at all like Windows" (reported live).
SmartPlacement::place tried a grid cell first, and grid's own cell count
is existing.len() + 1 - with nothing else open (opening one app at a
time, the ordinary case), that's always 1, so a 1x1 grid returns the same
single cell forever regardless of session history. Cascade had the same
bug in a second form (its own step was existing.len() % max_steps, also
always 0 with nothing open). Fixed: WindowManager::next_cascade_step (a
Cell - add_window's own target_monitor stays borrowed across the call)
advances on every real placement and is never reset by a window closing;
place() now skips grid entirely when nothing else is open, going
straight to cascade, since grid's real job (dividing space among
concurrent windows) has nothing to divide when there's no concurrency.
Full workspace build/test/clippy clean (223 core / 141 wayland / 29
platform / 24 ctl / 28 config / 10 x11 tests, 0 failed, 0 clippy
warnings), built and installed.
|
|
docs/TODO.md is this shift's single consolidated pending-work list (see
its own header for why it exists alongside PANEL_SUPPORT_TODO.md/
SESSION_HANDOFF.md rather than replacing them) - every commit in this
batch has its own dated entry there with the full root-cause/
verification narrative. docs/DEFAULTS.md corrected against the real
config engine and extended for every new general.* key this shift added
(aspect_ratio rule action, phone_mode). docs/FEATURE_GAP.md is a new
survey against niri/Hyprland/sway, requested directly. docs/
IMPLEMENTATION_STATUS.md and docs/PRIOR_ART.md updated to match.
Cargo.lock reflects the new resvg/usvg/tiny-skia dependencies (real
icon-theme SVG rendering).
|
|
Live-confirmed via a controlled test (move a window between differently-
scaled monitors, screenshot immediately after vs. a couple of minutes
later): the border briefly shows clipped/missing right after a
cross-monitor tiling move, then self-corrects on a later redraw.
Working hypothesis recorded (client configure/resize/commit round-trip
lagging the compositor's own already-updated model, likely wider on a
cross-scale move than a same-monitor tiling swap), but not confirmed --
reproduction via srd dispatch move window proved inconsistent (its
direction semantics swap within a monitor as often as they cross one),
and a live mouse-drag can't be synthesized here to test directly.
Not a corruption risk: an earlier resize-lag fix already
bounds every border/titlebar crop against the decoration buffer's real
last-built size, so the worst case is a stale/incomplete frame, never an
out-of-bounds read.
|
|
Reported live, in stages: general input sluggishness, then specifically
dock/bar buttons not responding on the secondary monitor. Root-caused
jointly with a peer session (dotfiles-16), who independently instrumented
AGS itself (both bar and dock report correct visible/realized/revealed
state - the client is asking for the right thing) and srdwm's own
layer_hit_test log (the dock received zero hits across ~40 minutes while
the same output's wallpaper and bar took hundreds).
Confirmed against smithay 0.7.0's own source (desktop/wayland/layer.rs::
arrange): LayerMap::arrange() divides the output's physical mode by its
own scale before arranging layers, so LayerMap::layer_geometry() is
logical, not physical. Two call sites used it as physical, this
compositor's convention everywhere else:
- input/layers.rs::layer_surface_under_layers compared the physical
pointer position directly against logical layer geometry. On a
sub-1.0 scale output, logical space is larger than physical, so a
bottom-anchored dock's rect sat entirely past the pointer's reachable
range - permanently unclickable. A top-anchored bar only lost its own
right-hand end, which is what made this look like "the dock is
broken" rather than a scale bug affecting every layer surface there.
- elements.rs::output_layer_elements pushed the same logical position
straight into the physical framebuffer - for the dock, past the
bottom edge entirely, painting nothing.
Both fixed the same way udev/platform.rs::monitors() and udev/outputs.rs
already fix the identical unit mismatch for usable-area computation
(existing precedent, not a new technique): multiply by output.
current_scale().fractional_scale(), rounding to the nearest physical
pixel, before use.
Also removed a temporary per-pointer-motion-event diagnostic log in
layer_hit_test, still live from an earlier debugging session and
explicitly marked for removal but never removed - a real, measurable
cost on the hot input path, likely the direct cause of the separately
reported general slowness.
Full workspace test suite and clippy clean.
|
|
Live testing found v1 genuinely broken, not just rough:
1. Icons weren't rendering reliably at all - ensure_desktop_icons only
ever computed the grid's origin once, on whichever render pass
happened to be first. AGS's own top bar registers its exclusive zone
after that first pass, so origin got permanently baked in at the
pre-bar geometry. Confirmed live via a temporary diagnostic log.
Fixed by re-deriving origin from the primary monitor's current
geometry on every call instead of just the first.
2. Fixed icons (Home/Computer/Trash) always sorted before real files --
confirmed wrong via direct question. The whole list now sorts
alphabetically by label, case-insensitive, fixed icons included.
3. "Set as Wallpaper" was the wrong feature: removed entirely
(DesktopMenuAction::SetWallpaper, general.wallpaper_command,
is_image_path). The user wants that handled by their real file
manager once opened, not reimplemented here.
Also adds real menu functionality per "where are all the options":
Rename (inline text edit, new CompState::renaming_icon field and
keyboard redirect mirroring NativeLock::password's existing precedent),
Delete (moves to ~/.local/share/Trash per the freedesktop.org spec, new
trash.rs module, same-filesystem case, no confirmation - this is the
reversible move-to-trash, not a permanent delete), Empty Trash on the
Trash icon, and Open Terminal Here / Open in File Manager on the
bare-desktop menu (new general.terminal config key).
133 wayland-crate tests (up from 106), full workspace build and clippy
clean.
|
|
mid-gesture
Reported live: moving a window onto the other monitor "looks very messed
up". This machine's two real monitors have genuinely different scales
(eDP-1 at 1.0, HDMI-A-1 at ~0.843) - the exact condition needed to expose
this.
WindowManager::update_drag/update_resize only corrected w.monitor once,
at end_drag (update_resize never corrected it at all, not even at the
end) - but state/geometry.rs::sync_geometry reads that field on every
motion tick to pick which monitor's scale converts the client's physical
size into the logical points xdg_toplevel::configure sends it. Crossing
onto a different-scale monitor mid-drag kept every configure computed
against the origin monitor's stale scale for the gesture's whole
remaining duration, only self-correcting once the button came up.
Both functions now re-derive w.monitor from which monitor the window's
live geometry actually overlaps, every motion tick - the same
Rect::overlaps lookup end_drag already used once at the end, now run
continuously instead. end_drag's own fixup stays as a final-word safety
net for a drag that starts and ends between two motion ticks.
Does not close the related, already-documented gap where a client that
doesn't speak wp-fractional-scale-v1 still mismatches once settled on a
sub-1.0-scaled monitor - this only fixes the stale-during-the-gesture
half.
Two new tests, full workspace suite and clippy clean.
|
|
Closes "right-click on bare desktop" - previously a true no-op, nothing
rendered above the wallpaper at all. Requested directly: a real desktop
"just like windows does" - Home/Computer/Trash plus one icon per real
~/Desktop entry, individually draggable with persisted grid positions,
double-click to open, right-click menus (per-icon "Open" plus "Set as
Wallpaper" for image files when general.wallpaper_command is set; bare
desktop "New Folder"/"Refresh").
Architecture mirrors the existing context_menu.rs/snap_flyout.rs
"compositor-owned floating UI" pattern: desktop_icons.rs (data model, grid
layout, filesystem scan, hit-test), desktop_icons_state.rs (JSON
persistence, same shape as monitor_layout.rs), desktop_menu.rs (the new
right-click menu, reusing decoration::render_context_menu's existing
rasterizer), state/desktop_icons.rs (CompState glue: rescan/select/drag/
open/persist), and a new decoration::render_desktop_icon rasterizer --
hand-drawn glyphs, since no PNG/SVG decoding capability exists anywhere in
this workspace. Wired into both render loops (udev and winit) above the
wallpaper and below every window, and into input/pointer.rs's button/
motion handlers for selection, drag, double-click, and both menus.
Four new config keys: general.desktop_icons (default true - a directly
requested, purely visual feature, unlike the opt-in-while-experimental
general.gpu), general.file_manager, general.desktop_icon_single_click,
general.wallpaper_command (all default off/empty).
Deliberately out of scope for this pass, stated up front: move-to-trash
and "Empty Trash" (destructive, no confirmation-dialog primitive to gate
them on yet), filesystem watching, multi-select, per-mimetype icon art,
icons on any monitor but the primary one.
124 wayland-crate tests (up from 106), full workspace build and clippy
clean.
|
|
Reported live: "terminal output/everything disappears when i sometimes
resize terminal." masked_content_buffer (the udev/Pixman rounded-corner
content-masking path, live on this machine via general.rounded_corners)
rendered a window's whole surface tree into an off-screen buffer and
returned Some(bytes) unconditionally, with no check for whether that
tree actually produced any drawable elements.
content_epoch bumps on every commit, and a fast interactive resize is a
rapid-fire sequence of commits - real odds that one races ahead of the
client's own texture import, making the off-screen render legitimately
come back empty. That blank result got returned as Some and cached under
the new epoch the same as a correct one would, and rounded_content_buffer
only rebuilds on the next epoch change - so the blank buffer stayed on
screen, fully transparent, until the window's next real content change,
indefinite for an idle terminal.
masked_content_buffer now returns None when the element tree is empty,
before doing the render+readback at all - the same "give up unmasked"
pattern already used for a genuine renderer error. rounded_content_buffer
drops rather than replaces its cache entry on None, so the render loop
falls back to unmasked content for that one frame and retries the masked
path on the next.
Scoped to the udev/Pixman backend; winit masks via a GLES shader with no
equivalent failure mode.
|
|
first attempt
effective_frame_of now returns the live drag target while a window is
being interactively resized (same change as the reverted first attempt),
but two things make it safe this time instead of reintroducing the
out-of-bounds texture sample that reversion was for:
- Every src crop rect built from a window's frame width in udev/render.rs
and winit/render.rs (titlebar, top border strip, bottom border strip)
is now clamped against DecorationSignature's own recorded width/
border_width - the bitmap's actual last-built size - before reaching
MemoryRenderBufferRenderElement::from_buffer, which does not itself
validate src against the real texture size. This is a structural floor
independent of timing, not a repeat of the previous unsafe approach.
- handle_pointer_position now calls redraw_decoration_buffer once per
resize motion event (throttled to 60Hz via a new
CompState::resize_redraw_at), closing the lag at its source instead of
only catching up on the next real client commit. This also fixes the
shadow bitmap's identical commit-vs-live-position gap for free, since
redraw_decoration_buffer rebuilds all three bitmaps together.
Updates the TODO.md entry for this bug with the full before/after.
|
|
focus-staleness report no longer reproduces
tools/toplevel-activate: a standalone (not a workspace member - its own
empty [workspace] table, so building srdwm itself never has to build
this too) wayland-client + wayland-protocols-wlr binary that lists every
open zwlr_foreign_toplevel_handle_v1, activates one by index, and prints
the resulting `activated` state from the protocol's own feedback.
wayland-client 0.31.14 / wayland-protocols-wlr 0.3.12 - the exact
versions smithay 0.7.0 already pulls in, so this talks to the same real
client library srdwm itself is built against, not a possibly-drifted one.
Used it to reproduce aegis's own exact repro (nested `srdwm --wayland`,
two plain alacritty windows, activate the non-focused one, check
`srd clients`) precisely: launched a real nested instance, activated
back and forth 5 times, checked `srd clients` immediately and after a
delay each time. Every check matched the protocol's own `activated`
feedback - no staleness found, on the nested/winit backend specifically
(the peer's own repro environment). Documented in docs/TODO.md as
likely already fixed by other focus/window-management work since the
original report, not re-root-caused after the fact, but confirmed not
currently reproducible via the exact repro that found it - left open
one more round in case it resurfaces, with this tool as the fastest way
back to a live repro if it does.
|
|
Traced the whole write/read path for srd clients' focused field going
stale after a zwlr_foreign_toplevel_handle_v1.activate-driven change --
ruled out several plausible causes (a caching/staleness bug at the IPC
layer, a same-cycle dispatch-order race in the nested backend) without
finding the actual mismatch. No live repro was run: this machine has
neither pywayland nor wlrctl, and building a minimal wayland-client
test binary to call activate directly is real, separate scope. Written
up as a lead for whoever picks this back up, not a fix.
|
|
Safety commit before reconciling this worktree with main, which has
diverged with its own separate fixes today. Nothing here is reviewed
or curated yet - this exists purely so none of this work can be lost
to a git operation, disk issue, or worktree cleanup while that
reconciliation happens.
|