Compare commits
6 commits
anims-mult
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5db14936e7 | |||
| f777b4c521 | |||
| b6502e1d8a | |||
| d756c8a6a2 | |||
| 5c2f631fdb | |||
| ce14169d6b |
57 changed files with 1418 additions and 952 deletions
|
|
@ -77,6 +77,20 @@ 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
|
In mono mode, scroll over the title bar to cycle between windows in the
|
||||||
container.
|
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
|
## Fullscreen
|
||||||
|
|
||||||
Press `alt-u` (`toggle-fullscreen`) to make the focused window fill the entire
|
Press `alt-u` (`toggle-fullscreen`) to make the focused window fill the entire
|
||||||
|
|
|
||||||
|
|
@ -1,313 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,493 +0,0 @@
|
||||||
# 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:
|
|
||||||
```
|
|
||||||
|
|
@ -640,6 +640,22 @@ impl ConfigClient {
|
||||||
self.send(&ClientMessage::SetWindowWorkspace { window, workspace });
|
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 {
|
pub fn seat_split(&self, seat: Seat) -> Axis {
|
||||||
let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat });
|
let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat });
|
||||||
get_response!(res, Axis::Horizontal, GetSplit { axis });
|
get_response!(res, Axis::Horizontal, GetSplit { axis });
|
||||||
|
|
@ -2079,6 +2095,12 @@ impl ConfigClient {
|
||||||
self.send(&ClientMessage::SetAutotile { enabled });
|
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) {
|
pub fn set_tab_title_align(&self, align: u32) {
|
||||||
self.send(&ClientMessage::SetTabTitleAlign { align });
|
self.send(&ClientMessage::SetTabTitleAlign { align });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,18 @@ pub enum ClientMessage<'a> {
|
||||||
seat: Seat,
|
seat: Seat,
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
},
|
},
|
||||||
|
SeatSendToScratchpad {
|
||||||
|
seat: Seat,
|
||||||
|
name: &'a str,
|
||||||
|
},
|
||||||
|
SeatToggleScratchpad {
|
||||||
|
seat: Seat,
|
||||||
|
name: &'a str,
|
||||||
|
},
|
||||||
|
SeatCycleScratchpad {
|
||||||
|
seat: Seat,
|
||||||
|
name: &'a str,
|
||||||
|
},
|
||||||
GetTimer {
|
GetTimer {
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
},
|
},
|
||||||
|
|
@ -687,6 +699,10 @@ pub enum ClientMessage<'a> {
|
||||||
window: Window,
|
window: Window,
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
},
|
},
|
||||||
|
WindowSendToScratchpad {
|
||||||
|
window: Window,
|
||||||
|
name: &'a str,
|
||||||
|
},
|
||||||
SetWindowFullscreen {
|
SetWindowFullscreen {
|
||||||
window: Window,
|
window: Window,
|
||||||
fullscreen: bool,
|
fullscreen: bool,
|
||||||
|
|
@ -923,6 +939,7 @@ pub enum ClientMessage<'a> {
|
||||||
SetAutotile {
|
SetAutotile {
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
},
|
},
|
||||||
|
GetAutotile,
|
||||||
SetTabTitleAlign {
|
SetTabTitleAlign {
|
||||||
align: u32,
|
align: u32,
|
||||||
},
|
},
|
||||||
|
|
@ -1189,6 +1206,9 @@ pub enum Response {
|
||||||
GetCornerRadius {
|
GetCornerRadius {
|
||||||
radius: f32,
|
radius: f32,
|
||||||
},
|
},
|
||||||
|
GetAutotile {
|
||||||
|
enabled: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,33 @@ impl Seat {
|
||||||
get!().set_seat_workspace(self, workspace)
|
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.
|
/// Toggles whether the currently focused window is fullscreen.
|
||||||
pub fn toggle_fullscreen(self) {
|
pub fn toggle_fullscreen(self) {
|
||||||
let c = get!();
|
let c = get!();
|
||||||
|
|
|
||||||
|
|
@ -453,14 +453,21 @@ pub fn get_corner_radius() -> f32 {
|
||||||
|
|
||||||
/// Enables or disables autotiling.
|
/// Enables or disables autotiling.
|
||||||
///
|
///
|
||||||
/// When enabled, new windows are automatically placed in a perpendicular
|
/// When enabled, newly tiled windows alternate split orientation from the
|
||||||
/// sub-container if the predicted body would be narrower than tall (or vice versa).
|
/// focused tiled window: the first split uses the containing group's direction,
|
||||||
|
/// then subsequent splits wrap the focused window in the perpendicular
|
||||||
|
/// direction.
|
||||||
///
|
///
|
||||||
/// The default is `false`.
|
/// The default is `false`.
|
||||||
pub fn set_autotile(enabled: bool) {
|
pub fn set_autotile(enabled: bool) {
|
||||||
get!().set_autotile(enabled)
|
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.
|
/// Sets the horizontal alignment of title text within tab buttons.
|
||||||
///
|
///
|
||||||
/// - `"start"` — left-aligned (default)
|
/// - `"start"` — left-aligned (default)
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,13 @@ impl Window {
|
||||||
get!().set_window_workspace(self, workspace)
|
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.
|
/// Toggles whether the currently focused window is fullscreen.
|
||||||
pub fn toggle_fullscreen(self) {
|
pub fn toggle_fullscreen(self) {
|
||||||
self.set_fullscreen(!self.fullscreen())
|
self.set_fullscreen(!self.fullscreen())
|
||||||
|
|
|
||||||
|
|
@ -403,6 +403,7 @@ fn start_compositor2(
|
||||||
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
|
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
|
||||||
virtual_outputs: Default::default(),
|
virtual_outputs: Default::default(),
|
||||||
clean_logs_older_than: Default::default(),
|
clean_logs_older_than: Default::default(),
|
||||||
|
scratchpads: Default::default(),
|
||||||
});
|
});
|
||||||
state.tracker.register(ClientId::from_raw(0));
|
state.tracker.register(ClientId::from_raw(0));
|
||||||
create_dummy_output(&state);
|
create_dummy_output(&state);
|
||||||
|
|
|
||||||
|
|
@ -1100,6 +1100,32 @@ impl ConfigProxyHandler {
|
||||||
Ok(())
|
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> {
|
fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> {
|
||||||
let window = self.get_window(window)?;
|
let window = self.get_window(window)?;
|
||||||
let name = self.get_workspace(ws)?;
|
let name = self.get_workspace(ws)?;
|
||||||
|
|
@ -1114,6 +1140,14 @@ impl ConfigProxyHandler {
|
||||||
Ok(())
|
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> {
|
fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> {
|
||||||
let dev = self.get_device_handler_data(device)?;
|
let dev = self.get_device_handler_data(device)?;
|
||||||
let name = dev.device.name();
|
let name = dev.device.name();
|
||||||
|
|
@ -2989,6 +3023,15 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::SetSeatWorkspace { seat, workspace } => self
|
ClientMessage::SetSeatWorkspace { seat, workspace } => self
|
||||||
.handle_set_seat_workspace(seat, workspace)
|
.handle_set_seat_workspace(seat, workspace)
|
||||||
.wrn("set_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 } => {
|
ClientMessage::GetConnector { ty, idx } => {
|
||||||
self.handle_get_connector(ty, idx).wrn("get_connector")?
|
self.handle_get_connector(ty, idx).wrn("get_connector")?
|
||||||
}
|
}
|
||||||
|
|
@ -3373,6 +3416,9 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::SetWindowWorkspace { window, workspace } => self
|
ClientMessage::SetWindowWorkspace { window, workspace } => self
|
||||||
.handle_set_window_workspace(window, workspace)
|
.handle_set_window_workspace(window, workspace)
|
||||||
.wrn("set_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
|
ClientMessage::SetWindowFullscreen { window, fullscreen } => self
|
||||||
.handle_set_window_fullscreen(window, fullscreen)
|
.handle_set_window_fullscreen(window, fullscreen)
|
||||||
.wrn("set_window_fullscreen")?,
|
.wrn("set_window_fullscreen")?,
|
||||||
|
|
@ -3587,6 +3633,11 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::SetAutotile { enabled } => {
|
ClientMessage::SetAutotile { enabled } => {
|
||||||
self.state.theme.autotile_enabled.set(enabled);
|
self.state.theme.autotile_enabled.set(enabled);
|
||||||
}
|
}
|
||||||
|
ClientMessage::GetAutotile => {
|
||||||
|
self.respond(Response::GetAutotile {
|
||||||
|
enabled: self.state.theme.autotile_enabled.get(),
|
||||||
|
});
|
||||||
|
}
|
||||||
ClientMessage::SeatToggleExpand { .. } => {
|
ClientMessage::SeatToggleExpand { .. } => {
|
||||||
// Removed feature; kept for binary protocol compatibility.
|
// Removed feature; kept for binary protocol compatibility.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1520,25 +1520,25 @@ impl WlSurface {
|
||||||
let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds());
|
let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds());
|
||||||
let pos = self.buffer_abs_pos.get();
|
let pos = self.buffer_abs_pos.get();
|
||||||
let apply_damage = |pos: Rect| {
|
let apply_damage = |pos: Rect| {
|
||||||
if pending.damage_full {
|
let clip_damage = |mut damage: Rect| {
|
||||||
let mut damage = pos;
|
damage = damage.intersect(pos);
|
||||||
if let Some(bounds) = bounds {
|
if let Some(bounds) = bounds {
|
||||||
damage = damage.intersect(bounds);
|
damage = damage.intersect(bounds);
|
||||||
}
|
}
|
||||||
self.client.state.damage(damage);
|
damage
|
||||||
|
};
|
||||||
|
if pending.damage_full {
|
||||||
|
self.client.state.damage(clip_damage(pos));
|
||||||
} else {
|
} else {
|
||||||
let matrix = self.damage_matrix.get();
|
let matrix = self.damage_matrix.get();
|
||||||
if let Some(buffer) = self.buffer.get() {
|
if let Some(buffer) = self.buffer.get() {
|
||||||
for damage in &pending.buffer_damage {
|
for damage in &pending.buffer_damage {
|
||||||
let mut damage = matrix.apply(
|
let damage = matrix.apply(
|
||||||
pos.x1(),
|
pos.x1(),
|
||||||
pos.y1(),
|
pos.y1(),
|
||||||
damage.intersect(buffer.buffer.buf.rect),
|
damage.intersect(buffer.buffer.buf.rect),
|
||||||
);
|
);
|
||||||
if let Some(bounds) = bounds {
|
self.client.state.damage(clip_damage(damage));
|
||||||
damage = damage.intersect(bounds);
|
|
||||||
}
|
|
||||||
self.client.state.damage(damage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for damage in &pending.surface_damage {
|
for damage in &pending.surface_damage {
|
||||||
|
|
@ -1550,8 +1550,7 @@ impl WlSurface {
|
||||||
let y2 = (damage.y2() + scale - 1) / scale;
|
let y2 = (damage.y2() + scale - 1) / scale;
|
||||||
damage = Rect::new_saturating(x1, y1, x2, y2);
|
damage = Rect::new_saturating(x1, y1, x2, y2);
|
||||||
}
|
}
|
||||||
damage = damage.intersect(bounds.unwrap_or(pos));
|
self.client.state.damage(clip_damage(damage));
|
||||||
self.client.state.damage(damage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,27 @@ 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) {
|
fn clear(&self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
if let Some(srv) = self.srv.take() {
|
if let Some(srv) = self.srv.take() {
|
||||||
|
|
@ -331,6 +352,10 @@ impl TestConfig {
|
||||||
pub fn set_show_titles(&self, show: bool) -> TestResult {
|
pub fn set_show_titles(&self, show: bool) -> TestResult {
|
||||||
self.send(ClientMessage::SetShowTitles { show })
|
self.send(ClientMessage::SetShowTitles { show })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_autotile(&self, enabled: bool) -> TestResult {
|
||||||
|
self.send(ClientMessage::SetAutotile { enabled })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TestConfig {
|
impl Drop for TestConfig {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,17 @@ impl TestViewport {
|
||||||
Ok(())
|
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> {
|
pub fn set_destination(&self, width: i32, height: i32) -> Result<(), TestError> {
|
||||||
self.tran.send(SetDestination {
|
self.tran.send(SetDestination {
|
||||||
self_id: self.id,
|
self_id: self.id,
|
||||||
|
|
@ -37,6 +48,15 @@ impl TestViewport {
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
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 {
|
impl Drop for TestViewport {
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ mod t0051_pointer_warp;
|
||||||
mod t0052_bar;
|
mod t0052_bar;
|
||||||
mod t0053_theme;
|
mod t0053_theme;
|
||||||
mod t0054_subsurface_already_attached;
|
mod t0054_subsurface_already_attached;
|
||||||
|
mod t0055_autotiling;
|
||||||
|
mod t0055_scratchpad;
|
||||||
|
|
||||||
pub trait TestCase: Sync {
|
pub trait TestCase: Sync {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
|
@ -158,5 +160,7 @@ pub fn tests() -> Vec<&'static dyn TestCase> {
|
||||||
t0052_bar,
|
t0052_bar,
|
||||||
t0053_theme,
|
t0053_theme,
|
||||||
t0054_subsurface_already_attached,
|
t0054_subsurface_already_attached,
|
||||||
|
t0055_autotiling,
|
||||||
|
t0055_scratchpad,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
it::{test_error::TestError, testrun::TestRun},
|
it::{test_error::TestError, testrun::TestRun},
|
||||||
rect::Rect,
|
|
||||||
tree::Node,
|
tree::Node,
|
||||||
},
|
},
|
||||||
std::rc::Rc,
|
std::rc::Rc,
|
||||||
|
|
@ -11,29 +10,19 @@ testcase!();
|
||||||
|
|
||||||
/// Create and map a single surface
|
/// Create and map a single surface
|
||||||
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
||||||
run.backend.install_default()?;
|
let ds = run.create_default_setup().await?;
|
||||||
|
|
||||||
let client = run.create_client().await?;
|
let client = run.create_client().await?;
|
||||||
|
|
||||||
let window = client.create_window().await?;
|
let window = client.create_window().await?;
|
||||||
window.map().await?;
|
window.map().await?;
|
||||||
|
|
||||||
tassert_eq!(window.tl.core.width.get(), 800);
|
let workspace_rect = ds.output.workspace_rect.get();
|
||||||
tassert_eq!(
|
|
||||||
window.tl.core.height.get(),
|
|
||||||
600 - 2 * run.state.theme.title_plus_underline_height()
|
|
||||||
);
|
|
||||||
|
|
||||||
tassert_eq!(
|
tassert_eq!(window.tl.core.width.get(), workspace_rect.width());
|
||||||
window.tl.server.node_absolute_position(),
|
tassert_eq!(window.tl.core.height.get(), workspace_rect.height());
|
||||||
Rect::new_sized(
|
|
||||||
0,
|
tassert_eq!(window.tl.server.node_absolute_position(), workspace_rect);
|
||||||
2 * run.state.theme.title_plus_underline_height(),
|
|
||||||
window.tl.core.width.get(),
|
|
||||||
window.tl.core.height.get(),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ testcase!();
|
||||||
|
|
||||||
/// Create and map two surfaces
|
/// Create and map two surfaces
|
||||||
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
||||||
run.backend.install_default()?;
|
let ds = run.create_default_setup().await?;
|
||||||
|
|
||||||
let client = run.create_client().await?;
|
let client = run.create_client().await?;
|
||||||
|
|
||||||
|
|
@ -21,17 +21,30 @@ async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
|
||||||
let window2 = client.create_window().await?;
|
let window2 = client.create_window().await?;
|
||||||
window2.map().await?;
|
window2.map().await?;
|
||||||
|
|
||||||
let otop = 2 * run.state.theme.title_plus_underline_height();
|
let workspace_rect = ds.output.workspace_rect.get();
|
||||||
let bw = run.state.theme.sizes.border_width.get();
|
let bw = run.state.theme.sizes.border_width.get();
|
||||||
|
let child_width = (workspace_rect.width() - bw) / 2;
|
||||||
|
|
||||||
tassert_eq!(
|
tassert_eq!(
|
||||||
window.tl.server.node_absolute_position(),
|
window.tl.server.node_absolute_position(),
|
||||||
Rect::new_sized(0, otop, (800 - bw) / 2, 600 - otop).unwrap()
|
Rect::new_sized(
|
||||||
|
workspace_rect.x1(),
|
||||||
|
workspace_rect.y1(),
|
||||||
|
child_width,
|
||||||
|
workspace_rect.height(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
tassert_eq!(
|
tassert_eq!(
|
||||||
window2.tl.server.node_absolute_position(),
|
window2.tl.server.node_absolute_position(),
|
||||||
Rect::new_sized((800 - bw) / 2 + bw, otop, (800 - bw) / 2, 600 - otop).unwrap()
|
Rect::new_sized(
|
||||||
|
workspace_rect.x1() + child_width + bw,
|
||||||
|
workspace_rect.y1(),
|
||||||
|
child_width,
|
||||||
|
workspace_rect.height(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -48,13 +48,18 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
|
|
||||||
let mono_container = w_mono2.tl.container_parent()?;
|
let mono_container = w_mono2.tl.container_parent()?;
|
||||||
let container_pos = mono_container.tl_data().pos.get();
|
let container_pos = mono_container.tl_data().pos.get();
|
||||||
let w_mono1_title = mono_container.render_data.borrow_mut().title_rects[0]
|
let (tab_x, tab_y) = {
|
||||||
.move_(container_pos.x1(), container_pos.y1());
|
let tab_bar = mono_container.tab_bar.borrow();
|
||||||
ds.mouse.abs(
|
let Some(tab_bar) = tab_bar.as_ref() else {
|
||||||
&ds.connector,
|
bail!("no tab bar");
|
||||||
w_mono1_title.x1() as _,
|
};
|
||||||
w_mono1_title.y1() as _,
|
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 _);
|
||||||
|
|
||||||
client.sync().await;
|
client.sync().await;
|
||||||
tassert!(enters.next().is_err());
|
tassert!(enters.next().is_err());
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,18 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
|
|
||||||
let container = w_mono2.tl.container_parent()?;
|
let container = w_mono2.tl.container_parent()?;
|
||||||
let pos = container.tl_data().pos.get();
|
let pos = container.tl_data().pos.get();
|
||||||
let w_mono1_title = container.render_data.borrow_mut().title_rects[0].move_(pos.x1(), pos.y1());
|
let (tab_x, tab_y) = {
|
||||||
ds.mouse.abs(
|
let tab_bar = container.tab_bar.borrow();
|
||||||
&ds.connector,
|
let Some(tab_bar) = tab_bar.as_ref() else {
|
||||||
w_mono1_title.x1() as f64,
|
bail!("no tab bar");
|
||||||
w_mono1_title.y1() as f64,
|
};
|
||||||
);
|
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);
|
||||||
client.sync().await;
|
client.sync().await;
|
||||||
|
|
||||||
let enters = dss.kb.enter.expect()?;
|
let enters = dss.kb.enter.expect()?;
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -2,7 +2,7 @@ use {
|
||||||
crate::{
|
crate::{
|
||||||
ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED,
|
ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED,
|
||||||
it::{
|
it::{
|
||||||
test_error::TestResult,
|
test_error::{TestErrorExt, TestResult},
|
||||||
test_utils::{
|
test_utils::{
|
||||||
test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt,
|
test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt,
|
||||||
},
|
},
|
||||||
|
|
@ -10,7 +10,7 @@ use {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isnt::std_1::collections::IsntHashSetExt,
|
isnt::std_1::collections::IsntHashSetExt,
|
||||||
std::rc::Rc,
|
std::{rc::Rc, time::Duration},
|
||||||
};
|
};
|
||||||
|
|
||||||
testcase!();
|
testcase!();
|
||||||
|
|
@ -19,6 +19,7 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
let ds = run.create_default_setup().await?;
|
let ds = run.create_default_setup().await?;
|
||||||
|
|
||||||
let client = run.create_client().await?;
|
let client = run.create_client().await?;
|
||||||
|
let default_seat = client.get_default_seat().await?;
|
||||||
|
|
||||||
let win1 = client.create_window().await?;
|
let win1 = client.create_window().await?;
|
||||||
win1.set_color(255, 0, 0, 255);
|
win1.set_color(255, 0, 0, 255);
|
||||||
|
|
@ -44,5 +45,23 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
client.sync().await;
|
client.sync().await;
|
||||||
tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED));
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -308,9 +308,8 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
let output_damage = connector_data.damage.borrow();
|
let output_damage = connector_data.damage.borrow();
|
||||||
tassert!(!output_damage.is_empty());
|
tassert!(!output_damage.is_empty());
|
||||||
|
|
||||||
// Buffer damage is transformed by the damage matrix which includes the surface position
|
// The test window maps its 1x1 buffer through a viewport to the full window size.
|
||||||
// The buffer damage (0,0,1,1) should be transformed to surface coordinates
|
let expected_buffer_damage = surface_pos;
|
||||||
let expected_buffer_damage = buffer_damage.move_(surface_pos.x1(), surface_pos.y1());
|
|
||||||
|
|
||||||
// Find the exact output damage that matches our expected buffer damage
|
// Find the exact output damage that matches our expected buffer damage
|
||||||
let mut found_exact_buffer_damage = false;
|
let mut found_exact_buffer_damage = false;
|
||||||
|
|
@ -331,10 +330,12 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
// Test 7: Check output damage from existing window's viewport (which already has scaling)
|
// Test 7: Check output damage from existing window's viewport (which already has scaling)
|
||||||
connector_data.damage.borrow_mut().clear();
|
connector_data.damage.borrow_mut().clear();
|
||||||
|
|
||||||
// The existing window was created with create_surface_ext() which automatically creates a viewport
|
// 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
|
// Commit the viewport size change separately; that commit intentionally damages the old/new extents.
|
||||||
// First, let's modify the viewport scaling that already exists on the window
|
window.surface.viewport.set_destination(150, 100)?;
|
||||||
window.surface.viewport.set_destination(150, 100)?; // Change scaling to 150x100
|
window.surface.commit()?;
|
||||||
|
client.sync().await;
|
||||||
|
connector_data.damage.borrow_mut().clear();
|
||||||
|
|
||||||
// Add buffer damage to test viewport scaling coordinate transformation
|
// Add buffer damage to test viewport scaling coordinate transformation
|
||||||
window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer
|
window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer
|
||||||
|
|
@ -346,8 +347,8 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
let output_damage = connector_data.damage.borrow();
|
let output_damage = connector_data.damage.borrow();
|
||||||
tassert!(!output_damage.is_empty());
|
tassert!(!output_damage.is_empty());
|
||||||
|
|
||||||
// With viewporter scaling, the 1x1 buffer damage should scale to 150x100
|
// With viewporter scaling, the 1x1 buffer damage should scale to the viewport destination.
|
||||||
// and be moved by surface position (0, 36) to get output coordinates (0, 36, 150, 136)
|
let surface_pos = window.surface.server.buffer_abs_pos.get();
|
||||||
let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap();
|
let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap();
|
||||||
let expected_output_damage =
|
let expected_output_damage =
|
||||||
expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1());
|
expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1());
|
||||||
|
|
@ -402,8 +403,9 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
rotation_window.map().await?;
|
rotation_window.map().await?;
|
||||||
client.sync().await;
|
client.sync().await;
|
||||||
|
|
||||||
// Disable viewporter by setting destination to 0x0 to rely purely on buffer dimensions
|
// Disable viewporter to rely purely on buffer dimensions.
|
||||||
rotation_window.surface.viewport.set_destination(0, 0)?; // Disable viewporter
|
rotation_window.surface.viewport.unset_source()?;
|
||||||
|
rotation_window.surface.viewport.unset_destination()?;
|
||||||
|
|
||||||
// Use a rectangular buffer (4x2) so rotation has a visible geometric effect
|
// 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
|
// Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer
|
||||||
|
|
|
||||||
58
src/it/tests/t0055_autotiling.rs
Normal file
58
src/it/tests/t0055_autotiling.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
it::{test_error::TestResult, testrun::TestRun},
|
||||||
|
tree::{ContainerSplit, Node, ToplevelNodeBase},
|
||||||
|
},
|
||||||
|
std::rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
testcase!();
|
||||||
|
|
||||||
|
async fn test(run: Rc<TestRun>) -> 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(())
|
||||||
|
}
|
||||||
107
src/it/tests/t0055_scratchpad.rs
Normal file
107
src/it/tests/t0055_scratchpad.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
it::{test_error::TestResult, testrun::TestRun},
|
||||||
|
tree::{Node, ToplevelNodeBase},
|
||||||
|
},
|
||||||
|
std::rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
testcase!();
|
||||||
|
|
||||||
|
async fn test(run: Rc<TestRun>) -> 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(())
|
||||||
|
}
|
||||||
203
src/state.rs
203
src/state.rs
|
|
@ -114,9 +114,11 @@ use {
|
||||||
tree::{
|
tree::{
|
||||||
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
|
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
|
||||||
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
|
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
|
||||||
PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier,
|
PlaceholderNode, TearingMode, TileState, ToplevelData,
|
||||||
ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder,
|
ToplevelIdentifier, ToplevelNode, ToplevelNodeBase, Transform, VrrMode,
|
||||||
WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output,
|
WorkspaceDisplayOrder, WorkspaceNode, WorkspaceNodeId, WsMoveConfig,
|
||||||
|
generic_node_visitor, move_ws_to_output, toplevel_hide_for_scratchpad,
|
||||||
|
toplevel_restore_from_scratchpad, toplevel_set_workspace,
|
||||||
},
|
},
|
||||||
udmabuf::UdmabufHolder,
|
udmabuf::UdmabufHolder,
|
||||||
utils::{
|
utils::{
|
||||||
|
|
@ -412,6 +414,7 @@ pub struct State {
|
||||||
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
|
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
|
||||||
pub virtual_outputs: VirtualOutputs,
|
pub virtual_outputs: VirtualOutputs,
|
||||||
pub clean_logs_older_than: Cell<Option<SystemTime>>,
|
pub clean_logs_older_than: Cell<Option<SystemTime>>,
|
||||||
|
pub scratchpads: RefCell<AHashMap<String, Vec<Rc<ScratchpadEntry>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Drop for State {
|
// impl Drop for State {
|
||||||
|
|
@ -459,6 +462,27 @@ pub struct IdleState {
|
||||||
pub in_grace_period: Cell<bool>,
|
pub in_grace_period: Cell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ScratchpadEntry {
|
||||||
|
node: Weak<dyn ToplevelNode>,
|
||||||
|
identifier: ToplevelIdentifier,
|
||||||
|
hidden: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScratchpadEntry {
|
||||||
|
fn alive(&self) -> bool {
|
||||||
|
self.node().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(&self) -> Option<Rc<dyn ToplevelNode>> {
|
||||||
|
let node = self.node.upgrade()?;
|
||||||
|
if node.tl_data().identifier.get() == self.identifier {
|
||||||
|
Some(node)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IdleState {
|
impl IdleState {
|
||||||
pub fn set_timeout(&self, state: &State, timeout: Duration) {
|
pub fn set_timeout(&self, state: &State, timeout: Duration) {
|
||||||
self.timeout.set(timeout);
|
self.timeout.set(timeout);
|
||||||
|
|
@ -925,19 +949,39 @@ impl State {
|
||||||
&& node.tl_data().kind.is_app_window()
|
&& node.tl_data().kind.is_app_window()
|
||||||
&& !node.tl_data().visible.get();
|
&& !node.tl_data().visible.get();
|
||||||
if animate_new_app_map {
|
if animate_new_app_map {
|
||||||
self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone()));
|
self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone(), true));
|
||||||
} else {
|
} else {
|
||||||
self.do_map_tiled(seat.as_deref(), node.clone());
|
self.do_map_tiled(seat.as_deref(), node.clone(), true);
|
||||||
}
|
}
|
||||||
self.focus_after_map(node, seat.as_deref());
|
self.focus_after_map(node, seat.as_deref());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_map_tiled(self: &Rc<Self>, seat: Option<&Rc<WlSeatGlobal>>, node: Rc<dyn ToplevelNode>) {
|
pub fn map_tiled_without_autotile(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
|
||||||
|
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<Self>,
|
||||||
|
seat: Option<&Rc<WlSeatGlobal>>,
|
||||||
|
node: Rc<dyn ToplevelNode>,
|
||||||
|
autotile: bool,
|
||||||
|
) {
|
||||||
let ws = self.ensure_map_workspace(seat);
|
let ws = self.ensure_map_workspace(seat);
|
||||||
self.map_tiled_on(node, &ws);
|
self.map_tiled_on_(node, &ws, autotile);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map_tiled_on(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, ws: &Rc<WorkspaceNode>) {
|
pub fn map_tiled_on(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, ws: &Rc<WorkspaceNode>) {
|
||||||
|
self.map_tiled_on_(node, ws, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_tiled_on_(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
node: Rc<dyn ToplevelNode>,
|
||||||
|
ws: &Rc<WorkspaceNode>,
|
||||||
|
autotile: bool,
|
||||||
|
) {
|
||||||
if let Some(c) = ws.container.get() {
|
if let Some(c) = ws.container.get() {
|
||||||
let la = c.clone().tl_last_active_child();
|
let la = c.clone().tl_last_active_child();
|
||||||
let lap = la
|
let lap = la
|
||||||
|
|
@ -946,7 +990,11 @@ impl State {
|
||||||
.get()
|
.get()
|
||||||
.and_then(|n| n.node_into_container());
|
.and_then(|n| n.node_into_container());
|
||||||
if let Some(lap) = lap {
|
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 {
|
} else {
|
||||||
c.append_child(node);
|
c.append_child(node);
|
||||||
}
|
}
|
||||||
|
|
@ -999,6 +1047,146 @@ impl State {
|
||||||
float
|
float
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_to_scratchpad(self: &Rc<Self>, name: &str, node: Rc<dyn ToplevelNode>) {
|
||||||
|
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<Self>, seat: &Rc<WlSeatGlobal>, 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<Self>, seat: &Rc<WlSeatGlobal>, 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<Self>, entry: &Rc<ScratchpadEntry>) {
|
||||||
|
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<Self>,
|
||||||
|
seat: &Rc<WlSeatGlobal>,
|
||||||
|
name: &str,
|
||||||
|
entry: &Rc<ScratchpadEntry>,
|
||||||
|
) {
|
||||||
|
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<Self>,
|
||||||
|
seat: &Rc<WlSeatGlobal>,
|
||||||
|
entry: &Rc<ScratchpadEntry>,
|
||||||
|
) {
|
||||||
|
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<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
|
fn focus_after_map(&self, node: Rc<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
|
||||||
if !node.node_visible() {
|
if !node.node_visible() {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1274,6 +1462,7 @@ impl State {
|
||||||
self.node_at_tree.borrow_mut().clear();
|
self.node_at_tree.borrow_mut().clear();
|
||||||
self.position_hint_requests.clear();
|
self.position_hint_requests.clear();
|
||||||
self.pending_warp_mouse_to_focus.clear();
|
self.pending_warp_mouse_to_focus.clear();
|
||||||
|
self.scratchpads.borrow_mut().clear();
|
||||||
self.head_managers.clear();
|
self.head_managers.clear();
|
||||||
self.head_managers_async.clear();
|
self.head_managers_async.clear();
|
||||||
self.const_40hz_latch.clear();
|
self.const_40hz_latch.clear();
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ use {
|
||||||
numcell::NumCell,
|
numcell::NumCell,
|
||||||
on_drop_event::OnDropEvent,
|
on_drop_event::OnDropEvent,
|
||||||
rc_eq::rc_eq,
|
rc_eq::rc_eq,
|
||||||
|
scroller::Scroller,
|
||||||
threshold_counter::ThresholdCounter,
|
threshold_counter::ThresholdCounter,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -150,6 +151,7 @@ pub struct ContainerNode {
|
||||||
pub child_removed: Rc<LazyEventSource>,
|
pub child_removed: Rc<LazyEventSource>,
|
||||||
pub all_children_resized: Rc<LazyEventSource>,
|
pub all_children_resized: Rc<LazyEventSource>,
|
||||||
pub tab_bar: RefCell<Option<TabBar>>,
|
pub tab_bar: RefCell<Option<TabBar>>,
|
||||||
|
scroll: Scroller,
|
||||||
pub update_tab_textures_scheduled: Cell<bool>,
|
pub update_tab_textures_scheduled: Cell<bool>,
|
||||||
pub ephemeral: Cell<Ephemeral>,
|
pub ephemeral: Cell<Ephemeral>,
|
||||||
}
|
}
|
||||||
|
|
@ -266,6 +268,7 @@ impl ContainerNode {
|
||||||
child_removed: state.lazy_event_sources.create_source(),
|
child_removed: state.lazy_event_sources.create_source(),
|
||||||
all_children_resized: state.post_layout_event_sources.create_source(),
|
all_children_resized: state.post_layout_event_sources.create_source(),
|
||||||
tab_bar: RefCell::new(None),
|
tab_bar: RefCell::new(None),
|
||||||
|
scroll: Default::default(),
|
||||||
update_tab_textures_scheduled: Cell::new(false),
|
update_tab_textures_scheduled: Cell::new(false),
|
||||||
ephemeral: Cell::new(Ephemeral::Off),
|
ephemeral: Cell::new(Ephemeral::Off),
|
||||||
});
|
});
|
||||||
|
|
@ -290,6 +293,47 @@ impl ContainerNode {
|
||||||
self.add_child_x(prev, new, |prev, new| self.add_child_after_(prev, new));
|
self.add_child_x(prev, new, |prev, new| self.add_child_after_(prev, new));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_tiled_child_after(self: &Rc<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) {
|
||||||
|
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<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) {
|
pub fn add_child_before(self: &Rc<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) {
|
||||||
self.add_child_x(prev, new, |prev, new| self.add_child_before_(prev, new));
|
self.add_child_x(prev, new, |prev, new| self.add_child_before_(prev, new));
|
||||||
}
|
}
|
||||||
|
|
@ -752,6 +796,18 @@ impl ContainerNode {
|
||||||
self.activate_child2(child, false);
|
self.activate_child2(child, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn activate_child_from_input(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
child: &NodeRef<ContainerChild>,
|
||||||
|
seat: &Rc<WlSeatGlobal>,
|
||||||
|
) {
|
||||||
|
self.activate_child(child);
|
||||||
|
child
|
||||||
|
.node
|
||||||
|
.clone()
|
||||||
|
.node_do_focus(seat, Direction::Unspecified);
|
||||||
|
}
|
||||||
|
|
||||||
fn activate_child2(self: &Rc<Self>, child: &NodeRef<ContainerChild>, preserve_focus: bool) {
|
fn activate_child2(self: &Rc<Self>, child: &NodeRef<ContainerChild>, preserve_focus: bool) {
|
||||||
if let Some(mc) = self.mono_child.get() {
|
if let Some(mc) = self.mono_child.get() {
|
||||||
if mc.node.node_id() == child.node.node_id() {
|
if mc.node.node_id() == child.node.node_id() {
|
||||||
|
|
@ -1369,42 +1425,6 @@ impl ContainerNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, direction: Direction) {
|
pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, 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);
|
let (split, right) = direction_to_split(direction);
|
||||||
if split != self.split.get() || right {
|
if split != self.split.get() || right {
|
||||||
self.append_child(node);
|
self.append_child(node);
|
||||||
|
|
@ -1514,7 +1534,7 @@ impl ContainerNode {
|
||||||
fn button(
|
fn button(
|
||||||
self: Rc<Self>,
|
self: Rc<Self>,
|
||||||
id: CursorType,
|
id: CursorType,
|
||||||
_seat: &Rc<WlSeatGlobal>,
|
seat: &Rc<WlSeatGlobal>,
|
||||||
_time_usec: u64,
|
_time_usec: u64,
|
||||||
pressed: bool,
|
pressed: bool,
|
||||||
button: u32,
|
button: u32,
|
||||||
|
|
@ -1544,7 +1564,7 @@ impl ContainerNode {
|
||||||
if let Some(child) = children.get(&child_id) {
|
if let Some(child) = children.get(&child_id) {
|
||||||
let child_ref = child.to_ref();
|
let child_ref = child.to_ref();
|
||||||
drop(children);
|
drop(children);
|
||||||
self.activate_child(&child_ref);
|
self.activate_child_from_input(&child_ref, seat);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2061,31 +2081,33 @@ impl Node for ContainerNode {
|
||||||
self.button(id, seat, time_usec, state == ButtonState::Pressed, button);
|
self.button(id, seat, time_usec, state == ButtonState::Pressed, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_on_axis_event(self: Rc<Self>, _seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
|
fn node_on_axis_event(self: Rc<Self>, seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
|
||||||
if self.mono_child.is_none() {
|
if self.mono_child.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Use vertical scroll (index 1) to switch tabs.
|
let steps = match self.scroll.handle(event) {
|
||||||
let v = match event.v120[1].get() {
|
Some(steps) => steps,
|
||||||
Some(v) if v != 0 => v,
|
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let mono = match self.mono_child.get() {
|
let mut target = match self.mono_child.get() {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let next = if v > 0 {
|
let current_id = target.node.node_id();
|
||||||
// Scroll down → next tab.
|
for _ in 0..steps.abs() {
|
||||||
mono.next().or_else(|| self.children.first())
|
let next = if steps > 0 {
|
||||||
|
target.next().or_else(|| self.children.first())
|
||||||
} else {
|
} else {
|
||||||
// Scroll up → previous tab.
|
target.prev().or_else(|| self.children.last())
|
||||||
mono.prev().or_else(|| self.children.last())
|
|
||||||
};
|
};
|
||||||
if let Some(next) = next {
|
match next {
|
||||||
if next.node.node_id() != mono.node.node_id() {
|
Some(next) => target = next,
|
||||||
self.activate_child(&next);
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if target.node.node_id() != current_id {
|
||||||
|
self.activate_child_from_input(&target, seat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_on_leave(&self, seat: &WlSeatGlobal) {
|
fn node_on_leave(&self, seat: &WlSeatGlobal) {
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,25 @@ use {
|
||||||
renderer::Renderer,
|
renderer::Renderer,
|
||||||
state::State,
|
state::State,
|
||||||
tree::{
|
tree::{
|
||||||
FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation,
|
Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink,
|
||||||
OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination,
|
NodeLocation, OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination,
|
||||||
WorkspaceNodeId, walker::NodeVisitor,
|
WorkspaceNodeId, walker::NodeVisitor,
|
||||||
},
|
},
|
||||||
utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList},
|
utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList},
|
||||||
},
|
},
|
||||||
std::{cell::Cell, ops::Deref, rc::Rc},
|
std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
mem,
|
||||||
|
ops::Deref,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct DisplayNode {
|
pub struct DisplayNode {
|
||||||
pub id: NodeId,
|
pub id: NodeId,
|
||||||
pub extents: Cell<Rect>,
|
pub extents: Cell<Rect>,
|
||||||
|
visible: Cell<bool>,
|
||||||
|
suspend_restore_kb_foci: RefCell<Vec<(Rc<WlSeatGlobal>, Weak<dyn Node>)>>,
|
||||||
pub outputs: CopyHashMap<ConnectorId, Rc<OutputNode>>,
|
pub outputs: CopyHashMap<ConnectorId, Rc<OutputNode>>,
|
||||||
pub stacked: Rc<LinkedList<Rc<dyn StackedNode>>>,
|
pub stacked: Rc<LinkedList<Rc<dyn StackedNode>>>,
|
||||||
pub stacked_above_layers: Rc<LinkedList<Rc<dyn StackedNode>>>,
|
pub stacked_above_layers: Rc<LinkedList<Rc<dyn StackedNode>>>,
|
||||||
|
|
@ -31,6 +38,8 @@ impl DisplayNode {
|
||||||
let slf = Self {
|
let slf = Self {
|
||||||
id,
|
id,
|
||||||
extents: Default::default(),
|
extents: Default::default(),
|
||||||
|
visible: Default::default(),
|
||||||
|
suspend_restore_kb_foci: Default::default(),
|
||||||
outputs: Default::default(),
|
outputs: Default::default(),
|
||||||
stacked: Default::default(),
|
stacked: Default::default(),
|
||||||
stacked_above_layers: Default::default(),
|
stacked_above_layers: Default::default(),
|
||||||
|
|
@ -71,6 +80,17 @@ impl DisplayNode {
|
||||||
|
|
||||||
pub fn update_visible(&self, state: &State) {
|
pub fn update_visible(&self, state: &State) {
|
||||||
let visible = state.root_visible();
|
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() {
|
for output in self.outputs.lock().values() {
|
||||||
output.update_visible();
|
output.update_visible();
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +102,20 @@ impl DisplayNode {
|
||||||
for seat in state.globals.seats.lock().values() {
|
for seat in state.globals.seats.lock().values() {
|
||||||
seat.set_visible(visible);
|
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 {
|
if visible {
|
||||||
state.damage(self.extents.get());
|
state.damage(self.extents.get());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -979,7 +979,7 @@ impl ToplevelData {
|
||||||
}
|
}
|
||||||
fd.workspace.remove_fullscreen_node();
|
fd.workspace.remove_fullscreen_node();
|
||||||
if fd.placeholder.is_destroyed() {
|
if fd.placeholder.is_destroyed() {
|
||||||
state.map_tiled(node);
|
state.map_tiled_without_autotile(node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let parent = fd.placeholder.tl_data().parent.take().unwrap();
|
let parent = fd.placeholder.tl_data().parent.take().unwrap();
|
||||||
|
|
@ -1262,7 +1262,7 @@ pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floati
|
||||||
};
|
};
|
||||||
if !floating {
|
if !floating {
|
||||||
parent.cnode_remove_child2(&*tl, true);
|
parent.cnode_remove_child2(&*tl, true);
|
||||||
state.map_tiled(tl);
|
state.map_tiled_without_autotile(tl);
|
||||||
} else if let Some(ws) = data.workspace.get() {
|
} else if let Some(ws) = data.workspace.get() {
|
||||||
let node_id = data.node_id;
|
let node_id = data.node_id;
|
||||||
let old_body =
|
let old_body =
|
||||||
|
|
@ -1323,3 +1323,54 @@ pub fn toplevel_set_workspace(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, ws: &
|
||||||
tl.tl_set_fullscreen(true, Some(ws.clone()));
|
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<dyn ToplevelNode>) -> 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<State>,
|
||||||
|
tl: Rc<dyn ToplevelNode>,
|
||||||
|
ws: &Rc<WorkspaceNode>,
|
||||||
|
) {
|
||||||
|
let (width, height) = tl.tl_data().float_size(ws);
|
||||||
|
state.map_floating(tl.clone(), width, height, ws, None);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,9 @@ pub enum SimpleCommand {
|
||||||
SetFloating(bool),
|
SetFloating(bool),
|
||||||
ToggleFullscreen,
|
ToggleFullscreen,
|
||||||
SetFullscreen(bool),
|
SetFullscreen(bool),
|
||||||
|
SendToScratchpad,
|
||||||
|
ToggleScratchpad,
|
||||||
|
CycleScratchpad,
|
||||||
Forward(bool),
|
Forward(bool),
|
||||||
EnableWindowManagement(bool),
|
EnableWindowManagement(bool),
|
||||||
SetFloatAboveFullscreen(bool),
|
SetFloatAboveFullscreen(bool),
|
||||||
|
|
@ -130,6 +133,15 @@ pub enum Action {
|
||||||
MoveToWorkspace {
|
MoveToWorkspace {
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
SendToScratchpad {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
ToggleScratchpad {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
CycleScratchpad {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
Multi {
|
Multi {
|
||||||
actions: Vec<Action>,
|
actions: Vec<Action>,
|
||||||
},
|
},
|
||||||
|
|
@ -600,6 +612,14 @@ pub struct Config {
|
||||||
pub simple_im: Option<SimpleIm>,
|
pub simple_im: Option<SimpleIm>,
|
||||||
pub fallback_output_mode: Option<FallbackOutputMode>,
|
pub fallback_output_mode: Option<FallbackOutputMode>,
|
||||||
pub mouse_follows_focus: Option<bool>,
|
pub mouse_follows_focus: Option<bool>,
|
||||||
|
pub scratchpads: Vec<Scratchpad>,
|
||||||
|
pub autotile: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Scratchpad {
|
||||||
|
pub name: String,
|
||||||
|
pub exec: Option<Exec>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ pub mod modified_keysym;
|
||||||
mod output;
|
mod output;
|
||||||
mod output_match;
|
mod output_match;
|
||||||
mod repeat_rate;
|
mod repeat_rate;
|
||||||
|
mod scratchpad;
|
||||||
pub mod shortcuts;
|
pub mod shortcuts;
|
||||||
mod simple_im;
|
mod simple_im;
|
||||||
mod status;
|
mod status;
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,9 @@ impl ActionParser<'_> {
|
||||||
"toggle-fullscreen" => ToggleFullscreen,
|
"toggle-fullscreen" => ToggleFullscreen,
|
||||||
"enter-fullscreen" => SetFullscreen(true),
|
"enter-fullscreen" => SetFullscreen(true),
|
||||||
"exit-fullscreen" => SetFullscreen(false),
|
"exit-fullscreen" => SetFullscreen(false),
|
||||||
|
"send-to-scratchpad" => SendToScratchpad,
|
||||||
|
"toggle-scratchpad" => ToggleScratchpad,
|
||||||
|
"cycle-scratchpad" => CycleScratchpad,
|
||||||
"focus-parent" => FocusParent,
|
"focus-parent" => FocusParent,
|
||||||
"close" => Close,
|
"close" => Close,
|
||||||
"disable-pointer-constraint" => DisablePointerConstraint,
|
"disable-pointer-constraint" => DisablePointerConstraint,
|
||||||
|
|
@ -222,6 +225,33 @@ impl ActionParser<'_> {
|
||||||
Ok(Action::MoveToWorkspace { name })
|
Ok(Action::MoveToWorkspace { name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_send_to_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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<Self> {
|
fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
|
||||||
let con = ext
|
let con = ext
|
||||||
.extract(val("connector"))?
|
.extract(val("connector"))?
|
||||||
|
|
@ -551,6 +581,9 @@ impl Parser for ActionParser<'_> {
|
||||||
"switch-to-vt" => self.parse_switch_to_vt(&mut ext),
|
"switch-to-vt" => self.parse_switch_to_vt(&mut ext),
|
||||||
"show-workspace" => self.parse_show_workspace(&mut ext),
|
"show-workspace" => self.parse_show_workspace(&mut ext),
|
||||||
"move-to-workspace" => self.parse_move_to_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-connector" => self.parse_configure_connector(&mut ext),
|
||||||
"configure-input" => self.parse_configure_input(&mut ext),
|
"configure-input" => self.parse_configure_input(&mut ext),
|
||||||
"configure-output" => self.parse_configure_output(&mut ext),
|
"configure-output" => self.parse_configure_output(&mut ext),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ use {
|
||||||
log_level::LogLevelParser,
|
log_level::LogLevelParser,
|
||||||
output::OutputsParser,
|
output::OutputsParser,
|
||||||
repeat_rate::RepeatRateParser,
|
repeat_rate::RepeatRateParser,
|
||||||
|
scratchpad::ScratchpadsParser,
|
||||||
shortcuts::{
|
shortcuts::{
|
||||||
ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError,
|
ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError,
|
||||||
parse_modified_keysym_str,
|
parse_modified_keysym_str,
|
||||||
|
|
@ -156,6 +157,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
mouse_follows_focus,
|
mouse_follows_focus,
|
||||||
animations_val,
|
animations_val,
|
||||||
),
|
),
|
||||||
|
(scratchpads_val, autotile),
|
||||||
) = ext.extract((
|
) = ext.extract((
|
||||||
(
|
(
|
||||||
opt(val("keymap")),
|
opt(val("keymap")),
|
||||||
|
|
@ -217,6 +219,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
recover(opt(bol("unstable-mouse-follows-focus"))),
|
recover(opt(bol("unstable-mouse-follows-focus"))),
|
||||||
opt(val("animations")),
|
opt(val("animations")),
|
||||||
),
|
),
|
||||||
|
(opt(val("scratchpads")), recover(opt(bol("autotile")))),
|
||||||
))?;
|
))?;
|
||||||
let mut keymap = None;
|
let mut keymap = None;
|
||||||
if let Some(value) = keymap_val {
|
if let Some(value) = keymap_val {
|
||||||
|
|
@ -568,6 +571,13 @@ 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 {
|
Ok(Config {
|
||||||
keymap,
|
keymap,
|
||||||
repeat_rate,
|
repeat_rate,
|
||||||
|
|
@ -618,6 +628,8 @@ impl Parser for ConfigParser<'_> {
|
||||||
simple_im,
|
simple_im,
|
||||||
fallback_output_mode,
|
fallback_output_mode,
|
||||||
mouse_follows_focus: mouse_follows_focus.despan(),
|
mouse_follows_focus: mouse_follows_focus.despan(),
|
||||||
|
scratchpads,
|
||||||
|
autotile: autotile.despan(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
toml-config/src/config/parsers/scratchpad.rs
Normal file
87
toml-config/src/config/parsers/scratchpad.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
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<String>, Spanned<Value>>,
|
||||||
|
) -> ParseResult<Self> {
|
||||||
|
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<Scratchpad>;
|
||||||
|
type Error = ScratchpadParserError;
|
||||||
|
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
|
||||||
|
|
||||||
|
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
|
||||||
|
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<String>, Spanned<Value>>,
|
||||||
|
) -> ParseResult<Self> {
|
||||||
|
log::warn!(
|
||||||
|
"`scratchpads` value should be an array: {}",
|
||||||
|
self.0.error3(span)
|
||||||
|
);
|
||||||
|
ScratchpadParser(self.0)
|
||||||
|
.parse_table(span, table)
|
||||||
|
.map(|v| vec![v])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ use {
|
||||||
config::{
|
config::{
|
||||||
Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
|
Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
|
||||||
ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
|
ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
|
||||||
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config,
|
OutputMatch, SimpleCommand, Status, Theme, WindowMatch, WindowRule, parse_config,
|
||||||
},
|
},
|
||||||
rules::{MatcherTemp, RuleMapper},
|
rules::{MatcherTemp, RuleMapper},
|
||||||
shortcuts::ModeState,
|
shortcuts::ModeState,
|
||||||
|
|
@ -27,7 +27,7 @@ use {
|
||||||
client::Client,
|
client::Client,
|
||||||
config, config_dir,
|
config, config_dir,
|
||||||
exec::{Command, set_env, unset_env},
|
exec::{Command, set_env, unset_env},
|
||||||
get_workspace,
|
get_autotile, get_workspace,
|
||||||
input::{
|
input::{
|
||||||
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, capability::CAP_SWITCH,
|
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, capability::CAP_SWITCH,
|
||||||
get_seat, input_devices, on_input_device_removed, on_new_input_device,
|
get_seat, input_devices, on_input_device_removed, on_new_input_device,
|
||||||
|
|
@ -40,11 +40,10 @@ use {
|
||||||
on_devices_enumerated, on_idle, on_unload, quit, reload, set_animation_cubic_bezier,
|
on_devices_enumerated, on_idle, on_unload, quit, reload, set_animation_cubic_bezier,
|
||||||
set_animation_curve, set_animation_duration_ms, set_animation_style,
|
set_animation_curve, set_animation_duration_ms, set_animation_style,
|
||||||
set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius,
|
set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius,
|
||||||
set_default_workspace_capture,
|
set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen,
|
||||||
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle,
|
set_floating_titles, set_idle, set_idle_grace_period, set_middle_click_paste_enabled,
|
||||||
set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar,
|
set_show_bar, set_show_float_pin_icon, set_show_titles, set_tab_title_align,
|
||||||
set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled,
|
set_ui_drag_enabled, set_ui_drag_threshold,
|
||||||
set_ui_drag_threshold,
|
|
||||||
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
|
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
|
||||||
switch_to_vt,
|
switch_to_vt,
|
||||||
tasks::{self, JoinHandle},
|
tasks::{self, JoinHandle},
|
||||||
|
|
@ -174,6 +173,9 @@ impl Action {
|
||||||
SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)),
|
SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)),
|
||||||
SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()),
|
SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()),
|
||||||
SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)),
|
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::FocusParent => b.new(move || s.focus_parent()),
|
||||||
SimpleCommand::Close => window_or_seat!(s, s.close()),
|
SimpleCommand::Close => window_or_seat!(s, s.close()),
|
||||||
SimpleCommand::DisablePointerConstraint => {
|
SimpleCommand::DisablePointerConstraint => {
|
||||||
|
|
@ -270,12 +272,7 @@ impl Action {
|
||||||
SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)),
|
SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)),
|
||||||
SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)),
|
SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)),
|
||||||
SimpleCommand::SetAutotile(enabled) => b.new(move || set_autotile(enabled)),
|
SimpleCommand::SetAutotile(enabled) => b.new(move || set_autotile(enabled)),
|
||||||
SimpleCommand::ToggleAutotile => {
|
SimpleCommand::ToggleAutotile => b.new(move || set_autotile(!get_autotile())),
|
||||||
b.new(move || {
|
|
||||||
// Toggle not directly supported; set to true
|
|
||||||
set_autotile(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Action::Multi { actions } => {
|
Action::Multi { actions } => {
|
||||||
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
|
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
|
||||||
|
|
@ -312,6 +309,9 @@ impl Action {
|
||||||
let workspace = get_workspace(&name);
|
let workspace = get_workspace(&name);
|
||||||
window_or_seat!(s, s.set_workspace(workspace))
|
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 || {
|
Action::ConfigureConnector { con } => b.new(move || {
|
||||||
for c in connectors() {
|
for c in connectors() {
|
||||||
if con.match_.matches(c) {
|
if con.match_.matches(c) {
|
||||||
|
|
@ -1463,6 +1463,43 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||||
window: Default::default(),
|
window: Default::default(),
|
||||||
});
|
});
|
||||||
state.clear_modes_after_reload();
|
state.clear_modes_after_reload();
|
||||||
|
// Desugar `[[scratchpads]]` into spawn-on-graphics-init plus an internal
|
||||||
|
// window rule that parks the spawned window. Each spawned process gets a
|
||||||
|
// unique tag so only its own windows are captured, never other windows of
|
||||||
|
// the same application.
|
||||||
|
if !config.scratchpads.is_empty() {
|
||||||
|
let mut spawn_actions = vec![];
|
||||||
|
for (i, sp) in config.scratchpads.drain(..).enumerate() {
|
||||||
|
let Some(mut exec) = sp.exec else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let tag = exec
|
||||||
|
.tag
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("__scratchpad.{i}.{}", sp.name));
|
||||||
|
exec.tag = Some(tag.clone());
|
||||||
|
spawn_actions.push(Action::Exec { exec });
|
||||||
|
config.window_rules.push(WindowRule {
|
||||||
|
name: None,
|
||||||
|
match_: WindowMatch {
|
||||||
|
tag: Some(tag),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
action: Some(Action::SendToScratchpad { name: sp.name }),
|
||||||
|
latch: None,
|
||||||
|
auto_focus: None,
|
||||||
|
initial_tile_state: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !spawn_actions.is_empty() {
|
||||||
|
let mut actions = Vec::with_capacity(spawn_actions.len() + 1);
|
||||||
|
if let Some(existing) = config.on_graphics_initialized.take() {
|
||||||
|
actions.push(existing);
|
||||||
|
}
|
||||||
|
actions.extend(spawn_actions);
|
||||||
|
config.on_graphics_initialized = Some(Action::Multi { actions });
|
||||||
|
}
|
||||||
|
}
|
||||||
let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules);
|
let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules);
|
||||||
persistent.client_rules.set(client_rules);
|
persistent.client_rules.set(client_rules);
|
||||||
*state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper);
|
*state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper);
|
||||||
|
|
@ -1747,6 +1784,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||||
.seat
|
.seat
|
||||||
.unstable_set_mouse_follows_focus(mouse_follows_focus);
|
.unstable_set_mouse_follows_focus(mouse_follows_focus);
|
||||||
}
|
}
|
||||||
|
if let Some(v) = config.autotile {
|
||||||
|
set_autotile(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_command(exec: &Exec) -> Command {
|
fn create_command(exec: &Exec) -> Command {
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,54 @@
|
||||||
"name"
|
"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",
|
"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",
|
"type": "object",
|
||||||
|
|
@ -1209,6 +1257,10 @@
|
||||||
"type": "boolean",
|
"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"
|
"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": {
|
"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",
|
"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",
|
"type": "object",
|
||||||
|
|
@ -1236,6 +1288,14 @@
|
||||||
"egui": {
|
"egui": {
|
||||||
"description": "Sets the egui settings of the compositor.\n",
|
"description": "Sets the egui settings of the compositor.\n",
|
||||||
"$ref": "#/$defs/Egui"
|
"$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": []
|
"required": []
|
||||||
|
|
@ -2050,6 +2110,23 @@
|
||||||
},
|
},
|
||||||
"required": []
|
"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": {
|
"SimpleActionName": {
|
||||||
"type": "string",
|
"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",
|
"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",
|
||||||
|
|
@ -2068,9 +2145,15 @@
|
||||||
"make-group-tab",
|
"make-group-tab",
|
||||||
"change-group-opposite",
|
"change-group-opposite",
|
||||||
"toggle-tab",
|
"toggle-tab",
|
||||||
|
"enable-autotile",
|
||||||
|
"disable-autotile",
|
||||||
|
"toggle-autotile",
|
||||||
"toggle-fullscreen",
|
"toggle-fullscreen",
|
||||||
"enter-fullscreen",
|
"enter-fullscreen",
|
||||||
"exit-fullscreen",
|
"exit-fullscreen",
|
||||||
|
"send-to-scratchpad",
|
||||||
|
"toggle-scratchpad",
|
||||||
|
"cycle-scratchpad",
|
||||||
"focus-parent",
|
"focus-parent",
|
||||||
"close",
|
"close",
|
||||||
"disable-pointer-constraint",
|
"disable-pointer-constraint",
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,76 @@ This table is a tagged union. The variant is determined by the `type` field. It
|
||||||
|
|
||||||
The value of this field should be a string.
|
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`:
|
- `move-to-output`:
|
||||||
|
|
||||||
Moves a workspace to a different output.
|
Moves a workspace to a different output.
|
||||||
|
|
@ -1007,6 +1077,7 @@ The string should have one of the following values:
|
||||||
supported plan exists.
|
supported plan exists.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="types-Animations"></a>
|
<a name="types-Animations"></a>
|
||||||
### `Animations`
|
### `Animations`
|
||||||
|
|
||||||
|
|
@ -2489,6 +2560,18 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a boolean.
|
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):
|
- `modes` (optional):
|
||||||
|
|
||||||
Configures the input modes.
|
Configures the input modes.
|
||||||
|
|
@ -2589,6 +2672,32 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a [Egui](#types-Egui).
|
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).
|
||||||
|
|
||||||
|
|
||||||
<a name="types-Connector"></a>
|
<a name="types-Connector"></a>
|
||||||
### `Connector`
|
### `Connector`
|
||||||
|
|
@ -4522,6 +4631,40 @@ The table has the following fields:
|
||||||
The value of this field should be a string.
|
The value of this field should be a string.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="types-Scratchpad"></a>
|
||||||
|
### `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).
|
||||||
|
|
||||||
|
|
||||||
<a name="types-SimpleActionName"></a>
|
<a name="types-SimpleActionName"></a>
|
||||||
### `SimpleActionName`
|
### `SimpleActionName`
|
||||||
|
|
||||||
|
|
@ -4613,6 +4756,18 @@ The string should have one of the following values:
|
||||||
|
|
||||||
Toggles the current group between tabbed and split mode.
|
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-fullscreen`:
|
||||||
|
|
||||||
Toggle the currently focused window between fullscreen and windowed.
|
Toggle the currently focused window between fullscreen and windowed.
|
||||||
|
|
@ -4625,6 +4780,18 @@ The string should have one of the following values:
|
||||||
|
|
||||||
Makes the currently focused window windowed.
|
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-parent`:
|
||||||
|
|
||||||
Focus the parent of the currently focused window.
|
Focus the parent of the currently focused window.
|
||||||
|
|
@ -5807,3 +5974,4 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a [XScalingMode](#types-XScalingMode).
|
The value of this field should be a [XScalingMode](#types-XScalingMode).
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,64 @@ Action:
|
||||||
description: The name of the workspace.
|
description: The name of the workspace.
|
||||||
required: true
|
required: true
|
||||||
kind: string
|
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:
|
move-to-output:
|
||||||
description: |
|
description: |
|
||||||
Moves a workspace to a different output.
|
Moves a workspace to a different output.
|
||||||
|
|
@ -1064,12 +1122,24 @@ SimpleActionName:
|
||||||
description: Toggles the current group's direction.
|
description: Toggles the current group's direction.
|
||||||
- value: toggle-tab
|
- value: toggle-tab
|
||||||
description: Toggles the current group between tabbed and split mode.
|
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
|
- value: toggle-fullscreen
|
||||||
description: Toggle the currently focused window between fullscreen and windowed.
|
description: Toggle the currently focused window between fullscreen and windowed.
|
||||||
- value: enter-fullscreen
|
- value: enter-fullscreen
|
||||||
description: Makes the currently focused window fullscreen.
|
description: Makes the currently focused window fullscreen.
|
||||||
- value: exit-fullscreen
|
- value: exit-fullscreen
|
||||||
description: Makes the currently focused window windowed.
|
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
|
- value: focus-parent
|
||||||
description: Focus the parent of the currently focused window.
|
description: Focus the parent of the currently focused window.
|
||||||
- value: close
|
- value: close
|
||||||
|
|
@ -3133,6 +3203,17 @@ Config:
|
||||||
Changing this has no effect on running applications.
|
Changing this has no effect on running applications.
|
||||||
|
|
||||||
The default is `true`.
|
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:
|
modes:
|
||||||
kind: map
|
kind: map
|
||||||
values:
|
values:
|
||||||
|
|
@ -3229,6 +3310,61 @@ Config:
|
||||||
required: false
|
required: false
|
||||||
description: |
|
description: |
|
||||||
Sets the egui settings of the compositor.
|
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:
|
Idle:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue