srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
AgeCommit message (Collapse)AuthorFilesLines
2026-05-11Three live bug reports after a restart: icon drag, lock cursor, lock boxsrdusr9-29/+200
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-28Stop a config reload from undoing a change the user just made by handsrdusr7-1/+132
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.
2026-04-27Build the eight asks recovered from the previous session's transcriptsrdusr33-80/+1184
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 ↵srdusr7-1/+130
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-31Fix zathura's double titlebar by broadening the CSD-detection heuristicsrdusr1-1/+27
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.
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 ↵srdusr9-114/+447
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 placeholdersrdusr10-1/+162
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, ↵srdusr8-80/+790
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-16Live-expose workspace.per_monitor, titlebar buttons, and desktop iconssrdusr6-5/+305
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.
2025-11-10Fix a maximized X11 window overhanging the screen by its own bordersrdusr3-6/+71
Reported by the aegis-fc peer session testing srdwm's own layer-shell strut handling: a maximized X11 client sat 4-8px past the right and bottom screen edges whenever its border was nonzero. set_border_width sets the frame's native X11 border-width attribute, which the X server draws outside a window's own declared width/height on all four sides - unlike every other backend's own border in this compositor (rendered as ordinary pixels inside the allocated geometry rect). apply_geometry configured the frame at geometry's own x/y/width/ height verbatim, so a nonzero native border pushed the frame's true visible footprint 2*border_width past every edge of what geometry actually promised. Fixed by shifting the configured origin inward and the configured size down by border_width on both axes (frame_geometry_for, pulled out as a pure function so it's unit-tested without a real X11 connection) - the visible footprint, native border included, now lands exactly on geometry. border_width == 0 reduces to the prior behavior exactly.
2025-11-07Make tiling's master/stack ratio live, add settings readback everywheresrdusr12-10/+548
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 bugssrdusr6-14/+165
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 monitorssrdusr5-26/+86
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 primarysrdusr5-1/+97
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 diagnosticssrdusr7-34/+152
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 entriessrdusr9-25/+134
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-10-03Split ipc.rs into ipc/ by concern, and fix a stale READMEsrdusr5-1894/+1920
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.
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 spotsrdusr10-23/+556
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,srdusr25-105/+1868
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-31Wire aspect_ratio, phone_mode and pin_input through config/IPC/CLIsrdusr5-5/+164
The Lua and IPC/CLI surface for the three new core primitives (crates/core: aspect_ratio rule action, general.phone_mode, virtual- pointer window pinning): - srd.rule(..., { aspect_ratio = "9:16" }): parses a "W:H" string into a validated (u32, u32), rejecting a malformed value as a real Lua error at config-load time rather than silently ignoring it. - general.phone_mode config default, plus srd set phone_mode <bool> for the live equivalent (same shape as animations/shadows/rounded_corners). - pin_input IPC dispatch ({"cmd":"pin_input","pid":<pid>,"id":<window id>}, id omitted to unpin) and its CLI surface, srd dispatch pin input <pid> <window-id> / unpin input <pid>. Keyed by the owning client's process id, not an opaque per-object id nothing outside the Wayland backend could ever learn - a controlling tool already knows its own pid for free. See docs/TODO.md for the full design reasoning behind each of these.
2025-08-29Core window manager: real fixes plus three new rule/placement primitivessrdusr7-61/+559
Several independent, real pieces landed in crates/core this shift - see docs/TODO.md for each one's full root-cause/verification narrative: - "Primary" monitor is now picked by which head sits at physical (0, 0) (the user's own configured anchor), not whichever connector DRM happened to probe first - fixes desktop icons and new-window placement landing on the wrong monitor depending on hotplug/probe order. - A new window's target monitor now prioritizes the pointer's own current monitor over the last-focused window's monitor, which goes stale the moment the user's attention moves to empty desktop, a panel, or a dock. - aspect_ratio window-rule action ("W:H") plus ResizeEdge::apply_aspect_ ratio: holds a floating window's aspect ratio through an interactive resize. The real, scoped "phone monitor" primitive - matches any VM/ emulator/scrcpy window by app_id, nothing Android- or VM-specific here. - general.phone_mode (WindowManager::phone_mode): a new window defaults to maximized instead of floating/tiled small, unless a rule explicitly floats it or sets maximized - the one placement default a phone-shaped screen actually needs. Exposed read-only via IPC so a shell panel can adapt its own chrome to the same signal. - input_pin.rs: the core half of pinning a virtual pointer to a specific window (Multi-cursor Phase 2) - a backend-agnostic request queue, same cross-boundary shape output_position_requests/lock_requested already use, since core has no real Wayland protocol object to reach into itself. Full workspace test suite covers all of the above (aspect-ratio resize math for every edge case, phone-mode default-vs-rule-override behavior, the pin-input request queue, the monitor-picking fixes).
2025-08-25X11 backend: right-click titlebar window menu, matching Wayland's ownsrdusr8-138/+427
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 menussrdusr13-101/+552
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-15Fix a dragged/resized window rendering wrong on a different-scale monitor ↵srdusr2-11/+91
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.
2025-08-15Add real desktop icons plus right-click desktop/icon context menussrdusr15-0/+1252
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-10Make corner resize reachable at the button corner, and widen it everywhere elsesrdusr2-4/+86
Reported live: "even where decorations are corner i should still be able to corner resize, just its... hitbox... does not get in the way of the close icon" - the titlebar corner holding Close/Maximize/Minimize had no resize target at all, by design (competing with the close button was judged worse than losing that one corner). The BUTTON_CLUSTER_MARGIN strip between the button cluster and the frame's true edge was already dead space no button claims, regardless of button_count - its own top DECORATED_TOP_RESIZE_MARGIN rows now register as the diagonal corner (TopLeft/TopRight) instead of falling through to Top/Drag, without touching the button's own hitbox at all. Also requested: widen the other three corners' own resize zone, since a user reaching for a plain edge-resize instinctively aims for the middle of that edge, not its corner - a bigger corner zone doesn't compete with that instinct the way a bigger RESIZE_MARGIN would compete with ordinary content clicks near an edge. CORNER_MARGIN raised from 3 to 5 (18px to 30px at the default resize_margin). Updated one existing test whose own per-window resize_margin override (30px) now put its plain-edge test point inside the widened corner zone on a window too short for the two to stay apart - taller geometry, same edge point relative to center, no change to what it actually verifies. Added coverage for the new button-corner resize target on both sides, and for the dead strip's own non-corner rows still just dragging as before.
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 ↵srdusr3-14/+41
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 pathsrdusr5-23/+65
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.
2025-05-26Render the real cursor on the GPU render path, not just a clear colorsrdusr2-20/+43
Past clear-color-only (Phase 2): the GPU-driven head now shows the same moving cursor the Pixman path renders, via cursor::render_elements - already generic over the renderer (R: Renderer + ImportAll + ImportMem), so it works unmodified against gpu.renderer (GlesRenderer) instead of udev.renderer (PixmanRenderer). GpuElement (gpu.rs) widened from a bare MemoryRenderBufferRenderElement to crate::elements::OverlayElement<GlesRenderer> - the same Surface/Memory/Solid enum the Pixman path's own custom_elements already uses, needed once the element list stopped always being empty. Window content and decorations remain a real gap - a GPU-driven head still shows no windows, just its own clear color and cursor. Untested on real GPU-enabled hardware this work (SRDWM_GPU unset on this machine's live session); builds, passes clippy, full test suite green.
2025-05-23Fix the border ring's inner cut using the wrong circle for undecorated windowssrdusr5-61/+230
carve_inner_corner_pixel (an earlier ring fix) cut the border strip's own disk using an inner circle at the *same* centre as the outer cut, radius - border_width - correct when a titlebar sits underneath (it shares that exact circle, by construction), but wrong whenever client content sits underneath instead: content's own rounded-corner mask (rounded_corners_pixman.rs's apply_corner_mask) is centred radius from *its own* buffer's edges, and that buffer starts border_width rows/columns inside the border strip's - a same-radius circle offset (border_width, border_width) diagonally from the border's own outer one, not a smaller concentric one. Reusing the titlebar-style ring left a real, if very small, gap along part of the seam and a thin sliver of double coverage along the rest - reported live, at extreme zoom against a solid-colour wallpaper (chosen specifically to make a sub-pixel gap easy to spot against, unlike the usual desktop image): "very tiny gaps... corner radius does not match." round_top_corners/round_bottom_corners's own `inner_radius: Option<u32>` parameter is now `inner: Option<InnerRing>`, an explicit (center_row, center_col, radius) rather than an implicit "same centre, smaller radius" - InnerRing's own doc comment has the full geometry for both cases. render_border_top gained a `decorated` parameter to pick the right one (titlebar-aligned when true, content-aligned - centre shifted by border_width on both axes, radius unchanged - when false); render_border_bottom always uses the content-aligned ring, since this compositor never draws a bottom titlebar. apply_corner_mask (rounded_corners_pixman.rs) made pub(crate) so the new regression test can build a real masked content buffer via the actual production function, not a reimplementation of its math. border_top_and_content_mask_have_no_gap_along_the_corner_diagonal_when_undecorated checks coverage along the diagonal ray between the two circles' centres - the direction they're actually offset along, and so the worst case for a gap opening up between them - against the real render_border_top/apply_corner_mask output, not a hand-rederived formula.
2025-05-23Clamp the winit backend's own CSD margin offset to non-negative toosrdusr1-1/+8
Same fix as the udev backend's matching content-position code: a real CSD shadow margin (dwindow.geometry().loc) is never negative, but a live Firefox window was observed reporting loc = (-10, -10) despite sync_geometry's tiled-state hint telling it to reserve no margin at all. This path (rounded_content_element, a live GLES shader rendering the client's own texture directly, not a separate pre-shifted buffer) doesn't have the udev backend's double-application bug, but it shares the same single-subtraction call and so needed the same clamp.
2025-05-15Extend GPU rendering to every head, not just the firstsrdusr4-76/+108
Phase 2 of the GPU-rendering plan deliberately targeted a single head (GpuContext::output: Option<(crtc::Handle, GpuOutput)>) as a narrow proof that GBM+EGL+DrmCompositor rendering works at all on this hardware. DrmOutputManager already supports driving several crtcs at once - initialize_output is a per-crtc call on one shared manager, the same way anvil drives multiple outputs - so this was purely an unexercised restriction, not an architectural limit. GpuContext::output is now GpuContext::outputs: Vec<(crtc::Handle, GpuOutput)>, and udev/platform.rs calls initialize_output for every connected head in its own bring-up loop instead of only the first after that loop finishes. A head this fails for individually (already logged, not fatal) still just has no entry and falls back to the existing legacy Pixman path, unchanged from before. session.rs's VBlank handler and its VT-switch resume path (which excludes GPU-driven crtcs from the legacy set_crtc reassert loop, a different device fd that must never issue mode-set commands against a crtc DrmOutputManager already owns) both now look a crtc up in the Vec instead of comparing against a single stored one. render.rs's own render-loop lookup uses direct field access (gpu.outputs.iter_mut().find(...)) rather than an equivalent &mut self method: the borrow checker treats a method call as borrowing all of GpuContext, including gpu.renderer needed a few lines later for the same head, where direct field access lets it see the two borrows are disjoint. Still gated behind SRDWM_GPU=1 (unset by default) and untested on real multi-monitor hardware with the flag on - this machine has one display, so the actual multi-head path itself only gets exercised whenever it's set on hardware that has more than one.
2025-04-29Fix border/content misalignment from a stale and double-applied CSD marginsrdusr1-4/+63
Two bugs in how a CSD client's own declared shadow-margin offset (dwindow.geometry().loc) drives where its content actually renders, both surfaced by a real Firefox window: 1. A live Firefox window was observed reporting loc = (-10, -10) -- negative, despite sync_geometry's tiled-state hint telling it to reserve no margin at all. effective_frame_of already clamps this value to non-negative for its own size calc; the content-position code (both the masked-content-buffer test and the real content push) didn't, so a negative margin shifted content the wrong way -- away from the border, not toward it. Clamped to match. 2. Separately, and the actual cause of a later "border isn't over the window" report on the same Firefox window (this time reporting loc = (10, 10)): the masked/rounded content buffer's own build step already renders the client's surface tree shifted by -content_offset so the buffer's own (0, 0) lands exactly on the real, margin- excluded content top-left (see rounded_content_buffer's own loc parameter). Placing that already-compensated buffer on screen at a *second* content_offset-shifted position double-applied the correction, landing it content_offset pixels too far up and left of the border wrapping it. Confirmed live via pixel sampling: Firefox's own chrome rendered starting 10px above the border's nominal top edge, fully exposed, square, with no border over it at all. Split the single `pos` into `content_pos` (unshifted - what the already-compensated masked buffer uses) and `pos` (content_pos further shifted by content_offset - kept only for the surface_content_elements fallback, which renders the client's raw surface tree with no prior compensation of its own and still needs the shift applied once). The winit/GLES backend's equivalent path doesn't have this bug: its rounded_content_element renders the client's live texture directly at `location` with no separate pre-shifted buffer, so a single content_offset-adjusted position there was already correct.
2025-04-03Fix border corner rendered as a solid wedge instead of a ringsrdusr5-58/+218
round_top_corners/round_bottom_corners only ever cut pixels *outside* the shared corner radius (the rounded outer silhouette). Nothing cut anything *inside* it, so a border strip's own "extra" rows (present whenever corner_radius > border_width) stayed a solid filled disk out to the centre column/row, then hit clip_middle_beyond_thickness's hard, unblended rectangular cut right at the disk's own most opaque point - a clean right-angle step, not a curve. Confirmed live, zoomed: a real square notch bitten into an otherwise smooth arc, reported as "squares on the inside corners of each vertex." Added carve_inner_corner_pixel, the same smoothstep falloff as the existing outer cut but inverted (cuts near the centre instead of far from it), applied at radius - border_width so the corner becomes a genuine ring of ~border_width visible thickness tapering smoothly to transparent, instead of a filled wedge. Only render_border_top/ render_border_bottom pass an inner_radius; the titlebar's own corner and the lock-screen box keep their existing solid-disk behaviour, which is correct for a single flat-coloured panel with nothing of a different colour underneath needing to show through. Also generalizes round_top_corners with an explicit center_col parameter, mirroring the existing center_row shift: the titlebar's own corner circle was never shifted horizontally to match the border strip's (only vertically), leaving a border_width-wide sliver of the titlebar's own misaligned curve poking through at the seam. border_top_and_titlebar_corners_meet_without_a_seam and border_top_curve_actually_closes_within_the_side_strips_own_width updated to match: both now compare the correct corresponding columns (the titlebar's own buffer starts border_width columns inside the border strip's), and the latter no longer demands exact 255 opacity at a point that legitimately sits within the new inner cut's own antialiasing band.
2025-04-02Wire VT-switch pause/activate for the GPU render pathsrdusr1-2/+53
Phase 2 (0274273) deliberately shipped without VT-switch support for the GPU-driven head, documented as an explicit gap rather than a silent risk. Before testing it live, wire the real fix instead of finding out empirically - this already burned three real reboots getting the *legacy* VT-switch path right, and DrmOutputManager uses a genuinely different API surface (pause()/activate(), calling through to DrmDevice's own master-lock acquire/release) than the manual set_crtc+DPMS reassertion register_session_notifier already does for legacy heads. PauseSession now also calls DrmOutputManager::pause() when SRDWM_GPU=1 and a GPU context exists - a separate device/fd from the legacy Card, so purely additive. ActivateSession calls DrmOutputManager::activate (false), then deliberately does *not* also force a fresh render for that head specifically: DrmCompositor::render_frame always issues a full state commit (atomic or legacy, whichever this device negotiated - see DrmDevice::is_atomic()), not just a buffer swap, so the existing data.render_udev_frame() call at the end of this handler already reasserts mode-set and CRTC-active state together for the GPU head via render.rs's own GPU branch, the same way it always does. Also fixed a real conflict Phase 2 introduced: the existing legacy crtc-reassert loop (explicit set_crtc through the legacy Card/fd) used to run for every head unconditionally, including one now driven by the GPU path through a completely different DrmDeviceFd - two separate fds issuing mode-set commands against the same physical CRTC, exactly the kind of conflict that produced the worst VT-switch incidents (EBUSY loops) when it was really one fd racing itself. The GPU-owned crtc (if any) is now excluded from that loop.
2025-03-26Wire real GBM+EGL+DrmCompositor rendering for one head (GPU Phase 2)srdusr5-55/+295
Extends the SRDWM_GPU=1 opt-in path (Phase 1, fa0c7f1) from a capability probe into an actual, working GPU render pipeline for exactly one head, per the plan this was built from the plan file. gpu.rs: probe now goes all the way through EGLContext, GlesRenderer, DrmDevice (real DRM device, separate duped fd from the existing legacy Card), and DrmOutputManager construction, returning both a GpuContext and its DrmDeviceNotifier on success. GpuContext::initialize_output drives one crtc/mode/connector through DrmOutputManager, storing the resulting GpuOutput for the render loop to find. Confirmed while reading smithay's own source directly (not assumed): DrmDevice::new's disable_connectors parameter is not an atomic-vs- legacy switch - DrmDevice::create_internal tries atomic capability first and falls back to a Legacy internal variant automatically, exposed via DrmDevice::is_atomic(), logged here rather than assumed. platform.rs: probes at startup, calls initialize_output for the first head only (Phase 2's deliberate scope - see the plan), registers the DrmDeviceNotifier as its own calloop event source alongside (not replacing) the existing legacy DRM-fd registration. render.rs: render_udev_frame's per-head loop checks, before any of the existing Pixman-specific element-building logic runs, whether this head's crtc matches the GPU context's initialized output; if so, renders a plain clear color through render_frame/queue_frame and continues to the next head, completely bypassing the Pixman path for that head. Every other head, and this same head whenever the GPU context or its output is absent, is entirely unaffected. session.rs: register_gpu_drm_notifier handles DrmEvent::VBlank by calling frame_submitted() on the matching GpuOutput (required per queue_frame's own doc comment, or the swapchain runs out of buffers) and logs DrmEvent::Error without treating it as fatal. Deliberately out of scope for this phase (documented in the plan): window content/decorations/cursor on the GPU head (clear color only), multi-monitor GPU rendering (one head only), and VT-switch pause/ resume for the GPU head specifically (DrmOutputManager's own pause()/ activate() calls are a different API surface from the existing manual set_crtc+DPMS reassertion, and porting that pairing correctly needs its own isolated verification pass). SRDWM_GPU unset (the default) is unaffected: every step above only runs when it's set to "1", and every failure at any step falls back to the existing, untouched Pixman path with a logged reason, same fallback contract Phase 1 already established.
2025-03-21Add opt-in GBM+EGL capability probe (Phase 1 of GPU rendering)srdusr4-0/+110
The udev backend is, by explicit design, 100% software: PixmanRenderer compositing into legacy KMS dumb buffers. That was a deliberate choice for portability (dumb buffers work on essentially any DRM driver, including a VM with no GBM/3D support), not an oversight - but this machine's real hardware (Intel UHD 620, i915) should support real GBM+EGL rendering, and the user has asked for a genuine GPU-accelerated path, GPU preferred with CPU fallback, built as a separate track that doesn't risk the working software path. Adds `udev/gpu.rs::probe`: gated behind `SRDWM_GPU=1` (unset by default - a no-op, zero behavior change for every session that doesn't set it), attempts GBM device creation on a duped DRM fd, EGL display/device creation, and a software-rasterizer check, logging exactly which step failed if any and falling back silently. Wired in at udev backend startup, right after the DRM fd is opened. Deliberately does not yet create an EGLContext, a GlesRenderer, or touch scanout at all. Reading smithay's own reference compositor (anvil/src/udev.rs) confirmed it wires GBM+EGL rendering together with atomic-KMS scanout as one unit via DrmCompositor, not as a renderer swapped into the existing legacy set_crtc/page_flip flip loop this backend uses today. Adopting DrmCompositor is separate, larger-scoped work than "swap the renderer" - it replaces the same UdevHead mode-set/flip machinery the VT-switch fixes (register_session_notifier's ActivateSession arm, copy_and_flip's retry backoff) live in, and needs its own plan. This probe answers the first question - does the hardware even support it at all - safely, before that larger integration is scoped and attempted. Cargo.toml: added backend_egl/backend_gbm smithay features, additive to the existing renderer_pixman path (unchanged, still the default).