| Age | Commit message (Collapse) | Author | Files | Lines |
|
Two independent pieces landed together this pass - both real, both
scoped, see docs/TODO.md for the full narrative on each:
Fake monitors: a genuinely independent, additional wl_output with no DRM
connector/CRTC behind it at all - distinct from srd.monitor.split
(divides one real output's own placement rectangle). Researched niri's
own Headless backend first (cloned at ~/reference-wms/niri): its render()
never actually composites anything, a no-render stub for that project's
test suite only. This one is real: it renders whatever is placed on it,
on demand, whenever a zwlr_screencopy_manager_v1 client asks for a frame.
New crates/wayland/src/udev/virtual_heads.rs (create/remove a real Output
+ global, render-on-demand for screencopy, integrated into platform.rs's
monitors() as a genuine srdwm_core::Monitor so core placement/workspace
code needs zero special-casing). New IPC/CLI: srd dispatch create
fake-monitor <name> <w>x<h> / remove fake-monitor <name>. Core-side
request queue in crates/core/src/manager/fake_monitor.rs.
Placement bug, root-caused and fixed: every new window opened alone
landed in the exact same spot, "not at all like Windows" (reported live).
SmartPlacement::place tried a grid cell first, and grid's own cell count
is existing.len() + 1 - with nothing else open (opening one app at a
time, the ordinary case), that's always 1, so a 1x1 grid returns the same
single cell forever regardless of session history. Cascade had the same
bug in a second form (its own step was existing.len() % max_steps, also
always 0 with nothing open). Fixed: WindowManager::next_cascade_step (a
Cell - add_window's own target_monitor stays borrowed across the call)
advances on every real placement and is never reset by a window closing;
place() now skips grid entirely when nothing else is open, going
straight to cascade, since grid's real job (dividing space among
concurrent windows) has nothing to divide when there's no concurrency.
Full workspace build/test/clippy clean (223 core / 141 wayland / 29
platform / 24 ctl / 28 config / 10 x11 tests, 0 failed, 0 clippy
warnings), built and installed.
|
|
Several independent, real pieces landed in crates/core this shift - see
docs/TODO.md for each one's full root-cause/verification narrative:
- "Primary" monitor is now picked by which head sits at physical (0, 0)
(the user's own configured anchor), not whichever connector DRM
happened to probe first - fixes desktop icons and new-window placement
landing on the wrong monitor depending on hotplug/probe order.
- A new window's target monitor now prioritizes the pointer's own current
monitor over the last-focused window's monitor, which goes stale the
moment the user's attention moves to empty desktop, a panel, or a dock.
- aspect_ratio window-rule action ("W:H") plus ResizeEdge::apply_aspect_
ratio: holds a floating window's aspect ratio through an interactive
resize. The real, scoped "phone monitor" primitive - matches any VM/
emulator/scrcpy window by app_id, nothing Android- or VM-specific here.
- general.phone_mode (WindowManager::phone_mode): a new window defaults
to maximized instead of floating/tiled small, unless a rule explicitly
floats it or sets maximized - the one placement default a phone-shaped
screen actually needs. Exposed read-only via IPC so a shell panel can
adapt its own chrome to the same signal.
- input_pin.rs: the core half of pinning a virtual pointer to a specific
window (Multi-cursor Phase 2) - a backend-agnostic request queue,
same cross-boundary shape output_position_requests/lock_requested
already use, since core has no real Wayland protocol object to reach
into itself.
Full workspace test suite covers all of the above (aspect-ratio resize
math for every edge case, phone-mode default-vs-rule-override behavior,
the pin-input request queue, the monitor-picking fixes).
|
|
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.
|
|
mid-gesture
Reported live: moving a window onto the other monitor "looks very messed
up". This machine's two real monitors have genuinely different scales
(eDP-1 at 1.0, HDMI-A-1 at ~0.843) - the exact condition needed to expose
this.
WindowManager::update_drag/update_resize only corrected w.monitor once,
at end_drag (update_resize never corrected it at all, not even at the
end) - but state/geometry.rs::sync_geometry reads that field on every
motion tick to pick which monitor's scale converts the client's physical
size into the logical points xdg_toplevel::configure sends it. Crossing
onto a different-scale monitor mid-drag kept every configure computed
against the origin monitor's stale scale for the gesture's whole
remaining duration, only self-correcting once the button came up.
Both functions now re-derive w.monitor from which monitor the window's
live geometry actually overlaps, every motion tick - the same
Rect::overlaps lookup end_drag already used once at the end, now run
continuously instead. end_drag's own fixup stays as a final-word safety
net for a drag that starts and ends between two motion ticks.
Does not close the related, already-documented gap where a client that
doesn't speak wp-fractional-scale-v1 still mismatches once settled on a
sub-1.0-scaled monitor - this only fixes the stale-during-the-gesture
half.
Two new tests, full workspace suite and clippy clean.
|
|
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.
|
|
Reported live: "even where decorations are corner i should still be able
to corner resize, just its... hitbox... does not get in the way of the
close icon" - the titlebar corner holding Close/Maximize/Minimize had no
resize target at all, by design (competing with the close button was
judged worse than losing that one corner). The BUTTON_CLUSTER_MARGIN
strip between the button cluster and the frame's true edge was already
dead space no button claims, regardless of button_count - its own top
DECORATED_TOP_RESIZE_MARGIN rows now register as the diagonal corner
(TopLeft/TopRight) instead of falling through to Top/Drag, without
touching the button's own hitbox at all.
Also requested: widen the other three corners' own resize zone, since a
user reaching for a plain edge-resize instinctively aims for the middle
of that edge, not its corner - a bigger corner zone doesn't compete with
that instinct the way a bigger RESIZE_MARGIN would compete with ordinary
content clicks near an edge. CORNER_MARGIN raised from 3 to 5 (18px to
30px at the default resize_margin).
Updated one existing test whose own per-window resize_margin override
(30px) now put its plain-edge test point inside the widened corner zone
on a window too short for the two to stay apart - taller geometry, same
edge point relative to center, no change to what it actually verifies.
Added coverage for the new button-corner resize target on both sides,
and for the dead strip's own non-corner rows still just dragging as
before.
|
|
SRDWM_GPU=1 was the only way to opt into the udev backend's GBM+EGL
GPU render path - an env var, not a real config option, with no way
to enable it from init.lua the way every other general.* flag works.
WindowManager::gpu_enabled (plain bool, false by default - unlike
rounded_corners_enabled's Option<bool>, GPU rendering has one
unambiguous default regardless of which backend ends up connecting,
so there's no "let the backend decide" case to preserve) is read from
general.gpu in apply_general_settings, same as every other general.*
key. gpu::probe now takes an explicit enabled: bool instead of
checking the env var itself; udev/platform.rs's call site computes it
as wm.gpu_enabled OR SRDWM_GPU=1, so the env var still works as a
quick manual override for testing without touching config, on top of
the new persistent option.
Falls back to the existing software (Pixman) path exactly as before
on any failure at any step (no GBM device, no atomic-modesetting
support, a software-only EGL renderer, ...) - gpu::probe's own
fallback behavior is unchanged, only how the initial enabled/disabled
decision gets made.
|
|
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.
|
|
sync()'s per-tick platform.focus(id) re-assertion (added to keep real
Wayland/X11 keyboard focus following core's own bookkeeping) ran
unconditionally on every dirty tick, including when nothing about focus
had actually changed. focus_window (core) has its own, separate side
effect of switching to the focused window's workspace when it differs
from the current one - correct when focus genuinely moves to a window
on another workspace, but this call was never gated on focus having
changed at all: switching workspace via activate_workspace left the
still-focused window's own workspace field untouched, so the very next
dirty tick's blind re-assertion of that same focus saw a mismatch against
the just-changed current_workspace and switched straight back.
Confirmed live via temporary core-side logging: two switch_workspace
calls a few milliseconds apart, the second one undoing the first every
single time, for every workspace switch that didn't also change which
window was focused.
Gated the re-assertion on the focused id actually changing since the
last sync() call. Real focus-follows-real-platform-focus still happens
on every genuine change, which is all the original fix needed.
|
|
1. A popup's xdg_popup.grab (Firefox's own right-click menu, concretely)
receiving zero pointer input at all - no hover highlight, no click
effect, not even dismiss-on-miss - logs whether grab_popup actually
succeeds, since a silent failure there would explain exactly this.
2. srd dispatch activate_workspace returning {"ok":true} without ever
changing the current workspace, confirmed via a raw socket request
bypassing the CLI entirely. switch_workspace's own logic reads
correct; logs its actual inputs/state to find out why the real
process disagrees with it.
Remove once both are resolved.
|
|
focus_window marked the target focused without clearing minimized, so
a dock icon's Activate (or the plain "focus" IPC command) on a
minimized window left it focused but still excluded from
visible_windows/rendering - reads exactly like the click did nothing,
since the window never actually reappears. Every focus_window caller
gets the restore for free now. Added a Super+n keybind for minimize
too, since none existed at all before this.
|
|
test coverage
Bundles several related changes to crates/core built up over this
session rather than committed incrementally:
- WindowManager::request_capture_workspace/drain_capture_requests (new
manager/capture.rs) - backend-agnostic queuing for an off-screen
workspace render, see the wayland-side commit for why this exists.
- focus_window now switches workspace as a side effect when the target
isn't on the current one, matching Hyprland's focuswindow convention
(manager/focus.rs).
- Assorted window/rules/theme field additions and their test coverage.
Left less granular than the repo's usual one-purpose-per-commit
convention deliberately: these accumulated across a long session
without being committed as they landed, and are too entangled
line-by-line to safely split apart now without risking mis-attributing
changes to the wrong commit message.
|
|
toggle_maximize previously targeted full_geometry outright (past every
reserved zone), on an earlier request specifically about the dock -
which also silently let it extend behind a top bar's zone, reported
back as its own bug once live-tested. maximize_geometry is a third
rect distinct from geometry (every zone) and full_geometry (none):
full_geometry with only a top-anchored bar's exclusive zone subtracted
back out. New test locks in dock-covered/bar-respected together.
|
|
A real ext-session-lock-v1 implementation drawn by the compositor
itself, not a hand-off to an external locker process: a live-blurred
background, PAM authentication on a background thread, and its own
config surface (lock_config.rs) rather than hardcoded appearance.
|
|
Auditing "clicking behavior and basics": general.focus_follows_mouse,
general.mouse_follows_focus, general.auto_raise, general.auto_focus,
the entire window.* namespace (8 more keys, a full duplicate of the
same four plus remember_position/size/state), and general.
smart_placement/border_width were all seeded into default_config() and
documented in DEFAULTS.md, but none were read anywhere - srd.set()/
srd.get() on any of them silently succeeded while doing nothing.
focus_follows_mouse is real, well-defined, and directly relevant to
clicking basics - implemented it plus auto_raise (raise, not just
focus, on hover) rather than just deleting the promise like the
others. WindowManager gained focus_follows_mouse/auto_raise bools,
wired from apply_general_settings the same way every other general.*
flag is. handle_pointer_position now tracks whichever window (content
or decoration) is under the pointer and, when the setting is on and
that differs from the currently-focused window, focuses it through the
same focus_window() free function every click-driven focus change
already uses (real keyboard focus, not just core state) - skipped
entirely while dragging/resizing or over a layer-shell surface, so the
pointer sweeping over other windows mid-drag or hovering a bar can't
steal focus from what's actually being manipulated.
mouse_follows_focus (pointer warp on keybinding-driven focus change)
and auto_focus (no clear distinct meaning beyond click-to-focus) stay
unimplemented and are now undocumented rather than promised.
|
|
MISSING.md had listed this as needing to touch srdwm_core::window::
hit_test's signature at every call site, including the honest-stub
Windows/macOS backends - overstated on closer inspection: that shared
function already takes a plain resize_margin: i32 parameter, agnostic
to where the value comes from. The only real change needed was reading
Window.resize_margin.unwrap_or(wm-wide default) instead of always the
WM-wide value, at the single call site inside WindowManager::hit_test
in core - no backend touched at all.
Window.resize_margin: Option<i32>, WindowRuleActions.resize_margin to
match, applied in add_window/reapply_rules_if_pending the same way
opacity already is. Settable via a rule action or
srd.window.set_resize_margin(n) on the focused window.
|
|
visible_windows' doc comment claimed windows show "on the active
workspace of whichever monitor they're assigned to" - the code never
reads w.monitor at all; current_workspace is one flat value shared by
every monitor, not per-output. Documented that explicitly on both the
field and the method, since this is a real behavioral difference from
Hyprland worth a reader actually seeing, not just an inaccurate comment
to fix quietly.
monitor.primary_workspace/monitor.workspace_count describe a per-
monitor-workspace design that doesn't exist; workspace.auto_switch/
workspace.persistent were never wired to any behavior. All four were
seeded into default_config() and documented in DEFAULTS.md, so
srd.set()/srd.get() on them silently succeeded while doing nothing --
removed from both, matching the precedent already set by general.
rounded_corners' deliberate absence from default_config for a
different reason (backend-dependent default rather than unbuilt).
|
|
WindowManager::hit_test/window_at filtered only by `!w.minimized`,
never by workspace - but a window on a workspace that isn't current
is not minimized, it's just not shown. Rendering (visible_windows/
visible_windows_front_to_back) already restricted to the current
workspace; hit-testing didn't, so a click landing on where an
invisible window's stale on-screen geometry happened to sit routed to
that window instead of whatever was actually visible underneath.
Reported live. Fixed by adding the same workspace check rendering
already uses, plus a regression test with two identically-positioned
windows on different workspaces.
|
|
Pure reorganization, no behavior change - verified by diffing the
function-name set before/after (identical 130 functions) plus a full
cargo test pass. WindowManager's struct/field definitions, Default,
new(), and the three trivial constructors (add_rule/register_layout/
available_layouts) stay in mod.rs; the rest of the single ~950-line
impl block is split into one file per the section comments the file
already had (monitors, windows, focus, winops, hittest, dragresize,
workspaces, layout). Three methods called across section boundaries
(monitor_for, windows_on_workspace, cycle_focus) went from private to
pub(super) - Rust's privacy model doesn't let sibling submodules see
each other's private items, only a defining module's own descendants.
The ~1000-line test module moves to manager/tests.rs unsplit: its
helpers (wm_with_monitor, two_monitors, monitor_with_dock) are shared
across tests for every section, so splitting further would mean
duplicating them or adding another shared-support file for little
benefit.
|