diff --git a/book/src/tiling.md b/book/src/tiling.md index 2ff61d5e..650cd73a 100644 --- a/book/src/tiling.md +++ b/book/src/tiling.md @@ -77,20 +77,6 @@ You can also right-click any title in a container to toggle mono mode. In mono mode, scroll over the title bar to cycle between windows in the container. -## Autotiling - -Autotiling makes newly tiled windows alternate split direction from the focused -tiled window. The first split uses the containing group direction, then later -windows wrap the focused tile in the opposite direction, producing a horizontal, -vertical, horizontal pattern as the layout grows. - -```toml -[shortcuts] -alt-a = "toggle-autotile" -``` - -Manual grouping and split commands still use the direction you request. - ## Fullscreen Press `alt-u` (`toggle-fullscreen`) to make the focused window fill the entire diff --git a/docs/window-animations-plan.md b/docs/window-animations-plan.md new file mode 100644 index 00000000..7cb60d03 --- /dev/null +++ b/docs/window-animations-plan.md @@ -0,0 +1,313 @@ +# Window Animations Plan + +This document is the working plan for Wry's window animation system. It records +the decisions already made, the implementation phases, and the risks that must +be handled deliberately. + +## Accepted Decisions + +- The first landed slice is plain interpolation only, disabled by default. +- Animation is presentation-only. Logical layout, input hit testing, focus, and + Wayland configure state use final geometry immediately. +- Pointer drag and resize initiated by the mouse or tablet do not animate. +- Plain animations restart only for windows whose destination changes. Other + in-flight windows keep their existing timelines. +- Spawn-in uses scale and position for newly mapped tiled and floating app + windows. Layer-shell, overlay, override-redirect, and fullscreen surfaces do + not use this path. Spawn-out uses retained visual content after the live node + is gone, when a stable retained surface tree can be captured before unmap or + destroy. +- Command-driven tile-to-float and float-to-tile transitions may animate. + Protocol drag/drop paths do not. +- The no-overlap multiphase system is a separate phase after the plain path is + working and testable. +- Content freezing will use retained per-surface texture references, not a full + offscreen snapshot as the default design. +- Retained records should keep using the existing renderer behavior for now, + including clipping and edge stretch/clamp behavior for undersized contents. A + dedicated retained-tree scaling path is deferred to a later polish phase. +- The multiphase animation concept is original to Wry. Hy3 is relevant only as + partial inspiration for tiling style and titlebar/grouping behavior. +- Mono mode should mostly avoid animations. Exceptions are windows entering or + exiting mono mode, where a visual transition can clarify the hierarchy change. +- Multiphase shrink steps should not normally need to reduce a tiled window far + below roughly one quarter of the relevant full size. The implementation may + enforce a conservative sanity minimum, and pathological cases may fall back. +- If the no-overlap planner cannot produce a legal sequence, only the affected + group should fall back to plain animation. This is expected to be rare for + valid tiling layouts. +- When entering mono mode, the active child should animate to the mono geometry. + Inactive siblings may snap invisible. Floats may overlap normally and do not + need the no-overlap planner. + +## Texture Freezing Decision + +The approved freezing design is to capture a renderable surface tree at animation +start: + +- texture references +- source sample rects +- target sizes +- alpha and color metadata +- subsurface offsets and stacking order +- enough synchronization/release state to keep referenced buffers alive safely + +This is lighter than rendering every toplevel into a compositor-owned offscreen +texture, and it should handle normal GPU-backed windows without an extra full +window copy. It also gives spawn-out a path: capture the surface tree before the +toplevel is logically destroyed, then animate the retained records after the live +node is gone. + +Tradeoffs: + +- Retained references can delay buffer release. For dmabuf clients this can + increase memory pressure or throttle clients if many large windows animate. +- SHM buffers still matter for simple clients, fallback paths, some utilities, + and cursor-like surfaces. They are probably not the common case for large app + windows, but the implementation must still treat SHM texture flipping as a + correctness issue. +- The release/sync contract must be explicit. A retained texture must not be + released back to the client while the compositor may still render it. +- True offscreen snapshots remain a possible fallback for cases where retained + references cannot safely preserve the rendered content. + +## Phase 1: Linear Presentation Animations + +Goal: add the smallest correct animation layer without changing layout semantics. + +Implementation shape: + +- Add animation state owned by `State`. +- Track per-toplevel animation entries keyed by `NodeId`. +- Store logical target rect, current presentation rect, previous damaged rect, + start time, duration, and curve. +- On command-driven tiled layout geometry changes, animate from current + presentation rect to new final rect. +- On interruption, restart only the affected window from its current + presentation rect. +- Drive frames from the existing output latch/presentation event flow. +- Damage the union of previous presentation rect, current presentation rect, and + final logical rect. + +Initial scope: + +- Tiled reflow animation. +- Floating command-driven moves and resizes are animated. Pointer and tablet + drag/resize paths still snap directly to the live cursor position. +- Cross-output and cross-scale movements snap for now. +- Linear mode may overlap windows during swaps. That is expected for the classic + interpolation mode; no-overlap is Phase 3. +- Live client buffers are rendered in Phase 1. Retained content freezing is + deferred, but animated windows must still be clipped to their presentation + bounds and must preserve the existing stretch behavior for undersized contents. +- Spawn-out is retained-content-only. If the surface cannot be retained safely + the window snaps out instead of animating an empty frame. +- No multiphase no-overlap planner. + +Tests: + +- rect interpolation is direction-independent +- interruption restarts only changed windows +- unchanged in-flight windows keep their original timeline +- drag-driven floating movement bypasses animation +- damage includes old, current, and final rects +- command-driven tile-to-float and float-to-tile transitions use linear motion +- command-driven floating moves and resizes animate without affecting pointer + drag/resize behavior +- pointer/header double-click unfloat bypasses the command-animation gate + +## Phase 2: Retained Texture Freezing + +Goal: freeze visual contents during movement and enable spawn-out. + +Initial retained-record implementation status: + +- Tiled animation can retain GPU/dmabuf-backed XDG and Xwayland surface trees. +- Spawn-in animation can retain GPU/dmabuf-backed XDG and Xwayland surface trees + for both tiled windows and floating child contents. +- Tile-to-float and float-to-tile transitions retain GPU/dmabuf-backed child + contents while the presentation geometry changes. +- Spawn-out captures retained app-window contents before XDG/Xwayland unmap or + destroy, then renders a detached shrinking presentation record until the + animation completes. +- Retained records hold both `GfxTexture` and `SurfaceBuffer` references so the + existing buffer release/sync path remains authoritative. +- Single-pixel buffers can be retained as color records. +- Retained records render through the same texture and stretch/clamp paths used + by live surfaces. This is the expected Phase 2 behavior. +- Async SHM textures are not retained yet because Wry's per-surface SHM + front/back textures can be reused by later commits while an animation is still + running. Those surfaces fall back to live rendering until an explicit offscreen + copy fallback exists. + +Implementation shape: + +- Add a retained render-record tree for toplevel surfaces. +- Capture records before movement animations that require freezing. +- Capture records before destroy/unmap for spawn-out. +- Render retained records through the normal renderer primitives where possible. +- Extend event/sync handling so retained buffers remain valid until the animation + is complete. + +Deferred/future polish: + +- Whether retained records participate in frame callbacks or presentation + feedback. Default assumption: no, because they are compositor animation frames, + not client commits. +- How to fall back when a buffer cannot be safely retained. +- A distinct retained-tree scaling render path for true spawn-in/spawn-out + content scaling. If added, start with retained GPU-backed records only, keep + the animated frame as the clip boundary, and avoid live SHM scaling until there + is an explicit snapshot/copy fallback. + +## Phase 3: Multiphase No-Overlap Animations + +Goal: implement Wry's staged no-overlap planner while preserving the rule that +windows never overlap. + +Manual verification checklist: `docs/window-animations-testing.md`. + +Core rules: + +- Each phase is a discrete animation using the full curve. +- A phase performs only one action kind per window: move or scale. +- Movement and scaling are split by axis. +- No diagonal motion. Mixed-action phases may combine different per-window + actions, but no single window may move or resize on more than one axis in one + step. +- A window or synchronized group owns its own timeline. +- New layout changes interrupt only windows/groups with changed destinations. +- Current hierarchy and target hierarchy both matter. The planner must know + whether a window is ascending toward a higher-level/toplevel position, + descending into a container, or moving between containers at the same depth. +- If some child windows require fewer phases than their parent/container + context, parent/container-space changes generally happen first so space exists + before the child moves into it. This rule can be overridden only when the + non-overlap invariant still clearly holds. +- Windows that become peers in the target hierarchy may synchronize later + phases even if they were not peers in the source hierarchy. + +Important parent/child synchronization issue: + +The planner must not let a parent container and child window animate independent +axes at the same time in a way that violates the visual rules. For example, a +parent scaling horizontally while a child scales vertically can accidentally +produce a diagonal or multi-axis motion in screen space. + +Preferred approach: + +- Plan in terms of leaf toplevel visual rectangles first. +- Treat containers as constraints and grouping boundaries, not as independently + animated visual actors. +- Derive every leaf's per-phase rect from one phase schedule so parent and child + effects cannot compose into forbidden motion. +- Build the planner as pure geometry first. Live integration should collect + eligible leaf `(old, new)` rects across a command-driven layout pass, then + submit planner-produced phases as a batch. Per-node `tl_change_extents` calls + are too incremental to plan safely by themselves. +- Add container-level grouping only after the leaf planner proves correct. +- Include hierarchy-transition metadata in the planner input: source parent, + target parent, source depth, target depth, and whether the window is ascending, + descending, or staying at the same hierarchy level. +- For mono containers, suppress ordinary in-mono focus/tab changes. Animate only + transitions into mono, out of mono, or across the mono boundary. +- When entering mono, the active child animates to the full mono area and + inactive siblings snap invisible. When exiting mono, ordinary tiled geometry + may animate from the mono child where that produces a clear hierarchy + transition. +- If a legal no-overlap sequence cannot be found for a group, fall back to the + linear animator for that group only. Float windows are outside this invariant. + +Current pure planner status: + +- Two-window same-axis swaps use shrink lanes, move, then grow. +- Swap lane choice follows motion direction, not node identity: right/down + moving windows take the first lane, and left/up moving windows take the second + lane. +- Stack extraction/return patterns are covered in both horizontal and vertical + orientations: peer/container space scales first, the extracted child moves + only after space exists, and orthogonal growth happens in the final phase. +- Same-axis size redistribution is handled as a single scale phase when the + exact validator proves adjacent windows stay non-overlapping. +- Nested size redistribution can use hierarchy metadata to decompose two-axis + resizing into parent-axis then child-axis scale phases, but only when the + source/target ancestor split depths give a deterministic order. +- A phase can carry mixed per-window actions when each window still performs one + classified move/scale on one axis and the exact validator proves the combined + phase is non-overlapping. +- Every produced plan is checked analytically for overlap over the full duration + of each phase before it is accepted. This solves the linear edge inequalities + for each pair of moving rectangles instead of relying on sampled frames. +- Live layout batches are partitioned by overlapping motion bounds, so unrelated + groups can still use multiphase animation when another group falls back to + linear motion. +- Planner requests now carry per-window hierarchy metadata for source/target + parent, depth, sibling index, split axis, nearest ancestor split depth per + axis, mono state, and transition kind. The current planner uses this for + parent-before-child scale ordering, but not yet for full nested move planning. +- Multiphase planning has a diagnostic entry point used by live fallback logs. + It distinguishes request validation errors, missing patterns, shrink-bound + rejections, invalid phase steps, and exact validation failures such as stale + starts or phase overlap. +- Multiphase planning also has an explained-plan entry point. Accepted plans + report the deterministic strategy, phase reasons, participating nodes, and + validation result; rejected plans report every attempted strategy and failure. +- Rejection diagnostics are treated as contractual test output for unsupported + patterns and analytically invalid candidate plans, including attempted strategy + order and exact validation failure. +- Planner tests now include a deterministic split-tree generator. It builds + valid weighted tiling layouts, derives leaf hierarchy metadata, mutates them + through supported transitions, and runs the real planner plus exact validator. +- The generated tests also include a bounded corpus of supported split-tree + transitions across both axes and directions. Each case is planned twice and + compared exactly to catch nondeterministic planner output. + +Tests: + +- horizontal swaps shrink, move, then grow without overlap +- extraction from a stack creates space before moving the extracted window +- nested size redistribution scales the parent axis before the child axis +- nested containers do not produce simultaneous cross-axis motion +- interruption restarts only affected phase groups +- reversing direction produces equivalent motion in reverse +- accepted and rejected plans expose deterministic strategy explanations +- bounded generated split-tree corpus produces identical plans on repeated runs +- unsupported and invalid candidate plans produce exact expected diagnostics +- mixed-action phases are accepted only under exact continuous validation +- diagonal per-window motion remains a hard rejection even inside mixed phases +- child waits for parent/container-space phases when moving upward into a + toplevel peer position +- mono-mode tab switches do not animate, while entering/exiting mono can animate + +## Configuration + +Phase 1 should expose a disabled-by-default setting for: + +- enabled/disabled +- duration +- style: `plain` or `multiphase` +- curve preset or cubic bezier + +Initial TOML shape: + +```toml +[animations] +enabled = false +duration-ms = 160 +style = "multiphase" +curve = "ease-out" +# or: +curve = [0.25, 0.1, 0.25, 1.0] +``` + +Bezier curves are analyzed when configuration is applied and stored as a +piecewise curve that is cheap to evaluate during rendering. Custom curves use +CSS cubic-bezier semantics: `(0, 0)` and `(1, 1)` are implicit, while the four +configured numbers are `x1`, `y1`, `x2`, and `y2`. The x control points must be +between `0` and `1`. + +## Existing Note + +`docs/animation-integration.md` appears to document a prior animation attempt +whose `src/animation/` implementation is not present in this checkout. Treat +this plan as the current source of truth until implementation docs are updated. diff --git a/docs/window-animations-testing.md b/docs/window-animations-testing.md new file mode 100644 index 00000000..f5bdd416 --- /dev/null +++ b/docs/window-animations-testing.md @@ -0,0 +1,493 @@ +# Window Animations Manual Test Plan + +This is the manual verification checklist for Wry's animation work. Use it after +building a test compositor and booting into a normal graphical session. + +The goal is to catch visual, synchronization, damage, and retained-content +problems that unit tests cannot prove from geometry alone. + +## Setup + +- Build and install the `codex-anims-next` branch. +- Start with animations enabled and a deliberately slow duration, around + `400-700ms`, so phase ordering and damage artifacts are visible. +- Test at least one normal Wayland/XDG app, one Xwayland app if available, and + one fast-updating app such as a terminal running output, a browser animation, + a video, or a GL/Vulkan demo. +- Use visible gaps, borders, titlebars, and rounding. These make clipping and + damage mistakes much easier to see. +- If available, test on both a single-output setup and a multi-output setup. +- If logging is convenient, run with debug logging and keep any multiphase + fallback messages. A fallback is useful evidence, not automatically a bug. + +Relevant internal config hooks: + +- `SetAnimationsEnabled` +- `SetAnimationDurationMs` +- `SetAnimationStyle` +- `SetAnimationCurve` +- `SetAnimationCubicBezier` + +TOML example: + +```toml +[animations] +enabled = true +duration-ms = 600 +style = "multiphase" +curve = "ease-out" +``` + +Set `style = "plain"` to force ordinary one-step movement interpolation while +keeping the configured curve. `curve = "linear"` only changes easing; it does +not select the plain animation style. + +Current curve IDs in code: + +- `0`: linear +- `1`: CSS `ease` +- `2`: CSS `ease-in` +- `3` or any other unrecognized value: CSS `ease-out` +- `4`: CSS `ease-in-out` + +## Enabling Multiphase Tests + +To exercise the multiphase planner: + +1. Enable animations with `SetAnimationsEnabled`. +2. Set a slow duration with `SetAnimationDurationMs`, around `400-700ms`. +3. Select `style = "multiphase"` in TOML, or call `SetAnimationStyle` with + `AnimationStyle::MULTIPHASE`. +4. Use tiled layout commands that are wired through `State::with_layout_animations`. +5. Use layouts where at least two tiled windows change geometry in the same + container layout batch. + +The compositor then attempts multiphase planning automatically when the batched +layout pass completes. If the planner proves a legal no-overlap sequence, that +group uses phased animation. If it cannot prove one, only that motion group falls +back to ordinary plain animation. + +Good command families for multiphase testing: + +- seat/window move in a tiled direction +- split changes +- tab/group operations +- group-opposite changes +- equalize +- move-tab +- mono enter/exit +- command-driven window resize + +These paths should not be used as evidence of multiphase behavior: + +- tile-to-float and float-to-tile, which deliberately use plain animation +- command-driven floating move/resize, which may animate but can overlap +- pointer or tablet drag/resize, which should not animate +- spawn-in and spawn-out, which are single-phase and use the configured curve +- cross-output or cross-scale movement, which should snap +- layer-shell, overlay, override-redirect, and fullscreen map/unmap paths + +Useful debug signal: + +- `falling back to plain layout animation for group ...` means the group entered + the multiphase gate but the planner rejected it. That is acceptable for + unsupported patterns, but unexpected for the supported swap/extraction cases + below. + +## Pass Criteria + +A test passes when: + +- layout, focus, hit testing, and configure behavior use the final logical + geometry immediately +- visible presentation motion is smooth and bounded by the animated frame +- no old pixels, trails, black strips, transparent holes, or stale titlebar + fragments remain after motion +- tiled multiphase movement never overlaps and never moves a single window + diagonally during a phase +- interruption starts changed windows from their current visual rect without + restarting unaffected windows +- drag-driven pointer movement remains direct and does not lag behind the cursor +- cross-output or cross-scale movements snap instead of animating + +Record a failure with: + +- the layout before and after +- whether the window was tiled, floating, mono, XDG, Xwayland, or layer-shell +- whether the app was GPU/dmabuf-backed or likely SHM, if known +- animation duration and curve +- whether the failure was visual overlap, diagonal motion, debris, clipping, + stale content, a missing retained frame, or an incorrect animation trigger + +## Known Current Limits + +These are acceptable unless they produce worse behavior than described: + +- Spawn-out is retained-content-only. If the surface cannot be retained safely, + it should snap out rather than animate an empty frame. +- Async SHM surfaces are not retained yet. GPU/dmabuf-backed app windows are the + primary retained-content path for this phase. +- A dedicated retained-tree scaling/offscreen fallback is deferred. Retained + records currently render through the normal texture and stretch/clamp paths. +- Floats may overlap. The no-overlap invariant is for tiled multiphase motion. +- Linear fallback may overlap. This should be rare for valid tiled layouts, and + the fallback should be scoped to the affected motion group. +- Cross-output and cross-scale movements should not animate yet. + +## 1. Basic Enable/Disable + +1. Disable animations. +2. Move, resize, spawn, close, and toggle floating on a few windows. +3. Confirm all affected windows snap with no delayed presentation state. +4. Enable animations at a slow duration. +5. Repeat the same operations and confirm only eligible paths animate. +6. Disable animations while an animation is in flight. + +Expected: + +- disabling animations clears any in-flight visual state +- no stale damage remains after disabling +- newly enabled animations use the configured duration and curve + +## 2. Spawn-In + +Test newly mapped windows: + +- tiled XDG window +- floating XDG window +- Xwayland window, if available +- fullscreen window +- layer-shell or overlay surface, such as a bar, launcher, menu, notification, + or lock/overlay component, if available + +Expected: + +- newly mapped tiled and floating app windows animate in +- layer-shell, overlay, override-redirect, and fullscreen surfaces do not use + the app-window spawn-in path +- contents stay clipped to the animated frame +- if contents are smaller than the frame during the animation, no empty strips + are visible + +## 3. Spawn-Out + +Close windows from these states: + +- tiled app window +- floating app window +- Xwayland app window +- fast-updating app window +- a likely SHM/simple app, if available + +Expected: + +- retained app content shrinks out after the live node is gone +- there is no black, transparent, or unfilled moving rectangle +- if content cannot be retained, the window snaps out cleanly +- neighboring tiled windows reflow without debris left in the old area + +Hard failure: + +- a destroyed window leaves a moving empty frame +- the last frame shows unrelated newer content +- screen debris remains after the animation completes + +## 4. Linear Tiled Reflow + +Use a slow duration and a non-linear curve, then repeat with linear. + +Cases: + +- open two tiled windows and change split ratio by command +- open three tiled windows and resize the active split repeatedly +- move focus and issue command-driven swaps +- interrupt a resize by issuing another resize before the first completes +- create a layout that forces a linear fallback if possible + +Expected: + +- final layout is usable immediately +- changed windows animate from their current visual rect on interruption +- unaffected windows keep their existing timeline +- linear fallback is visually smooth, even if overlap occurs +- no pointer drag path becomes animated + +## 5. Float Movement And Tile/Float Transitions + +Cases: + +- command-toggle a tiled window to floating +- command-toggle the same window back to tiled +- command-move and command-resize a floating window +- mouse-drag a floating window +- mouse-resize a floating window +- double-click/header pointer path if that is part of the local workflow + +Expected: + +- command-driven tile-to-float and float-to-tile transitions animate linearly +- command-driven floating move/resize animates +- mouse or tablet drag/resize remains direct and tracks the pointer +- pointer/header paths that are intentionally outside the command-animation gate + do not unexpectedly use delayed animation +- retained child content remains clipped during tile/float transitions + +## 6. Multiphase Horizontal And Vertical Swaps + +Horizontal: + +1. Create two horizontally adjacent tiled windows. +2. Swap their positions. +3. Reverse the swap. + +Vertical: + +1. Create two vertically adjacent tiled windows. +2. Swap their positions. +3. Reverse the swap. + +Expected: + +- first phase shrinks into lanes on the orthogonal axis +- second phase moves only horizontally or only vertically +- third phase grows out of lanes +- no phase overlaps windows +- no window moves diagonally +- reverse direction uses the same visual logic in reverse +- titlebars, borders, gaps, and rounded corners remain respected + +## 7. Stack Extraction And Return + +Build this shape: + +```text +[ A | [ B + C ] ] +``` + +Then move `B` out so the target is: + +```text +[ A | B | C ] +``` + +Reverse the operation by putting `B` back into the stack. + +Expected: + +- peer/container space opens first +- `B` waits until there is a legal horizontal or vertical lane +- `B` moves on one axis only +- `B` and the affected peer grow together in the final phase when appropriate +- reversing the operation is visually equivalent in reverse + +## 8. Nested Parent/Child Synchronization + +Create nested split layouts where a parent changes one axis and children change +the other. Use both horizontal-parent/vertical-child and +vertical-parent/horizontal-child variants. + +Expected: + +- parent/container-space axis changes happen before child-axis changes when the + hierarchy metadata gives a deterministic order +- child windows do not visually compose parent and child transforms into + diagonal motion +- any unsupported group falls back as a group rather than partially violating the + one-axis rule + +Hard failure: + +- a child visibly changes width and height in the same phase +- a child moves diagonally because parent and child animation compound +- a child clips outside its animated frame + +## 9. Mixed-Action Phases + +This case is easiest to prove with the planner tests because Wry does not yet +have a confirmed stock command that reliably creates a same-batch move for one +tiled window and an independent resize for another. The canonical geometry is: + +```text +start: A = (0,0)-(80,80) B = (200,0)-(280,80) +target: A = (40,0)-(120,80) B = (200,0)-(280,120) +``` + +`A` moves horizontally while `B` scales vertically. The windows are far enough +apart that the mixed phase is provably non-overlapping. + +To exercise the current proof directly, run: + +```sh +cargo test animation::multiphase::tests::mixed_single_phase_accepts_move_and_scale_when_proven +``` + +For visual/manual testing, the target shape is: + +```text +before: [ A ] [ B ] +after: [ A ] [ B ] + [ ] +``` + +`A` must move horizontally without resizing. `B` must resize vertically without +moving. The two motion bounds must remain separate for the whole animation. If a +normal command sequence cannot produce that in one layout batch, treat the unit +test as the authority and record the visual test as not applicable rather than a +failure. + +Expected: + +- mixed phases are allowed only when each individual window performs one legal + move or scale on one axis +- no individual window moves diagonally +- no overlap occurs at any point during the phase + +A fallback here is acceptable if no normal user command can create this geometry; +the planner test above is the authority for the mixed-action rule. + +## 10. Mono Mode + +Cases: + +- enter mono mode with several siblings +- exit mono mode +- switch active tabs/windows inside mono +- move a window into mono +- move a window out of mono + +Expected: + +- entering/exiting mono may animate where it clarifies hierarchy change +- active child animates to the mono geometry +- inactive siblings snap invisible +- ordinary tab switches inside mono do not animate +- no hidden inactive sibling leaves debris or stale retained content + +## 11. Interruption And Retargeting + +Use a long duration, then issue commands mid-animation: + +- swap, then reverse before completion +- resize, then resize in the other direction before completion +- build `[A | [B | C | D]]`, move `C` left to form `[A | C | [B | D]]`, + then move `C` back into the stack before completion +- start a multiphase group, then change only one window's destination if a + command sequence allows it + +Expected: + +- affected windows restart from their current visual rect +- unaffected windows do not restart if their destination is unchanged +- a new valid multiphase plan replaces the old plan cleanly +- retained content remains the same frozen content during the retarget +- damage covers old, current, and new visual regions + +## 12. Damage And Clipping Stress + +Use a high-contrast wallpaper/background and high contrast window contents. + +Cases: + +- fast repeated swaps +- repeated spawn-in/spawn-out +- rounded corners with large gaps +- titlebar-heavy layouts +- resize while a terminal is rapidly updating +- move/resize over another window's old location +- run on different output scales if available + +Expected: + +- no trails remain in gaps, borders, or titlebar strips +- rounded corners do not reveal old pixels outside the frame +- contents never draw outside the animated bounds +- final frame exactly matches the steady layout + +## 13. Texture Freezing + +Use fast-updating contents so freezing is obvious. + +Cases: + +- tiled GPU/dmabuf-backed app during reflow +- floating GPU/dmabuf-backed app during command move/resize +- tile-to-float and float-to-tile with dynamic content +- spawn-out with dynamic content +- likely SHM/simple app, if available + +Expected: + +- retained GPU/dmabuf-backed windows freeze visually during animation +- spawn-out uses the last retained content, not a blank or unrelated frame +- undersized contents stretch/clamp to avoid unfilled frame regions +- SHM/unretained surfaces either render live safely or snap where retention is + required + +Record separately: + +- content continues updating during movement +- content freezes but samples the wrong source region +- edges show empty/black strips while scaling +- spawn-out skips because capture was unavailable + +## 14. Cross-Output And Scale Boundaries + +Cases: + +- move a tiled window to another output +- move a floating window to another output +- move between outputs with different scale factors, if available +- move a workspace between outputs, if supported locally + +Expected: + +- movement snaps instead of animating +- no retained content is rendered at the wrong scale +- no stale damage remains on the source output +- destination output renders the final layout immediately + +## 15. Regression Sweep + +After visual tests, return to normal animation duration and curve. + +Repeat: + +- ordinary tiling navigation +- workspace switching +- fullscreen enter/exit +- focus changes +- app launch/close loops +- suspend/resume or VT switch if convenient + +Expected: + +- animation state does not survive across unrelated compositor state changes +- no stuck retained frames +- no persistent high CPU/GPU use after animations stop +- no obvious client throttling after many retained-content animations + +## Summary Result Template + +```text +Commit: +Build: +Outputs/scales: +GPU/session: +Animation config: + +Passed: +- + +Known-limit observations: +- + +Failures: +- case: + app: + layout: + expected: + actual: + reproducible: + logs: +``` diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 151e7591..71927bbc 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -640,22 +640,6 @@ impl ConfigClient { self.send(&ClientMessage::SetWindowWorkspace { window, workspace }); } - pub fn seat_send_to_scratchpad(&self, seat: Seat, name: &str) { - self.send(&ClientMessage::SeatSendToScratchpad { seat, name }); - } - - pub fn seat_toggle_scratchpad(&self, seat: Seat, name: &str) { - self.send(&ClientMessage::SeatToggleScratchpad { seat, name }); - } - - pub fn seat_cycle_scratchpad(&self, seat: Seat, name: &str) { - self.send(&ClientMessage::SeatCycleScratchpad { seat, name }); - } - - pub fn window_send_to_scratchpad(&self, window: Window, name: &str) { - self.send(&ClientMessage::WindowSendToScratchpad { window, name }); - } - pub fn seat_split(&self, seat: Seat) -> Axis { let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat }); get_response!(res, Axis::Horizontal, GetSplit { axis }); @@ -2095,12 +2079,6 @@ impl ConfigClient { self.send(&ClientMessage::SetAutotile { enabled }); } - pub fn get_autotile(&self) -> bool { - let res = self.send_with_response(&ClientMessage::GetAutotile); - get_response!(res, false, GetAutotile { enabled }); - enabled - } - pub fn set_tab_title_align(&self, align: u32) { self.send(&ClientMessage::SetTabTitleAlign { align }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 743acc57..e86e79ca 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -286,18 +286,6 @@ pub enum ClientMessage<'a> { seat: Seat, workspace: Workspace, }, - SeatSendToScratchpad { - seat: Seat, - name: &'a str, - }, - SeatToggleScratchpad { - seat: Seat, - name: &'a str, - }, - SeatCycleScratchpad { - seat: Seat, - name: &'a str, - }, GetTimer { name: &'a str, }, @@ -699,10 +687,6 @@ pub enum ClientMessage<'a> { window: Window, workspace: Workspace, }, - WindowSendToScratchpad { - window: Window, - name: &'a str, - }, SetWindowFullscreen { window: Window, fullscreen: bool, @@ -939,7 +923,6 @@ pub enum ClientMessage<'a> { SetAutotile { enabled: bool, }, - GetAutotile, SetTabTitleAlign { align: u32, }, @@ -1206,9 +1189,6 @@ pub enum Response { GetCornerRadius { radius: f32, }, - GetAutotile { - enabled: bool, - }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 450597e2..dbdef1ba 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -466,33 +466,6 @@ impl Seat { get!().set_seat_workspace(self, workspace) } - /// Sends the currently focused window to a scratchpad. - /// - /// Use an empty string for the default scratchpad. - pub fn send_to_scratchpad(self, name: &str) { - get!().seat_send_to_scratchpad(self, name) - } - - /// Toggles a scratchpad. - /// - /// If the scratchpad has a visible window, that window is hidden. Otherwise, the - /// most recently hidden window in the scratchpad is shown on the current workspace. - /// Scratchpad windows are always shown floating. - /// Use an empty string for the default scratchpad. - pub fn toggle_scratchpad(self, name: &str) { - get!().seat_toggle_scratchpad(self, name) - } - - /// Cycles through the windows of a scratchpad, one at a time. - /// - /// With nothing shown, the first window is brought up; each further invocation - /// hides the current window and shows the next; after the last window the - /// scratchpad is hidden again. Scratchpad windows are always shown floating. - /// Use an empty string for the default scratchpad. - pub fn cycle_scratchpad(self, name: &str) { - get!().seat_cycle_scratchpad(self, name) - } - /// Toggles whether the currently focused window is fullscreen. pub fn toggle_fullscreen(self) { let c = get!(); diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index fff94506..c95c6620 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -453,21 +453,14 @@ pub fn get_corner_radius() -> f32 { /// Enables or disables autotiling. /// -/// When enabled, newly tiled windows alternate split orientation from the -/// focused tiled window: the first split uses the containing group's direction, -/// then subsequent splits wrap the focused window in the perpendicular -/// direction. +/// When enabled, new windows are automatically placed in a perpendicular +/// sub-container if the predicted body would be narrower than tall (or vice versa). /// /// The default is `false`. pub fn set_autotile(enabled: bool) { get!().set_autotile(enabled) } -/// Returns whether autotiling is enabled. -pub fn get_autotile() -> bool { - get!(false).get_autotile() -} - /// Sets the horizontal alignment of title text within tab buttons. /// /// - `"start"` — left-aligned (default) diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index 96e4d3b1..662cda44 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -205,13 +205,6 @@ impl Window { get!().set_window_workspace(self, workspace) } - /// Sends the window to a scratchpad. - /// - /// Use an empty string for the default scratchpad. - pub fn send_to_scratchpad(self, name: &str) { - get!().window_send_to_scratchpad(self, name) - } - /// Toggles whether the currently focused window is fullscreen. pub fn toggle_fullscreen(self) { self.set_fullscreen(!self.fullscreen()) diff --git a/src/compositor.rs b/src/compositor.rs index 4dd47342..11f23808 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -403,7 +403,6 @@ fn start_compositor2( bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), virtual_outputs: Default::default(), clean_logs_older_than: Default::default(), - scratchpads: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); diff --git a/src/config/handler.rs b/src/config/handler.rs index 68ea93f5..336da9ff 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1100,32 +1100,6 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_seat_send_to_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let seat = self.get_seat(seat)?; - if let Some(toplevel) = seat.get_keyboard_node().node_toplevel() { - self.state.send_to_scratchpad(name, toplevel); - } - Ok(()) - }) - } - - fn handle_seat_toggle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let seat = self.get_seat(seat)?; - self.state.toggle_scratchpad(&seat, name); - Ok(()) - }) - } - - fn handle_seat_cycle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let seat = self.get_seat(seat)?; - self.state.cycle_scratchpad(&seat, name); - Ok(()) - }) - } - fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> { let window = self.get_window(window)?; let name = self.get_workspace(ws)?; @@ -1140,14 +1114,6 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_window_send_to_scratchpad(&self, window: Window, name: &str) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let window = self.get_window(window)?; - self.state.send_to_scratchpad(name, window); - Ok(()) - }) - } - fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; let name = dev.device.name(); @@ -3023,15 +2989,6 @@ impl ConfigProxyHandler { ClientMessage::SetSeatWorkspace { seat, workspace } => self .handle_set_seat_workspace(seat, workspace) .wrn("set_seat_workspace")?, - ClientMessage::SeatSendToScratchpad { seat, name } => self - .handle_seat_send_to_scratchpad(seat, name) - .wrn("seat_send_to_scratchpad")?, - ClientMessage::SeatToggleScratchpad { seat, name } => self - .handle_seat_toggle_scratchpad(seat, name) - .wrn("seat_toggle_scratchpad")?, - ClientMessage::SeatCycleScratchpad { seat, name } => self - .handle_seat_cycle_scratchpad(seat, name) - .wrn("seat_cycle_scratchpad")?, ClientMessage::GetConnector { ty, idx } => { self.handle_get_connector(ty, idx).wrn("get_connector")? } @@ -3416,9 +3373,6 @@ impl ConfigProxyHandler { ClientMessage::SetWindowWorkspace { window, workspace } => self .handle_set_window_workspace(window, workspace) .wrn("set_window_workspace")?, - ClientMessage::WindowSendToScratchpad { window, name } => self - .handle_window_send_to_scratchpad(window, name) - .wrn("window_send_to_scratchpad")?, ClientMessage::SetWindowFullscreen { window, fullscreen } => self .handle_set_window_fullscreen(window, fullscreen) .wrn("set_window_fullscreen")?, @@ -3633,11 +3587,6 @@ impl ConfigProxyHandler { ClientMessage::SetAutotile { enabled } => { self.state.theme.autotile_enabled.set(enabled); } - ClientMessage::GetAutotile => { - self.respond(Response::GetAutotile { - enabled: self.state.theme.autotile_enabled.get(), - }); - } ClientMessage::SeatToggleExpand { .. } => { // Removed feature; kept for binary protocol compatibility. } diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 4224e727..547b7e2a 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -1520,25 +1520,25 @@ impl WlSurface { let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds()); let pos = self.buffer_abs_pos.get(); let apply_damage = |pos: Rect| { - let clip_damage = |mut damage: Rect| { - damage = damage.intersect(pos); + if pending.damage_full { + let mut damage = pos; if let Some(bounds) = bounds { damage = damage.intersect(bounds); } - damage - }; - if pending.damage_full { - self.client.state.damage(clip_damage(pos)); + self.client.state.damage(damage); } else { let matrix = self.damage_matrix.get(); if let Some(buffer) = self.buffer.get() { for damage in &pending.buffer_damage { - let damage = matrix.apply( + let mut damage = matrix.apply( pos.x1(), pos.y1(), damage.intersect(buffer.buffer.buf.rect), ); - self.client.state.damage(clip_damage(damage)); + if let Some(bounds) = bounds { + damage = damage.intersect(bounds); + } + self.client.state.damage(damage); } } for damage in &pending.surface_damage { @@ -1550,7 +1550,8 @@ impl WlSurface { let y2 = (damage.y2() + scale - 1) / scale; damage = Rect::new_saturating(x1, y1, x2, y2); } - self.client.state.damage(clip_damage(damage)); + damage = damage.intersect(bounds.unwrap_or(pos)); + self.client.state.damage(damage); } } }; diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 8cb39935..56ee5272 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -284,27 +284,6 @@ impl TestConfig { }) } - pub fn send_to_scratchpad(&self, seat: SeatId, name: &str) -> TestResult { - self.send(ClientMessage::SeatSendToScratchpad { - seat: Seat(seat.raw() as _), - name, - }) - } - - pub fn toggle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult { - self.send(ClientMessage::SeatToggleScratchpad { - seat: Seat(seat.raw() as _), - name, - }) - } - - pub fn cycle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult { - self.send(ClientMessage::SeatCycleScratchpad { - seat: Seat(seat.raw() as _), - name, - }) - } - fn clear(&self) { unsafe { if let Some(srv) = self.srv.take() { @@ -352,10 +331,6 @@ impl TestConfig { pub fn set_show_titles(&self, show: bool) -> TestResult { self.send(ClientMessage::SetShowTitles { show }) } - - pub fn set_autotile(&self, enabled: bool) -> TestResult { - self.send(ClientMessage::SetAutotile { enabled }) - } } impl Drop for TestConfig { diff --git a/src/it/test_ifs/test_viewport.rs b/src/it/test_ifs/test_viewport.rs index e08266de..b25105c8 100644 --- a/src/it/test_ifs/test_viewport.rs +++ b/src/it/test_ifs/test_viewport.rs @@ -29,17 +29,6 @@ impl TestViewport { Ok(()) } - pub fn unset_source(&self) -> Result<(), TestError> { - self.tran.send(SetSource { - self_id: self.id, - x: Fixed::from_int(-1), - y: Fixed::from_int(-1), - width: Fixed::from_int(-1), - height: Fixed::from_int(-1), - })?; - Ok(()) - } - pub fn set_destination(&self, width: i32, height: i32) -> Result<(), TestError> { self.tran.send(SetDestination { self_id: self.id, @@ -48,15 +37,6 @@ impl TestViewport { })?; Ok(()) } - - pub fn unset_destination(&self) -> Result<(), TestError> { - self.tran.send(SetDestination { - self_id: self.id, - width: -1, - height: -1, - })?; - Ok(()) - } } impl Drop for TestViewport { diff --git a/src/it/tests.rs b/src/it/tests.rs index 35b6be97..dc28888c 100644 --- a/src/it/tests.rs +++ b/src/it/tests.rs @@ -85,8 +85,6 @@ mod t0051_pointer_warp; mod t0052_bar; mod t0053_theme; mod t0054_subsurface_already_attached; -mod t0055_autotiling; -mod t0055_scratchpad; pub trait TestCase: Sync { fn name(&self) -> &'static str; @@ -160,7 +158,5 @@ pub fn tests() -> Vec<&'static dyn TestCase> { t0052_bar, t0053_theme, t0054_subsurface_already_attached, - t0055_autotiling, - t0055_scratchpad, } } diff --git a/src/it/tests/t0002_window.rs b/src/it/tests/t0002_window.rs index 28ee359f..84571c57 100644 --- a/src/it/tests/t0002_window.rs +++ b/src/it/tests/t0002_window.rs @@ -1,6 +1,7 @@ use { crate::{ it::{test_error::TestError, testrun::TestRun}, + rect::Rect, tree::Node, }, std::rc::Rc, @@ -10,19 +11,29 @@ testcase!(); /// Create and map a single surface async fn test(run: Rc) -> Result<(), TestError> { - let ds = run.create_default_setup().await?; + run.backend.install_default()?; let client = run.create_client().await?; let window = client.create_window().await?; window.map().await?; - let workspace_rect = ds.output.workspace_rect.get(); + tassert_eq!(window.tl.core.width.get(), 800); + tassert_eq!( + window.tl.core.height.get(), + 600 - 2 * run.state.theme.title_plus_underline_height() + ); - tassert_eq!(window.tl.core.width.get(), workspace_rect.width()); - tassert_eq!(window.tl.core.height.get(), workspace_rect.height()); - - tassert_eq!(window.tl.server.node_absolute_position(), workspace_rect); + tassert_eq!( + window.tl.server.node_absolute_position(), + Rect::new_sized( + 0, + 2 * run.state.theme.title_plus_underline_height(), + window.tl.core.width.get(), + window.tl.core.height.get(), + ) + .unwrap() + ); Ok(()) } diff --git a/src/it/tests/t0003_multi_window.rs b/src/it/tests/t0003_multi_window.rs index db726f90..3fbf599c 100644 --- a/src/it/tests/t0003_multi_window.rs +++ b/src/it/tests/t0003_multi_window.rs @@ -11,7 +11,7 @@ testcase!(); /// Create and map two surfaces async fn test(run: Rc) -> Result<(), TestError> { - let ds = run.create_default_setup().await?; + run.backend.install_default()?; let client = run.create_client().await?; @@ -21,30 +21,17 @@ async fn test(run: Rc) -> Result<(), TestError> { let window2 = client.create_window().await?; window2.map().await?; - let workspace_rect = ds.output.workspace_rect.get(); + let otop = 2 * run.state.theme.title_plus_underline_height(); let bw = run.state.theme.sizes.border_width.get(); - let child_width = (workspace_rect.width() - bw) / 2; tassert_eq!( window.tl.server.node_absolute_position(), - Rect::new_sized( - workspace_rect.x1(), - workspace_rect.y1(), - child_width, - workspace_rect.height(), - ) - .unwrap() + Rect::new_sized(0, otop, (800 - bw) / 2, 600 - otop).unwrap() ); tassert_eq!( window2.tl.server.node_absolute_position(), - Rect::new_sized( - workspace_rect.x1() + child_width + bw, - workspace_rect.y1(), - child_width, - workspace_rect.height(), - ) - .unwrap() + Rect::new_sized((800 - bw) / 2 + bw, otop, (800 - bw) / 2, 600 - otop).unwrap() ); Ok(()) diff --git a/src/it/tests/t0007_subsurface/screenshot_1.qoi b/src/it/tests/t0007_subsurface/screenshot_1.qoi index b5954651..230c0408 100644 Binary files a/src/it/tests/t0007_subsurface/screenshot_1.qoi and b/src/it/tests/t0007_subsurface/screenshot_1.qoi differ diff --git a/src/it/tests/t0007_subsurface/screenshot_2.qoi b/src/it/tests/t0007_subsurface/screenshot_2.qoi index 718d5c29..722271f6 100644 Binary files a/src/it/tests/t0007_subsurface/screenshot_2.qoi and b/src/it/tests/t0007_subsurface/screenshot_2.qoi differ diff --git a/src/it/tests/t0014_container_scroll_focus.rs b/src/it/tests/t0014_container_scroll_focus.rs index dccd1096..0186cbaf 100644 --- a/src/it/tests/t0014_container_scroll_focus.rs +++ b/src/it/tests/t0014_container_scroll_focus.rs @@ -48,18 +48,13 @@ async fn test(run: Rc) -> TestResult { let mono_container = w_mono2.tl.container_parent()?; let container_pos = mono_container.tl_data().pos.get(); - let (tab_x, tab_y) = { - let tab_bar = mono_container.tab_bar.borrow(); - let Some(tab_bar) = tab_bar.as_ref() else { - bail!("no tab bar"); - }; - let w_mono1_title = &tab_bar.entries[0]; - ( - container_pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2, - container_pos.y1() + tab_bar.height / 2, - ) - }; - ds.mouse.abs(&ds.connector, tab_x as _, tab_y as _); + let w_mono1_title = mono_container.render_data.borrow_mut().title_rects[0] + .move_(container_pos.x1(), container_pos.y1()); + ds.mouse.abs( + &ds.connector, + w_mono1_title.x1() as _, + w_mono1_title.y1() as _, + ); client.sync().await; tassert!(enters.next().is_err()); diff --git a/src/it/tests/t0015_scroll_partial.rs b/src/it/tests/t0015_scroll_partial.rs index f5cb6e3c..c6cf49b7 100644 --- a/src/it/tests/t0015_scroll_partial.rs +++ b/src/it/tests/t0015_scroll_partial.rs @@ -26,18 +26,12 @@ async fn test(run: Rc) -> TestResult { let container = w_mono2.tl.container_parent()?; let pos = container.tl_data().pos.get(); - let (tab_x, tab_y) = { - let tab_bar = container.tab_bar.borrow(); - let Some(tab_bar) = tab_bar.as_ref() else { - bail!("no tab bar"); - }; - let w_mono1_title = &tab_bar.entries[0]; - ( - pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2, - pos.y1() + tab_bar.height / 2, - ) - }; - ds.mouse.abs(&ds.connector, tab_x as f64, tab_y as f64); + let w_mono1_title = container.render_data.borrow_mut().title_rects[0].move_(pos.x1(), pos.y1()); + ds.mouse.abs( + &ds.connector, + w_mono1_title.x1() as f64, + w_mono1_title.y1() as f64, + ); client.sync().await; let enters = dss.kb.enter.expect()?; diff --git a/src/it/tests/t0020_surface_offset/screenshot_1.qoi b/src/it/tests/t0020_surface_offset/screenshot_1.qoi index 4c826f86..eef5f37a 100644 Binary files a/src/it/tests/t0020_surface_offset/screenshot_1.qoi and b/src/it/tests/t0020_surface_offset/screenshot_1.qoi differ diff --git a/src/it/tests/t0020_surface_offset/screenshot_2.qoi b/src/it/tests/t0020_surface_offset/screenshot_2.qoi index 0fb763e2..7e8cf143 100644 Binary files a/src/it/tests/t0020_surface_offset/screenshot_2.qoi and b/src/it/tests/t0020_surface_offset/screenshot_2.qoi differ diff --git a/src/it/tests/t0022_toplevel_suspended.rs b/src/it/tests/t0022_toplevel_suspended.rs index 524856e3..1fdacb1a 100644 --- a/src/it/tests/t0022_toplevel_suspended.rs +++ b/src/it/tests/t0022_toplevel_suspended.rs @@ -2,7 +2,7 @@ use { crate::{ ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED, it::{ - test_error::{TestErrorExt, TestResult}, + test_error::TestResult, test_utils::{ test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt, }, @@ -10,7 +10,7 @@ use { }, }, isnt::std_1::collections::IsntHashSetExt, - std::{rc::Rc, time::Duration}, + std::rc::Rc, }; testcase!(); @@ -19,7 +19,6 @@ async fn test(run: Rc) -> TestResult { let ds = run.create_default_setup().await?; let client = run.create_client().await?; - let default_seat = client.get_default_seat().await?; let win1 = client.create_window().await?; win1.set_color(255, 0, 0, 255); @@ -45,23 +44,5 @@ async fn test(run: Rc) -> TestResult { client.sync().await; tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED)); - let leaves = default_seat.kb.leave.expect()?; - let enters = default_seat.kb.enter.expect()?; - - run.cfg.set_idle(Duration::from_micros(100))?; - run.cfg.set_idle_grace_period(Duration::from_secs(0))?; - run.state.wheel.timeout(3).await?; - - client.sync().await; - tassert!(win2.tl.core.states.borrow().contains(&STATE_SUSPENDED)); - let leave = leaves.next().with_context(|| "no leave on suspend")?; - tassert_eq!(leave.surface, win2.surface.id); - - ds.mouse.rel(1.0, 1.0); - client.sync().await; - tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED)); - let enter = enters.next().with_context(|| "no enter on restore")?; - tassert_eq!(enter.surface, win2.surface.id); - Ok(()) } diff --git a/src/it/tests/t0023_xdg_activation/screenshot_1.qoi b/src/it/tests/t0023_xdg_activation/screenshot_1.qoi index 960da20a..1fa8d204 100644 Binary files a/src/it/tests/t0023_xdg_activation/screenshot_1.qoi and b/src/it/tests/t0023_xdg_activation/screenshot_1.qoi differ diff --git a/src/it/tests/t0026_output_transform/screenshot_1.qoi b/src/it/tests/t0026_output_transform/screenshot_1.qoi index f11111bb..2206fc85 100644 Binary files a/src/it/tests/t0026_output_transform/screenshot_1.qoi and b/src/it/tests/t0026_output_transform/screenshot_1.qoi differ diff --git a/src/it/tests/t0028_top_level_restacking/screenshot_1.qoi b/src/it/tests/t0028_top_level_restacking/screenshot_1.qoi index 9f5fca3c..f7bf53bf 100644 Binary files a/src/it/tests/t0028_top_level_restacking/screenshot_1.qoi and b/src/it/tests/t0028_top_level_restacking/screenshot_1.qoi differ diff --git a/src/it/tests/t0028_top_level_restacking/screenshot_2.qoi b/src/it/tests/t0028_top_level_restacking/screenshot_2.qoi index aaf1b108..b454acd3 100644 Binary files a/src/it/tests/t0028_top_level_restacking/screenshot_2.qoi and b/src/it/tests/t0028_top_level_restacking/screenshot_2.qoi differ diff --git a/src/it/tests/t0029_double_click_float/screenshot_1.qoi b/src/it/tests/t0029_double_click_float/screenshot_1.qoi index e08dc525..dd974ccf 100644 Binary files a/src/it/tests/t0029_double_click_float/screenshot_1.qoi and b/src/it/tests/t0029_double_click_float/screenshot_1.qoi differ diff --git a/src/it/tests/t0029_double_click_float/screenshot_2.qoi b/src/it/tests/t0029_double_click_float/screenshot_2.qoi index e08dc525..f49edd4d 100644 Binary files a/src/it/tests/t0029_double_click_float/screenshot_2.qoi and b/src/it/tests/t0029_double_click_float/screenshot_2.qoi differ diff --git a/src/it/tests/t0037_toplevel_drag/screenshot_2.qoi b/src/it/tests/t0037_toplevel_drag/screenshot_2.qoi index 36c68e4e..b9826001 100644 Binary files a/src/it/tests/t0037_toplevel_drag/screenshot_2.qoi and b/src/it/tests/t0037_toplevel_drag/screenshot_2.qoi differ diff --git a/src/it/tests/t0038_subsurface_parent_state/screenshot_1.qoi b/src/it/tests/t0038_subsurface_parent_state/screenshot_1.qoi index e6f6db74..988bc767 100644 Binary files a/src/it/tests/t0038_subsurface_parent_state/screenshot_1.qoi and b/src/it/tests/t0038_subsurface_parent_state/screenshot_1.qoi differ diff --git a/src/it/tests/t0038_subsurface_parent_state/screenshot_2.qoi b/src/it/tests/t0038_subsurface_parent_state/screenshot_2.qoi index 9abc8de3..a7509404 100644 Binary files a/src/it/tests/t0038_subsurface_parent_state/screenshot_2.qoi and b/src/it/tests/t0038_subsurface_parent_state/screenshot_2.qoi differ diff --git a/src/it/tests/t0039_alpha_modifier/screenshot_1.qoi b/src/it/tests/t0039_alpha_modifier/screenshot_1.qoi index 80a29c84..8fe5d0b2 100644 Binary files a/src/it/tests/t0039_alpha_modifier/screenshot_1.qoi and b/src/it/tests/t0039_alpha_modifier/screenshot_1.qoi differ diff --git a/src/it/tests/t0039_alpha_modifier/screenshot_2.qoi b/src/it/tests/t0039_alpha_modifier/screenshot_2.qoi index 735af290..9874e2f5 100644 Binary files a/src/it/tests/t0039_alpha_modifier/screenshot_2.qoi and b/src/it/tests/t0039_alpha_modifier/screenshot_2.qoi differ diff --git a/src/it/tests/t0041_input_method/screenshot_1.qoi b/src/it/tests/t0041_input_method/screenshot_1.qoi index cd07ecd4..d25fcf64 100644 Binary files a/src/it/tests/t0041_input_method/screenshot_1.qoi and b/src/it/tests/t0041_input_method/screenshot_1.qoi differ diff --git a/src/it/tests/t0041_input_method/screenshot_2.qoi b/src/it/tests/t0041_input_method/screenshot_2.qoi index d76ea9a0..7f93231a 100644 Binary files a/src/it/tests/t0041_input_method/screenshot_2.qoi and b/src/it/tests/t0041_input_method/screenshot_2.qoi differ diff --git a/src/it/tests/t0041_input_method/screenshot_3.qoi b/src/it/tests/t0041_input_method/screenshot_3.qoi index cd07ecd4..d25fcf64 100644 Binary files a/src/it/tests/t0041_input_method/screenshot_3.qoi and b/src/it/tests/t0041_input_method/screenshot_3.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_1.qoi b/src/it/tests/t0042_toplevel_select/screenshot_1.qoi index 6d57d140..6423ef6d 100644 Binary files a/src/it/tests/t0042_toplevel_select/screenshot_1.qoi and b/src/it/tests/t0042_toplevel_select/screenshot_1.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_2.qoi b/src/it/tests/t0042_toplevel_select/screenshot_2.qoi index 478b3c43..823fd750 100644 Binary files a/src/it/tests/t0042_toplevel_select/screenshot_2.qoi and b/src/it/tests/t0042_toplevel_select/screenshot_2.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_3.qoi b/src/it/tests/t0042_toplevel_select/screenshot_3.qoi index 478b3c43..823fd750 100644 Binary files a/src/it/tests/t0042_toplevel_select/screenshot_3.qoi and b/src/it/tests/t0042_toplevel_select/screenshot_3.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_4.qoi b/src/it/tests/t0042_toplevel_select/screenshot_4.qoi index 07dd87fb..714222f1 100644 Binary files a/src/it/tests/t0042_toplevel_select/screenshot_4.qoi and b/src/it/tests/t0042_toplevel_select/screenshot_4.qoi differ diff --git a/src/it/tests/t0047_surface_damage.rs b/src/it/tests/t0047_surface_damage.rs index c2d0d6dd..d9760bc8 100644 --- a/src/it/tests/t0047_surface_damage.rs +++ b/src/it/tests/t0047_surface_damage.rs @@ -308,8 +308,9 @@ async fn test(run: Rc) -> TestResult { let output_damage = connector_data.damage.borrow(); tassert!(!output_damage.is_empty()); - // The test window maps its 1x1 buffer through a viewport to the full window size. - let expected_buffer_damage = surface_pos; + // Buffer damage is transformed by the damage matrix which includes the surface position + // The buffer damage (0,0,1,1) should be transformed to surface coordinates + let expected_buffer_damage = buffer_damage.move_(surface_pos.x1(), surface_pos.y1()); // Find the exact output damage that matches our expected buffer damage let mut found_exact_buffer_damage = false; @@ -330,12 +331,10 @@ async fn test(run: Rc) -> TestResult { // Test 7: Check output damage from existing window's viewport (which already has scaling) connector_data.damage.borrow_mut().clear(); - // The existing window was created with create_surface_ext() which automatically creates a viewport. - // Commit the viewport size change separately; that commit intentionally damages the old/new extents. - window.surface.viewport.set_destination(150, 100)?; - window.surface.commit()?; - client.sync().await; - connector_data.damage.borrow_mut().clear(); + // The existing window was created with create_surface_ext() which automatically creates a viewport + // Let's verify that the viewport's existing scaling affects buffer damage correctly + // First, let's modify the viewport scaling that already exists on the window + window.surface.viewport.set_destination(150, 100)?; // Change scaling to 150x100 // Add buffer damage to test viewport scaling coordinate transformation window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer @@ -347,8 +346,8 @@ async fn test(run: Rc) -> TestResult { let output_damage = connector_data.damage.borrow(); tassert!(!output_damage.is_empty()); - // With viewporter scaling, the 1x1 buffer damage should scale to the viewport destination. - let surface_pos = window.surface.server.buffer_abs_pos.get(); + // With viewporter scaling, the 1x1 buffer damage should scale to 150x100 + // and be moved by surface position (0, 36) to get output coordinates (0, 36, 150, 136) let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap(); let expected_output_damage = expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1()); @@ -403,9 +402,8 @@ async fn test(run: Rc) -> TestResult { rotation_window.map().await?; client.sync().await; - // Disable viewporter to rely purely on buffer dimensions. - rotation_window.surface.viewport.unset_source()?; - rotation_window.surface.viewport.unset_destination()?; + // Disable viewporter by setting destination to 0x0 to rely purely on buffer dimensions + rotation_window.surface.viewport.set_destination(0, 0)?; // Disable viewporter // Use a rectangular buffer (4x2) so rotation has a visible geometric effect // Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer diff --git a/src/it/tests/t0055_autotiling.rs b/src/it/tests/t0055_autotiling.rs deleted file mode 100644 index 4b3611c4..00000000 --- a/src/it/tests/t0055_autotiling.rs +++ /dev/null @@ -1,58 +0,0 @@ -use { - crate::{ - it::{test_error::TestResult, testrun::TestRun}, - tree::{ContainerSplit, Node, ToplevelNodeBase}, - }, - std::rc::Rc, -}; - -testcase!(); - -async fn test(run: Rc) -> TestResult { - run.backend.install_default()?; - run.cfg.set_autotile(true)?; - - let client = run.create_client().await?; - - let win1 = client.create_window().await?; - win1.map().await?; - let root = win1.tl.container_parent()?; - tassert_eq!(root.split.get(), ContainerSplit::Horizontal); - - let win2 = client.create_window().await?; - win2.map().await?; - client.sync().await; - - tassert_eq!(root.split.get(), ContainerSplit::Horizontal); - tassert_eq!(win1.tl.container_parent()?.node_id(), root.node_id()); - tassert_eq!(win2.tl.container_parent()?.node_id(), root.node_id()); - - let win3 = client.create_window().await?; - win3.map().await?; - client.sync().await; - - let v_group = win3.tl.container_parent()?; - tassert_eq!(root.split.get(), ContainerSplit::Horizontal); - tassert_eq!(v_group.split.get(), ContainerSplit::Vertical); - tassert_eq!(win2.tl.container_parent()?.node_id(), v_group.node_id()); - - let win4 = client.create_window().await?; - win4.map().await?; - client.sync().await; - - let h_group = win4.tl.container_parent()?; - tassert_eq!(h_group.split.get(), ContainerSplit::Horizontal); - tassert_eq!(win3.tl.container_parent()?.node_id(), h_group.node_id()); - let h_parent = match h_group - .tl_data() - .parent - .get() - .and_then(|p| p.node_into_container()) - { - Some(parent) => parent, - None => bail!("autotile group does not have a container parent"), - }; - tassert_eq!(h_parent.node_id(), v_group.node_id()); - - Ok(()) -} diff --git a/src/it/tests/t0055_scratchpad.rs b/src/it/tests/t0055_scratchpad.rs deleted file mode 100644 index 5abf2440..00000000 --- a/src/it/tests/t0055_scratchpad.rs +++ /dev/null @@ -1,107 +0,0 @@ -use { - crate::{ - it::{test_error::TestResult, testrun::TestRun}, - tree::{Node, ToplevelNodeBase}, - }, - std::rc::Rc, -}; - -testcase!(); - -async fn test(run: Rc) -> TestResult { - let ds = run.create_default_setup().await?; - - let client = run.create_client().await?; - let win1 = client.create_window().await?; - win1.map2().await?; - let win2 = client.create_window().await?; - win2.map2().await?; - - run.cfg.send_to_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(win1.tl.server.node_visible()); - tassert!(!win2.tl.server.node_visible()); - - run.cfg.show_workspace(ds.seat.id(), "2")?; - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(win2.tl.server.node_visible()); - tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2"); - - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(!win2.tl.server.node_visible()); - - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(win2.tl.server.node_visible()); - tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2"); - - run.cfg.show_workspace(ds.seat.id(), "3")?; - client.sync().await; - tassert!(!win2.tl.server.node_visible()); - - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(win2.tl.server.node_visible()); - tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "3"); - // Scratchpad windows are always shown floating. - tassert!(win2.tl.server.tl_data().parent_is_float.get()); - - // Park win2 again, then build a multi-window scratchpad and cycle it. - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(!win2.tl.server.node_visible()); - - // Build a three-window scratchpad. Each window is focused right after it is - // mapped, so sending the focused window parks them in a known order. - let cyc1 = client.create_window().await?; - cyc1.map2().await?; - run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?; - let cyc2 = client.create_window().await?; - cyc2.map2().await?; - run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?; - let cyc3 = client.create_window().await?; - cyc3.map2().await?; - run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(!cyc1.tl.server.node_visible()); - tassert!(!cyc2.tl.server.node_visible()); - tassert!(!cyc3.tl.server.node_visible()); - - // Nothing shown: cycle brings up the first window (insertion order: cyc1). - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(cyc1.tl.server.node_visible()); - tassert!(!cyc2.tl.server.node_visible()); - tassert!(!cyc3.tl.server.node_visible()); - // Scratchpad windows are always shown floating. - tassert!(cyc1.tl.server.tl_data().parent_is_float.get()); - - // Cycle advances one at a time. - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(!cyc1.tl.server.node_visible()); - tassert!(cyc2.tl.server.node_visible()); - tassert!(!cyc3.tl.server.node_visible()); - - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(!cyc1.tl.server.node_visible()); - tassert!(!cyc2.tl.server.node_visible()); - tassert!(cyc3.tl.server.node_visible()); - - // On the final window, the next cycle hides everything. - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(!cyc1.tl.server.node_visible()); - tassert!(!cyc2.tl.server.node_visible()); - tassert!(!cyc3.tl.server.node_visible()); - - // And it wraps back to the first window. - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(cyc1.tl.server.node_visible()); - - Ok(()) -} diff --git a/src/state.rs b/src/state.rs index 74facaf1..42dd909d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -114,11 +114,9 @@ use { tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode, FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode, - PlaceholderNode, TearingMode, TileState, ToplevelData, - ToplevelIdentifier, ToplevelNode, ToplevelNodeBase, Transform, VrrMode, - WorkspaceDisplayOrder, WorkspaceNode, WorkspaceNodeId, WsMoveConfig, - generic_node_visitor, move_ws_to_output, toplevel_hide_for_scratchpad, - toplevel_restore_from_scratchpad, toplevel_set_workspace, + PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier, + ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder, + WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output, }, udmabuf::UdmabufHolder, utils::{ @@ -414,7 +412,6 @@ pub struct State { pub bo_drop_queue: Rc>>, pub virtual_outputs: VirtualOutputs, pub clean_logs_older_than: Cell>, - pub scratchpads: RefCell>>>, } // impl Drop for State { @@ -462,27 +459,6 @@ pub struct IdleState { pub in_grace_period: Cell, } -pub struct ScratchpadEntry { - node: Weak, - identifier: ToplevelIdentifier, - hidden: Cell, -} - -impl ScratchpadEntry { - fn alive(&self) -> bool { - self.node().is_some() - } - - fn node(&self) -> Option> { - let node = self.node.upgrade()?; - if node.tl_data().identifier.get() == self.identifier { - Some(node) - } else { - None - } - } -} - impl IdleState { pub fn set_timeout(&self, state: &State, timeout: Duration) { self.timeout.set(timeout); @@ -949,39 +925,19 @@ impl State { && node.tl_data().kind.is_app_window() && !node.tl_data().visible.get(); if animate_new_app_map { - self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone(), true)); + self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone())); } else { - self.do_map_tiled(seat.as_deref(), node.clone(), true); + self.do_map_tiled(seat.as_deref(), node.clone()); } self.focus_after_map(node, seat.as_deref()); } - pub fn map_tiled_without_autotile(self: &Rc, node: Rc) { - let seat = self.seat_queue.last(); - self.do_map_tiled(seat.as_deref(), node.clone(), false); - self.focus_after_map(node, seat.as_deref()); - } - - fn do_map_tiled( - self: &Rc, - seat: Option<&Rc>, - node: Rc, - autotile: bool, - ) { + fn do_map_tiled(self: &Rc, seat: Option<&Rc>, node: Rc) { let ws = self.ensure_map_workspace(seat); - self.map_tiled_on_(node, &ws, autotile); + self.map_tiled_on(node, &ws); } pub fn map_tiled_on(self: &Rc, node: Rc, ws: &Rc) { - self.map_tiled_on_(node, ws, false); - } - - fn map_tiled_on_( - self: &Rc, - node: Rc, - ws: &Rc, - autotile: bool, - ) { if let Some(c) = ws.container.get() { let la = c.clone().tl_last_active_child(); let lap = la @@ -990,11 +946,7 @@ impl State { .get() .and_then(|n| n.node_into_container()); if let Some(lap) = lap { - if autotile { - lap.add_tiled_child_after(&*la, node); - } else { - lap.add_child_after(&*la, node); - } + lap.add_child_after(&*la, node); } else { c.append_child(node); } @@ -1047,146 +999,6 @@ impl State { float } - pub fn send_to_scratchpad(self: &Rc, name: &str, node: Rc) { - if node.node_is_placeholder() { - return; - } - let identifier = node.tl_data().identifier.get(); - if !toplevel_hide_for_scratchpad(node.clone()) { - return; - } - let entry = Rc::new(ScratchpadEntry { - node: Rc::downgrade(&node), - identifier, - hidden: Cell::new(true), - }); - { - let mut scratchpads = self.scratchpads.borrow_mut(); - for entries in scratchpads.values_mut() { - entries.retain(|entry| entry.alive() && entry.identifier != identifier); - } - scratchpads - .entry(name.to_string()) - .or_default() - .push(entry); - } - self.tree_changed(); - } - - pub fn toggle_scratchpad(self: &Rc, seat: &Rc, name: &str) { - let entry = { - let mut scratchpads = self.scratchpads.borrow_mut(); - let Some(entries) = scratchpads.get_mut(name) else { - return; - }; - entries.retain(|entry| entry.alive()); - // Prefer the currently-shown window; otherwise act on the most recent. - entries - .iter() - .rev() - .find(|entry| !entry.hidden.get()) - .or_else(|| entries.last()) - .cloned() - }; - let Some(entry) = entry else { - return; - }; - if entry.hidden.get() { - self.show_scratchpad_entry(seat, name, &entry); - } else if entry.node().is_some_and(|node| !node.node_visible()) { - self.move_scratchpad_entry_to_current_workspace(seat, &entry); - } else { - self.hide_scratchpad_entry(&entry); - } - } - - /// Cycles through the windows of a scratchpad, one at a time: - /// nothing shown -> first window -> ... -> last window -> nothing shown. - pub fn cycle_scratchpad(self: &Rc, seat: &Rc, name: &str) { - let (current, next) = { - let mut scratchpads = self.scratchpads.borrow_mut(); - let Some(entries) = scratchpads.get_mut(name) else { - return; - }; - entries.retain(|entry| entry.alive()); - match entries.iter().position(|entry| !entry.hidden.get()) { - // Nothing shown yet: bring up the first window. - None => (None, entries.first().cloned()), - // Hide the shown window and advance; on the last window, `next` - // is `None`, so the scratchpad toggles off. - Some(i) => (entries.get(i).cloned(), entries.get(i + 1).cloned()), - } - }; - if let Some(current) = ¤t { - self.hide_scratchpad_entry(current); - } - if let Some(next) = &next { - self.show_scratchpad_entry(seat, name, next); - } - } - - fn hide_scratchpad_entry(self: &Rc, entry: &Rc) { - if entry.hidden.get() { - return; - } - let Some(node) = entry.node() else { - return; - }; - if toplevel_hide_for_scratchpad(node) { - entry.hidden.set(true); - self.tree_changed(); - } - } - - fn show_scratchpad_entry( - self: &Rc, - seat: &Rc, - name: &str, - entry: &Rc, - ) { - if !entry.hidden.get() { - return; - } - let Some(node) = entry.node() else { - return; - }; - // Only one window of a scratchpad is visible at a time. - let siblings: Vec<_> = { - let scratchpads = self.scratchpads.borrow(); - scratchpads - .get(name) - .into_iter() - .flatten() - .filter(|sibling| !Rc::ptr_eq(sibling, entry) && !sibling.hidden.get()) - .cloned() - .collect() - }; - for sibling in siblings { - self.hide_scratchpad_entry(&sibling); - } - let ws = seat.get_fallback_output().ensure_workspace(); - toplevel_restore_from_scratchpad(self, node.clone(), &ws); - entry.hidden.set(false); - node.node_do_focus(seat, Direction::Unspecified); - seat.maybe_schedule_warp_mouse_to_focus(); - self.tree_changed(); - } - - fn move_scratchpad_entry_to_current_workspace( - self: &Rc, - seat: &Rc, - entry: &Rc, - ) { - let Some(node) = entry.node() else { - return; - }; - let ws = seat.get_fallback_output().ensure_workspace(); - toplevel_set_workspace(self, node.clone(), &ws); - node.node_do_focus(seat, Direction::Unspecified); - seat.maybe_schedule_warp_mouse_to_focus(); - self.tree_changed(); - } - fn focus_after_map(&self, node: Rc, seat: Option<&Rc>) { if !node.node_visible() { return; @@ -1462,7 +1274,6 @@ impl State { self.node_at_tree.borrow_mut().clear(); self.position_hint_requests.clear(); self.pending_warp_mouse_to_focus.clear(); - self.scratchpads.borrow_mut().clear(); self.head_managers.clear(); self.head_managers_async.clear(); self.const_40hz_latch.clear(); diff --git a/src/tree/container.rs b/src/tree/container.rs index 44a6a778..b8de7b25 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -32,7 +32,6 @@ use { numcell::NumCell, on_drop_event::OnDropEvent, rc_eq::rc_eq, - scroller::Scroller, threshold_counter::ThresholdCounter, }, }, @@ -151,7 +150,6 @@ pub struct ContainerNode { pub child_removed: Rc, pub all_children_resized: Rc, pub tab_bar: RefCell>, - scroll: Scroller, pub update_tab_textures_scheduled: Cell, pub ephemeral: Cell, } @@ -268,7 +266,6 @@ impl ContainerNode { child_removed: state.lazy_event_sources.create_source(), all_children_resized: state.post_layout_event_sources.create_source(), tab_bar: RefCell::new(None), - scroll: Default::default(), update_tab_textures_scheduled: Cell::new(false), ephemeral: Cell::new(Ephemeral::Off), }); @@ -293,47 +290,6 @@ impl ContainerNode { self.add_child_x(prev, new, |prev, new| self.add_child_after_(prev, new)); } - pub fn add_tiled_child_after(self: &Rc, prev: &dyn Node, new: Rc) { - if !self.state.theme.autotile_enabled.get() - || self.mono_child.is_some() - || self.num_children.get() <= 1 - { - self.add_child_after(prev, new); - return; - } - let focused = self - .child_nodes - .borrow() - .get(&prev.node_id()) - .map(|n| n.to_ref()); - let Some(focused) = focused else { - log::error!( - "Tried to autotile a child into a container but the preceding node is not in the container" - ); - return; - }; - let focused_node = focused.node.clone(); - let focused_active = focused_node.tl_data().active(); - let sub = ContainerNode::new( - &self.state, - &self.workspace.get(), - focused_node.clone(), - self.split.get().other(), - ); - // Autotile-created groups are structural and collapse once only one - // child remains. Explicit make-group commands control their own - // grouping through the regular manual paths. - sub.ephemeral.set(Ephemeral::On); - sub.append_child(new); - let sub_id = sub.node_id(); - self.clone().cnode_replace_child(&*focused_node, sub); - if focused_active - && let Some(group) = self.child_nodes.borrow().get(&sub_id).map(|n| n.to_ref()) - { - self.update_child_active(&group, true, 1); - } - } - pub fn add_child_before(self: &Rc, prev: &dyn Node, new: Rc) { self.add_child_x(prev, new, |prev, new| self.add_child_before_(prev, new)); } @@ -796,18 +752,6 @@ impl ContainerNode { self.activate_child2(child, false); } - fn activate_child_from_input( - self: &Rc, - child: &NodeRef, - seat: &Rc, - ) { - self.activate_child(child); - child - .node - .clone() - .node_do_focus(seat, Direction::Unspecified); - } - fn activate_child2(self: &Rc, child: &NodeRef, preserve_focus: bool) { if let Some(mc) = self.mono_child.get() { if mc.node.node_id() == child.node.node_id() { @@ -1425,6 +1369,42 @@ impl ContainerNode { } pub fn insert_child(self: &Rc, node: Rc, direction: Direction) { + // Autotile: if the container would become too narrow/tall, wrap the + // focused child and new node in a perpendicular sub-container. + if self.state.theme.autotile_enabled.get() && self.mono_child.is_none() { + let (pw, ph) = self.predict_child_body_size(); + let opposite = match self.split.get() { + ContainerSplit::Horizontal if pw > 0 && ph > 0 && pw < ph => { + Some(ContainerSplit::Vertical) + } + ContainerSplit::Vertical if pw > 0 && ph > 0 && ph < pw => { + Some(ContainerSplit::Horizontal) + } + _ => None, + }; + if let Some(opp_split) = opposite { + if let Some(focused) = self.focus_history.last() { + if self.num_children.get() <= 1 { + // Single child, autotile not applicable. + } else { + let focused_node = focused.node.clone(); + let was_ephemeral = self.ephemeral.replace(Ephemeral::Off); + self.clone().cnode_remove_child2(&*focused_node, true); + self.ephemeral.set(was_ephemeral); + let sub = ContainerNode::new( + &self.state, + &self.workspace.get(), + focused_node, + opp_split, + ); + sub.ephemeral.set(Ephemeral::On); + sub.append_child(node); + self.append_child(sub); + return; + } + } + } + } let (split, right) = direction_to_split(direction); if split != self.split.get() || right { self.append_child(node); @@ -1534,7 +1514,7 @@ impl ContainerNode { fn button( self: Rc, id: CursorType, - seat: &Rc, + _seat: &Rc, _time_usec: u64, pressed: bool, button: u32, @@ -1564,7 +1544,7 @@ impl ContainerNode { if let Some(child) = children.get(&child_id) { let child_ref = child.to_ref(); drop(children); - self.activate_child_from_input(&child_ref, seat); + self.activate_child(&child_ref); } return; } @@ -2081,33 +2061,31 @@ impl Node for ContainerNode { self.button(id, seat, time_usec, state == ButtonState::Pressed, button); } - fn node_on_axis_event(self: Rc, seat: &Rc, event: &PendingScroll) { + fn node_on_axis_event(self: Rc, _seat: &Rc, event: &PendingScroll) { if self.mono_child.is_none() { return; } - let steps = match self.scroll.handle(event) { - Some(steps) => steps, + // Use vertical scroll (index 1) to switch tabs. + let v = match event.v120[1].get() { + Some(v) if v != 0 => v, _ => return, }; - let mut target = match self.mono_child.get() { + let mono = match self.mono_child.get() { Some(m) => m, None => return, }; - let current_id = target.node.node_id(); - for _ in 0..steps.abs() { - let next = if steps > 0 { - target.next().or_else(|| self.children.first()) - } else { - target.prev().or_else(|| self.children.last()) - }; - match next { - Some(next) => target = next, - None => break, + let next = if v > 0 { + // Scroll down → next tab. + mono.next().or_else(|| self.children.first()) + } else { + // Scroll up → previous tab. + mono.prev().or_else(|| self.children.last()) + }; + if let Some(next) = next { + if next.node.node_id() != mono.node.node_id() { + self.activate_child(&next); } } - if target.node.node_id() != current_id { - self.activate_child_from_input(&target, seat); - } } fn node_on_leave(&self, seat: &WlSeatGlobal) { diff --git a/src/tree/display.rs b/src/tree/display.rs index 26b31a88..440916bf 100644 --- a/src/tree/display.rs +++ b/src/tree/display.rs @@ -8,25 +8,18 @@ use { renderer::Renderer, state::State, tree::{ - Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, - NodeLocation, OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, + OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination, WorkspaceNodeId, walker::NodeVisitor, }, utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList}, }, - std::{ - cell::{Cell, RefCell}, - mem, - ops::Deref, - rc::{Rc, Weak}, - }, + std::{cell::Cell, ops::Deref, rc::Rc}, }; pub struct DisplayNode { pub id: NodeId, pub extents: Cell, - visible: Cell, - suspend_restore_kb_foci: RefCell, Weak)>>, pub outputs: CopyHashMap>, pub stacked: Rc>>, pub stacked_above_layers: Rc>>, @@ -38,8 +31,6 @@ impl DisplayNode { let slf = Self { id, extents: Default::default(), - visible: Default::default(), - suspend_restore_kb_foci: Default::default(), outputs: Default::default(), stacked: Default::default(), stacked_above_layers: Default::default(), @@ -80,17 +71,6 @@ impl DisplayNode { pub fn update_visible(&self, state: &State) { let visible = state.root_visible(); - let was_visible = self.visible.replace(visible); - if !visible && was_visible { - let mut foci = self.suspend_restore_kb_foci.borrow_mut(); - foci.clear(); - for seat in state.globals.seats.lock().values() { - let node = seat.get_keyboard_node(); - if node.node_id() != self.id { - foci.push((seat.clone(), Rc::downgrade(&node))); - } - } - } for output in self.outputs.lock().values() { output.update_visible(); } @@ -102,20 +82,6 @@ impl DisplayNode { for seat in state.globals.seats.lock().values() { seat.set_visible(visible); } - if visible && !was_visible { - for (seat, node) in mem::take(&mut *self.suspend_restore_kb_foci.borrow_mut()) { - if seat.get_keyboard_node().node_id() == self.id { - if let Some(node) = node.upgrade() - && node.node_visible() - { - seat.focus_node(node); - } else { - seat.get_fallback_output() - .take_keyboard_navigation_focus(&seat, Direction::Unspecified); - } - } - } - } if visible { state.damage(self.extents.get()); } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index c0a2f013..312b4ac6 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -979,7 +979,7 @@ impl ToplevelData { } fd.workspace.remove_fullscreen_node(); if fd.placeholder.is_destroyed() { - state.map_tiled_without_autotile(node); + state.map_tiled(node); return; } let parent = fd.placeholder.tl_data().parent.take().unwrap(); @@ -1262,7 +1262,7 @@ pub fn toplevel_set_floating(state: &Rc, tl: Rc, floati }; if !floating { parent.cnode_remove_child2(&*tl, true); - state.map_tiled_without_autotile(tl); + state.map_tiled(tl); } else if let Some(ws) = data.workspace.get() { let node_id = data.node_id; let old_body = @@ -1323,54 +1323,3 @@ pub fn toplevel_set_workspace(state: &Rc, tl: Rc, ws: & tl.tl_set_fullscreen(true, Some(ws.clone())); } } - -/// Removes a toplevel from the tree so it can be parked in a scratchpad. -/// -/// Returns `true` if the window was hidden. A placeholder, a window without a -/// parent, or a window that refuses to leave fullscreen cannot be parked. -pub fn toplevel_hide_for_scratchpad(tl: Rc) -> bool { - if tl.node_is_placeholder() { - return false; - } - let data = tl.tl_data(); - let workspace = data.workspace.get(); - if data.is_fullscreen.get() { - tl.clone().tl_set_fullscreen(false, None); - if data.is_fullscreen.get() { - return false; - } - } - let Some(parent) = data.parent.get() else { - return false; - }; - let kb_foci = collect_kb_foci(tl.clone()); - parent.cnode_remove_child2(&*tl, true); - data.parent.take(); - data.float.take(); - if data.parent_is_float.replace(false) { - data.property_changed(TL_CHANGED_FLOATING); - } - if data.workspace.take().is_some() { - data.property_changed(TL_CHANGED_WORKSPACE); - } - tl.tl_set_visible(false); - if let Some(workspace) = &workspace { - for seat in kb_foci { - workspace - .clone() - .node_do_focus(&seat, Direction::Unspecified); - } - } - true -} - -/// Maps a parked scratchpad window back onto `ws`. Scratchpad windows always -/// return floating, regardless of how they were laid out before parking. -pub fn toplevel_restore_from_scratchpad( - state: &Rc, - tl: Rc, - ws: &Rc, -) { - let (width, height) = tl.tl_data().float_size(ws); - state.map_floating(tl.clone(), width, height, ws, None); -} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index b57de5ad..35aca02c 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -64,9 +64,6 @@ pub enum SimpleCommand { SetFloating(bool), ToggleFullscreen, SetFullscreen(bool), - SendToScratchpad, - ToggleScratchpad, - CycleScratchpad, Forward(bool), EnableWindowManagement(bool), SetFloatAboveFullscreen(bool), @@ -133,15 +130,6 @@ pub enum Action { MoveToWorkspace { name: String, }, - SendToScratchpad { - name: String, - }, - ToggleScratchpad { - name: String, - }, - CycleScratchpad { - name: String, - }, Multi { actions: Vec, }, @@ -612,14 +600,6 @@ pub struct Config { pub simple_im: Option, pub fallback_output_mode: Option, pub mouse_follows_focus: Option, - pub scratchpads: Vec, - pub autotile: Option, -} - -#[derive(Debug, Clone)] -pub struct Scratchpad { - pub name: String, - pub exec: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 98d3ab73..e353a2f8 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -41,7 +41,6 @@ pub mod modified_keysym; mod output; mod output_match; mod repeat_rate; -mod scratchpad; pub mod shortcuts; mod simple_im; mod status; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 29fdc3e4..7581198d 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -117,9 +117,6 @@ impl ActionParser<'_> { "toggle-fullscreen" => ToggleFullscreen, "enter-fullscreen" => SetFullscreen(true), "exit-fullscreen" => SetFullscreen(false), - "send-to-scratchpad" => SendToScratchpad, - "toggle-scratchpad" => ToggleScratchpad, - "cycle-scratchpad" => CycleScratchpad, "focus-parent" => FocusParent, "close" => Close, "disable-pointer-constraint" => DisablePointerConstraint, @@ -225,33 +222,6 @@ impl ActionParser<'_> { Ok(Action::MoveToWorkspace { name }) } - fn parse_send_to_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let name = ext - .extract(opt(str("name")))? - .map(|name| name.value) - .unwrap_or("") - .to_string(); - Ok(Action::SendToScratchpad { name }) - } - - fn parse_toggle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let name = ext - .extract(opt(str("name")))? - .map(|name| name.value) - .unwrap_or("") - .to_string(); - Ok(Action::ToggleScratchpad { name }) - } - - fn parse_cycle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let name = ext - .extract(opt(str("name")))? - .map(|name| name.value) - .unwrap_or("") - .to_string(); - Ok(Action::CycleScratchpad { name }) - } - fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let con = ext .extract(val("connector"))? @@ -581,9 +551,6 @@ impl Parser for ActionParser<'_> { "switch-to-vt" => self.parse_switch_to_vt(&mut ext), "show-workspace" => self.parse_show_workspace(&mut ext), "move-to-workspace" => self.parse_move_to_workspace(&mut ext), - "send-to-scratchpad" => self.parse_send_to_scratchpad(&mut ext), - "toggle-scratchpad" => self.parse_toggle_scratchpad(&mut ext), - "cycle-scratchpad" => self.parse_cycle_scratchpad(&mut ext), "configure-connector" => self.parse_configure_connector(&mut ext), "configure-input" => self.parse_configure_input(&mut ext), "configure-output" => self.parse_configure_output(&mut ext), diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 8e776860..d82be95b 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -28,7 +28,6 @@ use { log_level::LogLevelParser, output::OutputsParser, repeat_rate::RepeatRateParser, - scratchpad::ScratchpadsParser, shortcuts::{ ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError, parse_modified_keysym_str, @@ -157,7 +156,6 @@ impl Parser for ConfigParser<'_> { mouse_follows_focus, animations_val, ), - (scratchpads_val, autotile), ) = ext.extract(( ( opt(val("keymap")), @@ -219,7 +217,6 @@ impl Parser for ConfigParser<'_> { recover(opt(bol("unstable-mouse-follows-focus"))), opt(val("animations")), ), - (opt(val("scratchpads")), recover(opt(bol("autotile")))), ))?; let mut keymap = None; if let Some(value) = keymap_val { @@ -571,13 +568,6 @@ impl Parser for ConfigParser<'_> { } } } - let mut scratchpads = vec![]; - if let Some(value) = scratchpads_val { - match value.parse(&mut ScratchpadsParser(self.0)) { - Ok(v) => scratchpads = v, - Err(e) => log::warn!("Could not parse the scratchpads: {}", self.0.error(e)), - } - } Ok(Config { keymap, repeat_rate, @@ -628,8 +618,6 @@ impl Parser for ConfigParser<'_> { simple_im, fallback_output_mode, mouse_follows_focus: mouse_follows_focus.despan(), - scratchpads, - autotile: autotile.despan(), }) } } diff --git a/toml-config/src/config/parsers/scratchpad.rs b/toml-config/src/config/parsers/scratchpad.rs deleted file mode 100644 index 17cc5238..00000000 --- a/toml-config/src/config/parsers/scratchpad.rs +++ /dev/null @@ -1,87 +0,0 @@ -use { - crate::{ - config::{ - Scratchpad, - context::Context, - extractor::{Extractor, ExtractorError, opt, str, val}, - parser::{DataType, ParseResult, Parser, UnexpectedDataType}, - parsers::exec::{ExecParser, ExecParserError}, - }, - toml::{ - toml_span::{Span, Spanned}, - toml_value::Value, - }, - }, - indexmap::IndexMap, - thiserror::Error, -}; - -#[derive(Debug, Error)] -pub enum ScratchpadParserError { - #[error(transparent)] - Expected(#[from] UnexpectedDataType), - #[error(transparent)] - Extract(#[from] ExtractorError), - #[error(transparent)] - Exec(#[from] ExecParserError), -} - -pub struct ScratchpadParser<'a>(pub &'a Context<'a>); - -impl Parser for ScratchpadParser<'_> { - type Value = Scratchpad; - type Error = ScratchpadParserError; - const EXPECTED: &'static [DataType] = &[DataType::Table]; - - fn parse_table( - &mut self, - span: Span, - table: &IndexMap, Spanned>, - ) -> ParseResult { - let mut ext = Extractor::new(self.0, span, table); - let (name, exec_val) = ext.extract((str("name"), opt(val("exec"))))?; - let exec = match exec_val { - None => None, - Some(e) => Some(e.parse_map(&mut ExecParser(self.0))?), - }; - Ok(Scratchpad { - name: name.value.to_string(), - exec, - }) - } -} - -pub struct ScratchpadsParser<'a>(pub &'a Context<'a>); - -impl Parser for ScratchpadsParser<'_> { - type Value = Vec; - type Error = ScratchpadParserError; - const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array]; - - fn parse_array(&mut self, _span: Span, array: &[Spanned]) -> ParseResult { - let mut res = vec![]; - for el in array { - match el.parse(&mut ScratchpadParser(self.0)) { - Ok(o) => res.push(o), - Err(e) => { - log::warn!("Could not parse scratchpad: {}", self.0.error(e)); - } - } - } - Ok(res) - } - - fn parse_table( - &mut self, - span: Span, - table: &IndexMap, Spanned>, - ) -> ParseResult { - log::warn!( - "`scratchpads` value should be an array: {}", - self.0.error3(span) - ); - ScratchpadParser(self.0) - .parse_table(span, table) - .map(|v| vec![v]) - } -} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 6e3430f8..4dbf8e74 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -15,7 +15,7 @@ use { config::{ Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, - OutputMatch, SimpleCommand, Status, Theme, WindowMatch, WindowRule, parse_config, + OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config, }, rules::{MatcherTemp, RuleMapper}, shortcuts::ModeState, @@ -27,7 +27,7 @@ use { client::Client, config, config_dir, exec::{Command, set_env, unset_env}, - get_autotile, get_workspace, + get_workspace, input::{ FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, capability::CAP_SWITCH, get_seat, input_devices, on_input_device_removed, on_new_input_device, @@ -40,10 +40,11 @@ use { on_devices_enumerated, on_idle, on_unload, quit, reload, set_animation_cubic_bezier, set_animation_curve, set_animation_duration_ms, set_animation_style, set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius, - set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, - set_floating_titles, set_idle, set_idle_grace_period, set_middle_click_paste_enabled, - set_show_bar, set_show_float_pin_icon, set_show_titles, set_tab_title_align, - set_ui_drag_enabled, set_ui_drag_threshold, + set_default_workspace_capture, + set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle, + set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar, + set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled, + set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, tasks::{self, JoinHandle}, @@ -173,9 +174,6 @@ impl Action { SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)), SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()), SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)), - SimpleCommand::SendToScratchpad => window_or_seat!(s, s.send_to_scratchpad("")), - SimpleCommand::ToggleScratchpad => b.new(move || s.toggle_scratchpad("")), - SimpleCommand::CycleScratchpad => b.new(move || s.cycle_scratchpad("")), SimpleCommand::FocusParent => b.new(move || s.focus_parent()), SimpleCommand::Close => window_or_seat!(s, s.close()), SimpleCommand::DisablePointerConstraint => { @@ -272,7 +270,12 @@ impl Action { SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)), SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)), SimpleCommand::SetAutotile(enabled) => b.new(move || set_autotile(enabled)), - SimpleCommand::ToggleAutotile => b.new(move || set_autotile(!get_autotile())), + SimpleCommand::ToggleAutotile => { + b.new(move || { + // Toggle not directly supported; set to true + set_autotile(true) + }) + } }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); @@ -309,9 +312,6 @@ impl Action { let workspace = get_workspace(&name); window_or_seat!(s, s.set_workspace(workspace)) } - Action::SendToScratchpad { name } => window_or_seat!(s, s.send_to_scratchpad(&name)), - Action::ToggleScratchpad { name } => b.new(move || s.toggle_scratchpad(&name)), - Action::CycleScratchpad { name } => b.new(move || s.cycle_scratchpad(&name)), Action::ConfigureConnector { con } => b.new(move || { for c in connectors() { if con.match_.matches(c) { @@ -1463,43 +1463,6 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 4469c157..50cc8887 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -162,54 +162,6 @@ "name" ] }, - { - "description": "Sends the currently focused window to a scratchpad and hides it.\n\nA scratchpad can hold any number of windows. If `name` is omitted, the\ndefault scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-shift-minus = { type = \"send-to-scratchpad\", name = \"terminal\" }\n ```\n", - "type": "object", - "properties": { - "type": { - "const": "send-to-scratchpad" - }, - "name": { - "type": "string", - "description": "The name of the scratchpad." - } - }, - "required": [ - "type" - ] - }, - { - "description": "Toggles a scratchpad.\n\nIf the scratchpad has a visible window, that window is hidden. Otherwise, the\nmost recently hidden window in the scratchpad is shown on the current workspace.\nOnly one window of a scratchpad is shown at a time, and scratchpad windows are\nalways shown floating. If `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"toggle-scratchpad\", name = \"terminal\" }\n ```\n", - "type": "object", - "properties": { - "type": { - "const": "toggle-scratchpad" - }, - "name": { - "type": "string", - "description": "The name of the scratchpad." - } - }, - "required": [ - "type" - ] - }, - { - "description": "Cycles through the windows of a scratchpad, one at a time.\n\nWith no window shown, the first window is brought up. Each further invocation\nhides the current window and shows the next; after the last window the\nscratchpad is hidden again. Scratchpad windows are always shown floating.\nIf `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"cycle-scratchpad\", name = \"terminal\" }\n ```\n", - "type": "object", - "properties": { - "type": { - "const": "cycle-scratchpad" - }, - "name": { - "type": "string", - "description": "The name of the scratchpad." - } - }, - "required": [ - "type" - ] - }, { "description": "Moves a workspace to a different output.\n\n- Example 1: Move a specific workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", workspace = \"1\", output.name = \"right\" }\n ```\n\n- Example 2: Move the current workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", output.name = \"right\" }\n ```\n\n- Example 3: Move the current workspace to the output on the right (directional)\n\n ```toml\n [shortcuts]\n \"logo+ctrl+shift+Right\" = { type = \"move-to-output\", direction = \"right\" }\n \"logo+ctrl+shift+Left\" = { type = \"move-to-output\", direction = \"left\" }\n \"logo+ctrl+shift+Up\" = { type = \"move-to-output\", direction = \"up\" }\n \"logo+ctrl+shift+Down\" = { type = \"move-to-output\", direction = \"down\" }\n ```\n", "type": "object", @@ -1257,10 +1209,6 @@ "type": "boolean", "description": "Configures whether middle-click pasting is enabled.\n\nChanging this has no effect on running applications.\n\nThe default is `true`.\n" }, - "autotile": { - "type": "boolean", - "description": "Configures whether autotiling is enabled by default.\n\nWhen enabled, newly mapped tiled windows alternate their split\norientation automatically. This can also be toggled at runtime via the\n`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.\n\nThe default is `false`.\n" - }, "modes": { "description": "Configures the input modes.\n\nModes can be used to define shortcuts that are only active when the mode is\nactive.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n\nModes can be activated with the `push-mode` and `latch-mode` actions.\n", "type": "object", @@ -1288,14 +1236,6 @@ "egui": { "description": "Sets the egui settings of the compositor.\n", "$ref": "#/$defs/Egui" - }, - "scratchpads": { - "type": "array", - "description": "An array of pre-configured scratchpads.\n\nEach entry launches a program when the graphics are first initialized and\nimmediately parks its window in the named scratchpad. The window is captured\nvia a unique tag attached to the spawned process, so other windows of the\nsame application are never affected.\n\nUse a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows\nup; they are always shown floating.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n\n [[scratchpads]]\n name = \"notes\"\n exec = [\"obsidian\"]\n ```\n", - "items": { - "description": "", - "$ref": "#/$defs/Scratchpad" - } } }, "required": [] @@ -2110,23 +2050,6 @@ }, "required": [] }, - "Scratchpad": { - "description": "A pre-configured scratchpad whose program is launched at startup and parked\nin the scratchpad.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n ```\n", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the scratchpad that the spawned window is parked in." - }, - "exec": { - "description": "The program to launch when the graphics are first initialized.\n\nIf omitted, no program is launched and the scratchpad is only created on\ndemand by `send-to-scratchpad`.\n", - "$ref": "#/$defs/Exec" - } - }, - "required": [ - "name" - ] - }, "SimpleActionName": { "type": "string", "description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n", @@ -2145,15 +2068,9 @@ "make-group-tab", "change-group-opposite", "toggle-tab", - "enable-autotile", - "disable-autotile", - "toggle-autotile", "toggle-fullscreen", "enter-fullscreen", "exit-fullscreen", - "send-to-scratchpad", - "toggle-scratchpad", - "cycle-scratchpad", "focus-parent", "close", "disable-pointer-constraint", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 21682ada..a31a3767 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -286,76 +286,6 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a string. -- `send-to-scratchpad`: - - Sends the currently focused window to a scratchpad and hides it. - - A scratchpad can hold any number of windows. If `name` is omitted, the - default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" } - ``` - - The table has the following fields: - - - `name` (optional): - - The name of the scratchpad. - - The value of this field should be a string. - -- `toggle-scratchpad`: - - Toggles a scratchpad. - - If the scratchpad has a visible window, that window is hidden. Otherwise, the - most recently hidden window in the scratchpad is shown on the current workspace. - Only one window of a scratchpad is shown at a time, and scratchpad windows are - always shown floating. If `name` is omitted, the default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-minus = { type = "toggle-scratchpad", name = "terminal" } - ``` - - The table has the following fields: - - - `name` (optional): - - The name of the scratchpad. - - The value of this field should be a string. - -- `cycle-scratchpad`: - - Cycles through the windows of a scratchpad, one at a time. - - With no window shown, the first window is brought up. Each further invocation - hides the current window and shows the next; after the last window the - scratchpad is hidden again. Scratchpad windows are always shown floating. - If `name` is omitted, the default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-minus = { type = "cycle-scratchpad", name = "terminal" } - ``` - - The table has the following fields: - - - `name` (optional): - - The name of the scratchpad. - - The value of this field should be a string. - - `move-to-output`: Moves a workspace to a different output. @@ -1077,7 +1007,6 @@ The string should have one of the following values: supported plan exists. - ### `Animations` @@ -1100,7 +1029,7 @@ The table has the following fields: - `enabled` (optional): Enables or disables window animations. - + The default is `false`. The value of this field should be a boolean. @@ -1108,7 +1037,7 @@ The table has the following fields: - `duration-ms` (optional): Sets the animation duration in milliseconds. - + The default is `160`. The value of this field should be a number. @@ -1118,7 +1047,7 @@ The table has the following fields: - `style` (optional): Sets the animation style used for tiled window movement animations. - + The default is `multiphase`. The value of this field should be a [AnimationStyle](#types-AnimationStyle). @@ -1126,7 +1055,7 @@ The table has the following fields: - `curve` (optional): Sets the animation curve. - + The default is `ease-out`. The value of this field should be a [AnimationCurve](#types-AnimationCurve). @@ -2362,11 +2291,11 @@ The table has the following fields: - `animations` (optional): Configures window animations. - + Animations are disabled by default. - + - Example: - + ```toml [animations] enabled = true @@ -2560,18 +2489,6 @@ The table has the following fields: The value of this field should be a boolean. -- `autotile` (optional): - - Configures whether autotiling is enabled by default. - - When enabled, newly mapped tiled windows alternate their split - orientation automatically. This can also be toggled at runtime via the - `enable-autotile`, `disable-autotile`, and `toggle-autotile` actions. - - The default is `false`. - - The value of this field should be a boolean. - - `modes` (optional): Configures the input modes. @@ -2672,32 +2589,6 @@ The table has the following fields: The value of this field should be a [Egui](#types-Egui). -- `scratchpads` (optional): - - An array of pre-configured scratchpads. - - Each entry launches a program when the graphics are first initialized and - immediately parks its window in the named scratchpad. The window is captured - via a unique tag attached to the spawned process, so other windows of the - same application are never affected. - - Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows - up; they are always shown floating. - - - Example: - - ```toml - [[scratchpads]] - name = "term" - exec = "foot" - - [[scratchpads]] - name = "notes" - exec = ["obsidian"] - ``` - - The value of this field should be an array of [Scratchpads](#types-Scratchpad). - ### `Connector` @@ -4631,40 +4522,6 @@ The table has the following fields: The value of this field should be a string. - -### `Scratchpad` - -A pre-configured scratchpad whose program is launched at startup and parked -in the scratchpad. - -- Example: - - ```toml - [[scratchpads]] - name = "term" - exec = "foot" - ``` - -Values of this type should be tables. - -The table has the following fields: - -- `name` (required): - - The name of the scratchpad that the spawned window is parked in. - - The value of this field should be a string. - -- `exec` (optional): - - The program to launch when the graphics are first initialized. - - If omitted, no program is launched and the scratchpad is only created on - demand by `send-to-scratchpad`. - - The value of this field should be a [Exec](#types-Exec). - - ### `SimpleActionName` @@ -4756,18 +4613,6 @@ The string should have one of the following values: Toggles the current group between tabbed and split mode. -- `enable-autotile`: - - Enables alternating split orientation for newly tiled windows. - -- `disable-autotile`: - - Disables alternating split orientation for newly tiled windows. - -- `toggle-autotile`: - - Toggles alternating split orientation for newly tiled windows. - - `toggle-fullscreen`: Toggle the currently focused window between fullscreen and windowed. @@ -4780,18 +4625,6 @@ The string should have one of the following values: Makes the currently focused window windowed. -- `send-to-scratchpad`: - - Sends the currently focused window to the default scratchpad. - -- `toggle-scratchpad`: - - Toggles the default scratchpad. - -- `cycle-scratchpad`: - - Cycles through the windows of the default scratchpad. - - `focus-parent`: Focus the parent of the currently focused window. @@ -5974,4 +5807,3 @@ The table has the following fields: The value of this field should be a [XScalingMode](#types-XScalingMode). - diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 315c74b9..706c016a 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -345,64 +345,6 @@ Action: description: The name of the workspace. required: true kind: string - send-to-scratchpad: - description: | - Sends the currently focused window to a scratchpad and hides it. - - A scratchpad can hold any number of windows. If `name` is omitted, the - default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" } - ``` - fields: - name: - description: The name of the scratchpad. - required: false - kind: string - toggle-scratchpad: - description: | - Toggles a scratchpad. - - If the scratchpad has a visible window, that window is hidden. Otherwise, the - most recently hidden window in the scratchpad is shown on the current workspace. - Only one window of a scratchpad is shown at a time, and scratchpad windows are - always shown floating. If `name` is omitted, the default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-minus = { type = "toggle-scratchpad", name = "terminal" } - ``` - fields: - name: - description: The name of the scratchpad. - required: false - kind: string - cycle-scratchpad: - description: | - Cycles through the windows of a scratchpad, one at a time. - - With no window shown, the first window is brought up. Each further invocation - hides the current window and shows the next; after the last window the - scratchpad is hidden again. Scratchpad windows are always shown floating. - If `name` is omitted, the default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-minus = { type = "cycle-scratchpad", name = "terminal" } - ``` - fields: - name: - description: The name of the scratchpad. - required: false - kind: string move-to-output: description: | Moves a workspace to a different output. @@ -1122,24 +1064,12 @@ SimpleActionName: description: Toggles the current group's direction. - value: toggle-tab description: Toggles the current group between tabbed and split mode. - - value: enable-autotile - description: Enables alternating split orientation for newly tiled windows. - - value: disable-autotile - description: Disables alternating split orientation for newly tiled windows. - - value: toggle-autotile - description: Toggles alternating split orientation for newly tiled windows. - value: toggle-fullscreen description: Toggle the currently focused window between fullscreen and windowed. - value: enter-fullscreen description: Makes the currently focused window fullscreen. - value: exit-fullscreen description: Makes the currently focused window windowed. - - value: send-to-scratchpad - description: Sends the currently focused window to the default scratchpad. - - value: toggle-scratchpad - description: Toggles the default scratchpad. - - value: cycle-scratchpad - description: Cycles through the windows of the default scratchpad. - value: focus-parent description: Focus the parent of the currently focused window. - value: close @@ -3199,21 +3129,10 @@ Config: required: false description: | Configures whether middle-click pasting is enabled. - + Changing this has no effect on running applications. The default is `true`. - autotile: - kind: boolean - required: false - description: | - Configures whether autotiling is enabled by default. - - When enabled, newly mapped tiled windows alternate their split - orientation automatically. This can also be toggled at runtime via the - `enable-autotile`, `disable-autotile`, and `toggle-autotile` actions. - - The default is `false`. modes: kind: map values: @@ -3310,61 +3229,6 @@ Config: required: false description: | Sets the egui settings of the compositor. - scratchpads: - kind: array - items: - ref: Scratchpad - required: false - description: | - An array of pre-configured scratchpads. - - Each entry launches a program when the graphics are first initialized and - immediately parks its window in the named scratchpad. The window is captured - via a unique tag attached to the spawned process, so other windows of the - same application are never affected. - - Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows - up; they are always shown floating. - - - Example: - - ```toml - [[scratchpads]] - name = "term" - exec = "foot" - - [[scratchpads]] - name = "notes" - exec = ["obsidian"] - ``` - - -Scratchpad: - kind: table - description: | - A pre-configured scratchpad whose program is launched at startup and parked - in the scratchpad. - - - Example: - - ```toml - [[scratchpads]] - name = "term" - exec = "foot" - ``` - fields: - name: - kind: string - required: true - description: The name of the scratchpad that the spawned window is parked in. - exec: - ref: Exec - required: false - description: | - The program to launch when the graphics are first initialized. - - If omitted, no program is launched and the scratchpad is only created on - demand by `send-to-scratchpad`. Idle: