srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src
AgeCommit message (Collapse)AuthorFilesLines
2026-08-24Look for a free spot before piling a new window on the last onesrdusr1-0/+11
Reported: windows spawn predominantly on one side and on top of each other, with no smart placement. Measured first, in a nested compositor: five windows opened at 30,30 then 60,60 then 90,90 then 120,120 then 150,150 -- every pair overlapping, all in the top-left. The cascade was the only thing running. The grid never engaged, and could not. It asked whether a grid CELL was free and then placed the window at that cell's corner at its own, larger size. An ordinary 800x600 window on a 1280x800 screen overlaps every cell of a 2x2 grid, so no cell was ever free, the grid returned nothing, and everything fell through to the cascade. Placement now looks for a position where the window overlaps nothing at all, and only cascades when the screen genuinely cannot fit one - which is what Windows does once its own screen fills up, and what makes the cascade the right last resort rather than the first answer. The candidates are the edges of what is already on screen: every window's left and right edge plus the monitor's own, taken both as "put my left edge here" and "put my right edge here", and the same vertically. That is Openbox's place_overlap reduced to this case (~/reference-wms/openbox), and it works because a rectangle packed against other rectangles is always flush with one of their edges - nothing is gained by testing the space in between. Two things the naive version got wrong, both fixed here: Ties go to the position nearest the middle of the monitor, and the choice rotates through the four most central free spots. Least-overlap placement is deterministic, so opening one window at a time - open, use, close, open the next - put every one of them in exactly the same place, which is this project's own earlier bug report. Every candidate rotated between is free, so variety never costs the guarantee. And placement now runs again once the client's real size is known. It has to happen before the client commits anything, so it was deciding where an 800x600 placeholder should go rather than the window - a small terminal was told it was 800x600, no two of those fit, and it cascaded. Only when the client actually chooses a different size: re-running it otherwise consumed a second cascade step for nothing, and the cascade wraps, which measured as two windows landing on exactly the same spot. 287 core tests pass, clippy clean.
2026-08-21Put back the allow attribute my last commit orphanedsrdusr2-2/+2
Inserting the title-elision helper directly above render_titlebar pushed that function away from its own #[allow(clippy::too_many_arguments)], which then applied to a const and left the function warning again. Third time I have done this to an attribute in this tree; the pattern is inserting at a "just before this function" anchor without checking what sits immediately above it. Also `"x".repeat(20_000)` in the new test, which clippy asked for. Clippy is clean again - and it was not when I said it was in the previous commit: the check printed its own "ok" line unconditionally, so three real warnings scrolled past above it.
2026-08-13Elide a long window title instead of cutting it mid-glyphsrdusr2-15/+110
A title that did not fit was hard-cut at whatever character crossed the button reservation. Nothing marked the cut, so a truncated name read as the whole name - "annual-report-final-v7-reviewed-2026-with-appendix.ods - LibreO" looks like a filename, not like a filename with its tail missing. Titles now end in an ellipsis when they are shortened, which is what Windows, GNOME and KDE all do, and it keeps the informative half: an application or document title almost always begins distinctively and ends in boilerplate. Whole characters are dropped until the ellipsis fits beside what remains, so the result never overruns the buttons. A titlebar with no room even for the ellipsis draws nothing, rather than a lone "..." that says less than an empty titlebar does. The mark itself is "…" where the system font has that glyph and "..." where it does not. A missing glyph rasterizes to nothing at all, which would have quietly reintroduced the invisible-truncation problem on any font without it. Also bounded the work: a client may set a title of any length - a browser tab carrying a whole data URL - and every character cost a rasterization before the layout could decide it did not fit. Measurement stops at 256 characters, well past anything legible in a titlebar, and a title that long is elided many times over regardless. Four tests: an over-long title fits its span and ends in the ellipsis mark; a title that fits is left exactly alone; a titlebar too narrow for the ellipsis draws nothing; a 20,000-character title never lays out more than the cap. Checked on screen too, at 600px and at 220px.
2026-08-11Read the window-memory store at a moment when it can actually matchsrdusr2-2/+34
Reported twice, as two complaints: windows do not remember their size or position across a close or a reboot, and windows spawn stacked on one side with no smart placement. One bug. `add_window` looks the store up by app_id. A Wayland toplevel role exists before its client sends set_app_id, so at the moment srdwm placed a window the app_id was the empty string, every lookup missed, and every window fell through to the cascade - which is exactly what "they all open on top of each other" looks like. The store was being written correctly the whole time and read at the one moment it could not match. The lookup now runs again the instant a real app_id arrives, which is still before the client's first buffer, so nothing is drawn in the wrong place first. It only moves a window still sitting where the cascade put it: a rule's explicit geometry, a maximize, a dialog's centring and a client's own committed size are each more specific than "wherever I last left this app", and a test asserts none of them is overridden. A second bug sat underneath the first and only appeared once it was fixed: the position came back and the size did not, which is stranger than nothing being restored. The backend keeps its own copy of "this size is only a guess" (provisional_size) and adopt_provisional_size reads that one rather than the core flag, so the client's next commit overwrote the size that had just been restored. Cleared with the same call. Verified end to end in a nested compositor, driving a real edge-drag with the virtual-pointer tool: seeded store 400,300 500x400 -> opened at exactly 400,300 500x400 dragged the right edge -> 646 wide, store rewritten to 646 on release closed and reopened -> 400,300 646x400 Before this the same first step opened at 30,30 800x600. Also: window_memory::save_all's nested guard now allows a write when the instance was given its own state directory (SRDWM_STATE_PATH or XDG_STATE_HOME). The blanket refusal added earlier kept the owner's store safe but made the feature impossible to test without pointing a test compositor at the real desktop, which is how this went unverified in the first place.
2026-07-27Reach GTK4's window buttons too, and speak the decoration protocol GTK knowssrdusr5-0/+97
Two findings from reading what other projects do, and then measuring this machine rather than trusting the reading. GTK has never implemented xdg-decoration, so a compositor that advertises only that protocol is invisible to every GTK client on the decoration question. What GTK does implement is KDE's older org_kde_kwin_server_decoration - confirmed by reading libgtk-4's own symbol strings, where the manager, the mode enum and the default-mode handler are all present, and absent from libgtk-3. That is the channel a KDE session uses. srdwm now advertises it alongside xdg-decoration, with both answering from the same policy (theme.default_decorated, theme.force_server_side) so a client is told the same thing whichever it asks through. Measured what that actually buys, with WAYLAND_DEBUG on a real GTK4 client: it binds the manager and receives default_mode(2) = Server, and then never creates a decoration object for its window. So it changes nothing for a GTK application's own header bar, and it is kept because it is the correct thing to advertise and because clients that do honour it - Qt and KDE's own -- now get server-side decoration from srdwm instead of nothing. The second finding is the one that fixes what was reported. GTK3 and GTK4 put their window buttons under different CSS selectors, and the generated stylesheet named only GTK3's. A diagnostic rule proved it both ways: a flat colour reached Nemo (GTK3) through `headerbar button.titlebutton` and gnome-calculator (GTK4) through `windowcontrols button`, and neither selector reached the other toolkit. Every style is now written for both, so a GTK4 application is styled rather than silently skipped. `headerbar` itself works in both, so the titlebar block needed no split. Verified on screen: gnome-calculator, a GTK4/libadwaita application, now draws its header bar in srdwm's own titlebar colour with srdwm's text colour, where before it kept its theme's. Also worth writing down, because it bounds what any of this can achieve: an application's header bar is its own widget. No protocol removes it. GTK_CSD=0 does not, the KDE protocol does not, and neither does forcing server-side decoration - that only adds a second titlebar above the first. What a compositor can do is make the two look like one, which is what this does.
2026-07-27Hide a window only when srdwm knows it has not drawn, not when a lookup says sosrdusr10-61/+64
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.
2026-07-26Draw a window's content from the same rect as the frame around itsrdusr2-3/+30
Reported live: "resizing shrinks/grows only the right side", and "the titlebar seems separate when resizing, it doesn't size at the same time". Both are one bug. Every decoration - border strips, titlebar, shadow -- is drawn from `frame`: the client's last committed size, anchored to whichever edge the drag is not holding. The content was drawn from `geom`: this compositor's live drag target, which moves on the same frame the pointer does. A client is always at least one commit behind a drag, so for that whole interval the two rects disagree, and the window is drawn as two pieces that move independently - the stale buffer sliding left with the pointer, carrying its old width, while the border it belongs in stays where the committed size puts it. Dragging a left edge therefore looked like it moved the right one. All three content paths now position from `frame` (both udev render loops and the winit one). Outside a resize this changes nothing at all: `committed_frame` only ever corrects the far edge, so `frame.x`/`frame.y` and `geom.x`/`geom.y` are the same value. Measured in a nested compositor, driving a real left-edge drag with the virtual-pointer tool and sampling both the model's target rect and the rendered pixels at the same moments: the content sits exactly one border width inside the border on both sides in every frame, the right edge holds at 830 throughout, and the left edge tracks the pointer (305, then 405). The earlier decorated-window run measured the same thing for the titlebar: its left edge moved with the window, its right edge did not move at all.
2026-07-23Do not draw a window before its client has painted anythingsrdusr10-13/+112
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.
2026-07-20Fix the nested guard: it answered wrong for a real session, and missed a casesrdusr2-0/+43
The guard added earlier tonight asked "is WAYLAND_DISPLAY or DISPLAY set". Both Wayland backends call set_var("WAYLAND_DISPLAY", ...) on themselves the moment they bind their own socket, so after startup that question answers "yes" for a real udev session too. Every publish on the config-reload path runs after that point, which means a live session that reloaded its config would have stopped publishing its own GTK settings - the exact opposite of what the guard is for. It also treated srdwm-x as nested, because an X11 session naturally has DISPLAY set, even though srdwm is that display's window manager and not a client of anything. Nestedness is now read once, at startup, before any backend is up, and only the Wayland backend can be nested. A test pins the second half. The same reasoning applies to window_memory::save_all, which had no guard at all: a nested instance shares HOME with the session it runs inside, so dragging a test window would overwrite where that application opens in the real session - a 1280x800 test window's position applied to a 3840x1080 desktop. Loading stays unconditional and deliberate: honouring what a real session remembered is right, writing back over it is not. That side reads srdwm_wayland::running_nested, recorded by connect at the moment it picks the winit backend, since the environment can no longer be asked afterward. Verified: a nested run with a scratch config left both the stylesheet and window-memory.json untouched (md5 before and after, and no test window's app-id in the store).
2026-07-15Draw a resizing window's decoration from the same rect as its contentsrdusr1-6/+48
Reported as the titlebar not resizing at the same time as the window, and as resizing feeling cheap. effective_frame_of returned the live drag target while a resize was active, so the titlebar and border tracked the pointer while the client's actual pixels were still whatever it last committed. The two disagreed for the whole drag, and the decoration leading its own content is what reads as broken. It now returns the committed size anchored to whichever edge the drag is holding still - exactly the rect the content occupies, since sync_geometry positions it the same way, so the two agree by construction rather than by timing. The consequence is that the frame sits one commit behind the pointer instead of ahead of its own content. That is the trade every other compositor makes, and it is the right way round: a frame glued to its content and slightly behind the cursor reads as solid. The committed-size correction was extracted into committed_frame so the resize path and the ordinary path share it rather than having two versions that can drift, and so the resize path is no longer short-circuited by the pending-configure branch, which fires constantly during a drag precisely because every tick sends a configure. 533 tests pass, clippy clean. The arithmetic is covered where it is testable; how it feels mid-drag is a judgement only real hardware can make, and a nested drag did not reproduce a clean enough scenario to claim it.
2026-07-14Workspace capture writes a readable image, and left-edge resize holds its anchorsrdusr2-3/+114
Two things, and the first is smaller than I told anyone. WORKSPACE CAPTURE. I said off-screen workspace capture did not exist and would need building. It already did: udev/capture.rs renders a workspace that is not on screen, at the target monitor's native size, downscaled to a requested size, wallpaper included. Verified on the live DRM session rather than from the source - capturing the active workspace and a non-visible one gave 320x180 images with mean luminance 0.067 and 0.137, so the second is genuinely a different render and not a copy of what is presented. The only thing missing was the container. It wrote PPM, which the shells that want thumbnails cannot decode, so the file was written successfully, returned successfully, and silently not drawn - the same failure class as a capture pass that omits a tier. encode_capture now picks the format from the destination's extension: .ppm still writes PPM so existing callers keep working, .jpg/.jpeg write JPEG, anything else writes PNG. Four tests check the actual magic bytes rather than trusting the call, plus the unfamiliar extension fallback and a size-mismatch error. LEFT-EDGE RESIZE. Reported as content resizing "from the right side even when i resize from left". The window's origin moves the instant the pointer does, but the client only commits a matching buffer some frames later, so its still-old content was being placed at the new origin - which slides the whole window rather than growing it, and leaves the edge that should be nailed down drifting. sync_geometry now derives the origin from the size the client has actually committed when the drag is from a left or top edge, so the opposite edge stays exactly where the drag started and the dragged edge catches up as commits arrive. A right or bottom drag is untouched: its origin never moves. 533 tests pass, clippy clean.
2026-07-07Lock screen: show each key's alternate character, use a UI font, space the dotssrdusr3-12/+123
Three reports, three separate causes. Alternate characters were invisible. A keycap drew only the character the current shift state types, so there was no way to find a symbol without pressing Shift and hunting for it - a physical key is labelled with both. Each cap now draws the other state's character small and dimmed in its top-right, skipped where the two are the same (every letter differs only by case, which the cap already shows) and for named keys like Enter. "Enter Password" rendered in a monospace font. find_system_font deliberately prefers a mono face, which is right for a titlebar title or a menu row sitting in columns and wrong for a sentence; on this machine it resolves to DejaVu Sans Mono. New find_ui_font prefers a proportional face from a ranked list of widely-installed families, falling back to the mono one, and the lock screen's prose - prompt, clock, date, username, status -- uses it. Ranked rather than first-found so the result does not depend on directory order, which is how the mono scan once picked an italic face and rendered every titlebar in italic. The password dots had no spacing. They were drawn as a plain string, so a run of identical bullets separated only by their own advance read as one smeared blob rather than countable characters. Added explicit tracking, excluded from the centring width so the row does not sit half a gap left. 531 tests pass, clippy clean.
2026-07-06Drop placeholder-sized window-memory entries at loadsrdusr1-1/+67
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.
2026-07-04Use the real user avatar on the lock screen, and let its keyboard type every ↵srdusr1-8/+203
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.
2026-06-02Stop window memory poisoning itself, and publish the decoration side to GTKsrdusr1-0/+6
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.
2026-06-01Let negotiating clients be forced to server-side decoration, and document ↵srdusr1-0/+5
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.
2026-05-15Fix the maximize border on the path that actually runs, and three spawn faultssrdusr5-2/+44
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.
2026-05-11Fix spawn placement under the top bar, add per-window minimum sizes, and ↵srdusr6-7/+53
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.
2026-05-11Three live bug reports after a restart: icon drag, lock cursor, lock boxsrdusr7-29/+183
All three reported directly after the owner restarted into today's build. Desktop icons could not be dragged at all in single-click mode. The press handler opened the icon immediately when general.desktop_icon_single_click was on, so the branch that starts a drag was unreachable and an icon could never be moved. Deciding activation on press cannot distinguish a click from the first instant of a drag. Every press on an icon now starts a potential drag and release decides which it was, using a 4px movement threshold that latches once exceeded. Double-click mode goes through the same path, so both modes now drag identically. The lock screen drew no cursor. The cursor push in the udev render loop sits inside `if !locked`, and a locked head renders only the lock element list, so nothing drew a pointer - and on a bare TTY nothing else does. The on-screen keyboard's clicks were being handled correctly the whole time (native_lock_click); they simply could not be aimed. The pointer is now prepended to the lock element list, above the UI it is used to click. The password field's opaque panel is gone. New LockConfig::box_opacity, default 0.0: no fill, no border, no rounded rectangle, just the dots and status text over the blurred background. Raising it restores the panel at that opacity for anyone who wants a solid field. Drawing text on a transparent surface needed a new blit_glyph_over: the existing blit_glyph blends against one flat opaque colour and writes alpha 255, which would have turned every glyph into a block of the assumed background - the same box with its middle removed. VERIFICATION STATUS, stated plainly: all three are code-complete and the suite passes, but none is confirmed on screen. The nested backend's capture pass does not draw the desktop icon grid (a gap already recorded in winit/capture.rs), so the icon drag cannot be checked by screenshot there, and aiming blind is what this project's own rules forbid. The two lock changes were not visually checked either. 515 tests pass, clippy clean.
2026-05-04Support monitor split in the nested backend, correcting a wrong "blocked"srdusr2-3/+61
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.
2026-04-27Build the eight asks recovered from the previous session's transcriptsrdusr14-30/+411
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.
2026-04-20Confirm Nemo's popup works; fix the two bugs that hid it, and shadow bleed ↵srdusr8-38/+226
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.
2026-01-31Fix desktop-icon deselection and workspace-teleport-on-close; document a ↵srdusr1-0/+12
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.
2026-01-30Fix a real regression: dynamic-mode windows lost their shadow via ↵srdusr6-23/+90
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.
2026-01-28Redesign the titlebar right-click menu: real separators/headers, live ↵srdusr5-77/+199
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).
2025-12-03Stop giving tiled windows a shadow that lands on their neighboursrdusr2-1/+24
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.
2025-12-01Let a new window pick its own size instead of forcing a guessed placeholdersrdusr6-1/+83
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.
2025-11-20Redesign the native lock screen: clock/avatar header, on-screen keyboard, ↵srdusr5-80/+757
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).
2025-11-20Add border/titlebar decoration rendering to the GPU render pathsrdusr2-13/+80
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.
2025-11-07Make tiling's master/stack ratio live, add settings readback everywheresrdusr1-0/+5
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.
2025-11-04Fix window memory never saving on close, and split-screen icon/primary bugssrdusr4-13/+112
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.
2025-10-29Fix set_monitor_split never actually reaching srd monitorssrdusr1-0/+13
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.
2025-10-28Fix a fake monitor's layer-shell surfaces misrouting onto the real primarysrdusr2-0/+22
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).
2025-10-26Live-expose monitor split, clean up leftover debug diagnosticssrdusr3-7/+0
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.
2025-10-07Fix intermittent cursor ghosting when crossing between monitorssrdusr3-0/+41
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).
2025-10-03Make the secondary-cursor sprite opt-in and expire stale entriessrdusr3-23/+84
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.
2025-09-28Context/desktop menu polish: real hover tint, real separator line, Select Allsrdusr5-7/+148
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.
2025-09-28Fix multi-selected desktop icons only ever dragging one at a timesrdusr4-45/+131
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.
2025-09-26Fake (headless) monitors, and fix new windows all opening in one spotsrdusr3-0/+272
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.
2025-09-10Wayland backend: desktop icons v2, menu rebuild, layer-shell/scale fixes,srdusr23-105/+1853
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.
2025-08-25X11 backend: right-click titlebar window menu, matching Wayland's ownsrdusr1-131/+8
Closes the one real gap an X11/Wayland feature-parity audit found this session (desktop icons, window-position memory, and static exclusive-zone reservation were already shared or Wayland-only by nature - see docs/TODO.md's own audit entry for the full breakdown). MenuAction/ContextMenu (row set, labels, row_at hit-testing) move from crates/wayland/src/context_menu.rs into crates/core/src/context_menu.rs -- pure state and geometry with nothing Wayland-specific in it, so X11 needing the same rows is shared data, not duplicated logic. The Wayland crate's own context_menu.rs is now a one-line re-export so every existing crate::context_menu::... call site keeps working unchanged. X11 has no compositor-level input dispatch to intercept every click the way Wayland's input/pointer.rs does, so the X11 side (crates/x11/src/platform/context_menu.rs, new) draws the menu into its own small override-redirect popup window and grabs the pointer for the duration so a click anywhere dismisses it, matching the Wayland backend's own convention. events.rs's ButtonPress handler now reads the real button number instead of hardcoding every press as a left click - a real latent bug (right-clicking a titlebar button would have silently performed its left-click action). Live-verified end to end in an isolated Xvfb + srdwm --x11 instance: full row set including the workspace picker, Minimize runs and closes the menu, a second window's menu dismisses cleanly on outside click, normal focus/click behaviour continues working afterward. See docs/TODO.md for the full investigation and verification narrative.
2025-08-19Polish the hand-drawn desktop icon glyphs: rounded corners, gradient, bluesrdusr2-25/+87
Requested directly: default icons "look very rudimentary... make slightly blue as well, polished look". Two changes to decoration::render_desktop_icon and its five draw_*_glyph helpers: - New fill_rounded_rect primitive: softly rounded corners (the same clamp-then-distance smoothstep construction rounded_corners_pixman:: apply_corner_mask already established for content masking, not a new technique) plus a vertical top-to-bottom gradient instead of one flat fill - the same light-source cue buttons.rs's own glossy_shade uses for the titlebar dots, as a plain linear gradient here. Applied to each glyph's main body shape; small details (folder tab edge, computer stand, trash ridges, home roof) stay flat/sharp. - A dedicated ICON_COLOR constant (a clean mid-blue) instead of reading theme.titlebar_fg_focused - that field is whatever the user's own titlebar accent happens to be configured to, which could be any colour; these glyphs want a consistent, recognisable blue palette of their own, independent of theme. Build/test/clippy already verified clean as part of the layer-shell scale fix commit just before this one (same source tree, installed together).
2025-08-18Fix layer-shell surfaces unclickable/unpainted on a fractionally-scaled outputsrdusr2-29/+57
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.
2025-08-17Fix desktop icons v1 regressions: bar overlap, wrong order; add proper menussrdusr10-84/+540
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.
2025-08-15Add real desktop icons plus right-click desktop/icon context menussrdusr12-0/+1189
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.
2025-08-13Fix terminal content disappearing on resize: don't cache a blank content masksrdusr1-0/+24
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.
2025-08-09Fix interactive-resize border/shadow lag without the OOB risk that sank the ↵srdusr7-18/+128
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.
2025-08-08Render real window content on the GPU render pathsrdusr2-19/+43
Past clear-color + cursor: a GPU-driven head now renders every visible window's real content too, via surface_content_elements (the same generic-over-renderer helper the Pixman path uses, unmodified against gpu.renderer instead of udev.renderer). Content pushed after the cursor (so the cursor stays on top), in the same front-to-back `ids` order the Pixman path's own custom_elements already relies on for correct occlusion between windows - plain painter's-algorithm draw order, no separate clip needed since content is window-shaped. Deliberately the *unrounded* path: no corner masking (that's built against PixmanRenderer specifically on this backend) and no decorations (border, titlebar) - a GPU-driven head now shows real window content, square corners, no chrome. Decorations are the remaining real gap before this path has parity with the software one. Per-window geometry/position math (geom from window_anims or w.geometry, band for a decorated window's titlebar reservation, content_offset clamped non-negative) mirrors the Pixman path's own content push exactly, including an earlier double- subtraction and negative-margin fixes - so a CSD client with a real shadow margin positions the same way on either render path. Untested on real GPU-enabled hardware as of this writing: builds, passes clippy, full test suite green, and matches the existing Pixman path's geometry logic by inspection, but SRDWM_GPU/general.gpu were both unset on the machine this was built on - noted honestly in gpu.rs's own module doc comment, DEFAULTS.md, and IMPLEMENTATION_STATUS.md.
2025-05-30Detect XWayland dialogs via WM_TRANSIENT_FOR, not just native xdg_toplevel ↵srdusr2-5/+33
parent Window::is_dialog (close-button-only titlebar, no traffic lights) was only ever set from a native xdg_toplevel's own parent() - redraw_ decoration_buffer's is_dialog computation called dw.toplevel(), which is always None for an XWayland-backed DWindow (X11Surface's own accessor is x11_surface(), a different method), so the .unwrap_or(false) fallback made every XWayland dialog - a GTK "Save As", an app's own "About" box, anything setting the ICCCM transient-for hint - always draw with the full three-button titlebar and traffic-light colours, even though the feature this was built for explicitly wanted the opposite. Documented as a known gap at the time; now closed. redraw_decoration_buffer now also checks X11Surface::is_transient_for() for an XWayland window. property_notify gained a WmWindowProperty:: TransientFor arm that re-runs redraw_decoration_buffer, for a client that sets the hint slightly after its own initial map - the same "read fresh every call" pattern the existing xdg_toplevel::parent() check already relied on, extended to catch a late X11 property the way the Wayland equivalent (set_parent, any time) already was.
2025-05-28Add a real general.gpu config option for the GPU render pathsrdusr2-23/+35
SRDWM_GPU=1 was the only way to opt into the udev backend's GBM+EGL GPU render path - an env var, not a real config option, with no way to enable it from init.lua the way every other general.* flag works. WindowManager::gpu_enabled (plain bool, false by default - unlike rounded_corners_enabled's Option<bool>, GPU rendering has one unambiguous default regardless of which backend ends up connecting, so there's no "let the backend decide" case to preserve) is read from general.gpu in apply_general_settings, same as every other general.* key. gpu::probe now takes an explicit enabled: bool instead of checking the env var itself; udev/platform.rs's call site computes it as wm.gpu_enabled OR SRDWM_GPU=1, so the env var still works as a quick manual override for testing without touching config, on top of the new persistent option. Falls back to the existing software (Pixman) path exactly as before on any failure at any step (no GBM device, no atomic-modesetting support, a software-only EGL renderer, ...) - gpu::probe's own fallback behavior is unchanged, only how the initial enabled/disabled decision gets made.