srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/state/geometry.rs
AgeCommit message (Collapse)AuthorFilesLines
2026-08-30Indent doc list continuationssrdusr1-5/+5
2026-08-24Look for a free spot before piling a new window on the last onesrdusr1-0/+11
Reported: windows spawn predominantly on one side and on top of each other, with no smart placement. Measured first, in a nested compositor: five windows opened at 30,30 then 60,60 then 90,90 then 120,120 then 150,150 -- every pair overlapping, all in the top-left. The cascade was the only thing running. The grid never engaged, and could not. It asked whether a grid CELL was free and then placed the window at that cell's corner at its own, larger size. An ordinary 800x600 window on a 1280x800 screen overlaps every cell of a 2x2 grid, so no cell was ever free, the grid returned nothing, and everything fell through to the cascade. Placement now looks for a position where the window overlaps nothing at all, and only cascades when the screen genuinely cannot fit one - which is what Windows does once its own screen fills up, and what makes the cascade the right last resort rather than the first answer. The candidates are the edges of what is already on screen: every window's left and right edge plus the monitor's own, taken both as "put my left edge here" and "put my right edge here", and the same vertically. That is Openbox's place_overlap reduced to this case (~/reference-wms/openbox), and it works because a rectangle packed against other rectangles is always flush with one of their edges - nothing is gained by testing the space in between. Two things the naive version got wrong, both fixed here: Ties go to the position nearest the middle of the monitor, and the choice rotates through the four most central free spots. Least-overlap placement is deterministic, so opening one window at a time - open, use, close, open the next - put every one of them in exactly the same place, which is this project's own earlier bug report. Every candidate rotated between is free, so variety never costs the guarantee. And placement now runs again once the client's real size is known. It has to happen before the client commits anything, so it was deciding where an 800x600 placeholder should go rather than the window - a small terminal was told it was 800x600, no two of those fit, and it cascaded. Only when the client actually chooses a different size: re-running it otherwise consumed a second cascade step for nothing, and the cascade wraps, which measured as two windows landing on exactly the same spot. 287 core tests pass, clippy clean.
2026-07-27Hide a window only when srdwm knows it has not drawn, not when a lookup says sosrdusr1-25/+17
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-23Do not draw a window before its client has painted anythingsrdusr1-0/+35
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-15Draw a resizing window's decoration from the same rect as its contentsrdusr1-6/+48
Reported as the titlebar not resizing at the same time as the window, and as resizing feeling cheap. effective_frame_of returned the live drag target while a resize was active, so the titlebar and border tracked the pointer while the client's actual pixels were still whatever it last committed. The two disagreed for the whole drag, and the decoration leading its own content is what reads as broken. It now returns the committed size anchored to whichever edge the drag is holding still - exactly the rect the content occupies, since sync_geometry positions it the same way, so the two agree by construction rather than by timing. The consequence is that the frame sits one commit behind the pointer instead of ahead of its own content. That is the trade every other compositor makes, and it is the right way round: a frame glued to its content and slightly behind the cursor reads as solid. The committed-size correction was extracted into committed_frame so the resize path and the ordinary path share it rather than having two versions that can drift, and so the resize path is no longer short-circuited by the pending-configure branch, which fires constantly during a drag precisely because every tick sends a configure. 533 tests pass, clippy clean. The arithmetic is covered where it is testable; how it feels mid-drag is a judgement only real hardware can make, and a nested drag did not reproduce a clean enough scenario to claim it.
2026-07-14Workspace capture writes a readable image, and left-edge resize holds its anchorsrdusr1-1/+44
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-06-02Stop window memory poisoning itself, and publish the decoration side to GTKsrdusr1-0/+6
Two reports, both traced to a cause other than the one being blamed. "Windows still spawn as squares" was not placement. On the live session firefox was 800x632 and so were four other apps, and 800x632 is exactly the placeholder new_managed_window assigns before a client has chosen anything. Firefox's remembered size earlier the same day was 1389x933. The loop: a window closes while still carrying the placeholder, the placeholder is remembered, the next launch therefore has a remembered size and is no longer provisional, a non-provisional window is forced to its size instead of being asked to pick, and on close the placeholder is written back. Every app that ever closed early gets pinned to one identical box, and no amount of placement work can touch it because the size never came from placement. remove_window now refuses to remember a size the client never chose. That alone would have been wrong: adopt_provisional_size cleared its own tracking set but never cleared Window::size_is_provisional, so nothing would ever have been remembered again. Both halves are covered by tests. Five poisoned entries were dropped from the live store and the six real ones kept, with a backup alongside it. "When user sets decorations should override all applications": the earlier answer was true about the protocol and wrong about the outcome. GTK never negotiates decoration, but it does read the desktop's button-layout preference - GTK4 through xdg-desktop-portal, GTK3 through gtk-decoration-layout. srdwm now publishes its own button_side there at startup and after every reload, which is precisely the job kde-gtk-config does for KWin. Verified in both directions from a neutral starting value; testing the second direction is what exposed an ordering bug where the publish ran before apply_general_settings and broadcast the default instead of the configured side. 527 tests pass, clippy clean.
2025-12-01Let a new window pick its own size instead of forcing a guessed placeholdersrdusr1-1/+57
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-09-10Wayland backend: desktop icons v2, menu rebuild, layer-shell/scale fixes,srdusr1-2/+38
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-09Fix interactive-resize border/shadow lag without the OOB risk that sank the ↵srdusr1-12/+31
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-03-10Fix border/content-mask sized for a CSD client's whole buffer, margin includedsrdusr1-15/+38
The previous fix for a stale border size (switching effective_frame_of from dwindow.geometry() to raw dwindow.bbox()) traded one bug for another. bbox() is the window's entire committed buffer; geometry() is that buffer intersected with the client's own xdg_surface:: set_window_geometry hint, which excludes any invisible CSD shadow margin the client reserves around its real visible content. The assumption behind the switch - that sync_geometry's unconditional tiled-state bits make every compliant client reserve no such margin, so nothing would be lost - was wrong: confirmed live via temporary diagnostic logging, Chrome reserves a real, correctly-current 10px margin on all four sides regardless of the tiled hint (Firefox, the window that exposed the original staleness bug, does not - the two disagree on this). Raw bbox() therefore handed the border/content mask Chrome's entire buffer, margin included - 20px wider and taller than its real visible chrome on each axis, with no compensating position shift - so the rounded border curve traced a rectangle Chrome's real content never reached, and its true, still-square corner poked straight through the curve instead of being hidden by it. Reported live as a border not lining up with a window's content and a hard block cutting through an otherwise-rounded corner. Fixed by keeping both properties at once: dwindow.geometry().loc as the margin - assumed symmetric (left == right, top == bottom), which holds for every real CSD shadow margin observed here, since it's a fixed design constant that doesn't scale with window size and so has no equivalent staleness window even while the hint's absolute size does - subtracted from the always-fresh bbox(). Current size, correct visible-content bounds, for a client that reserves a margin (Chrome) and one that doesn't (Firefox, whose hint .loc is always (0, 0), where this reduces to plain bbox()) alike.
2025-03-07Fix border/decoration stuck at a stale size after a passive tiling reflowsrdusr1-5/+40
effective_frame_of sized a window's border/shadow/titlebar from dwindow.geometry() - xdg_surface::set_window_geometry. Per smithay's own implementation that value is the client's cached hint intersected with bbox(), falling back to bbox() only if never set. Nothing in the protocol obliges a client to resend the hint on every resize, and intersection() can never return something larger than its smaller operand - so once a client's cached hint is smaller than its current real buffer, geometry() stays clamped there permanently. Confirmed live: after a passive tiling reflow (this window resized only as a side effect of a sibling window moving, no direct action on this window itself), Firefox's real content filled the correct, much larger area immediately, but its border/decoration stayed rendered at a small fraction of that - unchanged for several seconds, well past both animation settling and any reasonable commit-throttle window -- until an unrelated maximize/restore cycle happened to prompt Firefox into resending a fresh hint and self-correcting. Switched to bbox(): the real bounding box of the window's current surface tree, which updates on every commit unconditionally. This gives up excluding a CSD client's own invisible drop-shadow margin, but sync_geometry already unconditionally sends all four tiled state bits specifically so a compliant client (GTK4/Firefox) reserves no such margin at all, so a compliant client loses nothing. Both the decoration-bitmap sizing (redraw_decoration_buffer) and the render loops' own border/shadow/occlusion positioning funnel through this one function, so they stay consistent with each other - avoiding the out-of-bounds texture-crop regression a previous, different attempt at this same lag hit (see this function's own doc comment history).
2025-02-15Checkpoint: preserve all uncommitted rust-rewrite worktree worksrdusr1-18/+231
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-04Fix CSD windows rendering with a wallpaper-visible gap at their cornersrdusr1-1/+18
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 ↵srdusr1-1/+34
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-20Send tiled xdg_toplevel state so GTK stops reserving its own shadowsrdusr1-0/+29
No configure this compositor ever sent set any xdg_toplevel::State bit at all - confirmed by grepping the whole crate, zero hits before this. GTK4 (Firefox concretely) reads the tiled bits to decide whether to reserve an invisible client-side shadow margin around its own content, independent of server- vs client-side decoration; with none ever sent it always assumed "floating, might need a shadow" and kept reserving one. That margin sits inside the committed buffer but is functionally invisible, so this compositor's own border - drawn at the full geometry, margin included, since nothing here knew the margin existed - ended up visibly offset from where the client's real chrome began. Root-caused from a live screenshot: Firefox's srdwm-drawn border sat clearly up-and-left of its actual toolbar, not framing it. Reported as "border is not with the window at start" and, more generally, borders never feeling like part of the window they're drawn around - which this is: decoration and content genuinely disagreeing about where the window's edge is. Sets all four Tiled* bits unconditionally on every xdg_toplevel configure - the same technique river/dwl use, telling every window it's flush against something and should skip its own shadow regardless of whether it's in a literal tiling layout, which is the outcome actually wanted here: this compositor draws the frame, nothing else should also be reserving room for one. Not visually verified against a live client yet - this needs an actual GTK app rendering under a restarted session to confirm the shadow margin is really gone, which no offline test can substitute for.
2024-07-30Split crates/wayland/src/state.rs (1276 lines) into state/srdusr1-0/+105
Pure reorganization, no behavior change - verified by diffing the function-name and struct-name sets before/after (both identical) plus a full cargo test pass. mod.rs keeps ClientState/OutputEntry/CompState/ WindowAnim/RepeatState's definitions, the key-repeat impl, and the output-lookup impl (all small and tightly coupled to the type definitions), plus mod declarations. The one large impl CompState block (previously ~600 lines) splits by concern: - lifecycle.rs: new_managed_window, set_decorated_from_mode, redraw_decoration_buffer, remove_window. - layers.rs: ensure_layer_initial_configure. - focus.rs: set_keyboard_focus, set_window_activated. - menu.rs: open/close/run_context_menu_action, is_double_click. - geometry.rs: raise_pinned, sync_geometry. - tick.rs: tick_dirty_broadcasts, tick_animations, resync_stacking_order. - toplevel.rs: the with_toplevel_title/app_id/sync_toplevel_metadata free functions. - tests.rs: unsplit, same reasoning as every other split this pass. CompState's fields were already pub(crate) (this crate's existing convention, unlike core's/config's plain-private), so no field- visibility changes were needed - only resync_stacking_order (called from geometry.rs, defined in tick.rs) needed bumping from private to pub(crate), matching that same convention.