srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src
AgeCommit message (Collapse)AuthorFilesLines
2025-09-26Fake (headless) monitors, and fix new windows all opening in one spotsrdusr5-23/+221
Two independent pieces landed together this pass - both real, both scoped, see docs/TODO.md for the full narrative on each: Fake monitors: a genuinely independent, additional wl_output with no DRM connector/CRTC behind it at all - distinct from srd.monitor.split (divides one real output's own placement rectangle). Researched niri's own Headless backend first (cloned at ~/reference-wms/niri): its render() never actually composites anything, a no-render stub for that project's test suite only. This one is real: it renders whatever is placed on it, on demand, whenever a zwlr_screencopy_manager_v1 client asks for a frame. New crates/wayland/src/udev/virtual_heads.rs (create/remove a real Output + global, render-on-demand for screencopy, integrated into platform.rs's monitors() as a genuine srdwm_core::Monitor so core placement/workspace code needs zero special-casing). New IPC/CLI: srd dispatch create fake-monitor <name> <w>x<h> / remove fake-monitor <name>. Core-side request queue in crates/core/src/manager/fake_monitor.rs. Placement bug, root-caused and fixed: every new window opened alone landed in the exact same spot, "not at all like Windows" (reported live). SmartPlacement::place tried a grid cell first, and grid's own cell count is existing.len() + 1 - with nothing else open (opening one app at a time, the ordinary case), that's always 1, so a 1x1 grid returns the same single cell forever regardless of session history. Cascade had the same bug in a second form (its own step was existing.len() % max_steps, also always 0 with nothing open). Fixed: WindowManager::next_cascade_step (a Cell - add_window's own target_monitor stays borrowed across the call) advances on every real placement and is never reset by a window closing; place() now skips grid entirely when nothing else is open, going straight to cascade, since grid's real job (dividing space among concurrent windows) has nothing to divide when there's no concurrency. Full workspace build/test/clippy clean (223 core / 141 wayland / 29 platform / 24 ctl / 28 config / 10 x11 tests, 0 failed, 0 clippy warnings), built and installed.
2025-08-29Core window manager: real fixes plus three new rule/placement primitivessrdusr7-61/+559
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).
2025-08-25X11 backend: right-click titlebar window menu, matching Wayland's ownsrdusr2-0/+197
Closes the one real gap an X11/Wayland feature-parity audit found this session (desktop icons, window-position memory, and static exclusive-zone reservation were already shared or Wayland-only by nature - see docs/TODO.md's own audit entry for the full breakdown). MenuAction/ContextMenu (row set, labels, row_at hit-testing) move from crates/wayland/src/context_menu.rs into crates/core/src/context_menu.rs -- pure state and geometry with nothing Wayland-specific in it, so X11 needing the same rows is shared data, not duplicated logic. The Wayland crate's own context_menu.rs is now a one-line re-export so every existing crate::context_menu::... call site keeps working unchanged. X11 has no compositor-level input dispatch to intercept every click the way Wayland's input/pointer.rs does, so the X11 side (crates/x11/src/platform/context_menu.rs, new) draws the menu into its own small override-redirect popup window and grabs the pointer for the duration so a click anywhere dismisses it, matching the Wayland backend's own convention. events.rs's ButtonPress handler now reads the real button number instead of hardcoding every press as a left click - a real latent bug (right-clicking a titlebar button would have silently performed its left-click action). Live-verified end to end in an isolated Xvfb + srdwm --x11 instance: full row set including the workspace picker, Minimize runs and closes the menu, a second window's menu dismisses cleanly on outside click, normal focus/click behaviour continues working afterward. See docs/TODO.md for the full investigation and verification narrative.
2025-08-17Fix desktop icons v1 regressions: bar overlap, wrong order; add proper menussrdusr1-11/+7
Live testing found v1 genuinely broken, not just rough: 1. Icons weren't rendering reliably at all - ensure_desktop_icons only ever computed the grid's origin once, on whichever render pass happened to be first. AGS's own top bar registers its exclusive zone after that first pass, so origin got permanently baked in at the pre-bar geometry. Confirmed live via a temporary diagnostic log. Fixed by re-deriving origin from the primary monitor's current geometry on every call instead of just the first. 2. Fixed icons (Home/Computer/Trash) always sorted before real files -- confirmed wrong via direct question. The whole list now sorts alphabetically by label, case-insensitive, fixed icons included. 3. "Set as Wallpaper" was the wrong feature: removed entirely (DesktopMenuAction::SetWallpaper, general.wallpaper_command, is_image_path). The user wants that handled by their real file manager once opened, not reimplemented here. Also adds real menu functionality per "where are all the options": Rename (inline text edit, new CompState::renaming_icon field and keyboard redirect mirroring NativeLock::password's existing precedent), Delete (moves to ~/.local/share/Trash per the freedesktop.org spec, new trash.rs module, same-filesystem case, no confirmation - this is the reversible move-to-trash, not a permanent delete), Empty Trash on the Trash icon, and Open Terminal Here / Open in File Manager on the bare-desktop menu (new general.terminal config key). 133 wayland-crate tests (up from 106), full workspace build and clippy clean.
2025-08-15Fix a dragged/resized window rendering wrong on a different-scale monitor ↵srdusr2-11/+91
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.
2025-08-15Add real desktop icons plus right-click desktop/icon context menussrdusr1-0/+39
Closes "right-click on bare desktop" - previously a true no-op, nothing rendered above the wallpaper at all. Requested directly: a real desktop "just like windows does" - Home/Computer/Trash plus one icon per real ~/Desktop entry, individually draggable with persisted grid positions, double-click to open, right-click menus (per-icon "Open" plus "Set as Wallpaper" for image files when general.wallpaper_command is set; bare desktop "New Folder"/"Refresh"). Architecture mirrors the existing context_menu.rs/snap_flyout.rs "compositor-owned floating UI" pattern: desktop_icons.rs (data model, grid layout, filesystem scan, hit-test), desktop_icons_state.rs (JSON persistence, same shape as monitor_layout.rs), desktop_menu.rs (the new right-click menu, reusing decoration::render_context_menu's existing rasterizer), state/desktop_icons.rs (CompState glue: rescan/select/drag/ open/persist), and a new decoration::render_desktop_icon rasterizer -- hand-drawn glyphs, since no PNG/SVG decoding capability exists anywhere in this workspace. Wired into both render loops (udev and winit) above the wallpaper and below every window, and into input/pointer.rs's button/ motion handlers for selection, drag, double-click, and both menus. Four new config keys: general.desktop_icons (default true - a directly requested, purely visual feature, unlike the opt-in-while-experimental general.gpu), general.file_manager, general.desktop_icon_single_click, general.wallpaper_command (all default off/empty). Deliberately out of scope for this pass, stated up front: move-to-trash and "Empty Trash" (destructive, no confirmation-dialog primitive to gate them on yet), filesystem watching, multi-select, per-mimetype icon art, icons on any monitor but the primary one. 124 wayland-crate tests (up from 106), full workspace build and clippy clean.
2025-08-10Make corner resize reachable at the button corner, and widen it everywhere elsesrdusr2-4/+86
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.
2025-05-30Detect XWayland dialogs via WM_TRANSIENT_FOR, not just native xdg_toplevel ↵srdusr1-9/+8
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.
2025-05-28Add a real general.gpu config option for the GPU render pathsrdusr1-0/+18
SRDWM_GPU=1 was the only way to opt into the udev backend's GBM+EGL GPU render path - an env var, not a real config option, with no way to enable it from init.lua the way every other general.* flag works. WindowManager::gpu_enabled (plain bool, false by default - unlike rounded_corners_enabled's Option<bool>, GPU rendering has one unambiguous default regardless of which backend ends up connecting, so there's no "let the backend decide" case to preserve) is read from general.gpu in apply_general_settings, same as every other general.* key. gpu::probe now takes an explicit enabled: bool instead of checking the env var itself; udev/platform.rs's call site computes it as wm.gpu_enabled OR SRDWM_GPU=1, so the env var still works as a quick manual override for testing without touching config, on top of the new persistent option. Falls back to the existing software (Pixman) path exactly as before on any failure at any step (no GBM device, no atomic-modesetting support, a software-only EGL renderer, ...) - gpu::probe's own fallback behavior is unchanged, only how the initial enabled/disabled decision gets made.
2025-02-15Checkpoint: preserve all uncommitted rust-rewrite worktree worksrdusr11-118/+1922
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
2025-02-14Fix workspace switches undoing themselves within millisecondssrdusr1-8/+1
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.
2025-02-11Add temporary diagnostics for two live-reproduced bugssrdusr1-1/+8
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.
2025-02-03Fix focusing a minimized window not restoring itsrdusr2-0/+37
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.
2024-11-30Accumulate core-crate additions: capture requests, focus/workspace fixes, ↵srdusr10-30/+533
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.
2024-11-26Add Monitor::maximize_geometry: maximize covers a dock, still stops at a top barsrdusr3-3/+64
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.
2024-11-01Add Snap-Layouts flyout (Windows 11-style maximize-button hover menu)srdusr1-0/+102
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.
2024-10-31Add global-menu support (dbusmenu/appmenu) for Wayland and X11 clientssrdusr1-5/+106
Exposes each window's application menu (Firefox/GTK's dbusmenu export, X11's _GTK_APPLICATION_OBJECT_PATH-style menus via global_menu.rs) so an external panel can render it as a system menu bar rather than each window drawing its own, the same convention appmenu.rs/gtk_shell.rs and appmenu_registrar.rs wire up across both backends.
2024-10-30Add srdwm's own native session-lock screen (blur, PAM auth)srdusr2-0/+105
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.
2024-08-24Implement general.focus_follows_mouse/auto_raise; remove the rest as deadsrdusr1-0/+13
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.
2024-08-22Add per-window resize-margin override (Hyprland's extend_border_grab_area)srdusr5-1/+34
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.
2024-08-20Give corner resize real priority over a plain edgesrdusr1-11/+118
Reported live, repeatedly: corners felt like they had no priority over sides. They didn't, structurally - a corner only ever registered in the exact pixel square where both edges' own resize_margin zones happened to overlap (6x6px at the default 6px margin), nothing wider. A click a little further along either axis, still clearly aiming for the corner, fell back to a single straight-edge resize instead. resize_edge_at now checks a separate, wider corner zone (CORNER_MARGIN * resize_margin) first, so a corner claims a real, deliberately larger target the way GNOME/KDE already do - diagonal resize is a harder grab than a straight edge and deserves more room, not the same or less. Also fixed a related, previously-unreachable case: a *decorated* window's whole titlebar band returned Drag/Close/Maximize/Minimize unconditionally, so top-left/top-right diagonal resize never had a code path at all, even at the titlebar's own corner pixels. Added top-left resize as a small, genuine corner square (both axes, not just one) checked before drag/buttons. Deliberately did NOT add the matching top-right case: that corner is the close button on every mainstream desktop, and trading a well-known, expected target for a rarely-wanted one at exactly the spot a miss costs the most isn't a trade worth making. Four new regression tests cover: reaching a corner past the old tight margin, falling back to a plain edge just past the new wider one, the newly-reachable decorated top-left corner, and confirming top-right still closes rather than competing with resize.
2024-08-09Fix a misleading workspace comment and drop four dead config keyssrdusr2-1/+11
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).
2024-08-08Fix clicks landing on a window hidden on another workspacesrdusr2-5/+39
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.
2024-07-14Fix keybindings silently never firing when a named key is lowercasesrdusr1-2/+46
canonicalize_key_combo already reordered multi-modifier combos into dispatch's canonical Ctrl/Shift/Alt/Mod4 order, but passed the key name through verbatim. keysyms::keysym_to_name capitalizes every named key ("Space", "Return", "Escape", "BackSpace", ...) while leaving letters/digits alone, so srd.bind("Super+space", ...) stored "Mod4+space" while a real Space keypress dispatches as "Mod4+Space" -- never matching. Accepted silently at config-load time, so the only live symptom was the bind's own callback never running at all. Root-caused live: keybindings.lua's Super+space bind had a temporary diagnostic added (logs to /tmp/superspace.log before spawning ags) to tell "key never fired" apart from "key fired but ags failed" - the log file never existed, meaning the callback itself never ran. Fix: round-trip the key name through name_to_keysym (already case- insensitive) and back through keysym_to_name before storing, so any case the config writes normalizes to dispatch's canonical form.
2024-07-11Split crates/core/src/manager.rs (2048 lines) into manager/srdusr11-2048/+2128
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.
2024-07-09Rounded corners on udev/Pixman backend, opt-in and off by defaultsrdusr1-5/+10
CPU-side rounded corners for the software-only udev/Pixman renderer, which has no shader stage to hook the existing GLES version into. Reads a window's own committed wl_shm buffer, punches premultiplied- alpha holes into the four corner regions, and hands the masked copy to MemoryRenderBuffer - the same path already used for titlebar/ border/shadow bitmaps, so it composites through the ordinary unmasked path and the corners genuinely disappear rather than being painted over. Cached per window, invalidated by a per-commit content_epoch counter rather than rebuilt every frame, so an idle window costs nothing once masked. general.rounded_corners now defaults per backend instead of one global true: on for GLES/winit (a real GPU shader, no measurable cost), off for udev/Pixman (an untested-on-real-hardware CPU cost for constantly-repainting clients) - WindowManager.rounded_corners_enabled is Option<bool> so the backend can tell "unset" from "explicitly off".
2024-06-24Real rounded corners on client content (GLES/winit backend), via a custom shadersrdusr1-0/+7
decoration.rs's round_top_corners only ever clipped the compositor's own titlebar/border bitmap - its own doc comment already said why nothing more had been done: clipping arbitrary client content needs a real per-pixel mask, "a much bigger change than this cosmetic pass". That's this change, for the one backend that can do it cheaply: the udev backend's PixmanRenderer is software-only with no shader stage at all, but GlesRenderer (winit) has a real custom-shader path (`compile_custom_texture_shader`, `TextureShaderElement`) that a first look at smithay's higher-level convenience APIs missed entirely. crates/wayland/src/rounded_corners.rs: a GLSL fragment shader masking a window's texture against a rounded-rect signed-distance field while sampling it (the same technique cosmic-comp/niri use for GPU-side rounded corners) - built by hand from the surface's own committed texture/view/ damage state (`RendererSurfaceState`'s public accessors), since no smithay convenience wrapper builds a masked element at all (`CropRenderElement` only crops to a rectangle). A decorated window rounds only its bottom two corners - the top two are already rounded, on the titlebar's own CPU bitmap, by decoration.rs, at the exact same `CORNER_RADIUS` (now `pub(crate)`, shared between the two so the curve reads as one continuous radius, not two different ones meeting at a seam) - an undecorated/CSD window rounds all four, since its content is the window's whole visible extent. Falls back to plain unrounded content on any failure (shader didn't compile, no committed buffer yet, a single-pixel-buffer surface), same "always show something over a prettier maybe-nothing" reasoning cursor.rs's built-in-arrow fallback already uses. Deliberately scoped to a window's *main* surface only, not subsurfaces - documented as a real, if narrow, follow-up rather than attempted here. `TextureShaderElement` only implements `RenderElement<GlesRenderer>`, not the generic `RenderElement<R>` every `OverlayElement<R>` variant needs, so it can't be added to that shared enum without breaking `OverlayElement< PixmanRenderer>` (used identically by udev.rs) the moment a GLES-only variant showed up in it. `WinitElement<=GlesRenderer>` (new, winit.rs-only) wraps the existing enum as one variant instead of touching it - this is the same lesson as the fullscreen-hiding investigation earlier this session, just resolved cleanly this time: nesting a *foreign* generic type inside your own hits real bound-resolution walls; wrapping your own already-working type inside a new concrete-renderer enum doesn't, because smithay's own `render_elements!` macro documents exactly this `<=ConcreteRenderer>` form. Config: `general.rounded_corners` (default `true`), `srd.window` unaffected - this is a `general.*` compositor-behavior knob, not a per-window rule action like `opacity`. Verified live: shader compiles without error on this machine's real Mesa/ llvmpipe GL driver, and a decorated wezterm window's bottom-left and bottom-right corners both show a real, smoothly anti-aliased curve on the actual client-rendered pixels (not a compositor bitmap) in a host-session screenshot - qualitatively sharper than `round_top_corners`' deliberate hard cutoff, since a GPU shader can afford a ~2px smoothstep a CPU bitmap pass isn't worth adding for. cargo build --workspace (all 9 crates), cargo clippy --workspace (0 new warnings), cargo test --workspace (197 tests, 0 failed).
2024-06-22Bypass smithay's Space rendering for window/layer content: real per-window ↵srdusr3-0/+41
opacity The user asked for per-window opacity (MISSING.md's `windowrule = opacity` gap) and pushed back on treating smithay's convenience wrappers as a hard ceiling: "don't rely on smithay, it won't have everything we need." Looked again at why opacity was ruled out earlier - `render_output`/ `space_render_elements` take one `alpha` for the whole frame's `self.space` content, no per-element control - and found a path that doesn't need nesting smithay's internal `SpaceRenderElements` type (the approach that hit an unresolvable generic-bounds wall investigating fullscreen-hiding earlier): call `render_elements_from_surface_tree` directly, once per window and once per layer-shell surface, each with its own alpha, wrapping the result in the *existing* `OverlayElement::Surface` variant. That primitive was already proven safe in this codebase (cursor.rs's client-image path, this file's own popup rendering) - reusing it here for a window's main content is the same call, not a new one. Both `udev.rs` and `winit.rs`'s render loops now build window content and layer-shell surfaces themselves (`elements.rs`: `surface_content_elements`, `output_layer_elements`, `window_wl_surface` for the Wayland/XWayland split), in the correct front-to-back order, then call `OutputDamageTracker::render_output` directly instead of the `space::render_output`/`space_render_elements` convenience wrappers. Content itself needs no occlusion clipping against `occluders` (unlike border/ titlebar bitmaps) - pushed in the same front-to-back order as everything else, ordinary painter's-algorithm draw order already occludes it correctly, the same property it had via `self.space`'s own order before. `self.space` stays mapped and `resync_stacking_order`-maintained exactly as before; only the render step stopped reading from it. A comment in winit.rs's render loop warned that a near-identical earlier attempt was reverted for a real ordering bug (whichever window was created first always painted in front, regardless of focus). That bug's actual root cause, identified and fixed since, was `Space::map_element` silently re-stacking on every geometry sync, independent of which render path was used - see `resync_stacking_order`. This rewrite never reads `Space`'s internal order for rendering at all (`ids` comes from `WindowManager.order` directly, the same source `hit_test` already trusts), so that specific bug class can't recur here regardless of whether `resync_stacking_order` ever drifts again. Bonus from the same infrastructure: the bar/dock now genuinely don't render at all (not just get covered) for a fullscreen window - `output_layer_elements` skips `Layer::Top`/`Overlay` entirely when any visible window is fullscreen, the hardening this work backed away from earlier for being too risky to build via the nested-SpaceRenderElements approach. `capture_offscreen` (winit.rs's screencopy path) picked up opacity-aware content and layer-shell inclusion too, though not full parity with the on-screen loop (still no border/shadow strips there - a pre-existing, separately-flagged gap). Opacity itself: `Window.opacity` (core), `WindowRuleActions.opacity` / `srd.rule(..., { opacity = 0.9 })`, `srd.window.set_opacity()`. Caught live, before commit: opacity was wired into `add_window`'s own rule match but not `reapply_rules_if_pending` - the *only* path a class-based rule actually takes effect through for a native Wayland client, since `add_window`'s own attempt always runs against a still-empty `app_id` (see the regression test next to the existing one covering the identical historical bug for `decorated`). Found by setting an isolated `SRDWM_CONFIG_PATH` test config with `srd.rule({ class = "Alacritty" }, { opacity = 0.4 })` against a nested instance and pixel-sampling a real screenshot: predicted blend (242,230,53) at 0.4 over (10,10,15) is (103,98,30); measured (105,100,33). Verified live in a nested session, screenshotting the *host* compositor (shows the real on-screen render, unlike grim against the nested socket, which - separately discovered this work - routes through `capture_offscreen`): stacking order correct with two overlapping windows (topmost fully occludes the one behind it, the exact scenario the reverted attempt got wrong), opacity blend matches prediction. Also confirmed, by testing the previous commit against the same scene, that upside-down content on this backend is a pre-existing bug unrelated to this change -- noted, not fixed here. cargo build --workspace (all 9 crates), cargo clippy --workspace (0 new warnings), cargo test --workspace (197 tests, 0 failed, includes 2 new regression tests).
2024-06-20Add drop shadows and shrink the resize grab margin that was eating content ↵srdusr2-26/+47
clicks Two independent daily-driving gaps closed in one pass, both from MISSING.md and live user feedback: Drop shadows (general.shadows, default true). Reuses the exact "bitmap drawn outside geometry, cached like the border" technique border_strips/ render_border_top already established - decoration::shadow_bitmap rasterizes a linear alpha falloff (Chebyshev/square-ring distance, not a true blur -- no blur primitive exists without a GPU shader, and the udev backend's PixmanRenderer is software-only) from SHADOW_MAX_ALPHA (90/255, deliberately subtle) at the window's own edge down to fully transparent SHADOW_SIZE (12px) out. Cached in CompState::shadow_buffers, rebuilt at the same trigger points as border_top_decorations (redraw_decoration_buffer), for the identical damage-tracking reason: a fresh Id every frame means OutputDamageTracker never finds a previous-frame match. No shadow for a maximized or fullscreen window, matching the Hyprland/GNOME convention MISSING.md measures against. Resize grab margin: 10px -> 6px (general.resize_margin, now configurable, same call-site-count-preserving change as threading a new parameter through one indirection point: ResizeEdge::hit_test's only production caller is WindowManager::hit_test, so this didn't need touching every backend despite hit_test being shared verbatim across X11/Wayland/Windows/macOS). Reported live: ordinary clicks near any window edge - a link near a browser's edge, a button near a panel's edge - regularly registered as a resize-edge grab instead of reaching the client, not just an occasional near-miss, because the 10px band was measured inward from the client's own content rect. 6px stays comfortably grabbable while giving content back most of its edge. Verified: cargo build --workspace (all 9 crates including the windows/macos stub backends), cargo clippy --workspace (0 new warnings), cargo test across core/wayland/config/x11 (188 tests, 0 failed). Shadow rendering verified at the render-element level live in a nested session (correct geometry, alpha, buffer contents) - grim/screencopy itself turned out to route through winit.rs's separate capture_offscreen path, which only ever drew `decorations` (titlebars), never borders or shadows, so screenshots taken this way have never shown either; a real gap, not fixed in this pass.
2024-05-30Fix decoration drift during animated transitions; checkpoint ↵srdusr10-33/+1405
IPC/global-menu/output-management work Border/titlebar decoration was built from Window.geometry (the animation's final target) in both wayland backends' render loops, while sync_geometry already draws a window's actual content at window_anims' interpolated rect during any maximize/fullscreen/open-slide tween. Border and content read two different rectangles for the whole transition, so the border visibly detached from the window it was outlining - reported as "borders aren't flush." Both udev.rs and winit.rs now read the same animated rect for titlebar placement, border-strip placement, and the occlusion test against later windows in stacking order. Verified: cargo build --workspace, cargo clippy (0 new warnings), cargo test -p srdwm-core (111/111). Also checkpoints substantial protocol/IPC work from prior sessions that had accumulated uncommitted: gtk-shell1 support (gtk_shell.rs, the vendored gtk-shell.xml, xwayland.rs's X11-side mirror) backing the global app menu; zwlr_foreign_toplevel_manager_v1 (foreign_toplevel.rs) broadcasting distinct maximized/minimized/fullscreen/activated state per window; output_management (ext-output-management + layer-shell exclusive-zone reservation tracking); workspace.rs and context_menu.rs; a Unix-socket IPC crate (platform/src/ ipc.rs) and a `srd` control-CLI crate (crates/ctl); xkb_config.rs; and a theme module (core/src/theme.rs). A peer session working the AGS shell concurrently verified several of these live against a running srdwm: the global menu rendering a real app's File/Edit menu over gtk-shell1, and foreign-toplevel correctly reporting maximized and fullscreen as independent, non-simultaneous states with the geometry each implies (maximize stops at a reserved top bar and past a dock; fullscreen reaches the true monitor edge).
2024-05-29Config at ~/.config/srd; cursor shapes, key repeat, pin, mouse defaultssrdusr3-2/+100
Config path drops a level: ~/.config/srd, not ~/.config/srdwm/srd, which said the same thing twice. No other user-facing path had the same problem -- srdwm reads the config dir and writes nothing else. Cursor shapes. A client's own cursor surface is now rendered with the hotspot it declared, so an I-beam over text or a hand over a link shows the app's image instead of srdwm's arrow. The built-in arrow stays as the fallback when no client has set one, over decorations and the desktop. Named shapes still fall back to the arrow; most toolkits set a surface. Decorations and cursors now share one OverlayElement type, since render_output takes a single custom-element slice. Key repeat (srd.bind_repeat, Hyprland's binde). Held volume, brightness and switcher keys repeat at the seat's own rate rather than firing once. Driven from the poll loop, not a timer source: the winit backend has no calloop loop of its own, and poll_events already runs continuously in both backends. Repeat stops when *that* key is released, not when any key is. Always-on-top / pin, for the picture-in-picture and HUD rules that used it. Window::always_on_top was another declared-but-never-read field. Enforced in WindowManager's stacking order rather than at render time, so every consumer of stacking_order gets it and none can forget to honour it. Mouse-only window management, checked end to end: drag the titlebar to move, drag any edge or corner to resize, titlebar buttons to close/maximise/ minimise, click to focus, drag to a screen edge to snap, and now double-click the titlebar to maximise. The resize grab band went from 6px to 10px - a hairline is genuinely hard to hit with a mouse, which is why Hyprland ships extend_border_grab_area. Also removed the emoji status markers from docs/IMPLEMENTATION_STATUS.md.
2024-05-14Draw a cursor, handle the lid, and everything a real config needssrdusr3-12/+324
Groundwork for actually daily-driving this: porting the user's Hyprland config exposed what srdwm couldn't yet express, and testing on a bare TTY exposed something worse. A visible mouse cursor. Nothing drew a pointer at all - on a bare TTY the mouse was simply invisible. It hid because the nested backend runs inside another compositor, which draws a cursor over srdwm's window; only the DRM backend, i.e. the actual session path, was affected. A built-in arrow is now composited above everything on the output the pointer is on. It's a reviewable ASCII bitmap rather than an XCursor theme: a cursor that is always present beats a prettier one that sometimes isn't, the same reasoning as decoration.rs's font fallback. Client-set cursor surfaces and named shapes are still not rendered, so an app asking for an I-beam gets the arrow. Lid switch. libinput switch events are handled and surfaced to config as srd.on("lid_closed"/"lid_open", fn), so closing the lid can lock and suspend instead of doing nothing. Config-driven additions, each needed by a binding in the ported config and none of which existed: fullscreen (Window.fullscreen was a dead field -- declared, never read or written), directional window move that swaps with the neighbour and reorders the stack so tiling follows, focus cycling, modifier+drag to move/resize anywhere in a window rather than only by the titlebar, modifier+scroll to change workspace, and 8 XF86 media/power keysyms taken from the system's own XF86keysym.h. The keysym tables are hand-maintained in both directions and a key missing from either fails silently, so a round-trip test now covers every one the configs bind. Verified in the QEMU VM: a bare-TTY screendump shows a recognisable arrow at the pointer position (113 white fill + 58 black outline pixels at screen centre, where the pointer starts).
2024-04-12udev: connector hotplug, and rescue windows on an unplugged monitorsrdusr2-0/+181
Monitors were probed once at startup, so plugging or unplugging one while srdwm was running went unnoticed. A UdevBackend event source now watches for the kernel's `change` uevent and reconciles the head list against a fresh connector probe - forcing a re-probe rather than trusting cached status, since on a hotplug the cache is exactly what has gone stale. Removing a head tears down everything it owned: the wl_output global, its place in the Space, its DRM framebuffers and dumb buffers (dropping the Rust structs alone leaks the kernel-side objects, which matters when a cable is plugged repeatedly), and any lock surface for it - otherwise confirm_lock_if_presented would wait forever on a monitor that no longer exists. New connectors go through the same bring_up_head path as startup, so a monitor plugged in later is set up identically to one present at boot. Heads are then repositioned left-to-right, since removing one shifts the rest, and layer maps re-arranged so bars follow their moved output. set_monitors rehomes windows stranded by the change, and main.rs re-queries the whole monitor list on MonitorAdded/MonitorRemoved rather than applying the single monitor in the event, because the others' positions move too. The rehoming had a bug that unit tests missed and live testing caught. It originally keyed off Window::monitor, but that field records the monitor a window was *assigned* at creation, not where it is: add_window always sets it from the primary monitor, so a window placed on the second monitor by a rule - or dragged there - still reads monitor == 0. The field-only check saw a valid id, skipped the window, and left it at coordinates that no longer existed: invisible and unreachable. Found by unplugging a monitor out from under a real xterm and watching it vanish from both heads. It now keys off geometry, with a regression test that fails against the old logic. Verified in the QEMU VM, booting with one connector and toggling the second at runtime: plug in -> head added and rendering at its own resolution; unplug -> head removed cleanly; and an xterm at global x=1500 survived its monitor being unplugged, reappearing at x=680 (= min(1500, 1280-600)) with its size intact. Writing to /sys/class/drm/<connector>/status changes the connector but emits no uevent on this kernel, so the signal the kernel would send is synthesized with `udevadm trigger`; the whole reaction path is genuinely exercised.
2024-04-02Add window rules, real config validation, and a Wayland DRM/udev backendsrdusr4-4/+279
- srd.rule(): match windows by title/class, apply floating/maximized/ workspace/geometry/decoration actions on creation (crates/core/src/rules.rs) - srd.validate_config()/srd.debug.*: real range/format checks and status/profiling helpers, replacing the always-true stub - Wayland titlebar text rendering via fontdue, unit-tested without a display (crates/wayland/src/decoration.rs) - Wayland precise keybinding matching, replacing the "any Super-held key" heuristic, sharing the keysym table with X11 (moved to crates/core/src/keysyms.rs) - Wayland DRM/udev backend (crates/wayland/src/udev.rs): runs as the real compositor on a bare TTY via libseat/libinput/KMS, software rendering via Pixman + dumb buffers (no GBM/EGL required) - srdwm_platform::detect() fix, found via VM testing: a bare TTY with no DISPLAY/WAYLAND_DISPLAY now correctly resolves to Wayland instead of an X11 backend that can never work there - XWayland integration groundwork (crates/wayland/src/xwayland.rs): spawn, X11Wm, and full XwmHandler event routing into the same WindowManager/ Space pipeline as native clients. Windows don't render yet - a real glamor-vs-software-renderer conflict in XWayland's own fallback path, root-caused via WAYLAND_DEBUG tracing and documented in docs/IMPLEMENTATION_STATUS.md rather than worked around blind. All verified live in an isolated QEMU VM: X11 backend shows two decorated, correctly-tiled xterms with real title text; the DRM/udev Wayland backend opens the GPU, initializes input, and scans out a rendered frame via KMS page-flip.
2024-04-02Rewrite srdwm in Rust: working X11 and Wayland backends, Lua configsrdusr9-0/+1471
The C++ prototype (moved to legacy-cpp/) was mostly a design skeleton: X11 and Windows backends were partially real, Wayland created the wlroots object graph but never wired a single event listener, macOS was stub except monitor enumeration, and the Lua engine's srd.bind() stored a key-combo string but never the actual closure. See docs/PRIOR_ART.md for the full audit. This replaces it with a Cargo workspace: - srdwm-core: platform-independent window/workspace/monitor state, a real master-stack tiling layout, and SmartPlacement grid/cascade/ snap-to-edge placement - fixing several bugs in the C++ version (hardcoded 2-column grid, cascade that never cascaded, snap-to-edge that always returned a fixed rect). 35 unit tests. - srdwm-config: the srd Lua API via mlua, implementing the surface docs/DEFAULTS.md always documented but the C++ engine never actually built (srd.window.close()/focus(direction), srd.workspace.next(), real keybinding closures, require("srd") support). 10 unit tests. - srdwm-x11: a real reparenting WM with a drawn title bar (buttons, drag, resize), verified live under Xephyr - frame placement and client offset match srdwm-core's computed geometry exactly, and the decoration renders correctly on screen. - srdwm-wayland: a from-scratch smithay compositor (the C++ version had nothing working to port from) - runs via the winit backend, tracks xdg-shell toplevels through the same WindowManager and hit-testing code X11 uses, verified to start/render/run without crashing. Decorations are solid-color (no text yet); see docs/IMPLEMENTATION_STATUS.md for exact scope. - srdwm-windows / srdwm-macos: structured, cfg-gated designs informed by komorebi/glazewm and yabai/AeroSpace respectively (see docs/PRIOR_ART.md), honestly marked as unbuilt/unverified since this sandbox has no Windows or macOS target.