| Age | Commit message (Collapse) | Author | Files | Lines |
|
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
The maximize border was still drawn because the earlier fix landed on the
wrong branch. udev/render.rs has three border blocks: the SRDWM_GPU=1 path
at the top and two Pixman ones below. The patch replaced the first match in
the file, which is the GPU branch a real DRM session never runs. All four
sites across both backends are now gated on !maximized.
The verification had failed twice for a separate reason: winit/capture.rs
did not draw border strips at all, so a screenshot could never answer "is
there a border here" and the control passed for the wrong reason. Border
strips are now drawn into that pass as solid fills - corner rounding is not
reproduced, so a capture is not pixel-exact at the corners, but presence,
position, thickness and colour are. With that closed the test has a real
control: unmaximized gives 6 accent pixels at x=800..805, exactly the
configured border_width, and maximized gives none at the right edge or along
the top row. That proves the winit path; the Pixman path is the same change
at two more sites and is not separately confirmed on screen.
Windows spawning as squares, partly off-screen, and always on the left were
all SmartPlacement::grid. It returned size.min(cell), shrinking every window
to its grid cell whatever size it asked for; it scanned cells in reading
order and took the first free one, which is the leftmost; and nothing clamped
the result, so a window larger than its cell could hang off the edge with its
border out of view. The cell now decides only where a window goes, the scan
starts from a rotating cell, and both grid and cascade clamp into the usable
area. Four tests, one per reported symptom.
525 tests pass, clippy clean.
|
|
clean up maximize
Four reports after restarting into today's build, with a screenshot. The
screenshot was measured, not eyeballed: top bar y=0..29, a 4px accent border
at y=30..33, titlebar from y=34, bottom border at y=1027..1030, and 49px of
bare desktop below it.
Windows spawning too close to the top bar. A remembered position was
validated only by asking whether it landed on some monitor's full_geometry,
which includes the strip a top bar reserves, so an app whose remembered y was
small reopened with its titlebar under the bar. That is why it was
"sometimes": it depended on the stored value, and the live store holds
wezterm at y=44 and firefox at y=69 against a 30px bar. Remembered positions
are now clamped into the monitor's usable area.
Placement not surviving a logout. Window memory does persist, but five of the
eleven entries in the live store were saved with a second monitor attached,
at x >= 2000. Those points match no current monitor and were discarded
outright, falling back to a fresh cascade, so those apps appeared to remember
nothing. Such a position is now clamped onto a monitor that exists instead.
Per-window minimum sizes. One global floor is wrong in both directions.
Three sources now, in increasing precedence: the global floor, the client's
own declared minimum (xdg_toplevel.set_min_size or ICCCM hints), and a
min_width/min_height window rule overriding both. A rule wins permanently --
the backend refreshes the client's declared minimum on every decoration
redraw and must not undo a deliberate override.
Maximize, three faults in one report. A maximized window now draws no
border: its edges are the screen's edges, and the only place maximize stops
short is the bar strip, which is exactly where the measured line was.
maximize_geometry_for no longer subtracts a bottom-anchored dock's exclusive
zone, so maximize runs to the bottom of the screen and the dock floats over
it; top, left and right are still honoured.
general.maximize_covers_dock = false restores the old behaviour. With the
border gone the window sits flush under the bar instead of with an accent
line crowding it.
Verified: seven new tests on the real numbers from the live store, and
maximize geometry measured live in a nested instance (a window maximized on a
split half reports exactly that half's rect). NOT confirmed on screen: the
border removal and the dock behaviour - the nested backend has no bar or
dock to reserve a zone, and an attempt to check the border produced a failing
control, since srd set border_width only affects windows created after it.
515 tests pass, clippy clean.
|
|
Earlier today I wrote that a nested compositor cannot produce a second
monitor because split and fake monitors "need real head machinery that only
the DRM backend has". That was inferred from both commands returning ok and
changing nothing, not read from the code, and the first half is false.
udev/platform.rs was simply the only backend draining those request queues.
The winit poll never took them off, so the request sat there forever and the
dispatch looked like it had worked. Nothing about split is DRM-bound:
MonitorSplit is bookkeeping in WindowManager and split_rect is pure geometry
in core. The winit poll now drains split requests the same way, and its
monitors() expands a split into one Monitor per part with its own
full_geometry and maximize_geometry, matching the udev expansion. Splitting
the nested output into two 640x800 monitors with a seam now works, which is
what a multi-monitor repro needs. Fake monitors stay udev-only; that half was
not re-checked and is not claimed either way.
Also corrected: the capture pass measured the shadow rect from w.geometry
while both on-screen loops measure it from effective_frame, the client's real
committed size. src indexes into a buffer rasterised at the frame's size, so
the two disagreeing reads the wrong region whenever a client settles on a
different size than it was asked for.
The seam check this was meant to unblock is still not done. With the split
working, the negative control failed: a floating window off the seam had no
shadow either. Running both clients in one instance showed Alacritty renders
a shadow in a capture and Nemo renders none, same settings, both floating,
either focus. Nemo is server-side decorated and Alacritty is not, which is a
lead and not a conclusion. Recorded in docs/TODO.md as open rather than
guessed at.
515 tests pass, clippy clean.
|
|
The punch list was not the whole record. These are the owner's own typed
requests, read back out of the previous session's transcript rather than
guessed at, then each checked against the code before being treated as open.
Three they suspected were already done really were: dialogs already had a
Close-only titlebar, inactive dimming already existed, and the corner resize
hitbox had already been tuned.
Snap layouts on drag, asked for twice. Edge snapping worked but committed
silently on release with nothing shown first, so there was no way to know it
would happen or where. A translucent drop-target preview now follows the
drag, and throwing the pointer at a monitor's top edge drops down the
existing six-cell grid to aim at. The preview calls the same snap_zone that
end_drag does, so the two cannot disagree. Two defects found by screenshot
before landing: moving down onto the flyout closed it, and its labels
overflowed at a fixed cell width - the same "text goes out of view" fault
already fixed once for the context menu.
New File now offers real types, chosen by extension, with the de-duplication
counter placed before the extension so the file stays what it says it is.
Refresh re-reads init.lua and fires a new srd.on("refresh") handler instead
of only re-scanning the icon grid. What refresh means beyond srdwm's own
config stays the config's decision.
general.config_reload_on_write (default on) applies an edited config on save,
via an mtime sweep rather than an inotify watch: no new dependency, same
behaviour on every target, and unaffected by editors that write through a
temp file.
A real bug behind "what happens when our config fails": you lost every
keybinding. do_reload cleared the binding, handler and repeat tables before
re-executing and never restored them, so a syntax error left neither the old
config nor the new one, and the only key still working was the reload combo
nobody thinks to press. The tables are now restored on any failure and
config errors reach notify-send, not just the log.
srd.lock() and a default Mod4+Ctrl+l binding: the built-in lock screen could
not be reached from Lua at all. Native rather than shelling out, because a
lock key that shells out fails silently when the binary is not on PATH.
Default bindings added for srd.window.move and a dynamic/tiling toggle, both
of which existed with no way to reach them, plus srd.layout.get() so the
toggle reads the live workspace rather than the configured default.
Dialogs open centred, and are excluded from remembered geometry in both
directions - that table is keyed by app_id, which a dialog shares with the
window that spawned it, so dialogs inherited an unrelated position and size
and then overwrote it with their own.
theme.decorations.title_bar.button_mode (dynamic by default, or fixed) drops
the Maximize button on a window whose client pinned min == max size, where
pressing it can do nothing. Maximize is removed from the slot list rather
than skipped in place on both the render and hit-test sides, so the remaining
buttons close the gap identically; three tests pin that agreement, which is
what fails silently when it drifts.
Also: the nested backend's screencopy pass now draws both menus, the flyout
and the drag preview. Four investigations in one day started from a
screenshot missing a tier, so that pass carries an explicit list of what it
still omits and the on-screen loop points at it.
512 tests pass, clippy clean.
|
|
across a monitor seam
Nemo's right-click context menu was the last open punch-list item, parked
twice as untestable. It works: verified end to end in a throwaway nested
compositor, menu and submenu both, at the correct position and stacking.
The popup path itself needed no fix, so the POPUP-GEOM-DIAG/POPUP-GRAB-DIAG
diagnostics are removed.
Two real bugs turned up in the way of testing it.
zwlr_virtual_pointer was a silent no-op on the winit backend. Every
Motion/MotionAbsolute handler read UdevState::bounds() behind an early
return when state.udev was None, and that field is Some only for the DRM
backend. The protocol advertised its global, accepted create_virtual_pointer
and accepted every request, then discarded all motion with no error and no
log. That is the backend a nested instance runs on, so the only safe way to
drive a throwaway compositor - a Wayland client of that compositor, which
cannot reach any other session, unlike ydotool's /dev/uinput writes - did
not work at all. Bounds now come from WindowManager::monitors() when udev is
absent; both backends fill that list from Platform::monitors().
The winit backend's screencopy pass rendered no popups and no shadows. It
re-renders the scene offscreen, and that second scene was missing tiers, so
grim on a nested instance reported the opposite of the truth: a menu drawing
perfectly on screen photographed as absent. The DRM backend never had this,
since it serves screencopy from the on-screen frame it just drew. Border
strips are still missing from that pass, called out in the code rather than
left silent.
Also fixed, from the "windows show a bit in the other monitor" report:
shadow_rect expanded by SHADOW_SIZE on every side with no monitor-boundary
awareness, so a window flush against a seam put its 24px shadow strip on the
neighbouring screen. shadow_rect_clipped clips to the bounding box of the
monitors the window's geometry actually touches - not just its assigned
one, since a window straddling a seam really does occupy both and clipping
there would cut its shadow off mid-body. The bitmap's own extent stays
unclipped, because the src rectangle indexes into it; only the fragment list
is clipped. Six tests on the incident's own numbers. Not confirmed on
screen: the nested backend cannot produce a second monitor.
New tool: tools/virtual-pointer-click, a scriptable virtual-pointer driver
that acknowledges each command after its round-trip, so a test script can
put a screenshot between a move and the click that follows it.
489 tests pass, clippy clean.
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
Same fix as the udev backend's matching content-position code: a real
CSD shadow margin (dwindow.geometry().loc) is never negative, but a
live Firefox window was observed reporting loc = (-10, -10) despite
sync_geometry's tiled-state hint telling it to reserve no margin at
all. This path (rounded_content_element, a live GLES shader rendering
the client's own texture directly, not a separate pre-shifted buffer)
doesn't have the udev backend's double-application bug, but it shares
the same single-subtraction call and so needed the same clamp.
|
|
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.
|
|
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.
|
|
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.
|
|
# 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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
~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).
|
|
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 the module doc comment, imports,
WaylandPlatform's struct definition, and TARGET_FRAME_TIME, plus mod
declarations. The rest splits by concern:
- connect.rs: connect(), the ~200-line setup/init function.
- run.rs: accept_clients, pump_winit - the small per-poll pair.
- render.rs: render_frame, the per-frame render loop (left as one
intact ~330-line function, same reasoning as udev's render.rs: its
structure is deliberate and already documented inline, not a target
for further decomposition in a pure reorganization pass).
- capture.rs: capture_offscreen, the screencopy path.
- events.rs: handle_winit_event.
- platform.rs: `impl Platform for WaylandPlatform`.
No tests module existed in the original file, so none was split out.
A handful of methods/functions (accept_clients, pump_winit,
render_frame, capture_offscreen, handle_winit_event) went from private
to pub(super): called across what are now sibling submodules, which
Rust's privacy model doesn't let see each other's private items.
|