| Age | Commit message (Collapse) | Author | Files | Lines |
|
|
|
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.
|
|
Reported twice, as two complaints: windows do not remember their size or
position across a close or a reboot, and windows spawn stacked on one side
with no smart placement. One bug.
`add_window` looks the store up by app_id. A Wayland toplevel role exists
before its client sends set_app_id, so at the moment srdwm placed a window
the app_id was the empty string, every lookup missed, and every window fell
through to the cascade - which is exactly what "they all open on top of
each other" looks like. The store was being written correctly the whole
time and read at the one moment it could not match.
The lookup now runs again the instant a real app_id arrives, which is still
before the client's first buffer, so nothing is drawn in the wrong place
first. It only moves a window still sitting where the cascade put it: a
rule's explicit geometry, a maximize, a dialog's centring and a client's own
committed size are each more specific than "wherever I last left this app",
and a test asserts none of them is overridden.
A second bug sat underneath the first and only appeared once it was fixed:
the position came back and the size did not, which is stranger than nothing
being restored. The backend keeps its own copy of "this size is only a
guess" (provisional_size) and adopt_provisional_size reads that one rather
than the core flag, so the client's next commit overwrote the size that had
just been restored. Cleared with the same call.
Verified end to end in a nested compositor, driving a real edge-drag with
the virtual-pointer tool:
seeded store 400,300 500x400 -> opened at exactly 400,300 500x400
dragged the right edge -> 646 wide, store rewritten to 646 on release
closed and reopened -> 400,300 646x400
Before this the same first step opened at 30,30 800x600.
Also: window_memory::save_all's nested guard now allows a write when the
instance was given its own state directory (SRDWM_STATE_PATH or
XDG_STATE_HOME). The blanket refusal added earlier kept the owner's store
safe but made the feature impossible to test without pointing a test
compositor at the real desktop, which is how this went unverified in the
first place.
|
|
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 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
toggle_floating
The tiled-shadow-tint fix earlier today gated the shadow on Window::floating
alone. arrange_workspace only reads floating under the "tiling" layout, so
every window on this project's own default "dynamic" layout starts, and
stays, floating: false - the gate misread that as "tiled, no shadow"
regardless of which layout was actually running, so shadows silently
vanished under dynamic mode entirely, recoverable only by pressing Super+S
(toggle_floating), which then looked like that key toggles a tint rather
than floating. Fixed by checking the workspace's own layout name first:
a window is only "currently tiled" when its workspace runs "tiling" AND
it hasn't opted out via floating. DecorationSignature's floating field is
now currently_tiled, since a layout switch changes this for every window
on a workspace without touching any of their own floating fields.
Also disabled general.shadows in the user's own config per direct
request - never asked for, on by default, and a real problem for
color-accuracy work regardless of how correctly it renders otherwise.
Also fixed both context menus (titlebar and desktop) silently truncating
labels past a fixed 170px width with no indication - widened dynamically
to each menu's own real widest label via a new measure_text_width helper.
|
|
customization
Reported live: "looks very ugly currently and some of it doesn't make
sense." Both were real. Every row, including a bare divider, took one
full TITLEBAR_HEIGHT slot, so a separator was a 1px hairline in the
middle of 32px of empty space; "Move to Workspace" faked a section
caption by embedding box-drawing characters directly in an ordinary
item's label, which rendered - and behaved, until the click-dispatch
site's own special case - exactly like a clickable row that did
nothing. Separately, "Floating" was always offered even though
Window::floating only affects the "tiling" layout: toggling it under
this project's own default "dynamic" layout visibly changes nothing,
reading as a broken control rather than an inapplicable one.
ContextMenu (crates/core/src/context_menu.rs) gained real Separator
(9px) and Header (22px, non-interactive, dimmed) row kinds with their
own small heights, replacing the label-hack outright. Both backends'
rendering now sum each row's own real height instead of assuming one
uniform value, so hit-testing and pixels can't disagree about where a
row is. Floating is omitted entirely outside the tiling layout.
New, in direct response to "allow customizing from there as well": a
Customize section with live Button Style / Button Side toggles. Each
flips the matching ThemeConfig field and immediately redraws every open
window's titlebar - not routed through srd set's own path, which is
scoped to windows created after the call for lack of a redraw hook it
can reach; a menu action that didn't visibly change the titlebar you
clicked would be its own "doesn't make sense" bug.
Full workspace build/test/clippy clean (242 core tests, +8; 152
wayland, net-even after rewriting the old label-hack tests).
|
|
Diagnosed by a peer session (dotfiles-1a): SHADOW_SIZE is 24px, and a
tiling layout with a small gap_inner (as little as 1px live) leaves the
shadow nowhere to fall except onto the adjacent tile, darkening it by up
to SHADOW_MAX_ALPHA (~35%) on whichever side is unfocused. Not a content
tint or an opacity rule - verified against the actual rasteriser and the
live rule set before accepting the diagnosis.
A drop shadow separates a window from what's behind it; tiled windows are
coplanar and adjacent by construction, with nothing behind them to
separate from. redraw_decoration_buffer's shadow gate now requires
w.floating in addition to the existing !maximized/!fullscreen checks.
DecorationSignature gained a floating field so toggling floating on its
own invalidates the decoration cache instead of waiting for an unrelated
field to force a rebuild.
Live-verified in a nested compositor: two tiled windows show a clean
shared edge with no gradient bleeding across; floating a window still
detaches it from the tile group with its shadow intact; shadows still
toggle globally both ways.
|
|
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.
|
|
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.
|
|
srd.monitor.split only ever ran at Lua config load despite being a plain
WindowManager mutation that every backend's monitors() already reads
fresh on each call. Adds srd dispatch set output split <name|id> <parts>
[rows|columns] (IPC set_monitor_split), same id-resolves-to-name pattern
set_output_enabled already uses.
Also removes eight log::warn!("XXX-DIAG ...") lines left behind from live
debugging in the multi-session shift that landed in 3c41fc4 - the same
"temporary, never removed" pattern already fixed twice earlier this
session. Several fired on genuinely constant interaction (every title
change, every workspace switch, every layer-shell surface hide), not
just a one-off leftover. Left xdg_shell.rs's own POPUP-GEOM-DIAG/
POPUP-GRAB-DIAG alone - that one is a still-open, self-documented
investigation, not litter.
Also documents (docs/TODO.md, not a code change) a live incident where
creating a second fake monitor visibly corrupted the real monitor's
position and kept drifting with no further input - not root-caused
srdwm-side, flagged to the AGS peer session since a fake monitor's real
wl_output global is indistinguishable from a real hotplug to GDK/GTK.
And documents a deliberate decision not to blind-port window decoration
rendering onto the experimental, never-live-tested GPU render path.
|
|
Reported live: "looks weird and unpolished... need a lot more items".
Compared directly against the exact AGS reference this project's own
menu rebuild already targets rather than guessing:
- Highlighted rows used a flat, fully-saturated fill instead of the
reference's subtle 22%-accent-into-background wash. New decoration::
color::mix_rgb (channel-wise linear blend, generalizing brighten/
darken's fixed-target blends to an arbitrary second colour/ratio) lets
render_context_menu reproduce that same ratio.
- Every separator row was a label string of Unicode box-drawing
characters rendered as text glyphs, which render inconsistently at
small sizes - a label that's entirely U+2500 now draws a real 1px
hairline instead; a label that mixes it with real text ("--- Move to
Workspace ---", a deliberate section-header convention) still renders
as text, unchanged.
- "Select All" added to the bare-desktop menu, the one action every
mainstream desktop's own menu offers that this one lacked.
New tests needed real care: the panel's own rounded-corner distance
field softens alpha within its radius of any canvas edge, not just the
visible corners, so a naive full-row pixel scan against bg picked that
up as a false positive on the first attempt - fixed by scanning only
rows/columns confirmed (via a throwaway debug dump) to sit inside the
panel's genuinely flat interior.
Full workspace build/test/clippy clean, built and installed. Real
submenus and per-row icons remain real, separate scope - this
project's floating-menu UI has no nested-panel concept yet.
|
|
Reported live: "try move desktop items all at once somewhere else"
didn't work. Two compounding bugs, both real: CompState::
desktop_icon_drag only ever tracked one icon id, and the click handler
that starts a drag unconditionally collapsed any existing multi-
selection down to just the grabbed icon before the drag even began.
desktop_icon_drag is now Option<DesktopIconDrag> (crates/wayland/src/
desktop_icons.rs, new type): a grab offset, the grabbed icon's own live
position, and a members list - every currently-selected icon (the
grabbed one included), each a fixed offset from the grabbed icon's own
top-left at drag start, so the group moves as one rigid unit.
input/pointer.rs's click handler now only resets to single-selection
when the grabbed icon isn't already part of the current selection --
grabbing one inside an existing multi-selection keeps the whole group
selected and dragging, matching Windows/GNOME/macOS/KDE convention.
end_desktop_icon_drag snaps every dragged icon to its own nearest free
grid cell independently, tracking newly-claimed cells across the group
so two icons landing near each other never claim the same one.
Full workspace build/test/clippy clean, built and installed. Not unit-
testable (this module has no CompState test fixture for its own
selection/drag logic, an already-documented, accepted gap) - needs a
live drag to confirm.
|
|
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.
|
|
Requested directly: default icons "look very rudimentary... make slightly
blue as well, polished look".
Two changes to decoration::render_desktop_icon and its five draw_*_glyph
helpers:
- New fill_rounded_rect primitive: softly rounded corners (the same
clamp-then-distance smoothstep construction rounded_corners_pixman::
apply_corner_mask already established for content masking, not a new
technique) plus a vertical top-to-bottom gradient instead of one flat
fill - the same light-source cue buttons.rs's own glossy_shade uses
for the titlebar dots, as a plain linear gradient here. Applied to each
glyph's main body shape; small details (folder tab edge, computer
stand, trash ridges, home roof) stay flat/sharp.
- A dedicated ICON_COLOR constant (a clean mid-blue) instead of reading
theme.titlebar_fg_focused - that field is whatever the user's own
titlebar accent happens to be configured to, which could be any
colour; these glyphs want a consistent, recognisable blue palette of
their own, independent of theme.
Build/test/clippy already verified clean as part of the layer-shell scale
fix commit just before this one (same source tree, installed together).
|
|
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.
|
|
parent
Window::is_dialog (close-button-only titlebar, no traffic lights) was
only ever set from a native xdg_toplevel's own parent() - redraw_
decoration_buffer's is_dialog computation called dw.toplevel(), which
is always None for an XWayland-backed DWindow (X11Surface's own
accessor is x11_surface(), a different method), so the .unwrap_or(false)
fallback made every XWayland dialog - a GTK "Save As", an app's own
"About" box, anything setting the ICCCM transient-for hint - always
draw with the full three-button titlebar and traffic-light colours,
even though the feature this was built for explicitly wanted the
opposite. Documented as a known gap at the time; now closed.
redraw_decoration_buffer now also checks X11Surface::is_transient_for()
for an XWayland window. property_notify gained a WmWindowProperty::
TransientFor arm that re-runs redraw_decoration_buffer, for a client
that sets the hint slightly after its own initial map - the same
"read fresh every call" pattern the existing xdg_toplevel::parent()
check already relied on, extended to catch a late X11 property the way
the Wayland equivalent (set_parent, any time) already was.
|
|
carve_inner_corner_pixel (an earlier ring fix) cut the
border strip's own disk using an inner circle at the *same* centre as
the outer cut, radius - border_width - correct when a titlebar sits
underneath (it shares that exact circle, by construction), but wrong
whenever client content sits underneath instead: content's own
rounded-corner mask (rounded_corners_pixman.rs's apply_corner_mask) is
centred radius from *its own* buffer's edges, and that buffer starts
border_width rows/columns inside the border strip's - a same-radius
circle offset (border_width, border_width) diagonally from the
border's own outer one, not a smaller concentric one. Reusing the
titlebar-style ring left a real, if very small, gap along part of the
seam and a thin sliver of double coverage along the rest - reported
live, at extreme zoom against a solid-colour wallpaper (chosen
specifically to make a sub-pixel gap easy to spot against, unlike the
usual desktop image): "very tiny gaps... corner radius does not
match."
round_top_corners/round_bottom_corners's own `inner_radius: Option<u32>`
parameter is now `inner: Option<InnerRing>`, an explicit (center_row,
center_col, radius) rather than an implicit "same centre, smaller
radius" - InnerRing's own doc comment has the full geometry for both
cases. render_border_top gained a `decorated` parameter to pick the
right one (titlebar-aligned when true, content-aligned - centre
shifted by border_width on both axes, radius unchanged - when false);
render_border_bottom always uses the content-aligned ring, since this
compositor never draws a bottom titlebar.
apply_corner_mask (rounded_corners_pixman.rs) made pub(crate) so the
new regression test can build a real masked content buffer via the
actual production function, not a reimplementation of its math.
border_top_and_content_mask_have_no_gap_along_the_corner_diagonal_when_undecorated
checks coverage along the diagonal ray between the two circles'
centres - the direction they're actually offset along, and so the
worst case for a gap opening up between them - against the real
render_border_top/apply_corner_mask output, not a hand-rederived
formula.
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
Right-clicking (or hovering) the maximize button opens a flyout of
fixed half/quarter positions (snap_flyout.rs renders it); picking one
applies that zone via WindowManager::apply_snap_zone, the click-driven
equivalent of dragging a window to that edge and releasing.
|
|
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.
|
|
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.
|
|
~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 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.
|