srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev
AgeCommit message (Collapse)AuthorFilesLines
2026-08-30Use as_chunks for fixed-size pixel iterationHEADmainsrdusr1-1/+1
2026-08-30Indent doc list continuationssrdusr3-20/+20
2026-07-27Reach GTK4's window buttons too, and speak the decoration protocol GTK knowssrdusr1-0/+8
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 sosrdusr3-4/+4
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 itsrdusr1-2/+24
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 anythingsrdusr3-0/+19
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-14Workspace capture writes a readable image, and left-edge resize holds its anchorsrdusr1-2/+70
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-05-15Fix the maximize border on the path that actually runs, and three spawn faultssrdusr2-2/+10
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 ↵srdusr2-2/+11
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 boxsrdusr1-2/+24
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-04-27Build the eight asks recovered from the previous session's transcriptsrdusr2-0/+16
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 ↵srdusr2-3/+14
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.
2025-12-01Let a new window pick its own size instead of forcing a guessed placeholdersrdusr1-0/+1
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, ↵srdusr1-1/+13
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-04Fix window memory never saving on close, and split-screen icon/primary bugssrdusr1-1/+10
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-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-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,srdusr5-10/+303
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-17Fix desktop icons v1 regressions: bar overlap, wrong order; add proper menussrdusr2-2/+3
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 menussrdusr2-0/+34
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-09Fix interactive-resize border/shadow lag without the OOB risk that sank the ↵srdusr2-3/+36
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-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.
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-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-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)srdusr3-0/+103
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).
2025-03-20Fix border strips staying sized for a window's previous geometrysrdusr1-0/+21
apply_geometry and restore - the Platform callbacks core's toggle_ maximize/apply_snap_zone/restore_window drive for a pure geometry change - only called sync_geometry, never redraw_decoration_buffer. The cached border-strip/titlebar bitmaps (self.border_top_decorations, self.border_bottom_decorations, self.decorations) size themselves from effective_frame, which can differ from w.geometry alone once a CSD client's own invisible shadow margin is involved (see that function's own doc comment) - but nothing here rebuilt them right when this callback changed w.geometry. The next rebuild only happened whenever this window's client next committed a frame or some other, unrelated trigger reached redraw_decoration_buffer, not reliably right away. Confirmed live: maximizing then restoring a Chrome window left its border strips sized for the maximized frame while its real content had already settled back to the smaller restored size, immediately and permanently until some later trigger happened to catch it up - a real, visible gap between content and border on the far (east/south) edges, a different bug from the half-pixel corner seam fixed separately in blend_corner_pixel. Both apply_geometry and restore now also call redraw_decoration_buffer right after sync_geometry, in both the udev and winit backends.
2025-03-07Fix stale ghost content after window move/resize/close on real hardwaresrdusr3-1/+51
The udev backend's damage tracking only forced a full repaint (ages = [0, 0]) on a workspace switch or a VT-switch resume. An ordinary move/resize/open/close/restack within the same workspace relied entirely on OutputDamageTracker's own per-element diffing to compute correct damage for the region a window vacated - which doesn't always hold: maximizing a window over a second one, then un-maximizing, left a persistent ghost of the second window's old titlebar/status text sitting in the vacated corner, unchanged across multiple otherwise- idle frames. render_udev_frame now also hashes every visible window's id and rect each frame and forces the same full-repaint reset whenever that signature changes - the same "defensive, not a fix for a proven bug in the diffing itself" reasoning the existing workspace-switch reset already uses, just triggered by a second, complementary condition.
2025-03-06Fix total input death after VT switch back (libinput never resumed)srdusr2-5/+54
The kernel revokes every input device fd across a VT switch away. libinput has a documented pair of calls for this exact case, suspend()/resume() (libinput_suspend/libinput_resume), which reopen every device through the session once it is reactivated. This codebase never called either one, so after switching back to the compositor's VT, libinput's device list stayed pointed at fds the kernel had already revoked - reads on them don't error, they just silently stop producing events, forever. Rendering, DRM/KMS, and libseat's own session activation all recovered on their own, which is what made this look like a display bug rather than an input one; it took three real forced reboots today, with no visible input from keyboard or mouse for 30+ minutes after switching back to tty1 each time, to isolate it as this specific missing call. register_libinput now returns the Libinput context (a clone of the one already handed to LibinputInputBackend - it's a reference-counted handle, not a deep copy, and LibinputInputBackend only exposes an immutable accessor once it's moved into the calloop event source). register_session_notifier takes that handle and calls suspend() on PauseSession, resume() on ActivateSession.
2025-03-03Fix VT-switch DPMS blank-screen and CSD corner-crop staircase bugssrdusr1-0/+33
VT-switch resume: reasserting the CRTC's mode-setting state was never enough on its own - display power (DPMS) is separate KMS state, and nothing here ever touched it after a real switch-away-and-back. Page flips kept succeeding with zero errors logged for the rest of a real 30+ minute session while the panel itself simply stayed dark, which is what actually explains a user report of the screen and keyboard input never recovering after one VT switch. Sets DPMS-on unconditionally on every resume now, the same property zwlr_output_power_v1 already writes for an explicit client request. Corner rendering: both backends' side-strip crop (the fix that keeps a window's flat left/right border strips from poking a solid-coloured square through the top/bottom strip's own rounded curve) only ever activated for `w.decorated` windows. An undecorated/CSD window's crop depends on its *content* actually getting masked to match - the winit backend never checked that at all, so every CSD window with a nonzero border_width got the uncropped, "staircase" artifact unconditionally, confirmed live via a highlighted border colour and raw pixel sampling. Ported the udev backend's own `border_curve_is_safe` check (decorated OR content-will-be-masked) into winit, using the cheap "does this surface have subsurface children" test both content- masking code paths already gate success on, rather than duplicating either one's real (comparatively expensive) rendering work just to probe it. Full workspace build + clippy -D warnings + test suite (378 tests) green.
2025-02-21Remove resolved corner-rounding diagnosticssrdusr1-29/+0
The top-rounds-but-bottom-doesn't investigation these were tracking is closed: live-verified via a real screenshot (pixel-level, not eyeballed) that both corners round correctly on both a decorated window and an undecorated/CSD one relying on content masking. Removes three log::debug! blocks (corner-mask state, TOP/BOTTOM border strip position dumps, and a raw alpha-byte dump of the bottom border buffer) that were firing on every single render pass regardless of whether anything changed, adding real per-frame overhead for output no longer needed. Build + clippy + test (378 passing) all still green.
2025-02-18Fix clippy warnings surfaced by the mergesrdusr2-3/+3
Pre-existing issues in the uncommitted rust-rewrite work (unused imports, over-arity glyph-drawing functions, a couple of complex inline types, or_insert_with(T::default) instead of or_default(), and one unsimplified test-only arithmetic expression), plus two imports left unused by switching to or_default(). None of these are behavior changes. Full workspace build + clippy -D warnings + test suite (378 tests) all green after this.
2025-02-17Merge branch 'main' into rust-rewritesrdusr4-12/+72
# Conflicts: # crates/config/src/engine/general.rs # crates/wayland/src/udev/drm.rs # crates/wayland/src/udev/mod.rs # crates/wayland/src/udev/render.rs # crates/wayland/src/winit/render.rs
2025-02-17Checkpoint: today's fixes before reconciling with the rust-rewrite worktreesrdusr4-12/+72
Fixes a live-reproduced VT-switch busy loop (failed page_flip retried with no backoff), shadow rendering bleeding onto occluding windows unclipped, and a winit-backend buffer-age correctness bug that left stale cross-window pixels on screen. Committing before merging in the much larger uncommitted rust-rewrite worktree, which independently touches several of the same files - this is the pre-merge baseline to diff against, not a claim that these are the final versions of these fixes.
2025-02-15Checkpoint: preserve all uncommitted rust-rewrite worktree worksrdusr7-173/+1367
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
2025-02-13Implement the Wayland implicit pointer grabsrdusr1-0/+2
Every pointer motion event re-ran the same popup/layer/content hit-test from scratch and delivered focus to whatever it found right now - there was no notion of "a button is held, keep delivering to the surface that received the press" at all, which is standard, expected Wayland compositor behavior (every real compositor does this; it's how dragging, text selection, and scrollbar-thumb dragging all stay coherent even when the pointer briefly leaves the widget's bounds mid-gesture). Without it, a real human's hand drifting even slightly outside the pressed surface mid-drag - trivially easy during a fast, non-perfectly- straight mouse motion - sent that client an unrequested `leave` event in the middle of its own gesture. GTK's drag recognizers (a GtkHeaderBar's move-the-window gesture, concretely) treat a mid-gesture leave as "this isn't coherent, abort," which reads as "dragging this window by its title bar does nothing at all" - live-reproduced this work on Nemo, and consistent with move_request never having fired once all session despite real attempts. pointer_button_grab captures the (surface, origin) resolved on a button press once the held-button count goes from 0 to 1, and every event under the grab - motion or button, this press's or a later one overlapping it - is delivered there instead of wherever a fresh hit-test lands, until every held button is back up.
2025-02-06Fix layer surfaces spuriously hiding/re-showing on their own realizationsrdusr1-0/+1
sync_layer_visibility could not tell a real hide (null-buffer commit on an already-visible surface) apart from a layer-shell client's ordinary realization sequence (commit with no buffer -> configure -> ack-commit with no buffer again -> attach real content): both look like "committed, no buffer" from has_buffer alone. Every layer surface's first realization was spuriously unmapped and immediately remapped, doubling LayerMap arrange() passes on every single popup open. Live-reproduced via an AGS peer session: a full-monitor click-outside-to- close popup surface came back from a hit-test with geometry wider than the real output after several open/close cycles on a wl_surface GTK had reused across role destroy/recreate, and sat in the Top layer above every real window with no input region set - silently swallowing clicks meant for windows, dropdowns, and CSD title bars alike. layer_surfaces_shown_once now gates the hide path on a surface having actually shown a buffer at least once, and is cleared in layer_destroyed so a reused wl_surface's next role starts clean rather than inheriting the previous role's flag.
2025-02-04Fix CSD windows rendering with a wallpaper-visible gap at their cornersrdusr2-2/+30
srdwm never read xdg_surface.set_window_geometry anywhere. A CSD client (GTK4/Firefox) declares its real visible content as a sub-rect inset within a larger buffer that also reserves an invisible shadow margin - that margin stays reserved in the buffer even once the tiled-state hint tells the client to stop drawing the shadow itself. Every render path was positioning content at the client's raw buffer origin instead of subtracting that declared offset, leaving the margin's width/height as a gap with wallpaper visible through it at the window's top-left corner. Confirmed by pixel-diffing the gap against the real wallpaper at that exact screen position: an exact match, ruling out "just the client's own dark theme." Fixed in three places that have to move together: udev/render.rs and winit/render.rs's content-positioning code, and sync_geometry's space.map_element call. That last one matters as much as the other two - it's what smithay's Space (and therefore click hit-testing) reads, so a render-only fix would have traded a visible gap for an invisible, same-size hit-test offset in the other direction. Also applied to the new capture-workspace off-screen render for the same reason. This likely also explains real dropdown/context-menu misplacement (and the clicks landing on the wrong spot) in CSD apps: popup positioning anchors against the same window position this fix corrects.
2024-12-24Accumulate wayland-crate additions: capture rendering, focus/Space sync, VT ↵srdusr5-11/+374
resume, plumbing Bundles the remaining wayland-crate changes built up here, touching both backends (udev and winit) and the shared input/rendering code: - udev/capture.rs: off-screen Pixman render of an arbitrary (not necessarily on-screen) workspace's window content to a PPM file -- what crates/core's capture-request queue drives, for a workspace switcher's thumbnail previews. wlr-screencopy structurally can't do this (it can only see what an output is presenting), which is why this exists as a separate render path rather than reusing it. - input::focus_window now also raises the window in smithay's own Space, not just core's stacking order - Space is what actually renders on top and what pointer hit-testing reads, so any focus path that skipped this (an IPC "focus" dispatch, concretely) left a window genuinely focused while still rendering, and receiving clicks, underneath whatever was already topmost. Both backends' poll loops now re-sync this after any IPC mutation. - udev/session.rs's VT-switch resume fix (drains a stale pending page flip before reasserting CRTCs) already has its own earlier, cleanly isolated commit - not duplicated here. - Assorted decoration/cursor/rounded-corners/output-management/ screencopy/XWayland changes and their cross-backend wiring. Coarser than the repo's usual one-purpose-per-commit convention, deliberately - see the core-crate sweep commit's own message for why.
2024-08-29Fix VT-switch resume getting stuck on a stale pending page flipsrdusr1-1/+24
A flip issued right before a VT switch away could still be undelivered when the session resumed - the kernel refuses a new page flip on a CRTC with one already outstanding, which showed up live as a black screen that never recovered across two switch attempts, with a rapid repeating "Device or resource busy" loop in the log. Drain and apply any pending DRM events before reasserting CRTCs and rendering again on resume, so a stale flip from before the switch can't collide with the fresh one.
2024-08-23Round the bottom border strip's corners to match the topsrdusr2-8/+22
MISSING.md listed the border frame's bottom/left/right strips as staying square while the top one (and the titlebar above it) rounds -- "unrelated to client content rounding... not attempted." The left/ right strips genuinely can't participate (border_strips' geometry has them span only the height between the top and bottom strips, no corner to round), but the bottom strip is exactly the same shape as the top one and had no reason left to stay square. decoration::render_border_bottom mirrors render_border_top exactly (round_bottom_corners mirrors round_top_corners), cached the same way in a new border_bottom_decorations map, and drawn in both render loops via the same all-or-nothing occlusion check the top strip already uses - pulled out of the left/right strips' per-fragment occlusion splitting into its own dedicated bitmap path, matching top's existing trade-off (cropping a rounded bitmap's source rect per fragment is real extra work for a strip this thin) rather than inventing a new one. One real bug caught before it shipped: round_bottom_corners' corner- centre math (height - r - 1) panics on unsigned underflow whenever the radius clamp lands on the strip's own full height (a real, common case - a 2px-thick test strip hits it immediately). Fixed by computing the centre as a signed offset instead, mirroring how the existing dx/dy distance math already avoids the same class of issue.
2024-08-09Fix closing an XWayland window doing nothing on both backendssrdusr1-2/+11
Platform::close only ever called w.toplevel(), which is None for an XWayland window - closing one (the WM's own close binding, or `srd dispatch close`) silently did nothing at all, on both udev and winit. Found live: a leftover untitled fullscreen window wouldn't close via srd dispatch close even after several seconds, tracing back to this. Fixed by falling back to X11Surface::close() when there's no xdg toplevel - it already handles both cases smithay-side (a polite WM_DELETE_WINDOW for a cooperating client, outright destroy_window for one that doesn't support it), so no new logic was needed, just calling it.
2024-08-03Fix doc-comment file references left stale by today's six module splitssrdusr2-3/+3
~17 comments across the codebase still pointed at udev.rs/winit.rs/ state.rs by their old flat-file names after those became udev/, winit/, state/ directories - found while auditing what this work rushed, since the split verification (function/struct-name diffing, full test suite) checked structural correctness but never comment accuracy. Updated each to either the specific new file (e.g. "see state.rs's REPEAT_DELAY" -> "see state/mod.rs's REPEAT_DELAY", "udev.rs's monitors()" -> "udev/platform.rs's monitors()") or the bare module name where the reference was already generic ("the udev/winit backends", not a specific location).