Compare commits
41 commits
master
...
anims-mult
| Author | SHA1 | Date | |
|---|---|---|---|
| 158682757a | |||
| 09305ab026 | |||
| e7f9a5cb09 | |||
| 02222d5189 | |||
| 502a93a00a | |||
| 6c133018aa | |||
| 313323888b | |||
| 0f6f9f2602 | |||
| dfcb2d0fd6 | |||
| b211b53528 | |||
| 1a75f47709 | |||
| d2138b45f6 | |||
| 0fefe814c3 | |||
| 31c289f628 | |||
| b1717e2dd8 | |||
| a5845fb293 | |||
| 632873ec5a | |||
| 01d1545c40 | |||
| 332a7468f6 | |||
| 511e188d16 | |||
| 0b6da9d8e0 | |||
| a770089b88 | |||
| cc898590d2 | |||
| 90c00bcdf3 | |||
| a712786ecf | |||
| b109cdf6f2 | |||
| 4ee2c324e1 | |||
| a516b2e721 | |||
| 13722429b4 | |||
| b50e8d5683 | |||
| 41d2fef177 | |||
| 2115518edf | |||
| 501c508839 | |||
| cf61c080b6 | |||
| fa5c28ca3d | |||
| d0cc5dc3c7 | |||
| aeaea3419f | |||
| 18ffaef64d | |||
| 7575f851fe | |||
| fba9d65ba1 | |||
| 3540cdc4be |
31 changed files with 7763 additions and 114 deletions
313
docs/window-animations-plan.md
Normal file
313
docs/window-animations-plan.md
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
# Window Animations Plan
|
||||||
|
|
||||||
|
This document is the working plan for Wry's window animation system. It records
|
||||||
|
the decisions already made, the implementation phases, and the risks that must
|
||||||
|
be handled deliberately.
|
||||||
|
|
||||||
|
## Accepted Decisions
|
||||||
|
|
||||||
|
- The first landed slice is plain interpolation only, disabled by default.
|
||||||
|
- Animation is presentation-only. Logical layout, input hit testing, focus, and
|
||||||
|
Wayland configure state use final geometry immediately.
|
||||||
|
- Pointer drag and resize initiated by the mouse or tablet do not animate.
|
||||||
|
- Plain animations restart only for windows whose destination changes. Other
|
||||||
|
in-flight windows keep their existing timelines.
|
||||||
|
- Spawn-in uses scale and position for newly mapped tiled and floating app
|
||||||
|
windows. Layer-shell, overlay, override-redirect, and fullscreen surfaces do
|
||||||
|
not use this path. Spawn-out uses retained visual content after the live node
|
||||||
|
is gone, when a stable retained surface tree can be captured before unmap or
|
||||||
|
destroy.
|
||||||
|
- Command-driven tile-to-float and float-to-tile transitions may animate.
|
||||||
|
Protocol drag/drop paths do not.
|
||||||
|
- The no-overlap multiphase system is a separate phase after the plain path is
|
||||||
|
working and testable.
|
||||||
|
- Content freezing will use retained per-surface texture references, not a full
|
||||||
|
offscreen snapshot as the default design.
|
||||||
|
- Retained records should keep using the existing renderer behavior for now,
|
||||||
|
including clipping and edge stretch/clamp behavior for undersized contents. A
|
||||||
|
dedicated retained-tree scaling path is deferred to a later polish phase.
|
||||||
|
- The multiphase animation concept is original to Wry. Hy3 is relevant only as
|
||||||
|
partial inspiration for tiling style and titlebar/grouping behavior.
|
||||||
|
- Mono mode should mostly avoid animations. Exceptions are windows entering or
|
||||||
|
exiting mono mode, where a visual transition can clarify the hierarchy change.
|
||||||
|
- Multiphase shrink steps should not normally need to reduce a tiled window far
|
||||||
|
below roughly one quarter of the relevant full size. The implementation may
|
||||||
|
enforce a conservative sanity minimum, and pathological cases may fall back.
|
||||||
|
- If the no-overlap planner cannot produce a legal sequence, only the affected
|
||||||
|
group should fall back to plain animation. This is expected to be rare for
|
||||||
|
valid tiling layouts.
|
||||||
|
- When entering mono mode, the active child should animate to the mono geometry.
|
||||||
|
Inactive siblings may snap invisible. Floats may overlap normally and do not
|
||||||
|
need the no-overlap planner.
|
||||||
|
|
||||||
|
## Texture Freezing Decision
|
||||||
|
|
||||||
|
The approved freezing design is to capture a renderable surface tree at animation
|
||||||
|
start:
|
||||||
|
|
||||||
|
- texture references
|
||||||
|
- source sample rects
|
||||||
|
- target sizes
|
||||||
|
- alpha and color metadata
|
||||||
|
- subsurface offsets and stacking order
|
||||||
|
- enough synchronization/release state to keep referenced buffers alive safely
|
||||||
|
|
||||||
|
This is lighter than rendering every toplevel into a compositor-owned offscreen
|
||||||
|
texture, and it should handle normal GPU-backed windows without an extra full
|
||||||
|
window copy. It also gives spawn-out a path: capture the surface tree before the
|
||||||
|
toplevel is logically destroyed, then animate the retained records after the live
|
||||||
|
node is gone.
|
||||||
|
|
||||||
|
Tradeoffs:
|
||||||
|
|
||||||
|
- Retained references can delay buffer release. For dmabuf clients this can
|
||||||
|
increase memory pressure or throttle clients if many large windows animate.
|
||||||
|
- SHM buffers still matter for simple clients, fallback paths, some utilities,
|
||||||
|
and cursor-like surfaces. They are probably not the common case for large app
|
||||||
|
windows, but the implementation must still treat SHM texture flipping as a
|
||||||
|
correctness issue.
|
||||||
|
- The release/sync contract must be explicit. A retained texture must not be
|
||||||
|
released back to the client while the compositor may still render it.
|
||||||
|
- True offscreen snapshots remain a possible fallback for cases where retained
|
||||||
|
references cannot safely preserve the rendered content.
|
||||||
|
|
||||||
|
## Phase 1: Linear Presentation Animations
|
||||||
|
|
||||||
|
Goal: add the smallest correct animation layer without changing layout semantics.
|
||||||
|
|
||||||
|
Implementation shape:
|
||||||
|
|
||||||
|
- Add animation state owned by `State`.
|
||||||
|
- Track per-toplevel animation entries keyed by `NodeId`.
|
||||||
|
- Store logical target rect, current presentation rect, previous damaged rect,
|
||||||
|
start time, duration, and curve.
|
||||||
|
- On command-driven tiled layout geometry changes, animate from current
|
||||||
|
presentation rect to new final rect.
|
||||||
|
- On interruption, restart only the affected window from its current
|
||||||
|
presentation rect.
|
||||||
|
- Drive frames from the existing output latch/presentation event flow.
|
||||||
|
- Damage the union of previous presentation rect, current presentation rect, and
|
||||||
|
final logical rect.
|
||||||
|
|
||||||
|
Initial scope:
|
||||||
|
|
||||||
|
- Tiled reflow animation.
|
||||||
|
- Floating command-driven moves and resizes are animated. Pointer and tablet
|
||||||
|
drag/resize paths still snap directly to the live cursor position.
|
||||||
|
- Cross-output and cross-scale movements snap for now.
|
||||||
|
- Linear mode may overlap windows during swaps. That is expected for the classic
|
||||||
|
interpolation mode; no-overlap is Phase 3.
|
||||||
|
- Live client buffers are rendered in Phase 1. Retained content freezing is
|
||||||
|
deferred, but animated windows must still be clipped to their presentation
|
||||||
|
bounds and must preserve the existing stretch behavior for undersized contents.
|
||||||
|
- Spawn-out is retained-content-only. If the surface cannot be retained safely
|
||||||
|
the window snaps out instead of animating an empty frame.
|
||||||
|
- No multiphase no-overlap planner.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- rect interpolation is direction-independent
|
||||||
|
- interruption restarts only changed windows
|
||||||
|
- unchanged in-flight windows keep their original timeline
|
||||||
|
- drag-driven floating movement bypasses animation
|
||||||
|
- damage includes old, current, and final rects
|
||||||
|
- command-driven tile-to-float and float-to-tile transitions use linear motion
|
||||||
|
- command-driven floating moves and resizes animate without affecting pointer
|
||||||
|
drag/resize behavior
|
||||||
|
- pointer/header double-click unfloat bypasses the command-animation gate
|
||||||
|
|
||||||
|
## Phase 2: Retained Texture Freezing
|
||||||
|
|
||||||
|
Goal: freeze visual contents during movement and enable spawn-out.
|
||||||
|
|
||||||
|
Initial retained-record implementation status:
|
||||||
|
|
||||||
|
- Tiled animation can retain GPU/dmabuf-backed XDG and Xwayland surface trees.
|
||||||
|
- Spawn-in animation can retain GPU/dmabuf-backed XDG and Xwayland surface trees
|
||||||
|
for both tiled windows and floating child contents.
|
||||||
|
- Tile-to-float and float-to-tile transitions retain GPU/dmabuf-backed child
|
||||||
|
contents while the presentation geometry changes.
|
||||||
|
- Spawn-out captures retained app-window contents before XDG/Xwayland unmap or
|
||||||
|
destroy, then renders a detached shrinking presentation record until the
|
||||||
|
animation completes.
|
||||||
|
- Retained records hold both `GfxTexture` and `SurfaceBuffer` references so the
|
||||||
|
existing buffer release/sync path remains authoritative.
|
||||||
|
- Single-pixel buffers can be retained as color records.
|
||||||
|
- Retained records render through the same texture and stretch/clamp paths used
|
||||||
|
by live surfaces. This is the expected Phase 2 behavior.
|
||||||
|
- Async SHM textures are not retained yet because Wry's per-surface SHM
|
||||||
|
front/back textures can be reused by later commits while an animation is still
|
||||||
|
running. Those surfaces fall back to live rendering until an explicit offscreen
|
||||||
|
copy fallback exists.
|
||||||
|
|
||||||
|
Implementation shape:
|
||||||
|
|
||||||
|
- Add a retained render-record tree for toplevel surfaces.
|
||||||
|
- Capture records before movement animations that require freezing.
|
||||||
|
- Capture records before destroy/unmap for spawn-out.
|
||||||
|
- Render retained records through the normal renderer primitives where possible.
|
||||||
|
- Extend event/sync handling so retained buffers remain valid until the animation
|
||||||
|
is complete.
|
||||||
|
|
||||||
|
Deferred/future polish:
|
||||||
|
|
||||||
|
- Whether retained records participate in frame callbacks or presentation
|
||||||
|
feedback. Default assumption: no, because they are compositor animation frames,
|
||||||
|
not client commits.
|
||||||
|
- How to fall back when a buffer cannot be safely retained.
|
||||||
|
- A distinct retained-tree scaling render path for true spawn-in/spawn-out
|
||||||
|
content scaling. If added, start with retained GPU-backed records only, keep
|
||||||
|
the animated frame as the clip boundary, and avoid live SHM scaling until there
|
||||||
|
is an explicit snapshot/copy fallback.
|
||||||
|
|
||||||
|
## Phase 3: Multiphase No-Overlap Animations
|
||||||
|
|
||||||
|
Goal: implement Wry's staged no-overlap planner while preserving the rule that
|
||||||
|
windows never overlap.
|
||||||
|
|
||||||
|
Manual verification checklist: `docs/window-animations-testing.md`.
|
||||||
|
|
||||||
|
Core rules:
|
||||||
|
|
||||||
|
- Each phase is a discrete animation using the full curve.
|
||||||
|
- A phase performs only one action kind per window: move or scale.
|
||||||
|
- Movement and scaling are split by axis.
|
||||||
|
- No diagonal motion. Mixed-action phases may combine different per-window
|
||||||
|
actions, but no single window may move or resize on more than one axis in one
|
||||||
|
step.
|
||||||
|
- A window or synchronized group owns its own timeline.
|
||||||
|
- New layout changes interrupt only windows/groups with changed destinations.
|
||||||
|
- Current hierarchy and target hierarchy both matter. The planner must know
|
||||||
|
whether a window is ascending toward a higher-level/toplevel position,
|
||||||
|
descending into a container, or moving between containers at the same depth.
|
||||||
|
- If some child windows require fewer phases than their parent/container
|
||||||
|
context, parent/container-space changes generally happen first so space exists
|
||||||
|
before the child moves into it. This rule can be overridden only when the
|
||||||
|
non-overlap invariant still clearly holds.
|
||||||
|
- Windows that become peers in the target hierarchy may synchronize later
|
||||||
|
phases even if they were not peers in the source hierarchy.
|
||||||
|
|
||||||
|
Important parent/child synchronization issue:
|
||||||
|
|
||||||
|
The planner must not let a parent container and child window animate independent
|
||||||
|
axes at the same time in a way that violates the visual rules. For example, a
|
||||||
|
parent scaling horizontally while a child scales vertically can accidentally
|
||||||
|
produce a diagonal or multi-axis motion in screen space.
|
||||||
|
|
||||||
|
Preferred approach:
|
||||||
|
|
||||||
|
- Plan in terms of leaf toplevel visual rectangles first.
|
||||||
|
- Treat containers as constraints and grouping boundaries, not as independently
|
||||||
|
animated visual actors.
|
||||||
|
- Derive every leaf's per-phase rect from one phase schedule so parent and child
|
||||||
|
effects cannot compose into forbidden motion.
|
||||||
|
- Build the planner as pure geometry first. Live integration should collect
|
||||||
|
eligible leaf `(old, new)` rects across a command-driven layout pass, then
|
||||||
|
submit planner-produced phases as a batch. Per-node `tl_change_extents` calls
|
||||||
|
are too incremental to plan safely by themselves.
|
||||||
|
- Add container-level grouping only after the leaf planner proves correct.
|
||||||
|
- Include hierarchy-transition metadata in the planner input: source parent,
|
||||||
|
target parent, source depth, target depth, and whether the window is ascending,
|
||||||
|
descending, or staying at the same hierarchy level.
|
||||||
|
- For mono containers, suppress ordinary in-mono focus/tab changes. Animate only
|
||||||
|
transitions into mono, out of mono, or across the mono boundary.
|
||||||
|
- When entering mono, the active child animates to the full mono area and
|
||||||
|
inactive siblings snap invisible. When exiting mono, ordinary tiled geometry
|
||||||
|
may animate from the mono child where that produces a clear hierarchy
|
||||||
|
transition.
|
||||||
|
- If a legal no-overlap sequence cannot be found for a group, fall back to the
|
||||||
|
linear animator for that group only. Float windows are outside this invariant.
|
||||||
|
|
||||||
|
Current pure planner status:
|
||||||
|
|
||||||
|
- Two-window same-axis swaps use shrink lanes, move, then grow.
|
||||||
|
- Swap lane choice follows motion direction, not node identity: right/down
|
||||||
|
moving windows take the first lane, and left/up moving windows take the second
|
||||||
|
lane.
|
||||||
|
- Stack extraction/return patterns are covered in both horizontal and vertical
|
||||||
|
orientations: peer/container space scales first, the extracted child moves
|
||||||
|
only after space exists, and orthogonal growth happens in the final phase.
|
||||||
|
- Same-axis size redistribution is handled as a single scale phase when the
|
||||||
|
exact validator proves adjacent windows stay non-overlapping.
|
||||||
|
- Nested size redistribution can use hierarchy metadata to decompose two-axis
|
||||||
|
resizing into parent-axis then child-axis scale phases, but only when the
|
||||||
|
source/target ancestor split depths give a deterministic order.
|
||||||
|
- A phase can carry mixed per-window actions when each window still performs one
|
||||||
|
classified move/scale on one axis and the exact validator proves the combined
|
||||||
|
phase is non-overlapping.
|
||||||
|
- Every produced plan is checked analytically for overlap over the full duration
|
||||||
|
of each phase before it is accepted. This solves the linear edge inequalities
|
||||||
|
for each pair of moving rectangles instead of relying on sampled frames.
|
||||||
|
- Live layout batches are partitioned by overlapping motion bounds, so unrelated
|
||||||
|
groups can still use multiphase animation when another group falls back to
|
||||||
|
linear motion.
|
||||||
|
- Planner requests now carry per-window hierarchy metadata for source/target
|
||||||
|
parent, depth, sibling index, split axis, nearest ancestor split depth per
|
||||||
|
axis, mono state, and transition kind. The current planner uses this for
|
||||||
|
parent-before-child scale ordering, but not yet for full nested move planning.
|
||||||
|
- Multiphase planning has a diagnostic entry point used by live fallback logs.
|
||||||
|
It distinguishes request validation errors, missing patterns, shrink-bound
|
||||||
|
rejections, invalid phase steps, and exact validation failures such as stale
|
||||||
|
starts or phase overlap.
|
||||||
|
- Multiphase planning also has an explained-plan entry point. Accepted plans
|
||||||
|
report the deterministic strategy, phase reasons, participating nodes, and
|
||||||
|
validation result; rejected plans report every attempted strategy and failure.
|
||||||
|
- Rejection diagnostics are treated as contractual test output for unsupported
|
||||||
|
patterns and analytically invalid candidate plans, including attempted strategy
|
||||||
|
order and exact validation failure.
|
||||||
|
- Planner tests now include a deterministic split-tree generator. It builds
|
||||||
|
valid weighted tiling layouts, derives leaf hierarchy metadata, mutates them
|
||||||
|
through supported transitions, and runs the real planner plus exact validator.
|
||||||
|
- The generated tests also include a bounded corpus of supported split-tree
|
||||||
|
transitions across both axes and directions. Each case is planned twice and
|
||||||
|
compared exactly to catch nondeterministic planner output.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- horizontal swaps shrink, move, then grow without overlap
|
||||||
|
- extraction from a stack creates space before moving the extracted window
|
||||||
|
- nested size redistribution scales the parent axis before the child axis
|
||||||
|
- nested containers do not produce simultaneous cross-axis motion
|
||||||
|
- interruption restarts only affected phase groups
|
||||||
|
- reversing direction produces equivalent motion in reverse
|
||||||
|
- accepted and rejected plans expose deterministic strategy explanations
|
||||||
|
- bounded generated split-tree corpus produces identical plans on repeated runs
|
||||||
|
- unsupported and invalid candidate plans produce exact expected diagnostics
|
||||||
|
- mixed-action phases are accepted only under exact continuous validation
|
||||||
|
- diagonal per-window motion remains a hard rejection even inside mixed phases
|
||||||
|
- child waits for parent/container-space phases when moving upward into a
|
||||||
|
toplevel peer position
|
||||||
|
- mono-mode tab switches do not animate, while entering/exiting mono can animate
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Phase 1 should expose a disabled-by-default setting for:
|
||||||
|
|
||||||
|
- enabled/disabled
|
||||||
|
- duration
|
||||||
|
- style: `plain` or `multiphase`
|
||||||
|
- curve preset or cubic bezier
|
||||||
|
|
||||||
|
Initial TOML shape:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[animations]
|
||||||
|
enabled = false
|
||||||
|
duration-ms = 160
|
||||||
|
style = "multiphase"
|
||||||
|
curve = "ease-out"
|
||||||
|
# or:
|
||||||
|
curve = [0.25, 0.1, 0.25, 1.0]
|
||||||
|
```
|
||||||
|
|
||||||
|
Bezier curves are analyzed when configuration is applied and stored as a
|
||||||
|
piecewise curve that is cheap to evaluate during rendering. Custom curves use
|
||||||
|
CSS cubic-bezier semantics: `(0, 0)` and `(1, 1)` are implicit, while the four
|
||||||
|
configured numbers are `x1`, `y1`, `x2`, and `y2`. The x control points must be
|
||||||
|
between `0` and `1`.
|
||||||
|
|
||||||
|
## Existing Note
|
||||||
|
|
||||||
|
`docs/animation-integration.md` appears to document a prior animation attempt
|
||||||
|
whose `src/animation/` implementation is not present in this checkout. Treat
|
||||||
|
this plan as the current source of truth until implementation docs are updated.
|
||||||
493
docs/window-animations-testing.md
Normal file
493
docs/window-animations-testing.md
Normal file
|
|
@ -0,0 +1,493 @@
|
||||||
|
# Window Animations Manual Test Plan
|
||||||
|
|
||||||
|
This is the manual verification checklist for Wry's animation work. Use it after
|
||||||
|
building a test compositor and booting into a normal graphical session.
|
||||||
|
|
||||||
|
The goal is to catch visual, synchronization, damage, and retained-content
|
||||||
|
problems that unit tests cannot prove from geometry alone.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- Build and install the `codex-anims-next` branch.
|
||||||
|
- Start with animations enabled and a deliberately slow duration, around
|
||||||
|
`400-700ms`, so phase ordering and damage artifacts are visible.
|
||||||
|
- Test at least one normal Wayland/XDG app, one Xwayland app if available, and
|
||||||
|
one fast-updating app such as a terminal running output, a browser animation,
|
||||||
|
a video, or a GL/Vulkan demo.
|
||||||
|
- Use visible gaps, borders, titlebars, and rounding. These make clipping and
|
||||||
|
damage mistakes much easier to see.
|
||||||
|
- If available, test on both a single-output setup and a multi-output setup.
|
||||||
|
- If logging is convenient, run with debug logging and keep any multiphase
|
||||||
|
fallback messages. A fallback is useful evidence, not automatically a bug.
|
||||||
|
|
||||||
|
Relevant internal config hooks:
|
||||||
|
|
||||||
|
- `SetAnimationsEnabled`
|
||||||
|
- `SetAnimationDurationMs`
|
||||||
|
- `SetAnimationStyle`
|
||||||
|
- `SetAnimationCurve`
|
||||||
|
- `SetAnimationCubicBezier`
|
||||||
|
|
||||||
|
TOML example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[animations]
|
||||||
|
enabled = true
|
||||||
|
duration-ms = 600
|
||||||
|
style = "multiphase"
|
||||||
|
curve = "ease-out"
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `style = "plain"` to force ordinary one-step movement interpolation while
|
||||||
|
keeping the configured curve. `curve = "linear"` only changes easing; it does
|
||||||
|
not select the plain animation style.
|
||||||
|
|
||||||
|
Current curve IDs in code:
|
||||||
|
|
||||||
|
- `0`: linear
|
||||||
|
- `1`: CSS `ease`
|
||||||
|
- `2`: CSS `ease-in`
|
||||||
|
- `3` or any other unrecognized value: CSS `ease-out`
|
||||||
|
- `4`: CSS `ease-in-out`
|
||||||
|
|
||||||
|
## Enabling Multiphase Tests
|
||||||
|
|
||||||
|
To exercise the multiphase planner:
|
||||||
|
|
||||||
|
1. Enable animations with `SetAnimationsEnabled`.
|
||||||
|
2. Set a slow duration with `SetAnimationDurationMs`, around `400-700ms`.
|
||||||
|
3. Select `style = "multiphase"` in TOML, or call `SetAnimationStyle` with
|
||||||
|
`AnimationStyle::MULTIPHASE`.
|
||||||
|
4. Use tiled layout commands that are wired through `State::with_layout_animations`.
|
||||||
|
5. Use layouts where at least two tiled windows change geometry in the same
|
||||||
|
container layout batch.
|
||||||
|
|
||||||
|
The compositor then attempts multiphase planning automatically when the batched
|
||||||
|
layout pass completes. If the planner proves a legal no-overlap sequence, that
|
||||||
|
group uses phased animation. If it cannot prove one, only that motion group falls
|
||||||
|
back to ordinary plain animation.
|
||||||
|
|
||||||
|
Good command families for multiphase testing:
|
||||||
|
|
||||||
|
- seat/window move in a tiled direction
|
||||||
|
- split changes
|
||||||
|
- tab/group operations
|
||||||
|
- group-opposite changes
|
||||||
|
- equalize
|
||||||
|
- move-tab
|
||||||
|
- mono enter/exit
|
||||||
|
- command-driven window resize
|
||||||
|
|
||||||
|
These paths should not be used as evidence of multiphase behavior:
|
||||||
|
|
||||||
|
- tile-to-float and float-to-tile, which deliberately use plain animation
|
||||||
|
- command-driven floating move/resize, which may animate but can overlap
|
||||||
|
- pointer or tablet drag/resize, which should not animate
|
||||||
|
- spawn-in and spawn-out, which are single-phase and use the configured curve
|
||||||
|
- cross-output or cross-scale movement, which should snap
|
||||||
|
- layer-shell, overlay, override-redirect, and fullscreen map/unmap paths
|
||||||
|
|
||||||
|
Useful debug signal:
|
||||||
|
|
||||||
|
- `falling back to plain layout animation for group ...` means the group entered
|
||||||
|
the multiphase gate but the planner rejected it. That is acceptable for
|
||||||
|
unsupported patterns, but unexpected for the supported swap/extraction cases
|
||||||
|
below.
|
||||||
|
|
||||||
|
## Pass Criteria
|
||||||
|
|
||||||
|
A test passes when:
|
||||||
|
|
||||||
|
- layout, focus, hit testing, and configure behavior use the final logical
|
||||||
|
geometry immediately
|
||||||
|
- visible presentation motion is smooth and bounded by the animated frame
|
||||||
|
- no old pixels, trails, black strips, transparent holes, or stale titlebar
|
||||||
|
fragments remain after motion
|
||||||
|
- tiled multiphase movement never overlaps and never moves a single window
|
||||||
|
diagonally during a phase
|
||||||
|
- interruption starts changed windows from their current visual rect without
|
||||||
|
restarting unaffected windows
|
||||||
|
- drag-driven pointer movement remains direct and does not lag behind the cursor
|
||||||
|
- cross-output or cross-scale movements snap instead of animating
|
||||||
|
|
||||||
|
Record a failure with:
|
||||||
|
|
||||||
|
- the layout before and after
|
||||||
|
- whether the window was tiled, floating, mono, XDG, Xwayland, or layer-shell
|
||||||
|
- whether the app was GPU/dmabuf-backed or likely SHM, if known
|
||||||
|
- animation duration and curve
|
||||||
|
- whether the failure was visual overlap, diagonal motion, debris, clipping,
|
||||||
|
stale content, a missing retained frame, or an incorrect animation trigger
|
||||||
|
|
||||||
|
## Known Current Limits
|
||||||
|
|
||||||
|
These are acceptable unless they produce worse behavior than described:
|
||||||
|
|
||||||
|
- Spawn-out is retained-content-only. If the surface cannot be retained safely,
|
||||||
|
it should snap out rather than animate an empty frame.
|
||||||
|
- Async SHM surfaces are not retained yet. GPU/dmabuf-backed app windows are the
|
||||||
|
primary retained-content path for this phase.
|
||||||
|
- A dedicated retained-tree scaling/offscreen fallback is deferred. Retained
|
||||||
|
records currently render through the normal texture and stretch/clamp paths.
|
||||||
|
- Floats may overlap. The no-overlap invariant is for tiled multiphase motion.
|
||||||
|
- Linear fallback may overlap. This should be rare for valid tiled layouts, and
|
||||||
|
the fallback should be scoped to the affected motion group.
|
||||||
|
- Cross-output and cross-scale movements should not animate yet.
|
||||||
|
|
||||||
|
## 1. Basic Enable/Disable
|
||||||
|
|
||||||
|
1. Disable animations.
|
||||||
|
2. Move, resize, spawn, close, and toggle floating on a few windows.
|
||||||
|
3. Confirm all affected windows snap with no delayed presentation state.
|
||||||
|
4. Enable animations at a slow duration.
|
||||||
|
5. Repeat the same operations and confirm only eligible paths animate.
|
||||||
|
6. Disable animations while an animation is in flight.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- disabling animations clears any in-flight visual state
|
||||||
|
- no stale damage remains after disabling
|
||||||
|
- newly enabled animations use the configured duration and curve
|
||||||
|
|
||||||
|
## 2. Spawn-In
|
||||||
|
|
||||||
|
Test newly mapped windows:
|
||||||
|
|
||||||
|
- tiled XDG window
|
||||||
|
- floating XDG window
|
||||||
|
- Xwayland window, if available
|
||||||
|
- fullscreen window
|
||||||
|
- layer-shell or overlay surface, such as a bar, launcher, menu, notification,
|
||||||
|
or lock/overlay component, if available
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- newly mapped tiled and floating app windows animate in
|
||||||
|
- layer-shell, overlay, override-redirect, and fullscreen surfaces do not use
|
||||||
|
the app-window spawn-in path
|
||||||
|
- contents stay clipped to the animated frame
|
||||||
|
- if contents are smaller than the frame during the animation, no empty strips
|
||||||
|
are visible
|
||||||
|
|
||||||
|
## 3. Spawn-Out
|
||||||
|
|
||||||
|
Close windows from these states:
|
||||||
|
|
||||||
|
- tiled app window
|
||||||
|
- floating app window
|
||||||
|
- Xwayland app window
|
||||||
|
- fast-updating app window
|
||||||
|
- a likely SHM/simple app, if available
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- retained app content shrinks out after the live node is gone
|
||||||
|
- there is no black, transparent, or unfilled moving rectangle
|
||||||
|
- if content cannot be retained, the window snaps out cleanly
|
||||||
|
- neighboring tiled windows reflow without debris left in the old area
|
||||||
|
|
||||||
|
Hard failure:
|
||||||
|
|
||||||
|
- a destroyed window leaves a moving empty frame
|
||||||
|
- the last frame shows unrelated newer content
|
||||||
|
- screen debris remains after the animation completes
|
||||||
|
|
||||||
|
## 4. Linear Tiled Reflow
|
||||||
|
|
||||||
|
Use a slow duration and a non-linear curve, then repeat with linear.
|
||||||
|
|
||||||
|
Cases:
|
||||||
|
|
||||||
|
- open two tiled windows and change split ratio by command
|
||||||
|
- open three tiled windows and resize the active split repeatedly
|
||||||
|
- move focus and issue command-driven swaps
|
||||||
|
- interrupt a resize by issuing another resize before the first completes
|
||||||
|
- create a layout that forces a linear fallback if possible
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- final layout is usable immediately
|
||||||
|
- changed windows animate from their current visual rect on interruption
|
||||||
|
- unaffected windows keep their existing timeline
|
||||||
|
- linear fallback is visually smooth, even if overlap occurs
|
||||||
|
- no pointer drag path becomes animated
|
||||||
|
|
||||||
|
## 5. Float Movement And Tile/Float Transitions
|
||||||
|
|
||||||
|
Cases:
|
||||||
|
|
||||||
|
- command-toggle a tiled window to floating
|
||||||
|
- command-toggle the same window back to tiled
|
||||||
|
- command-move and command-resize a floating window
|
||||||
|
- mouse-drag a floating window
|
||||||
|
- mouse-resize a floating window
|
||||||
|
- double-click/header pointer path if that is part of the local workflow
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- command-driven tile-to-float and float-to-tile transitions animate linearly
|
||||||
|
- command-driven floating move/resize animates
|
||||||
|
- mouse or tablet drag/resize remains direct and tracks the pointer
|
||||||
|
- pointer/header paths that are intentionally outside the command-animation gate
|
||||||
|
do not unexpectedly use delayed animation
|
||||||
|
- retained child content remains clipped during tile/float transitions
|
||||||
|
|
||||||
|
## 6. Multiphase Horizontal And Vertical Swaps
|
||||||
|
|
||||||
|
Horizontal:
|
||||||
|
|
||||||
|
1. Create two horizontally adjacent tiled windows.
|
||||||
|
2. Swap their positions.
|
||||||
|
3. Reverse the swap.
|
||||||
|
|
||||||
|
Vertical:
|
||||||
|
|
||||||
|
1. Create two vertically adjacent tiled windows.
|
||||||
|
2. Swap their positions.
|
||||||
|
3. Reverse the swap.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- first phase shrinks into lanes on the orthogonal axis
|
||||||
|
- second phase moves only horizontally or only vertically
|
||||||
|
- third phase grows out of lanes
|
||||||
|
- no phase overlaps windows
|
||||||
|
- no window moves diagonally
|
||||||
|
- reverse direction uses the same visual logic in reverse
|
||||||
|
- titlebars, borders, gaps, and rounded corners remain respected
|
||||||
|
|
||||||
|
## 7. Stack Extraction And Return
|
||||||
|
|
||||||
|
Build this shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[ A | [ B
|
||||||
|
C ] ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then move `B` out so the target is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[ A | B | C ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Reverse the operation by putting `B` back into the stack.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- peer/container space opens first
|
||||||
|
- `B` waits until there is a legal horizontal or vertical lane
|
||||||
|
- `B` moves on one axis only
|
||||||
|
- `B` and the affected peer grow together in the final phase when appropriate
|
||||||
|
- reversing the operation is visually equivalent in reverse
|
||||||
|
|
||||||
|
## 8. Nested Parent/Child Synchronization
|
||||||
|
|
||||||
|
Create nested split layouts where a parent changes one axis and children change
|
||||||
|
the other. Use both horizontal-parent/vertical-child and
|
||||||
|
vertical-parent/horizontal-child variants.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- parent/container-space axis changes happen before child-axis changes when the
|
||||||
|
hierarchy metadata gives a deterministic order
|
||||||
|
- child windows do not visually compose parent and child transforms into
|
||||||
|
diagonal motion
|
||||||
|
- any unsupported group falls back as a group rather than partially violating the
|
||||||
|
one-axis rule
|
||||||
|
|
||||||
|
Hard failure:
|
||||||
|
|
||||||
|
- a child visibly changes width and height in the same phase
|
||||||
|
- a child moves diagonally because parent and child animation compound
|
||||||
|
- a child clips outside its animated frame
|
||||||
|
|
||||||
|
## 9. Mixed-Action Phases
|
||||||
|
|
||||||
|
This case is easiest to prove with the planner tests because Wry does not yet
|
||||||
|
have a confirmed stock command that reliably creates a same-batch move for one
|
||||||
|
tiled window and an independent resize for another. The canonical geometry is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
start: A = (0,0)-(80,80) B = (200,0)-(280,80)
|
||||||
|
target: A = (40,0)-(120,80) B = (200,0)-(280,120)
|
||||||
|
```
|
||||||
|
|
||||||
|
`A` moves horizontally while `B` scales vertically. The windows are far enough
|
||||||
|
apart that the mixed phase is provably non-overlapping.
|
||||||
|
|
||||||
|
To exercise the current proof directly, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test animation::multiphase::tests::mixed_single_phase_accepts_move_and_scale_when_proven
|
||||||
|
```
|
||||||
|
|
||||||
|
For visual/manual testing, the target shape is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
before: [ A ] [ B ]
|
||||||
|
after: [ A ] [ B ]
|
||||||
|
[ ]
|
||||||
|
```
|
||||||
|
|
||||||
|
`A` must move horizontally without resizing. `B` must resize vertically without
|
||||||
|
moving. The two motion bounds must remain separate for the whole animation. If a
|
||||||
|
normal command sequence cannot produce that in one layout batch, treat the unit
|
||||||
|
test as the authority and record the visual test as not applicable rather than a
|
||||||
|
failure.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- mixed phases are allowed only when each individual window performs one legal
|
||||||
|
move or scale on one axis
|
||||||
|
- no individual window moves diagonally
|
||||||
|
- no overlap occurs at any point during the phase
|
||||||
|
|
||||||
|
A fallback here is acceptable if no normal user command can create this geometry;
|
||||||
|
the planner test above is the authority for the mixed-action rule.
|
||||||
|
|
||||||
|
## 10. Mono Mode
|
||||||
|
|
||||||
|
Cases:
|
||||||
|
|
||||||
|
- enter mono mode with several siblings
|
||||||
|
- exit mono mode
|
||||||
|
- switch active tabs/windows inside mono
|
||||||
|
- move a window into mono
|
||||||
|
- move a window out of mono
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- entering/exiting mono may animate where it clarifies hierarchy change
|
||||||
|
- active child animates to the mono geometry
|
||||||
|
- inactive siblings snap invisible
|
||||||
|
- ordinary tab switches inside mono do not animate
|
||||||
|
- no hidden inactive sibling leaves debris or stale retained content
|
||||||
|
|
||||||
|
## 11. Interruption And Retargeting
|
||||||
|
|
||||||
|
Use a long duration, then issue commands mid-animation:
|
||||||
|
|
||||||
|
- swap, then reverse before completion
|
||||||
|
- resize, then resize in the other direction before completion
|
||||||
|
- build `[A | [B | C | D]]`, move `C` left to form `[A | C | [B | D]]`,
|
||||||
|
then move `C` back into the stack before completion
|
||||||
|
- start a multiphase group, then change only one window's destination if a
|
||||||
|
command sequence allows it
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- affected windows restart from their current visual rect
|
||||||
|
- unaffected windows do not restart if their destination is unchanged
|
||||||
|
- a new valid multiphase plan replaces the old plan cleanly
|
||||||
|
- retained content remains the same frozen content during the retarget
|
||||||
|
- damage covers old, current, and new visual regions
|
||||||
|
|
||||||
|
## 12. Damage And Clipping Stress
|
||||||
|
|
||||||
|
Use a high-contrast wallpaper/background and high contrast window contents.
|
||||||
|
|
||||||
|
Cases:
|
||||||
|
|
||||||
|
- fast repeated swaps
|
||||||
|
- repeated spawn-in/spawn-out
|
||||||
|
- rounded corners with large gaps
|
||||||
|
- titlebar-heavy layouts
|
||||||
|
- resize while a terminal is rapidly updating
|
||||||
|
- move/resize over another window's old location
|
||||||
|
- run on different output scales if available
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- no trails remain in gaps, borders, or titlebar strips
|
||||||
|
- rounded corners do not reveal old pixels outside the frame
|
||||||
|
- contents never draw outside the animated bounds
|
||||||
|
- final frame exactly matches the steady layout
|
||||||
|
|
||||||
|
## 13. Texture Freezing
|
||||||
|
|
||||||
|
Use fast-updating contents so freezing is obvious.
|
||||||
|
|
||||||
|
Cases:
|
||||||
|
|
||||||
|
- tiled GPU/dmabuf-backed app during reflow
|
||||||
|
- floating GPU/dmabuf-backed app during command move/resize
|
||||||
|
- tile-to-float and float-to-tile with dynamic content
|
||||||
|
- spawn-out with dynamic content
|
||||||
|
- likely SHM/simple app, if available
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- retained GPU/dmabuf-backed windows freeze visually during animation
|
||||||
|
- spawn-out uses the last retained content, not a blank or unrelated frame
|
||||||
|
- undersized contents stretch/clamp to avoid unfilled frame regions
|
||||||
|
- SHM/unretained surfaces either render live safely or snap where retention is
|
||||||
|
required
|
||||||
|
|
||||||
|
Record separately:
|
||||||
|
|
||||||
|
- content continues updating during movement
|
||||||
|
- content freezes but samples the wrong source region
|
||||||
|
- edges show empty/black strips while scaling
|
||||||
|
- spawn-out skips because capture was unavailable
|
||||||
|
|
||||||
|
## 14. Cross-Output And Scale Boundaries
|
||||||
|
|
||||||
|
Cases:
|
||||||
|
|
||||||
|
- move a tiled window to another output
|
||||||
|
- move a floating window to another output
|
||||||
|
- move between outputs with different scale factors, if available
|
||||||
|
- move a workspace between outputs, if supported locally
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- movement snaps instead of animating
|
||||||
|
- no retained content is rendered at the wrong scale
|
||||||
|
- no stale damage remains on the source output
|
||||||
|
- destination output renders the final layout immediately
|
||||||
|
|
||||||
|
## 15. Regression Sweep
|
||||||
|
|
||||||
|
After visual tests, return to normal animation duration and curve.
|
||||||
|
|
||||||
|
Repeat:
|
||||||
|
|
||||||
|
- ordinary tiling navigation
|
||||||
|
- workspace switching
|
||||||
|
- fullscreen enter/exit
|
||||||
|
- focus changes
|
||||||
|
- app launch/close loops
|
||||||
|
- suspend/resume or VT switch if convenient
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- animation state does not survive across unrelated compositor state changes
|
||||||
|
- no stuck retained frames
|
||||||
|
- no persistent high CPU/GPU use after animations stop
|
||||||
|
- no obvious client throttling after many retained-content animations
|
||||||
|
|
||||||
|
## Summary Result Template
|
||||||
|
|
||||||
|
```text
|
||||||
|
Commit:
|
||||||
|
Build:
|
||||||
|
Outputs/scales:
|
||||||
|
GPU/session:
|
||||||
|
Animation config:
|
||||||
|
|
||||||
|
Passed:
|
||||||
|
-
|
||||||
|
|
||||||
|
Known-limit observations:
|
||||||
|
-
|
||||||
|
|
||||||
|
Failures:
|
||||||
|
- case:
|
||||||
|
app:
|
||||||
|
layout:
|
||||||
|
expected:
|
||||||
|
actual:
|
||||||
|
reproducible:
|
||||||
|
logs:
|
||||||
|
```
|
||||||
|
|
@ -1023,6 +1023,26 @@ impl ConfigClient {
|
||||||
self.send(&ClientMessage::SetUiDragThreshold { threshold });
|
self.send(&ClientMessage::SetUiDragThreshold { threshold });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_animations_enabled(&self, enabled: bool) {
|
||||||
|
self.send(&ClientMessage::SetAnimationsEnabled { enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animation_duration_ms(&self, duration_ms: u32) {
|
||||||
|
self.send(&ClientMessage::SetAnimationDurationMs { duration_ms });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animation_curve(&self, curve: u32) {
|
||||||
|
self.send(&ClientMessage::SetAnimationCurve { curve });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animation_style(&self, style: u32) {
|
||||||
|
self.send(&ClientMessage::SetAnimationStyle { style });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||||
|
self.send(&ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_color_management_enabled(&self, enabled: bool) {
|
pub fn set_color_management_enabled(&self, enabled: bool) {
|
||||||
self.send(&ClientMessage::SetColorManagementEnabled { enabled });
|
self.send(&ClientMessage::SetColorManagementEnabled { enabled });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -545,6 +545,24 @@ pub enum ClientMessage<'a> {
|
||||||
SetUiDragThreshold {
|
SetUiDragThreshold {
|
||||||
threshold: i32,
|
threshold: i32,
|
||||||
},
|
},
|
||||||
|
SetAnimationsEnabled {
|
||||||
|
enabled: bool,
|
||||||
|
},
|
||||||
|
SetAnimationDurationMs {
|
||||||
|
duration_ms: u32,
|
||||||
|
},
|
||||||
|
SetAnimationCurve {
|
||||||
|
curve: u32,
|
||||||
|
},
|
||||||
|
SetAnimationStyle {
|
||||||
|
style: u32,
|
||||||
|
},
|
||||||
|
SetAnimationCubicBezier {
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
x2: f32,
|
||||||
|
y2: f32,
|
||||||
|
},
|
||||||
SetXScalingMode {
|
SetXScalingMode {
|
||||||
mode: XScalingMode,
|
mode: XScalingMode,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,27 @@ impl Axis {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The curve used for tiled window animations.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct AnimationCurve(pub u32);
|
||||||
|
|
||||||
|
impl AnimationCurve {
|
||||||
|
pub const LINEAR: Self = Self(0);
|
||||||
|
pub const EASE: Self = Self(1);
|
||||||
|
pub const EASE_IN: Self = Self(2);
|
||||||
|
pub const EASE_OUT: Self = Self(3);
|
||||||
|
pub const EASE_IN_OUT: Self = Self(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The presentation style used for tiled window movement animations.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct AnimationStyle(pub u32);
|
||||||
|
|
||||||
|
impl AnimationStyle {
|
||||||
|
pub const PLAIN: Self = Self(0);
|
||||||
|
pub const MULTIPHASE: Self = Self(1);
|
||||||
|
}
|
||||||
|
|
||||||
/// Exits the compositor.
|
/// Exits the compositor.
|
||||||
pub fn quit() {
|
pub fn quit() {
|
||||||
get!().quit()
|
get!().quit()
|
||||||
|
|
@ -287,6 +308,42 @@ pub fn set_ui_drag_threshold(threshold: i32) {
|
||||||
get!().set_ui_drag_threshold(threshold);
|
get!().set_ui_drag_threshold(threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enables or disables tiled window animations.
|
||||||
|
///
|
||||||
|
/// The default is `false`.
|
||||||
|
pub fn set_animations_enabled(enabled: bool) {
|
||||||
|
get!().set_animations_enabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the duration of tiled window animations in milliseconds.
|
||||||
|
///
|
||||||
|
/// The default is `160`.
|
||||||
|
pub fn set_animation_duration_ms(duration_ms: u32) {
|
||||||
|
get!().set_animation_duration_ms(duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the curve used by tiled window animations.
|
||||||
|
///
|
||||||
|
/// The default is [`AnimationCurve::EASE_OUT`].
|
||||||
|
pub fn set_animation_curve(curve: AnimationCurve) {
|
||||||
|
get!().set_animation_curve(curve.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the presentation style used for tiled window movement animations.
|
||||||
|
///
|
||||||
|
/// The default is [`AnimationStyle::MULTIPHASE`].
|
||||||
|
pub fn set_animation_style(style: AnimationStyle) {
|
||||||
|
get!().set_animation_style(style.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a custom cubic-bezier curve used by tiled window animations.
|
||||||
|
///
|
||||||
|
/// `x1` and `x2` must be between `0.0` and `1.0`. The curve starts at `(0, 0)`
|
||||||
|
/// and ends at `(1, 1)`.
|
||||||
|
pub fn set_animation_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||||
|
get!().set_animation_cubic_bezier(x1, y1, x2, y2);
|
||||||
|
}
|
||||||
|
|
||||||
/// Enables or disables the color-management protocol.
|
/// Enables or disables the color-management protocol.
|
||||||
///
|
///
|
||||||
/// The default is `false`.
|
/// The default is `false`.
|
||||||
|
|
|
||||||
1233
src/animation.rs
Normal file
1233
src/animation.rs
Normal file
File diff suppressed because it is too large
Load diff
3405
src/animation/multiphase.rs
Normal file
3405
src/animation/multiphase.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -360,6 +360,13 @@ fn start_compositor2(
|
||||||
cpu_worker,
|
cpu_worker,
|
||||||
ui_drag_enabled: Cell::new(true),
|
ui_drag_enabled: Cell::new(true),
|
||||||
ui_drag_threshold_squared: Cell::new(10),
|
ui_drag_threshold_squared: Cell::new(10),
|
||||||
|
animations: Default::default(),
|
||||||
|
layout_animations_requested: Default::default(),
|
||||||
|
layout_animations_active: Default::default(),
|
||||||
|
layout_animation_curve_override: Default::default(),
|
||||||
|
layout_animation_style_override: Default::default(),
|
||||||
|
layout_animation_batch: Default::default(),
|
||||||
|
suppress_animations_for_next_layout: Default::default(),
|
||||||
toplevels: Default::default(),
|
toplevels: Default::default(),
|
||||||
const_40hz_latch: Default::default(),
|
const_40hz_latch: Default::default(),
|
||||||
tray_item_ids: Default::default(),
|
tray_item_ids: Default::default(),
|
||||||
|
|
|
||||||
|
|
@ -658,17 +658,23 @@ impl ConfigProxyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> {
|
fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_layout_animations(|| {
|
||||||
seat.move_focused(direction.into());
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.move_focused(direction.into());
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> {
|
fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> {
|
||||||
let window = self.get_window(window)?;
|
self.state.with_layout_animations(|| {
|
||||||
if let Some(c) = toplevel_parent_container(&*window) {
|
let window = self.get_window(window)?;
|
||||||
c.move_child(window, direction.into());
|
if let Some(float) = window.tl_data().float.get() {
|
||||||
}
|
float.move_by_direction(direction.into());
|
||||||
Ok(())
|
} else if let Some(c) = toplevel_parent_container(&*window) {
|
||||||
|
c.move_child(window, direction.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> {
|
fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> {
|
||||||
|
|
@ -986,6 +992,31 @@ impl ConfigProxyHandler {
|
||||||
self.state.set_ui_drag_threshold(threshold.max(1));
|
self.state.set_ui_drag_threshold(threshold.max(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_set_animations_enabled(&self, enabled: bool) {
|
||||||
|
self.state.set_animations_enabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_set_animation_duration_ms(&self, duration_ms: u32) {
|
||||||
|
self.state
|
||||||
|
.set_animation_duration_ms(duration_ms.min(10_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_set_animation_curve(&self, curve: u32) {
|
||||||
|
self.state.set_animation_curve(curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_set_animation_style(&self, style: u32) {
|
||||||
|
if !self.state.set_animation_style(style) {
|
||||||
|
log::warn!("Ignoring invalid animation style");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||||
|
if !self.state.set_animation_cubic_bezier(x1, y1, x2, y2) {
|
||||||
|
log::warn!("Ignoring invalid animation cubic-bezier curve");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_set_direct_scanout_enabled(
|
fn handle_set_direct_scanout_enabled(
|
||||||
&self,
|
&self,
|
||||||
device: Option<DrmDevice>,
|
device: Option<DrmDevice>,
|
||||||
|
|
@ -1724,9 +1755,11 @@ impl ConfigProxyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> {
|
fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_layout_animations(|| {
|
||||||
seat.set_mono(mono);
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.set_mono(mono);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> {
|
fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> {
|
||||||
|
|
@ -1740,11 +1773,13 @@ impl ConfigProxyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> {
|
fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> {
|
||||||
let window = self.get_window(window)?;
|
self.state.with_layout_animations(|| {
|
||||||
if let Some(c) = toplevel_parent_container(&*window) {
|
let window = self.get_window(window)?;
|
||||||
c.set_mono(mono.then_some(window.as_ref()));
|
if let Some(c) = toplevel_parent_container(&*window) {
|
||||||
}
|
c.set_mono(mono.then_some(window.as_ref()));
|
||||||
Ok(())
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> {
|
fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> {
|
||||||
|
|
@ -1759,15 +1794,19 @@ impl ConfigProxyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> {
|
fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_layout_animations(|| {
|
||||||
seat.set_split(axis.into());
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.set_split(axis.into());
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> {
|
fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_layout_animations(|| {
|
||||||
seat.toggle_tab();
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.toggle_tab();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_seat_make_group(
|
fn handle_seat_make_group(
|
||||||
|
|
@ -1776,27 +1815,35 @@ impl ConfigProxyHandler {
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
ephemeral: bool,
|
ephemeral: bool,
|
||||||
) -> Result<(), CphError> {
|
) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_layout_animations(|| {
|
||||||
seat.make_group(axis.into(), ephemeral);
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.make_group(axis.into(), ephemeral);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> {
|
fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_layout_animations(|| {
|
||||||
seat.change_group_opposite();
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.change_group_opposite();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> {
|
fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_layout_animations(|| {
|
||||||
seat.equalize(recursive);
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.equalize(recursive);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> {
|
fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_layout_animations(|| {
|
||||||
seat.move_tab(right);
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.move_tab(right);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> {
|
fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> {
|
||||||
|
|
@ -1811,11 +1858,13 @@ impl ConfigProxyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> {
|
fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> {
|
||||||
let window = self.get_window(window)?;
|
self.state.with_layout_animations(|| {
|
||||||
if let Some(c) = toplevel_parent_container(&*window) {
|
let window = self.get_window(window)?;
|
||||||
c.set_split(axis.into());
|
if let Some(c) = toplevel_parent_container(&*window) {
|
||||||
}
|
c.set_split(axis.into());
|
||||||
Ok(())
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_add_shortcut(
|
fn handle_add_shortcut(
|
||||||
|
|
@ -1955,9 +2004,11 @@ impl ConfigProxyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> {
|
fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> {
|
||||||
let seat = self.get_seat(seat)?;
|
self.state.with_linear_layout_animations(|| {
|
||||||
seat.set_floating(floating);
|
let seat = self.get_seat(seat)?;
|
||||||
Ok(())
|
seat.set_floating(floating);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> {
|
fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> {
|
||||||
|
|
@ -1969,9 +2020,11 @@ impl ConfigProxyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> {
|
fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> {
|
||||||
let window = self.get_window(window)?;
|
self.state.with_linear_layout_animations(|| {
|
||||||
toplevel_set_floating(&self.state, window, floating);
|
let window = self.get_window(window)?;
|
||||||
Ok(())
|
toplevel_set_floating(&self.state, window, floating);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_add_pollable(self: &Rc<Self>, fd: i32) -> Result<(), CphError> {
|
fn handle_add_pollable(self: &Rc<Self>, fd: i32) -> Result<(), CphError> {
|
||||||
|
|
@ -2721,8 +2774,10 @@ impl ConfigProxyHandler {
|
||||||
dx2: i32,
|
dx2: i32,
|
||||||
dy2: i32,
|
dy2: i32,
|
||||||
) -> Result<(), CphError> {
|
) -> Result<(), CphError> {
|
||||||
self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2);
|
self.state.with_layout_animations(|| {
|
||||||
Ok(())
|
self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_window_exists(&self, window: Window) {
|
fn handle_window_exists(&self, window: Window) {
|
||||||
|
|
@ -3193,6 +3248,17 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::SetUiDragThreshold { threshold } => {
|
ClientMessage::SetUiDragThreshold { threshold } => {
|
||||||
self.handle_set_ui_drag_threshold(threshold)
|
self.handle_set_ui_drag_threshold(threshold)
|
||||||
}
|
}
|
||||||
|
ClientMessage::SetAnimationsEnabled { enabled } => {
|
||||||
|
self.handle_set_animations_enabled(enabled)
|
||||||
|
}
|
||||||
|
ClientMessage::SetAnimationDurationMs { duration_ms } => {
|
||||||
|
self.handle_set_animation_duration_ms(duration_ms)
|
||||||
|
}
|
||||||
|
ClientMessage::SetAnimationCurve { curve } => self.handle_set_animation_curve(curve),
|
||||||
|
ClientMessage::SetAnimationStyle { style } => self.handle_set_animation_style(style),
|
||||||
|
ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 } => {
|
||||||
|
self.handle_set_animation_cubic_bezier(x1, y1, x2, y2)
|
||||||
|
}
|
||||||
ClientMessage::SetXScalingMode { mode } => self
|
ClientMessage::SetXScalingMode { mode } => self
|
||||||
.handle_set_x_scaling_mode(mode)
|
.handle_set_x_scaling_mode(mode)
|
||||||
.wrn("set_x_scaling_mode")?,
|
.wrn("set_x_scaling_mode")?,
|
||||||
|
|
|
||||||
|
|
@ -936,6 +936,9 @@ impl WlSeatGlobal {
|
||||||
{
|
{
|
||||||
c.move_child(tl, direction);
|
c.move_child(tl, direction);
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
} else if let Some(float) = data.float.get() {
|
||||||
|
float.move_by_direction(direction);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -628,6 +628,11 @@ fn schedule_async_upload(
|
||||||
{
|
{
|
||||||
back_tex_opt = None;
|
back_tex_opt = None;
|
||||||
}
|
}
|
||||||
|
if let Some(back_tex) = &back_tex_opt
|
||||||
|
&& Rc::strong_count(back_tex) > 1
|
||||||
|
{
|
||||||
|
back_tex_opt = None;
|
||||||
|
}
|
||||||
let damage_full = || {
|
let damage_full = || {
|
||||||
back.damage.clear();
|
back.damage.clear();
|
||||||
back.damage.damage(slice::from_ref(&buf.rect));
|
back.damage.damage(slice::from_ref(&buf.rect));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
ifs::wl_surface::{
|
ifs::wl_surface::{
|
||||||
SurfaceExt, WlSurface, WlSurfaceError,
|
PendingState, SurfaceExt, WlSurface, WlSurfaceError,
|
||||||
x_surface::{xwayland_surface_v1::XwaylandSurfaceV1, xwindow::Xwindow},
|
x_surface::{xwayland_surface_v1::XwaylandSurfaceV1, xwindow::Xwindow},
|
||||||
},
|
},
|
||||||
leaks::Tracker,
|
leaks::Tracker,
|
||||||
|
|
@ -30,6 +30,22 @@ impl SurfaceExt for XSurface {
|
||||||
win.node_layer()
|
win.node_layer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn before_apply_commit(
|
||||||
|
self: Rc<Self>,
|
||||||
|
pending: &mut PendingState,
|
||||||
|
) -> Result<(), WlSurfaceError> {
|
||||||
|
if pending
|
||||||
|
.buffer
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|buffer| buffer.is_none())
|
||||||
|
&& self.surface.buffer.is_some()
|
||||||
|
&& let Some(xwindow) = self.xwindow.get()
|
||||||
|
{
|
||||||
|
xwindow.queue_spawn_out();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn after_apply_commit(self: Rc<Self>) {
|
fn after_apply_commit(self: Rc<Self>) {
|
||||||
if let Some(xwindow) = self.xwindow.get() {
|
if let Some(xwindow) = self.xwindow.get() {
|
||||||
xwindow.map_status_changed();
|
xwindow.map_status_changed();
|
||||||
|
|
@ -45,6 +61,7 @@ impl SurfaceExt for XSurface {
|
||||||
}
|
}
|
||||||
self.surface.unset_ext();
|
self.surface.unset_ext();
|
||||||
if let Some(xwindow) = self.xwindow.take() {
|
if let Some(xwindow) = self.xwindow.take() {
|
||||||
|
xwindow.queue_spawn_out();
|
||||||
xwindow.tl_destroy();
|
xwindow.tl_destroy();
|
||||||
xwindow.data.window.set(None);
|
xwindow.data.window.set(None);
|
||||||
xwindow.data.surface_id.set(None);
|
xwindow.data.surface_id.set(None);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
|
animation::RetainedToplevel,
|
||||||
client::Client,
|
client::Client,
|
||||||
cursor::KnownCursor,
|
cursor::KnownCursor,
|
||||||
fixed::Fixed,
|
fixed::Fixed,
|
||||||
|
|
@ -252,6 +253,11 @@ impl Xwindow {
|
||||||
self.x.surface.buffer.is_some() && self.data.info.mapped.get()
|
self.x.surface.buffer.is_some() && self.data.info.mapped.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn queue_spawn_out(&self) {
|
||||||
|
self.toplevel_data
|
||||||
|
.queue_spawn_out(self, self.tl_animation_snapshot());
|
||||||
|
}
|
||||||
|
|
||||||
fn map_change(&self) -> Change {
|
fn map_change(&self) -> Change {
|
||||||
match (self.may_be_mapped(), self.is_mapped()) {
|
match (self.may_be_mapped(), self.is_mapped()) {
|
||||||
(true, false) => Change::Map,
|
(true, false) => Change::Map,
|
||||||
|
|
@ -274,6 +280,7 @@ impl Xwindow {
|
||||||
match map_change {
|
match map_change {
|
||||||
Change::None => return,
|
Change::None => return,
|
||||||
Change::Unmap => {
|
Change::Unmap => {
|
||||||
|
self.queue_spawn_out();
|
||||||
self.data
|
self.data
|
||||||
.info
|
.info
|
||||||
.pending_extents
|
.pending_extents
|
||||||
|
|
@ -514,6 +521,10 @@ impl ToplevelNodeBase for Xwindow {
|
||||||
Some(self.x.surface.clone())
|
Some(self.x.surface.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
|
||||||
|
RetainedToplevel::capture_surface(&self.x.surface, (0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
fn tl_admits_children(&self) -> bool {
|
fn tl_admits_children(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,10 @@ pub trait XdgSurfaceExt: Debug {
|
||||||
// nothing
|
// nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_unmap(&self) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
fn extents_changed(&self) {
|
fn extents_changed(&self) {
|
||||||
// nothing
|
// nothing
|
||||||
}
|
}
|
||||||
|
|
@ -664,6 +668,15 @@ impl SurfaceExt for XdgSurface {
|
||||||
if let Some(serial) = pending.serial.take() {
|
if let Some(serial) = pending.serial.take() {
|
||||||
self.applied_serial.set(serial);
|
self.applied_serial.set(serial);
|
||||||
}
|
}
|
||||||
|
if pending
|
||||||
|
.buffer
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|buffer| buffer.is_none())
|
||||||
|
&& self.surface.buffer.is_some()
|
||||||
|
&& let Some(ext) = self.ext.get()
|
||||||
|
{
|
||||||
|
ext.prepare_unmap();
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pub mod xdg_dialog_v1;
|
||||||
|
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
|
animation::RetainedToplevel,
|
||||||
bugs,
|
bugs,
|
||||||
bugs::Bugs,
|
bugs::Bugs,
|
||||||
client::{Client, ClientError},
|
client::{Client, ClientError},
|
||||||
|
|
@ -259,6 +260,7 @@ impl XdgToplevelRequestHandler for XdgToplevel {
|
||||||
type Error = XdgToplevelError;
|
type Error = XdgToplevelError;
|
||||||
|
|
||||||
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
|
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
|
||||||
|
self.queue_spawn_out();
|
||||||
self.tl_destroy();
|
self.tl_destroy();
|
||||||
self.xdg.unset_ext();
|
self.xdg.unset_ext();
|
||||||
{
|
{
|
||||||
|
|
@ -398,6 +400,11 @@ impl XdgToplevelRequestHandler for XdgToplevel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XdgToplevel {
|
impl XdgToplevel {
|
||||||
|
fn queue_spawn_out(&self) {
|
||||||
|
self.toplevel_data
|
||||||
|
.queue_spawn_out(self, self.tl_animation_snapshot());
|
||||||
|
}
|
||||||
|
|
||||||
fn map(
|
fn map(
|
||||||
self: &Rc<Self>,
|
self: &Rc<Self>,
|
||||||
parent: Option<&XdgToplevel>,
|
parent: Option<&XdgToplevel>,
|
||||||
|
|
@ -779,6 +786,11 @@ impl ToplevelNodeBase for XdgToplevel {
|
||||||
Some(self.xdg.surface.clone())
|
Some(self.xdg.surface.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
|
||||||
|
let geo = self.xdg.geometry();
|
||||||
|
RetainedToplevel::capture_surface(&self.xdg.surface, (-geo.x1(), -geo.y1()))
|
||||||
|
}
|
||||||
|
|
||||||
fn tl_restack_popups(&self) {
|
fn tl_restack_popups(&self) {
|
||||||
self.xdg.restack_popups();
|
self.xdg.restack_popups();
|
||||||
}
|
}
|
||||||
|
|
@ -818,6 +830,10 @@ impl XdgSurfaceExt for XdgToplevel {
|
||||||
self.after_commit(None);
|
self.after_commit(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_unmap(&self) {
|
||||||
|
self.queue_spawn_out();
|
||||||
|
}
|
||||||
|
|
||||||
fn extents_changed(&self) {
|
fn extents_changed(&self) {
|
||||||
self.toplevel_data.pos.set(self.xdg.extents.get());
|
self.toplevel_data.pos.set(self.xdg.extents.get());
|
||||||
self.tl_extents_changed();
|
self.tl_extents_changed();
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ mod leaks;
|
||||||
mod tracy;
|
mod tracy;
|
||||||
mod acceptor;
|
mod acceptor;
|
||||||
mod allocator;
|
mod allocator;
|
||||||
|
mod animation;
|
||||||
mod async_engine;
|
mod async_engine;
|
||||||
mod backend;
|
mod backend;
|
||||||
mod backends;
|
mod backends;
|
||||||
|
|
|
||||||
367
src/renderer.rs
367
src/renderer.rs
|
|
@ -1,7 +1,11 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
|
animation::{
|
||||||
|
RetainedContent, RetainedExitFrame, RetainedExitLayer, RetainedSurface,
|
||||||
|
RetainedToplevel,
|
||||||
|
},
|
||||||
cmm::cmm_render_intent::RenderIntent,
|
cmm::cmm_render_intent::RenderIntent,
|
||||||
gfx_api::{AcquireSync, AlphaMode, GfxApiOpt, ReleaseSync, SampleRect},
|
gfx_api::{AcquireSync, AlphaMode, BufferResv, GfxApiOpt, ReleaseSync, SampleRect},
|
||||||
ifs::wl_surface::{
|
ifs::wl_surface::{
|
||||||
SurfaceBuffer, WlSurface,
|
SurfaceBuffer, WlSurface,
|
||||||
x_surface::xwindow::Xwindow,
|
x_surface::xwindow::Xwindow,
|
||||||
|
|
@ -14,8 +18,8 @@ use {
|
||||||
state::State,
|
state::State,
|
||||||
theme::{Color, CornerRadius},
|
theme::{Color, CornerRadius},
|
||||||
tree::{
|
tree::{
|
||||||
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
|
ContainerNode, DisplayNode, FloatNode, Node, OutputNode, PlaceholderNode, ToplevelData,
|
||||||
ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
ToplevelNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
std::{ops::Deref, rc::Rc, slice},
|
std::{ops::Deref, rc::Rc, slice},
|
||||||
|
|
@ -200,14 +204,22 @@ impl Renderer<'_> {
|
||||||
self.render_workspace(&ws, x, y);
|
self.render_workspace(&ws, x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let now = self.state.now_nsec();
|
||||||
|
let exit_frames = self.state.animations.exit_frames(now);
|
||||||
|
self.render_exit_frames(&exit_frames, RetainedExitLayer::Tiled, &opos);
|
||||||
macro_rules! render_stacked {
|
macro_rules! render_stacked {
|
||||||
($stack:expr) => {
|
($stack:expr) => {
|
||||||
for stacked in $stack.iter() {
|
for stacked in $stack.iter() {
|
||||||
if stacked.node_visible() {
|
if stacked.node_visible() {
|
||||||
self.base.sync();
|
self.base.sync();
|
||||||
let pos = stacked.node_absolute_position();
|
let pos = stacked.node_absolute_position();
|
||||||
if pos.intersects(&opos) {
|
let visual = self.state.animations.visual_rect(
|
||||||
let (x, y) = opos.translate(pos.x1(), pos.y1());
|
stacked.node_id(),
|
||||||
|
pos,
|
||||||
|
self.state.now_nsec(),
|
||||||
|
);
|
||||||
|
if visual.intersects(&opos) {
|
||||||
|
let (x, y) = opos.translate(visual.x1(), visual.y1());
|
||||||
stacked.node_render(self, x, y, None);
|
stacked.node_render(self, x, y, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +227,7 @@ impl Renderer<'_> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
render_stacked!(self.state.root.stacked);
|
render_stacked!(self.state.root.stacked);
|
||||||
|
self.render_exit_frames(&exit_frames, RetainedExitLayer::Floating, &opos);
|
||||||
// Flush RoundedFillRect ops from container/float borders so they don't
|
// Flush RoundedFillRect ops from container/float borders so they don't
|
||||||
// sort after (and render on top of) layer-shell CopyTexture ops.
|
// sort after (and render on top of) layer-shell CopyTexture ops.
|
||||||
self.base.sync();
|
self.base.sync();
|
||||||
|
|
@ -453,6 +466,265 @@ impl Renderer<'_> {
|
||||||
.fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y);
|
.fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn presentation_child_body(
|
||||||
|
&self,
|
||||||
|
container: &ContainerNode,
|
||||||
|
child: &Rc<dyn ToplevelNode>,
|
||||||
|
body: Rect,
|
||||||
|
) -> Rect {
|
||||||
|
let abs = body.move_(container.abs_x1.get(), container.abs_y1.get());
|
||||||
|
let visual = self
|
||||||
|
.state
|
||||||
|
.animations
|
||||||
|
.visual_rect(child.node_id(), abs, self.state.now_nsec());
|
||||||
|
visual.move_(-container.abs_x1.get(), -container.abs_y1.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_child_or_snapshot(
|
||||||
|
&mut self,
|
||||||
|
child: &Rc<dyn ToplevelNode>,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
bounds: Option<&Rect>,
|
||||||
|
) {
|
||||||
|
if let Some(retained) = self
|
||||||
|
.state
|
||||||
|
.animations
|
||||||
|
.retained_snapshot(child.node_id(), self.state.now_nsec())
|
||||||
|
{
|
||||||
|
self.render_retained_toplevel(&retained, x, y, bounds);
|
||||||
|
} else {
|
||||||
|
child.node_render(self, x, y, bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_retained_toplevel(
|
||||||
|
&mut self,
|
||||||
|
retained: &RetainedToplevel,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
bounds: Option<&Rect>,
|
||||||
|
) {
|
||||||
|
let (x, y) = self
|
||||||
|
.base
|
||||||
|
.scale_point(x + retained.offset.0, y + retained.offset.1);
|
||||||
|
self.render_retained_surface_scaled(&retained.surface, x, y, None, bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_exit_frames(
|
||||||
|
&mut self,
|
||||||
|
frames: &[RetainedExitFrame],
|
||||||
|
layer: RetainedExitLayer,
|
||||||
|
output_rect: &Rect,
|
||||||
|
) {
|
||||||
|
for frame in frames {
|
||||||
|
if frame.layer != layer || !frame.rect.intersects(output_rect) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.render_exit_frame(frame, output_rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_exit_frame(&mut self, frame: &RetainedExitFrame, output_rect: &Rect) {
|
||||||
|
let (x, y) = output_rect.translate(frame.rect.x1(), frame.rect.y1());
|
||||||
|
let inset = frame.frame_inset;
|
||||||
|
if inset > 0 {
|
||||||
|
let color = if frame.active {
|
||||||
|
self.state.theme.colors.active_border.get()
|
||||||
|
} else {
|
||||||
|
self.state.theme.colors.border.get()
|
||||||
|
};
|
||||||
|
self.render_rounded_frame(
|
||||||
|
Rect::new_sized_saturating(0, 0, frame.rect.width(), frame.rect.height()),
|
||||||
|
&color,
|
||||||
|
self.state.theme.corner_radius.get(),
|
||||||
|
inset,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let body = Rect::new_sized_saturating(
|
||||||
|
x + inset,
|
||||||
|
y + inset,
|
||||||
|
frame.rect.width() - 2 * inset,
|
||||||
|
frame.rect.height() - 2 * inset,
|
||||||
|
);
|
||||||
|
if body.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if inset > 0 && !self.state.theme.corner_radius.get().is_zero() {
|
||||||
|
let inner_cr = self.scale_corner_radius(
|
||||||
|
self.state
|
||||||
|
.theme
|
||||||
|
.corner_radius
|
||||||
|
.get()
|
||||||
|
.expanded_by(-(inset as f32)),
|
||||||
|
);
|
||||||
|
self.corner_radius = Some(inner_cr);
|
||||||
|
}
|
||||||
|
self.render_window_body_background(body);
|
||||||
|
let bounds = self.base.scale_rect(body);
|
||||||
|
self.stretch = if frame.source_body_size != body.size() {
|
||||||
|
Some(self.base.scale_point(body.width(), body.height()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
self.render_retained_toplevel(&frame.retained, body.x1(), body.y1(), Some(&bounds));
|
||||||
|
self.stretch = None;
|
||||||
|
self.corner_radius = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_window_body_background(&mut self, body: Rect) {
|
||||||
|
if body.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let color = self.state.theme.colors.background.get();
|
||||||
|
let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||||
|
let srgb = &srgb_srgb.linear;
|
||||||
|
let perceptual = RenderIntent::Perceptual;
|
||||||
|
self.base.sync();
|
||||||
|
if let Some(cr) = self.corner_radius
|
||||||
|
&& !cr.is_zero()
|
||||||
|
{
|
||||||
|
self.base
|
||||||
|
.fill_rounded_rect(body, &color, None, srgb, perceptual, cr, 0.0);
|
||||||
|
} else {
|
||||||
|
let bounds = self.base.scale_rect(body);
|
||||||
|
self.base
|
||||||
|
.fill_scaled_boxes(slice::from_ref(&bounds), &color, None, srgb, perceptual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_retained_surface_scaled(
|
||||||
|
&mut self,
|
||||||
|
retained: &RetainedSurface,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
pos_rel: Option<(i32, i32)>,
|
||||||
|
bounds: Option<&Rect>,
|
||||||
|
) {
|
||||||
|
let stretch = self.stretch.take();
|
||||||
|
let corner_radius = self.corner_radius.take();
|
||||||
|
let mut size = retained.size;
|
||||||
|
if let Some((x_rel, y_rel)) = pos_rel {
|
||||||
|
let (x, y) = self.base.scale_point(x_rel, y_rel);
|
||||||
|
let (w, h) = self.base.scale_point(x_rel + size.0, y_rel + size.1);
|
||||||
|
size = (w - x, h - y);
|
||||||
|
} else {
|
||||||
|
size = self.base.scale_point(size.0, size.1);
|
||||||
|
}
|
||||||
|
let mut stretched_source = None;
|
||||||
|
if let Some(s) = stretch {
|
||||||
|
if let RetainedContent::Texture { source, .. } = &retained.content {
|
||||||
|
let mut source = *source;
|
||||||
|
if size.0 > 0 && size.1 > 0 {
|
||||||
|
let sx = s.0 as f32 / size.0 as f32;
|
||||||
|
let sy = s.1 as f32 / size.1 as f32;
|
||||||
|
source.x2 *= sx;
|
||||||
|
source.y2 *= sy;
|
||||||
|
}
|
||||||
|
stretched_source = Some(source);
|
||||||
|
}
|
||||||
|
size = s;
|
||||||
|
}
|
||||||
|
for child in &retained.below {
|
||||||
|
let (x1, y1) = self.base.scale_point(child.offset.0, child.offset.1);
|
||||||
|
self.render_retained_surface_scaled(child, x + x1, y + y1, Some(child.offset), bounds);
|
||||||
|
}
|
||||||
|
self.corner_radius = corner_radius;
|
||||||
|
self.render_retained_content(retained, stretched_source, x, y, size, bounds);
|
||||||
|
for child in &retained.above {
|
||||||
|
let (x1, y1) = self.base.scale_point(child.offset.0, child.offset.1);
|
||||||
|
self.render_retained_surface_scaled(child, x + x1, y + y1, Some(child.offset), bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_retained_content(
|
||||||
|
&mut self,
|
||||||
|
retained: &RetainedSurface,
|
||||||
|
stretched_source: Option<SampleRect>,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
size: (i32, i32),
|
||||||
|
bounds: Option<&Rect>,
|
||||||
|
) {
|
||||||
|
let corner_radius = self.corner_radius.take();
|
||||||
|
match &retained.content {
|
||||||
|
RetainedContent::Texture {
|
||||||
|
texture,
|
||||||
|
buffer,
|
||||||
|
source,
|
||||||
|
alpha,
|
||||||
|
color_description,
|
||||||
|
render_intent,
|
||||||
|
alpha_mode,
|
||||||
|
opaque,
|
||||||
|
} => {
|
||||||
|
let source = stretched_source.unwrap_or(*source);
|
||||||
|
if let Some(cr) = corner_radius {
|
||||||
|
self.base.render_rounded_texture(
|
||||||
|
texture,
|
||||||
|
*alpha,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
Some(source),
|
||||||
|
Some(size),
|
||||||
|
self.base.scale,
|
||||||
|
bounds,
|
||||||
|
Some(buffer.clone() as Rc<dyn BufferResv>),
|
||||||
|
AcquireSync::Unnecessary,
|
||||||
|
buffer.release_sync,
|
||||||
|
color_description,
|
||||||
|
*render_intent,
|
||||||
|
*alpha_mode,
|
||||||
|
cr,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.base.render_texture(
|
||||||
|
texture,
|
||||||
|
*alpha,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
Some(source),
|
||||||
|
Some(size),
|
||||||
|
self.base.scale,
|
||||||
|
bounds,
|
||||||
|
Some(buffer.clone() as Rc<dyn BufferResv>),
|
||||||
|
AcquireSync::Unnecessary,
|
||||||
|
buffer.release_sync,
|
||||||
|
*opaque,
|
||||||
|
color_description,
|
||||||
|
*render_intent,
|
||||||
|
*alpha_mode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RetainedContent::Color {
|
||||||
|
color,
|
||||||
|
alpha,
|
||||||
|
color_description,
|
||||||
|
render_intent,
|
||||||
|
} => {
|
||||||
|
if let Some(rect) = Rect::new_sized(x, y, size.0, size.1) {
|
||||||
|
let rect = match bounds {
|
||||||
|
None => rect,
|
||||||
|
Some(bounds) => rect.intersect(*bounds),
|
||||||
|
};
|
||||||
|
if !rect.is_empty() {
|
||||||
|
self.base.sync();
|
||||||
|
self.base.fill_scaled_boxes(
|
||||||
|
&[rect],
|
||||||
|
color,
|
||||||
|
*alpha,
|
||||||
|
&color_description.linear,
|
||||||
|
*render_intent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) {
|
pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) {
|
||||||
self.render_container_decorations(container, x, y);
|
self.render_container_decorations(container, x, y);
|
||||||
|
|
||||||
|
|
@ -465,6 +737,7 @@ impl Renderer<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mb = container.mono_body.get();
|
let mb = container.mono_body.get();
|
||||||
|
let visual_mb = self.presentation_child_body(container, &child.node, mb);
|
||||||
if self.state.theme.sizes.gap.get() != 0 {
|
if self.state.theme.sizes.gap.get() != 0 {
|
||||||
let bw = self.state.theme.sizes.border_width.get();
|
let bw = self.state.theme.sizes.border_width.get();
|
||||||
let border_color = self.state.theme.colors.border.get();
|
let border_color = self.state.theme.colors.border.get();
|
||||||
|
|
@ -476,10 +749,10 @@ impl Renderer<'_> {
|
||||||
};
|
};
|
||||||
if !child.node.node_is_container() {
|
if !child.node.node_is_container() {
|
||||||
let frame = Rect::new_sized_saturating(
|
let frame = Rect::new_sized_saturating(
|
||||||
mb.x1() - bw,
|
visual_mb.x1() - bw,
|
||||||
mb.y1() - bw,
|
visual_mb.y1() - bw,
|
||||||
mb.width() + 2 * bw,
|
visual_mb.width() + 2 * bw,
|
||||||
mb.height() + 2 * bw,
|
visual_mb.height() + 2 * bw,
|
||||||
);
|
);
|
||||||
self.render_rounded_frame(
|
self.render_rounded_frame(
|
||||||
frame,
|
frame,
|
||||||
|
|
@ -491,14 +764,17 @@ impl Renderer<'_> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let body = mb.move_(x, y);
|
let body = visual_mb.move_(x, y);
|
||||||
let body = self.base.scale_rect(body);
|
let content = container
|
||||||
let content = container.mono_content.get();
|
.mono_content
|
||||||
self.stretch = if content.width() != mb.width() || content.height() != mb.height() {
|
.get()
|
||||||
Some(self.base.scale_point(mb.width(), mb.height()))
|
.at_point(visual_mb.x1(), visual_mb.y1());
|
||||||
} else {
|
self.stretch =
|
||||||
None
|
if content.width() != visual_mb.width() || content.height() != visual_mb.height() {
|
||||||
};
|
Some(self.base.scale_point(visual_mb.width(), visual_mb.height()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
if self.state.theme.sizes.gap.get() != 0 && !child.node.node_is_container() {
|
if self.state.theme.sizes.gap.get() != 0 && !child.node.node_is_container() {
|
||||||
let cr = self.state.theme.corner_radius.get();
|
let cr = self.state.theme.corner_radius.get();
|
||||||
if !cr.is_zero() {
|
if !cr.is_zero() {
|
||||||
|
|
@ -507,9 +783,16 @@ impl Renderer<'_> {
|
||||||
self.corner_radius = Some(inner_cr);
|
self.corner_radius = Some(inner_cr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
child
|
if !child.node.node_is_container() {
|
||||||
.node
|
self.render_window_body_background(body);
|
||||||
.node_render(self, x + content.x1(), y + content.y1(), Some(&body));
|
}
|
||||||
|
let body = self.base.scale_rect(body);
|
||||||
|
self.render_child_or_snapshot(
|
||||||
|
&child.node,
|
||||||
|
x + content.x1(),
|
||||||
|
y + content.y1(),
|
||||||
|
Some(&body),
|
||||||
|
);
|
||||||
self.stretch = None;
|
self.stretch = None;
|
||||||
self.corner_radius = None;
|
self.corner_radius = None;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -524,10 +807,13 @@ impl Renderer<'_> {
|
||||||
};
|
};
|
||||||
let cr = self.state.theme.corner_radius.get();
|
let cr = self.state.theme.corner_radius.get();
|
||||||
for child in container.children.iter() {
|
for child in container.children.iter() {
|
||||||
let body = child.body.get();
|
let layout_body = child.body.get();
|
||||||
if body.x1() >= container.width.get() || body.y1() >= container.height.get() {
|
if layout_body.x1() >= container.width.get()
|
||||||
|
|| layout_body.y1() >= container.height.get()
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
let body = self.presentation_child_body(container, &child.node, layout_body);
|
||||||
if gap != 0 {
|
if gap != 0 {
|
||||||
let c = if child.border_color_is_focused.get() {
|
let c = if child.border_color_is_focused.get() {
|
||||||
&focused_border_color
|
&focused_border_color
|
||||||
|
|
@ -544,7 +830,7 @@ impl Renderer<'_> {
|
||||||
self.render_rounded_frame(frame, c, cr, bw, x, y);
|
self.render_rounded_frame(frame, c, cr, bw, x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let content = child.content.get();
|
let content = child.content.get().at_point(body.x1(), body.y1());
|
||||||
self.stretch =
|
self.stretch =
|
||||||
if content.width() != body.width() || content.height() != body.height() {
|
if content.width() != body.width() || content.height() != body.height() {
|
||||||
Some(self.base.scale_point(body.width(), body.height()))
|
Some(self.base.scale_point(body.width(), body.height()))
|
||||||
|
|
@ -556,10 +842,16 @@ impl Renderer<'_> {
|
||||||
self.corner_radius = Some(inner_cr);
|
self.corner_radius = Some(inner_cr);
|
||||||
}
|
}
|
||||||
let body = body.move_(x, y);
|
let body = body.move_(x, y);
|
||||||
|
if !child.node.node_is_container() {
|
||||||
|
self.render_window_body_background(body);
|
||||||
|
}
|
||||||
let body = self.base.scale_rect(body);
|
let body = self.base.scale_rect(body);
|
||||||
child
|
self.render_child_or_snapshot(
|
||||||
.node
|
&child.node,
|
||||||
.node_render(self, x + content.x1(), y + content.y1(), Some(&body));
|
x + content.x1(),
|
||||||
|
y + content.y1(),
|
||||||
|
Some(&body),
|
||||||
|
);
|
||||||
self.stretch = None;
|
self.stretch = None;
|
||||||
self.corner_radius = None;
|
self.corner_radius = None;
|
||||||
}
|
}
|
||||||
|
|
@ -793,6 +1085,10 @@ impl Renderer<'_> {
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let pos = floating.position.get();
|
let pos = floating.position.get();
|
||||||
|
let visual =
|
||||||
|
self.state
|
||||||
|
.animations
|
||||||
|
.visual_rect(floating.node_id(), pos, self.state.now_nsec());
|
||||||
let theme = &self.state.theme;
|
let theme = &self.state.theme;
|
||||||
let bw = theme.sizes.border_width.get();
|
let bw = theme.sizes.border_width.get();
|
||||||
let bc = if floating.active.get() {
|
let bc = if floating.active.get() {
|
||||||
|
|
@ -801,16 +1097,27 @@ impl Renderer<'_> {
|
||||||
theme.colors.border.get()
|
theme.colors.border.get()
|
||||||
};
|
};
|
||||||
let cr = theme.corner_radius.get();
|
let cr = theme.corner_radius.get();
|
||||||
let outer = Rect::new_sized_saturating(0, 0, pos.width(), pos.height());
|
let outer = Rect::new_sized_saturating(0, 0, visual.width(), visual.height());
|
||||||
self.render_rounded_frame(outer, &bc, cr, bw, x, y);
|
self.render_rounded_frame(outer, &bc, cr, bw, x, y);
|
||||||
let body =
|
let body = Rect::new_sized_saturating(
|
||||||
Rect::new_sized_saturating(x + bw, y + bw, pos.width() - 2 * bw, pos.height() - 2 * bw);
|
x + bw,
|
||||||
|
y + bw,
|
||||||
|
visual.width() - 2 * bw,
|
||||||
|
visual.height() - 2 * bw,
|
||||||
|
);
|
||||||
let scissor_body = self.base.scale_rect(body);
|
let scissor_body = self.base.scale_rect(body);
|
||||||
|
self.stretch = if pos.width() != visual.width() || pos.height() != visual.height() {
|
||||||
|
Some(self.base.scale_point(body.width(), body.height()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
if !cr.is_zero() {
|
if !cr.is_zero() {
|
||||||
let inner_cr = self.scale_corner_radius(cr.expanded_by(-(bw as f32)));
|
let inner_cr = self.scale_corner_radius(cr.expanded_by(-(bw as f32)));
|
||||||
self.corner_radius = Some(inner_cr);
|
self.corner_radius = Some(inner_cr);
|
||||||
}
|
}
|
||||||
child.node_render(self, body.x1(), body.y1(), Some(&scissor_body));
|
self.render_window_body_background(body);
|
||||||
|
self.render_child_or_snapshot(&child, body.x1(), body.y1(), Some(&scissor_body));
|
||||||
|
self.stretch = None;
|
||||||
self.corner_radius = None;
|
self.corner_radius = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
886
src/state.rs
886
src/state.rs
|
|
@ -2,6 +2,17 @@ use {
|
||||||
crate::{
|
crate::{
|
||||||
acceptor::Acceptor,
|
acceptor::Acceptor,
|
||||||
allocator::BufferObject,
|
allocator::BufferObject,
|
||||||
|
animation::{
|
||||||
|
AnimationCurve, AnimationState, AnimationStyle, AnimationTick, RetainedExitLayer,
|
||||||
|
RetainedToplevel,
|
||||||
|
expand_damage_rect,
|
||||||
|
multiphase::{
|
||||||
|
MultiphasePhase, MultiphasePlan, MultiphasePlanFailure, MultiphaseRequest,
|
||||||
|
MultiphaseWindow, MultiphaseWindowHierarchy,
|
||||||
|
partition_motion_groups, plan_no_overlap_with_diagnostics, validate_phase_paths,
|
||||||
|
},
|
||||||
|
spawn_in_start_rect,
|
||||||
|
},
|
||||||
async_engine::{AsyncEngine, SpawnedFuture},
|
async_engine::{AsyncEngine, SpawnedFuture},
|
||||||
backend::{
|
backend::{
|
||||||
Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice,
|
Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice,
|
||||||
|
|
@ -102,11 +113,10 @@ use {
|
||||||
time::Time,
|
time::Time,
|
||||||
tree::{
|
tree::{
|
||||||
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
|
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
|
||||||
FoundNode, LatchListener, Node, NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode,
|
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
|
||||||
TearingMode, TileState, ToplevelData, ToplevelIdentifier, ToplevelNode,
|
PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier,
|
||||||
ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder, WorkspaceNode,
|
ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder,
|
||||||
WorkspaceNodeId,
|
WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output,
|
||||||
WsMoveConfig, generic_node_visitor, move_ws_to_output,
|
|
||||||
},
|
},
|
||||||
udmabuf::UdmabufHolder,
|
udmabuf::UdmabufHolder,
|
||||||
utils::{
|
utils::{
|
||||||
|
|
@ -154,6 +164,98 @@ use {
|
||||||
uapi::{OwnedFd, c},
|
uapi::{OwnedFd, c},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct LayoutAnimationCandidate {
|
||||||
|
node_id: NodeId,
|
||||||
|
old: Rect,
|
||||||
|
new: Rect,
|
||||||
|
curve: AnimationCurve,
|
||||||
|
style: AnimationStyle,
|
||||||
|
hierarchy: MultiphaseWindowHierarchy,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coalesce_layout_animation_candidates(
|
||||||
|
candidates: Vec<LayoutAnimationCandidate>,
|
||||||
|
) -> Vec<LayoutAnimationCandidate> {
|
||||||
|
let mut merged: Vec<LayoutAnimationCandidate> = vec![];
|
||||||
|
for candidate in candidates {
|
||||||
|
if let Some(existing) = merged
|
||||||
|
.iter_mut()
|
||||||
|
.find(|existing| existing.node_id == candidate.node_id)
|
||||||
|
{
|
||||||
|
existing.new = candidate.new;
|
||||||
|
existing.curve = candidate.curve;
|
||||||
|
existing.style = candidate.style;
|
||||||
|
existing.hierarchy = MultiphaseWindowHierarchy::new(
|
||||||
|
existing.hierarchy.source,
|
||||||
|
candidate.hierarchy.target,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
merged.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_animation_group_uses_plain(
|
||||||
|
candidates: &[LayoutAnimationCandidate],
|
||||||
|
group: &[usize],
|
||||||
|
) -> bool {
|
||||||
|
group
|
||||||
|
.iter()
|
||||||
|
.any(|&idx| candidates[idx].style == AnimationStyle::Plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bridged_retarget_plan(
|
||||||
|
request: &MultiphaseRequest,
|
||||||
|
candidates: &[LayoutAnimationCandidate],
|
||||||
|
group: &[usize],
|
||||||
|
bridge_paths: &[Vec<(Rect, Rect)>],
|
||||||
|
bridge_phase_count: usize,
|
||||||
|
follow_phases: &[MultiphasePhase],
|
||||||
|
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||||
|
let mut paths = vec![];
|
||||||
|
for (group_pos, &idx) in group.iter().enumerate() {
|
||||||
|
let candidate = &candidates[idx];
|
||||||
|
let window = request.windows[group_pos];
|
||||||
|
let Some(bridge_path) = bridge_paths.get(group_pos) else {
|
||||||
|
return Err(MultiphasePlanFailure::NoPattern);
|
||||||
|
};
|
||||||
|
let mut path = bridge_path.clone();
|
||||||
|
let mut current = path
|
||||||
|
.last()
|
||||||
|
.map(|(_, to)| *to)
|
||||||
|
.unwrap_or(window.from);
|
||||||
|
while path.len() < bridge_phase_count {
|
||||||
|
path.push((current, current));
|
||||||
|
}
|
||||||
|
if current != candidate.old {
|
||||||
|
return Err(MultiphasePlanFailure::NoPattern);
|
||||||
|
}
|
||||||
|
for phase in follow_phases {
|
||||||
|
match phase
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.find(|step| step.node_id == candidate.node_id)
|
||||||
|
{
|
||||||
|
Some(step) => {
|
||||||
|
if step.from != current {
|
||||||
|
return Err(MultiphasePlanFailure::NoPattern);
|
||||||
|
}
|
||||||
|
path.push((step.from, step.to));
|
||||||
|
current = step.to;
|
||||||
|
}
|
||||||
|
None => path.push((current, current)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current != window.to {
|
||||||
|
return Err(MultiphasePlanFailure::NoPattern);
|
||||||
|
}
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
validate_phase_paths(request, &paths)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub pid: c::pid_t,
|
pub pid: c::pid_t,
|
||||||
pub kb_ctx: KbvmContext,
|
pub kb_ctx: KbvmContext,
|
||||||
|
|
@ -264,6 +366,13 @@ pub struct State {
|
||||||
pub cpu_worker: Rc<CpuWorker>,
|
pub cpu_worker: Rc<CpuWorker>,
|
||||||
pub ui_drag_enabled: Cell<bool>,
|
pub ui_drag_enabled: Cell<bool>,
|
||||||
pub ui_drag_threshold_squared: Cell<i32>,
|
pub ui_drag_threshold_squared: Cell<i32>,
|
||||||
|
pub animations: AnimationState,
|
||||||
|
pub layout_animations_requested: Cell<bool>,
|
||||||
|
pub layout_animations_active: Cell<bool>,
|
||||||
|
pub layout_animation_curve_override: Cell<Option<AnimationCurve>>,
|
||||||
|
pub layout_animation_style_override: Cell<Option<AnimationStyle>>,
|
||||||
|
pub(crate) layout_animation_batch: RefCell<Option<Vec<LayoutAnimationCandidate>>>,
|
||||||
|
pub suppress_animations_for_next_layout: Cell<bool>,
|
||||||
pub toplevels: CopyHashMap<ToplevelIdentifier, Weak<dyn ToplevelNode>>,
|
pub toplevels: CopyHashMap<ToplevelIdentifier, Weak<dyn ToplevelNode>>,
|
||||||
pub const_40hz_latch: EventSource<dyn LatchListener>,
|
pub const_40hz_latch: EventSource<dyn LatchListener>,
|
||||||
pub tray_item_ids: TrayItemIds,
|
pub tray_item_ids: TrayItemIds,
|
||||||
|
|
@ -812,7 +921,14 @@ impl State {
|
||||||
|
|
||||||
pub fn map_tiled(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
|
pub fn map_tiled(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
|
||||||
let seat = self.seat_queue.last();
|
let seat = self.seat_queue.last();
|
||||||
self.do_map_tiled(seat.as_deref(), node.clone());
|
let animate_new_app_map = node.tl_data().parent.is_none()
|
||||||
|
&& node.tl_data().kind.is_app_window()
|
||||||
|
&& !node.tl_data().visible.get();
|
||||||
|
if animate_new_app_map {
|
||||||
|
self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone()));
|
||||||
|
} else {
|
||||||
|
self.do_map_tiled(seat.as_deref(), node.clone());
|
||||||
|
}
|
||||||
self.focus_after_map(node, seat.as_deref());
|
self.focus_after_map(node, seat.as_deref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -847,7 +963,7 @@ impl State {
|
||||||
mut height: i32,
|
mut height: i32,
|
||||||
workspace: &Rc<WorkspaceNode>,
|
workspace: &Rc<WorkspaceNode>,
|
||||||
abs_pos: Option<(i32, i32)>,
|
abs_pos: Option<(i32, i32)>,
|
||||||
) {
|
) -> Rc<FloatNode> {
|
||||||
width += 2 * self.theme.sizes.border_width.get();
|
width += 2 * self.theme.sizes.border_width.get();
|
||||||
height +=
|
height +=
|
||||||
2 * self.theme.sizes.border_width.get() + self.theme.title_plus_underline_height();
|
2 * self.theme.sizes.border_width.get() + self.theme.title_plus_underline_height();
|
||||||
|
|
@ -878,8 +994,9 @@ impl State {
|
||||||
}
|
}
|
||||||
Rect::new_sized_saturating(x1, y1, width, height)
|
Rect::new_sized_saturating(x1, y1, width, height)
|
||||||
};
|
};
|
||||||
FloatNode::new(self, workspace, position, node.clone());
|
let float = FloatNode::new(self, workspace, position, node.clone());
|
||||||
self.focus_after_map(node, self.seat_queue.last().as_deref());
|
self.focus_after_map(node, self.seat_queue.last().as_deref());
|
||||||
|
float
|
||||||
}
|
}
|
||||||
|
|
||||||
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>>) {
|
||||||
|
|
@ -1115,6 +1232,12 @@ impl State {
|
||||||
self.pending_screencast_reallocs_or_reconfigures.clear();
|
self.pending_screencast_reallocs_or_reconfigures.clear();
|
||||||
self.pending_placeholder_render_textures.clear();
|
self.pending_placeholder_render_textures.clear();
|
||||||
self.pending_container_tab_render_textures.clear();
|
self.pending_container_tab_render_textures.clear();
|
||||||
|
self.animations.clear();
|
||||||
|
self.layout_animations_requested.set(false);
|
||||||
|
self.layout_animations_active.set(false);
|
||||||
|
self.layout_animation_curve_override.set(None);
|
||||||
|
self.layout_animation_style_override.set(None);
|
||||||
|
self.suppress_animations_for_next_layout.set(false);
|
||||||
self.render_ctx_watchers.clear();
|
self.render_ctx_watchers.clear();
|
||||||
self.workspace_watchers.clear();
|
self.workspace_watchers.clear();
|
||||||
self.toplevel_lists.clear();
|
self.toplevel_lists.clear();
|
||||||
|
|
@ -1461,6 +1584,532 @@ impl State {
|
||||||
self.eng.now().msec()
|
self.eng.now().msec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn queue_tiled_animation(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
node_id: NodeId,
|
||||||
|
old: Rect,
|
||||||
|
new: Rect,
|
||||||
|
) {
|
||||||
|
let curve = self
|
||||||
|
.layout_animation_curve_override
|
||||||
|
.get()
|
||||||
|
.unwrap_or_else(|| self.animations.curve.get());
|
||||||
|
self.queue_layout_animation(
|
||||||
|
node_id,
|
||||||
|
old,
|
||||||
|
new,
|
||||||
|
curve,
|
||||||
|
MultiphaseWindowHierarchy::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_tiled_animation_with_hierarchy(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
node_id: NodeId,
|
||||||
|
old: Rect,
|
||||||
|
new: Rect,
|
||||||
|
hierarchy: MultiphaseWindowHierarchy,
|
||||||
|
) {
|
||||||
|
let curve = self
|
||||||
|
.layout_animation_curve_override
|
||||||
|
.get()
|
||||||
|
.unwrap_or_else(|| self.animations.curve.get());
|
||||||
|
self.queue_layout_animation(node_id, old, new, curve, hierarchy);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_linear_layout_animation(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
node_id: NodeId,
|
||||||
|
old: Rect,
|
||||||
|
new: Rect,
|
||||||
|
) {
|
||||||
|
self.queue_layout_animation(
|
||||||
|
node_id,
|
||||||
|
old,
|
||||||
|
new,
|
||||||
|
AnimationCurve::Linear,
|
||||||
|
MultiphaseWindowHierarchy::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_layout_animation(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
node_id: NodeId,
|
||||||
|
old: Rect,
|
||||||
|
new: Rect,
|
||||||
|
curve: AnimationCurve,
|
||||||
|
hierarchy: MultiphaseWindowHierarchy,
|
||||||
|
) {
|
||||||
|
if !self.animations.enabled.get()
|
||||||
|
|| !self.layout_animations_active.get()
|
||||||
|
|| self.suppress_animations_for_next_layout.get()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (old_output, old_scale) = {
|
||||||
|
let (x, y) = old.center();
|
||||||
|
let (output, _, _) = self.find_closest_output(x, y);
|
||||||
|
(output.id, output.global.persistent.scale.get())
|
||||||
|
};
|
||||||
|
let (new_output, new_scale) = {
|
||||||
|
let (x, y) = new.center();
|
||||||
|
let (output, _, _) = self.find_closest_output(x, y);
|
||||||
|
(output.id, output.global.persistent.scale.get())
|
||||||
|
};
|
||||||
|
if old_output != new_output || old_scale != new_scale {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let candidate = LayoutAnimationCandidate {
|
||||||
|
node_id,
|
||||||
|
old,
|
||||||
|
new,
|
||||||
|
curve,
|
||||||
|
style: self
|
||||||
|
.layout_animation_style_override
|
||||||
|
.get()
|
||||||
|
.unwrap_or_else(|| self.animations.style.get()),
|
||||||
|
hierarchy,
|
||||||
|
};
|
||||||
|
if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() {
|
||||||
|
batch.push(candidate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.start_layout_animation_candidate(candidate, self.now_nsec());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_layout_animation_candidate(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
candidate: LayoutAnimationCandidate,
|
||||||
|
now_nsec: u64,
|
||||||
|
) {
|
||||||
|
let started = self.animations.set_target(
|
||||||
|
candidate.node_id,
|
||||||
|
candidate.old,
|
||||||
|
candidate.new,
|
||||||
|
None,
|
||||||
|
now_nsec,
|
||||||
|
self.animations.duration_ms.get(),
|
||||||
|
candidate.curve,
|
||||||
|
);
|
||||||
|
if started {
|
||||||
|
self.damage(expand_damage_rect(
|
||||||
|
candidate.old.union(candidate.new),
|
||||||
|
self.theme.sizes.border_width.get().max(0),
|
||||||
|
));
|
||||||
|
self.ensure_animation_tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_layout_animation_batch(&self) {
|
||||||
|
self.layout_animation_batch
|
||||||
|
.borrow_mut()
|
||||||
|
.get_or_insert_with(Vec::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish_layout_animation_batch(self: &Rc<Self>) {
|
||||||
|
let Some(candidates) = self.layout_animation_batch.borrow_mut().take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let candidates = coalesce_layout_animation_candidates(candidates);
|
||||||
|
if candidates.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let now = self.now_nsec();
|
||||||
|
let windows: Vec<_> = candidates
|
||||||
|
.iter()
|
||||||
|
.map(|candidate| {
|
||||||
|
MultiphaseWindow::with_hierarchy(
|
||||||
|
candidate.node_id,
|
||||||
|
self.animations
|
||||||
|
.visual_rect(candidate.node_id, candidate.old, now),
|
||||||
|
candidate.new,
|
||||||
|
candidate.hierarchy,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for group in partition_motion_groups(&windows, self.layout_animation_clearance()) {
|
||||||
|
if layout_animation_group_uses_plain(&candidates, &group) {
|
||||||
|
for idx in group {
|
||||||
|
self.start_layout_animation_candidate(candidates[idx].clone(), now);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for idx in group {
|
||||||
|
self.start_layout_animation_candidate(candidates[idx].clone(), now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_animation_clearance(&self) -> i32 {
|
||||||
|
let border = self.theme.sizes.border_width.get().max(0);
|
||||||
|
let gap = self.theme.sizes.gap.get().max(0);
|
||||||
|
if gap == 0 { border } else { gap + 2 * border }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_multiphase_layout_animation(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
candidates: &[LayoutAnimationCandidate],
|
||||||
|
windows: &[MultiphaseWindow],
|
||||||
|
group: &[usize],
|
||||||
|
now_nsec: u64,
|
||||||
|
) -> bool {
|
||||||
|
let request_windows: Vec<_> = group.iter().map(|&idx| windows[idx]).collect();
|
||||||
|
let Some(first) = request_windows.first() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut bounds = first.from.union(first.to);
|
||||||
|
for window in &request_windows[1..] {
|
||||||
|
bounds = bounds.union(window.from).union(window.to);
|
||||||
|
}
|
||||||
|
let request = MultiphaseRequest {
|
||||||
|
bounds,
|
||||||
|
windows: request_windows,
|
||||||
|
clearance: self.layout_animation_clearance(),
|
||||||
|
};
|
||||||
|
if self.start_existing_phased_retarget(candidates, windows, group, &request, now_nsec) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.start_bridged_phased_retarget(candidates, windows, group, &request, now_nsec) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let plan = match plan_no_overlap_with_diagnostics(&request) {
|
||||||
|
Ok(plan) => plan,
|
||||||
|
Err(diagnostic) => {
|
||||||
|
log::debug!(
|
||||||
|
"falling back to plain layout animation for group {:?}: {:?}",
|
||||||
|
group,
|
||||||
|
diagnostic
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_existing_phased_retarget(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
candidates: &[LayoutAnimationCandidate],
|
||||||
|
windows: &[MultiphaseWindow],
|
||||||
|
group: &[usize],
|
||||||
|
request: &MultiphaseRequest,
|
||||||
|
now_nsec: u64,
|
||||||
|
) -> bool {
|
||||||
|
let mut paths = vec![];
|
||||||
|
for &idx in group {
|
||||||
|
let candidate = &candidates[idx];
|
||||||
|
let window = windows[idx];
|
||||||
|
let Some(path) =
|
||||||
|
self.animations
|
||||||
|
.phased_route_to(candidate.node_id, window.to, now_nsec)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
let plan = match validate_phase_paths(request, &paths) {
|
||||||
|
Ok(plan) => plan,
|
||||||
|
Err(error) => {
|
||||||
|
log::debug!(
|
||||||
|
"existing phased retarget rejected for group {:?}: {:?}",
|
||||||
|
group,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::debug!("retargeting active phased animation for group {:?}", group);
|
||||||
|
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_bridged_phased_retarget(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
candidates: &[LayoutAnimationCandidate],
|
||||||
|
windows: &[MultiphaseWindow],
|
||||||
|
group: &[usize],
|
||||||
|
request: &MultiphaseRequest,
|
||||||
|
now_nsec: u64,
|
||||||
|
) -> bool {
|
||||||
|
let mut bridge_paths = vec![];
|
||||||
|
let mut bridge_phase_count = 0;
|
||||||
|
let mut has_bridge = false;
|
||||||
|
for &idx in group {
|
||||||
|
let candidate = &candidates[idx];
|
||||||
|
let window = windows[idx];
|
||||||
|
if window.from == candidate.old {
|
||||||
|
bridge_paths.push(vec![]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(path) =
|
||||||
|
self.animations
|
||||||
|
.phased_route_to(candidate.node_id, candidate.old, now_nsec)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !path.is_empty() {
|
||||||
|
has_bridge = true;
|
||||||
|
bridge_phase_count = bridge_phase_count.max(path.len());
|
||||||
|
}
|
||||||
|
bridge_paths.push(path);
|
||||||
|
}
|
||||||
|
if !has_bridge {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let settled_windows: Vec<_> = group
|
||||||
|
.iter()
|
||||||
|
.map(|&idx| {
|
||||||
|
let candidate = &candidates[idx];
|
||||||
|
MultiphaseWindow::with_hierarchy(
|
||||||
|
candidate.node_id,
|
||||||
|
candidate.old,
|
||||||
|
candidate.new,
|
||||||
|
candidate.hierarchy,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let Some(first) = settled_windows.first() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut bounds = first.from.union(first.to);
|
||||||
|
for window in &settled_windows[1..] {
|
||||||
|
bounds = bounds.union(window.from).union(window.to);
|
||||||
|
}
|
||||||
|
let settled_request = MultiphaseRequest {
|
||||||
|
bounds,
|
||||||
|
windows: settled_windows,
|
||||||
|
clearance: self.layout_animation_clearance(),
|
||||||
|
};
|
||||||
|
let follow_plan = match plan_no_overlap_with_diagnostics(&settled_request) {
|
||||||
|
Ok(plan) => plan,
|
||||||
|
Err(diagnostic) => {
|
||||||
|
log::debug!(
|
||||||
|
"bridged phased retarget follow-up rejected for group {:?}: {:?}",
|
||||||
|
group,
|
||||||
|
diagnostic
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let plan = match bridged_retarget_plan(
|
||||||
|
request,
|
||||||
|
candidates,
|
||||||
|
group,
|
||||||
|
&bridge_paths,
|
||||||
|
bridge_phase_count,
|
||||||
|
&follow_plan.phases,
|
||||||
|
) {
|
||||||
|
Ok(plan) => plan,
|
||||||
|
Err(error) => {
|
||||||
|
log::debug!(
|
||||||
|
"bridged phased retarget rejected for group {:?}: {:?}",
|
||||||
|
group,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::debug!("bridging active phased animation for group {:?}", group);
|
||||||
|
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_multiphase_plan(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
candidates: &[LayoutAnimationCandidate],
|
||||||
|
windows: &[MultiphaseWindow],
|
||||||
|
group: &[usize],
|
||||||
|
plan_phases: &[crate::animation::multiphase::MultiphasePhase],
|
||||||
|
now_nsec: u64,
|
||||||
|
) -> bool {
|
||||||
|
if plan_phases.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut entries = vec![];
|
||||||
|
for &idx in group {
|
||||||
|
let candidate = &candidates[idx];
|
||||||
|
let window = windows[idx];
|
||||||
|
let mut current = window.from;
|
||||||
|
let mut damage = current.union(window.to);
|
||||||
|
let mut phases = vec![];
|
||||||
|
for phase in plan_phases {
|
||||||
|
match phase
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.find(|step| step.node_id == candidate.node_id)
|
||||||
|
{
|
||||||
|
Some(step) => {
|
||||||
|
phases.push((step.from, step.to));
|
||||||
|
damage = damage.union(step.from).union(step.to);
|
||||||
|
current = step.to;
|
||||||
|
}
|
||||||
|
None => phases.push((current, current)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current != window.to {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entries.push((candidate.clone(), phases, damage));
|
||||||
|
}
|
||||||
|
let mut started_any = false;
|
||||||
|
for (candidate, phases, damage) in entries {
|
||||||
|
if self.animations.set_phased_target(
|
||||||
|
candidate.node_id,
|
||||||
|
phases,
|
||||||
|
None,
|
||||||
|
now_nsec,
|
||||||
|
self.animations.duration_ms.get(),
|
||||||
|
candidate.curve,
|
||||||
|
) {
|
||||||
|
started_any = true;
|
||||||
|
self.damage(expand_damage_rect(
|
||||||
|
damage,
|
||||||
|
self.theme.sizes.border_width.get().max(0),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if started_any {
|
||||||
|
self.ensure_animation_tick();
|
||||||
|
}
|
||||||
|
started_any
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_spawn_in_animation(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
node_id: NodeId,
|
||||||
|
target: Rect,
|
||||||
|
) {
|
||||||
|
if !self.animations.enabled.get() || target.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let start = spawn_in_start_rect(target);
|
||||||
|
let now = self.now_nsec();
|
||||||
|
let started = self.animations.set_spawn_in(
|
||||||
|
node_id,
|
||||||
|
target,
|
||||||
|
None,
|
||||||
|
now,
|
||||||
|
self.animations.duration_ms.get(),
|
||||||
|
self.animations.curve.get(),
|
||||||
|
);
|
||||||
|
if started {
|
||||||
|
self.damage(expand_damage_rect(
|
||||||
|
start.union(target),
|
||||||
|
self.theme.sizes.border_width.get().max(0),
|
||||||
|
));
|
||||||
|
self.ensure_animation_tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_spawn_out_animation(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
from: Rect,
|
||||||
|
frame_inset: i32,
|
||||||
|
retained: Rc<RetainedToplevel>,
|
||||||
|
active: bool,
|
||||||
|
layer: RetainedExitLayer,
|
||||||
|
) {
|
||||||
|
if !self.animations.enabled.get() || from.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let now = self.now_nsec();
|
||||||
|
let started = self.animations.set_spawn_out(
|
||||||
|
from,
|
||||||
|
frame_inset,
|
||||||
|
retained,
|
||||||
|
active,
|
||||||
|
layer,
|
||||||
|
now,
|
||||||
|
self.animations.duration_ms.get(),
|
||||||
|
self.animations.curve.get(),
|
||||||
|
);
|
||||||
|
if started {
|
||||||
|
self.damage(expand_damage_rect(
|
||||||
|
from,
|
||||||
|
self.theme.sizes.border_width.get().max(0),
|
||||||
|
));
|
||||||
|
self.ensure_animation_tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animations_enabled(&self, enabled: bool) {
|
||||||
|
if self.animations.enabled.replace(enabled) && !enabled {
|
||||||
|
self.animations.clear();
|
||||||
|
self.damage(self.root.extents.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animation_duration_ms(&self, duration_ms: u32) {
|
||||||
|
self.animations.duration_ms.set(duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animation_curve(&self, curve: u32) {
|
||||||
|
self.animations
|
||||||
|
.curve
|
||||||
|
.set(AnimationCurve::from_config(curve));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animation_style(&self, style: u32) -> bool {
|
||||||
|
let Some(style) = AnimationStyle::from_config(style) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
self.animations.style.set(style);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
|
||||||
|
let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
self.animations.curve.set(curve);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||||
|
let prev_requested = self.layout_animations_requested.replace(true);
|
||||||
|
let prev_active = self.layout_animations_active.replace(true);
|
||||||
|
let res = f();
|
||||||
|
self.layout_animations_requested.set(prev_requested);
|
||||||
|
self.layout_animations_active.set(prev_active);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_linear_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||||
|
let prev_requested = self.layout_animations_requested.replace(true);
|
||||||
|
let prev_active = self.layout_animations_active.replace(true);
|
||||||
|
let prev_curve = self
|
||||||
|
.layout_animation_curve_override
|
||||||
|
.replace(Some(AnimationCurve::Linear));
|
||||||
|
let prev_style = self
|
||||||
|
.layout_animation_style_override
|
||||||
|
.replace(Some(AnimationStyle::Plain));
|
||||||
|
let res = f();
|
||||||
|
self.layout_animations_requested.set(prev_requested);
|
||||||
|
self.layout_animations_active.set(prev_active);
|
||||||
|
self.layout_animation_curve_override.set(prev_curve);
|
||||||
|
self.layout_animation_style_override.set(prev_style);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_animation_tick(self: &Rc<Self>) {
|
||||||
|
if self.animations.tick_is_active() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let outputs: Vec<_> = self.root.outputs.lock().values().cloned().collect();
|
||||||
|
if outputs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tick = Rc::new_cyclic(|weak| AnimationTick::new(self, weak));
|
||||||
|
for output in &outputs {
|
||||||
|
tick.attach(output);
|
||||||
|
}
|
||||||
|
self.animations.set_tick(tick);
|
||||||
|
for output in &outputs {
|
||||||
|
self.damage(output.global.pos.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn output_extents_changed(&self) {
|
pub fn output_extents_changed(&self) {
|
||||||
self.root.update_extents();
|
self.root.update_extents();
|
||||||
for seat in self.globals.seats.lock().values() {
|
for seat in self.globals.seats.lock().values() {
|
||||||
|
|
@ -1989,6 +2638,227 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use {
|
||||||
|
super::*,
|
||||||
|
crate::animation::multiphase::MultiphaseHierarchyPosition,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect {
|
||||||
|
Rect::new_saturating(x1, y1, x2, y2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hierarchy(
|
||||||
|
source: MultiphaseHierarchyPosition,
|
||||||
|
target: MultiphaseHierarchyPosition,
|
||||||
|
) -> MultiphaseWindowHierarchy {
|
||||||
|
MultiphaseWindowHierarchy::new(source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate {
|
||||||
|
candidate_rects(
|
||||||
|
node_id,
|
||||||
|
rect(0, 0, 100, 100),
|
||||||
|
rect(100, 0, 200, 100),
|
||||||
|
style,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_rects(
|
||||||
|
node_id: u32,
|
||||||
|
old: Rect,
|
||||||
|
new: Rect,
|
||||||
|
style: AnimationStyle,
|
||||||
|
) -> LayoutAnimationCandidate {
|
||||||
|
LayoutAnimationCandidate {
|
||||||
|
node_id: NodeId(node_id),
|
||||||
|
old,
|
||||||
|
new,
|
||||||
|
curve: AnimationCurve::Linear,
|
||||||
|
style,
|
||||||
|
hierarchy: MultiphaseWindowHierarchy::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_style_candidate_forces_group_plain() {
|
||||||
|
let candidates = vec![
|
||||||
|
candidate(1, AnimationStyle::Multiphase),
|
||||||
|
candidate(2, AnimationStyle::Plain),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!(!layout_animation_group_uses_plain(&candidates, &[0]));
|
||||||
|
assert!(layout_animation_group_uses_plain(&candidates, &[0, 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bridged_retarget_handles_second_rotation_interrupt() {
|
||||||
|
let a_left = rect(0, 0, 100, 100);
|
||||||
|
let c_mid = rect(100, 0, 200, 100);
|
||||||
|
let c_left = a_left;
|
||||||
|
let a_mid = c_mid;
|
||||||
|
let c_current = rect(150, 50, 250, 100);
|
||||||
|
let c_mid_lane = rect(100, 50, 200, 100);
|
||||||
|
let candidates = vec![
|
||||||
|
candidate_rects(1, a_left, a_mid, AnimationStyle::Multiphase),
|
||||||
|
candidate_rects(3, c_mid, c_left, AnimationStyle::Multiphase),
|
||||||
|
];
|
||||||
|
let request = MultiphaseRequest {
|
||||||
|
bounds: rect(0, 0, 250, 100),
|
||||||
|
windows: vec![
|
||||||
|
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
|
||||||
|
MultiphaseWindow::new(NodeId(3), c_current, c_left),
|
||||||
|
],
|
||||||
|
clearance: 0,
|
||||||
|
};
|
||||||
|
let settled_request = MultiphaseRequest {
|
||||||
|
bounds: rect(0, 0, 200, 100),
|
||||||
|
windows: vec![
|
||||||
|
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
|
||||||
|
MultiphaseWindow::new(NodeId(3), c_mid, c_left),
|
||||||
|
],
|
||||||
|
clearance: 0,
|
||||||
|
};
|
||||||
|
let follow_plan = plan_no_overlap_with_diagnostics(&settled_request).unwrap();
|
||||||
|
let bridge_paths = vec![vec![], vec![(c_current, c_mid_lane), (c_mid_lane, c_mid)]];
|
||||||
|
|
||||||
|
let plan = bridged_retarget_plan(
|
||||||
|
&request,
|
||||||
|
&candidates,
|
||||||
|
&[0, 1],
|
||||||
|
&bridge_paths,
|
||||||
|
2,
|
||||||
|
&follow_plan.phases,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(plan
|
||||||
|
.phases
|
||||||
|
.iter()
|
||||||
|
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(1))));
|
||||||
|
assert!(plan
|
||||||
|
.phases
|
||||||
|
.iter()
|
||||||
|
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(3))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_animation_candidates_coalesce_duplicate_nodes() {
|
||||||
|
let source = MultiphaseHierarchyPosition {
|
||||||
|
parent: Some(NodeId(10)),
|
||||||
|
depth: 2,
|
||||||
|
sibling_index: Some(1),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let intermediate = MultiphaseHierarchyPosition {
|
||||||
|
parent: Some(NodeId(11)),
|
||||||
|
depth: 1,
|
||||||
|
sibling_index: Some(0),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let target = MultiphaseHierarchyPosition {
|
||||||
|
parent: Some(NodeId(12)),
|
||||||
|
depth: 0,
|
||||||
|
sibling_index: Some(2),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let second_source = MultiphaseHierarchyPosition {
|
||||||
|
parent: Some(NodeId(20)),
|
||||||
|
depth: 1,
|
||||||
|
sibling_index: Some(0),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let second_target = MultiphaseHierarchyPosition {
|
||||||
|
parent: Some(NodeId(20)),
|
||||||
|
depth: 1,
|
||||||
|
sibling_index: Some(1),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let candidates = vec![
|
||||||
|
LayoutAnimationCandidate {
|
||||||
|
node_id: NodeId(1),
|
||||||
|
old: rect(0, 0, 100, 100),
|
||||||
|
new: rect(0, 0, 80, 100),
|
||||||
|
curve: AnimationCurve::Linear,
|
||||||
|
style: AnimationStyle::Multiphase,
|
||||||
|
hierarchy: hierarchy(source, intermediate),
|
||||||
|
},
|
||||||
|
LayoutAnimationCandidate {
|
||||||
|
node_id: NodeId(2),
|
||||||
|
old: rect(100, 0, 200, 100),
|
||||||
|
new: rect(120, 0, 220, 100),
|
||||||
|
curve: AnimationCurve::Linear,
|
||||||
|
style: AnimationStyle::Multiphase,
|
||||||
|
hierarchy: hierarchy(second_source, second_target),
|
||||||
|
},
|
||||||
|
LayoutAnimationCandidate {
|
||||||
|
node_id: NodeId(1),
|
||||||
|
old: rect(0, 0, 80, 100),
|
||||||
|
new: rect(0, 0, 60, 100),
|
||||||
|
curve: AnimationCurve::from_config(4),
|
||||||
|
style: AnimationStyle::Plain,
|
||||||
|
hierarchy: hierarchy(intermediate, target),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let merged = coalesce_layout_animation_candidates(candidates);
|
||||||
|
|
||||||
|
assert_eq!(merged.len(), 2);
|
||||||
|
assert_eq!(merged[0].node_id, NodeId(1));
|
||||||
|
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
|
||||||
|
assert_eq!(merged[0].new, rect(0, 0, 60, 100));
|
||||||
|
assert_eq!(merged[0].curve, AnimationCurve::from_config(4));
|
||||||
|
assert_eq!(merged[0].style, AnimationStyle::Plain);
|
||||||
|
assert_eq!(merged[0].hierarchy, hierarchy(source, target));
|
||||||
|
assert_eq!(merged[1].node_id, NodeId(2));
|
||||||
|
assert_eq!(merged[1].old, rect(100, 0, 200, 100));
|
||||||
|
assert_eq!(merged[1].new, rect(120, 0, 220, 100));
|
||||||
|
assert_eq!(merged[1].hierarchy, hierarchy(second_source, second_target));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_animation_candidates_keep_coalesced_layout_noops() {
|
||||||
|
let hierarchy = MultiphaseWindowHierarchy::default();
|
||||||
|
let candidates = vec![
|
||||||
|
LayoutAnimationCandidate {
|
||||||
|
node_id: NodeId(1),
|
||||||
|
old: rect(0, 0, 100, 100),
|
||||||
|
new: rect(0, 0, 80, 100),
|
||||||
|
curve: AnimationCurve::Linear,
|
||||||
|
style: AnimationStyle::Multiphase,
|
||||||
|
hierarchy,
|
||||||
|
},
|
||||||
|
LayoutAnimationCandidate {
|
||||||
|
node_id: NodeId(1),
|
||||||
|
old: rect(0, 0, 80, 100),
|
||||||
|
new: rect(0, 0, 100, 100),
|
||||||
|
curve: AnimationCurve::Linear,
|
||||||
|
style: AnimationStyle::Plain,
|
||||||
|
hierarchy,
|
||||||
|
},
|
||||||
|
LayoutAnimationCandidate {
|
||||||
|
node_id: NodeId(2),
|
||||||
|
old: rect(100, 0, 200, 100),
|
||||||
|
new: rect(120, 0, 220, 100),
|
||||||
|
curve: AnimationCurve::Linear,
|
||||||
|
style: AnimationStyle::Multiphase,
|
||||||
|
hierarchy,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let merged = coalesce_layout_animation_candidates(candidates);
|
||||||
|
|
||||||
|
assert_eq!(merged.len(), 2);
|
||||||
|
assert_eq!(merged[0].node_id, NodeId(1));
|
||||||
|
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
|
||||||
|
assert_eq!(merged[0].new, rect(0, 0, 100, 100));
|
||||||
|
assert_eq!(merged[0].style, AnimationStyle::Plain);
|
||||||
|
assert_eq!(merged[1].node_id, NodeId(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ShmScreencopyError {
|
pub enum ShmScreencopyError {
|
||||||
#[error("There is no render context")]
|
#[error("There is no render context")]
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,8 @@ pub struct ContainerNode {
|
||||||
pub content_height: Cell<i32>,
|
pub content_height: Cell<i32>,
|
||||||
pub sum_factors: Cell<f64>,
|
pub sum_factors: Cell<f64>,
|
||||||
pub layout_scheduled: Cell<bool>,
|
pub layout_scheduled: Cell<bool>,
|
||||||
|
animate_next_layout: Cell<bool>,
|
||||||
|
pub mono_transition_animation_pending: Cell<bool>,
|
||||||
compute_render_positions_scheduled: Cell<bool>,
|
compute_render_positions_scheduled: Cell<bool>,
|
||||||
num_children: NumCell<usize>,
|
num_children: NumCell<usize>,
|
||||||
pub children: LinkedList<ContainerChild>,
|
pub children: LinkedList<ContainerChild>,
|
||||||
|
|
@ -238,6 +240,8 @@ impl ContainerNode {
|
||||||
content_height: Cell::new(0),
|
content_height: Cell::new(0),
|
||||||
sum_factors: Cell::new(1.0),
|
sum_factors: Cell::new(1.0),
|
||||||
layout_scheduled: Cell::new(false),
|
layout_scheduled: Cell::new(false),
|
||||||
|
animate_next_layout: Cell::new(false),
|
||||||
|
mono_transition_animation_pending: Cell::new(false),
|
||||||
compute_render_positions_scheduled: Cell::new(false),
|
compute_render_positions_scheduled: Cell::new(false),
|
||||||
num_children: NumCell::new(1),
|
num_children: NumCell::new(1),
|
||||||
children,
|
children,
|
||||||
|
|
@ -436,6 +440,10 @@ impl ContainerNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_layout(self: &Rc<Self>) {
|
fn schedule_layout(self: &Rc<Self>) {
|
||||||
|
if self.state.layout_animations_requested.get() || self.state.layout_animations_active.get()
|
||||||
|
{
|
||||||
|
self.animate_next_layout.set(true);
|
||||||
|
}
|
||||||
if !self.layout_scheduled.replace(true) {
|
if !self.layout_scheduled.replace(true) {
|
||||||
self.state.pending_container_layout.push(self.clone());
|
self.state.pending_container_layout.push(self.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -467,6 +475,7 @@ impl ContainerNode {
|
||||||
fn perform_layout(self: &Rc<Self>) {
|
fn perform_layout(self: &Rc<Self>) {
|
||||||
self.layout_scheduled.set(false);
|
self.layout_scheduled.set(false);
|
||||||
if self.num_children.get() == 0 {
|
if self.num_children.get() == 0 {
|
||||||
|
self.mono_transition_animation_pending.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(child) = self.mono_child.get() {
|
if let Some(child) = self.mono_child.get() {
|
||||||
|
|
@ -484,6 +493,7 @@ impl ContainerNode {
|
||||||
self.damage();
|
self.damage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.mono_transition_animation_pending.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_mono_layout(self: &Rc<Self>, child: &ContainerChild) {
|
fn perform_mono_layout(self: &Rc<Self>, child: &ContainerChild) {
|
||||||
|
|
@ -656,6 +666,7 @@ impl ContainerNode {
|
||||||
op.child.factor.set(child_factor);
|
op.child.factor.set(child_factor);
|
||||||
self.sum_factors.set(sum_factors);
|
self.sum_factors.set(sum_factors);
|
||||||
// log::info!("pointer_move");
|
// log::info!("pointer_move");
|
||||||
|
self.state.suppress_animations_for_next_layout.set(true);
|
||||||
self.schedule_layout_immediate();
|
self.schedule_layout_immediate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -816,6 +827,7 @@ impl ContainerNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.mono_child.set(child.clone());
|
self.mono_child.set(child.clone());
|
||||||
|
self.mono_transition_animation_pending.set(true);
|
||||||
if child.is_some() {
|
if child.is_some() {
|
||||||
self.rebuild_tab_bar();
|
self.rebuild_tab_bar();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1759,10 +1771,42 @@ enum SeatOpKind {
|
||||||
|
|
||||||
pub async fn container_layout(state: Rc<State>) {
|
pub async fn container_layout(state: Rc<State>) {
|
||||||
loop {
|
loop {
|
||||||
let container = state.pending_container_layout.pop().await;
|
let first = state.pending_container_layout.pop().await;
|
||||||
if container.layout_scheduled.get() {
|
let mut containers = vec![first];
|
||||||
container.perform_layout();
|
while let Some(container) = state.pending_container_layout.try_pop() {
|
||||||
|
containers.push(container);
|
||||||
}
|
}
|
||||||
|
let mut animated = vec![];
|
||||||
|
let mut immediate = vec![];
|
||||||
|
for container in containers {
|
||||||
|
if !container.layout_scheduled.get() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let animate = container.animate_next_layout.replace(false)
|
||||||
|
&& !state.suppress_animations_for_next_layout.get();
|
||||||
|
if animate {
|
||||||
|
animated.push(container);
|
||||||
|
} else {
|
||||||
|
immediate.push(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !animated.is_empty() {
|
||||||
|
let prev_active = state.layout_animations_active.replace(true);
|
||||||
|
state.begin_layout_animation_batch();
|
||||||
|
for container in animated {
|
||||||
|
container.perform_layout();
|
||||||
|
}
|
||||||
|
state.finish_layout_animation_batch();
|
||||||
|
state.layout_animations_active.set(prev_active);
|
||||||
|
}
|
||||||
|
if !immediate.is_empty() {
|
||||||
|
let prev_active = state.layout_animations_active.replace(false);
|
||||||
|
for container in immediate {
|
||||||
|
container.perform_layout();
|
||||||
|
}
|
||||||
|
state.layout_animations_active.set(prev_active);
|
||||||
|
}
|
||||||
|
state.suppress_animations_for_next_layout.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2259,6 +2303,11 @@ impl ContainingNode for ContainerNode {
|
||||||
}
|
}
|
||||||
// log::info!("cnode_remove_child2");
|
// log::info!("cnode_remove_child2");
|
||||||
self.rebuild_tab_bar();
|
self.rebuild_tab_bar();
|
||||||
|
if self.state.animations.enabled.get()
|
||||||
|
&& !self.state.suppress_animations_for_next_layout.get()
|
||||||
|
{
|
||||||
|
self.animate_next_layout.set(true);
|
||||||
|
}
|
||||||
self.schedule_layout();
|
self.schedule_layout();
|
||||||
self.cancel_seat_ops();
|
self.cancel_seat_ops();
|
||||||
self.child_removed.trigger();
|
self.child_removed.trigger();
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ use {
|
||||||
};
|
};
|
||||||
|
|
||||||
tree_id!(FloatNodeId);
|
tree_id!(FloatNodeId);
|
||||||
|
|
||||||
|
const COMMAND_MOVE_DELTA: i32 = 100;
|
||||||
|
|
||||||
pub struct FloatNode {
|
pub struct FloatNode {
|
||||||
pub id: FloatNodeId,
|
pub id: FloatNodeId,
|
||||||
pub state: Rc<State>,
|
pub state: Rc<State>,
|
||||||
|
|
@ -153,6 +156,13 @@ impl FloatNode {
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let pos = self.position.get();
|
let pos = self.position.get();
|
||||||
|
let spawn_in_pending = {
|
||||||
|
let data = child.tl_data();
|
||||||
|
data.spawn_in_pending.get() && data.kind.is_app_window() && !data.is_fullscreen.get()
|
||||||
|
};
|
||||||
|
if spawn_in_pending && self.visible.get() {
|
||||||
|
self.state.queue_spawn_in_animation(self.id.into(), pos);
|
||||||
|
}
|
||||||
let theme = &self.state.theme;
|
let theme = &self.state.theme;
|
||||||
let bw = theme.sizes.border_width.get();
|
let bw = theme.sizes.border_width.get();
|
||||||
let cpos = Rect::new_sized_saturating(
|
let cpos = Rect::new_sized_saturating(
|
||||||
|
|
@ -363,6 +373,50 @@ impl FloatNode {
|
||||||
y2 += y1 - pos.y1();
|
y2 += y1 - pos.y1();
|
||||||
}
|
}
|
||||||
let new_pos = Rect::new_saturating(x1, y1, x2, y2);
|
let new_pos = Rect::new_saturating(x1, y1, x2, y2);
|
||||||
|
self.set_position(new_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_by_direction(self: &Rc<Self>, direction: Direction) {
|
||||||
|
let (dx, dy) = match direction {
|
||||||
|
Direction::Left => (-COMMAND_MOVE_DELTA, 0),
|
||||||
|
Direction::Down => (0, COMMAND_MOVE_DELTA),
|
||||||
|
Direction::Up => (0, -COMMAND_MOVE_DELTA),
|
||||||
|
Direction::Right => (COMMAND_MOVE_DELTA, 0),
|
||||||
|
Direction::Unspecified => return,
|
||||||
|
};
|
||||||
|
self.set_position(self.position.get().move_(dx, dy));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_for_outer(&self, outer: Rect) -> Rect {
|
||||||
|
let bw = self.state.theme.sizes.border_width.get();
|
||||||
|
Rect::new_sized_saturating(
|
||||||
|
outer.x1() + bw,
|
||||||
|
outer.y1() + bw,
|
||||||
|
outer.width() - 2 * bw,
|
||||||
|
outer.height() - 2 * bw,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_position_animation(&self, old_pos: Rect, new_pos: Rect) {
|
||||||
|
self.state
|
||||||
|
.clone()
|
||||||
|
.queue_tiled_animation(self.id.into(), old_pos, new_pos);
|
||||||
|
let Some(child) = self.child.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.state.clone().queue_tiled_animation(
|
||||||
|
child.node_id(),
|
||||||
|
self.body_for_outer(old_pos),
|
||||||
|
self.body_for_outer(new_pos),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_position(self: &Rc<Self>, new_pos: Rect) {
|
||||||
|
let pos = self.position.get();
|
||||||
|
if new_pos == pos {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.queue_position_animation(pos, new_pos);
|
||||||
self.position.set(new_pos);
|
self.position.set(new_pos);
|
||||||
if self.visible.get() {
|
if self.visible.get() {
|
||||||
self.state.damage(pos);
|
self.state.damage(pos);
|
||||||
|
|
@ -791,13 +845,7 @@ impl ContainingNode for FloatNode {
|
||||||
let bw = theme.sizes.border_width.get();
|
let bw = theme.sizes.border_width.get();
|
||||||
let (x, y) = (x - bw, y - bw);
|
let (x, y) = (x - bw, y - bw);
|
||||||
let pos = self.position.get();
|
let pos = self.position.get();
|
||||||
if pos.position() != (x, y) {
|
self.set_position(pos.at_point(x, y));
|
||||||
let new_pos = pos.at_point(x, y);
|
|
||||||
self.position.set(new_pos);
|
|
||||||
self.state.damage(pos);
|
|
||||||
self.state.damage(new_pos);
|
|
||||||
self.schedule_layout();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cnode_resize_child(
|
fn cnode_resize_child(
|
||||||
|
|
@ -828,14 +876,7 @@ impl ContainingNode for FloatNode {
|
||||||
y2 = (v + bw).max(y1 + bw + bw);
|
y2 = (v + bw).max(y1 + bw + bw);
|
||||||
}
|
}
|
||||||
let new_pos = Rect::new_saturating(x1, y1, x2, y2);
|
let new_pos = Rect::new_saturating(x1, y1, x2, y2);
|
||||||
if new_pos != pos {
|
self.set_position(new_pos);
|
||||||
self.position.set(new_pos);
|
|
||||||
if self.visible.get() {
|
|
||||||
self.state.damage(pos);
|
|
||||||
self.state.damage(new_pos);
|
|
||||||
}
|
|
||||||
self.schedule_layout();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cnode_pinned(&self) -> bool {
|
fn cnode_pinned(&self) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
|
animation::{
|
||||||
|
RetainedExitLayer, RetainedToplevel,
|
||||||
|
multiphase::{
|
||||||
|
MultiphaseHierarchyPosition, MultiphaseHierarchyTransition,
|
||||||
|
MultiphaseWindowHierarchy, PhaseAxis,
|
||||||
|
},
|
||||||
|
},
|
||||||
client::{Client, ClientId},
|
client::{Client, ClientId},
|
||||||
criteria::{
|
criteria::{
|
||||||
CritDestroyListener, CritMatcherId,
|
CritDestroyListener, CritMatcherId,
|
||||||
|
|
@ -117,6 +124,7 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
|
||||||
let parent_was_none = data.parent.set(Some(parent.clone())).is_none();
|
let parent_was_none = data.parent.set(Some(parent.clone())).is_none();
|
||||||
if parent_was_none {
|
if parent_was_none {
|
||||||
data.mapped_during_iteration.set(data.state.eng.iteration());
|
data.mapped_during_iteration.set(data.state.eng.iteration());
|
||||||
|
data.spawn_in_pending.set(data.kind.is_app_window());
|
||||||
data.property_changed(TL_CHANGED_NEW);
|
data.property_changed(TL_CHANGED_NEW);
|
||||||
}
|
}
|
||||||
let was_floating = data.parent_is_float.get();
|
let was_floating = data.parent_is_float.get();
|
||||||
|
|
@ -184,6 +192,57 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
|
||||||
fn tl_change_extents(self: Rc<Self>, rect: &Rect) {
|
fn tl_change_extents(self: Rc<Self>, rect: &Rect) {
|
||||||
let data = self.tl_data();
|
let data = self.tl_data();
|
||||||
let prev = data.desired_extents.replace(*rect);
|
let prev = data.desired_extents.replace(*rect);
|
||||||
|
let target_hierarchy = self.tl_multiphase_hierarchy_position();
|
||||||
|
let hierarchy = MultiphaseWindowHierarchy::new(
|
||||||
|
data.layout_animation_position.replace(target_hierarchy),
|
||||||
|
target_hierarchy,
|
||||||
|
);
|
||||||
|
let spawn_in_pending = data.spawn_in_pending.get();
|
||||||
|
let spawn_in_eligible = spawn_in_pending
|
||||||
|
&& !rect.is_empty()
|
||||||
|
&& data.visible.get()
|
||||||
|
&& !data.is_fullscreen.get()
|
||||||
|
&& data.kind.is_app_window()
|
||||||
|
&& !self.node_is_container();
|
||||||
|
let parent_container = data
|
||||||
|
.parent
|
||||||
|
.get()
|
||||||
|
.and_then(|parent| parent.node_into_container());
|
||||||
|
let parent_is_mono = parent_container
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|container| container.mono_child.is_some());
|
||||||
|
let parent_mono_transition = parent_container
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|container| container.mono_transition_animation_pending.get());
|
||||||
|
let active_mono_boundary = matches!(
|
||||||
|
hierarchy.transition,
|
||||||
|
MultiphaseHierarchyTransition::EnteringMono
|
||||||
|
| MultiphaseHierarchyTransition::ExitingMono
|
||||||
|
) && parent_mono_transition
|
||||||
|
&& (hierarchy.source.mono_active || hierarchy.target.mono_active);
|
||||||
|
if prev != *rect
|
||||||
|
&& !prev.is_empty()
|
||||||
|
&& !rect.is_empty()
|
||||||
|
&& data.visible.get()
|
||||||
|
&& !data.parent_is_float.get()
|
||||||
|
&& !self.node_is_container()
|
||||||
|
&& (!parent_is_mono || active_mono_boundary)
|
||||||
|
{
|
||||||
|
data.state.clone().queue_tiled_animation_with_hierarchy(
|
||||||
|
data.node_id,
|
||||||
|
prev,
|
||||||
|
*rect,
|
||||||
|
hierarchy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if spawn_in_eligible {
|
||||||
|
data.state
|
||||||
|
.clone()
|
||||||
|
.queue_spawn_in_animation(data.node_id, *rect);
|
||||||
|
}
|
||||||
|
if spawn_in_eligible {
|
||||||
|
data.spawn_in_pending.set(false);
|
||||||
|
}
|
||||||
if prev.size() != rect.size() {
|
if prev.size() != rect.size() {
|
||||||
for sc in data.jay_screencasts.lock().values() {
|
for sc in data.jay_screencasts.lock().values() {
|
||||||
sc.schedule_realloc_or_reconfigure();
|
sc.schedule_realloc_or_reconfigure();
|
||||||
|
|
@ -275,6 +334,35 @@ pub trait ToplevelNodeBase: Node {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tl_multiphase_hierarchy_position(&self) -> MultiphaseHierarchyPosition {
|
||||||
|
let data = self.tl_data();
|
||||||
|
let Some(parent) = data.parent.get() else {
|
||||||
|
return Default::default();
|
||||||
|
};
|
||||||
|
let mut position = MultiphaseHierarchyPosition {
|
||||||
|
parent: Some(parent.node_id()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
populate_multiphase_ancestor_splits(&mut position, Some(parent.clone()));
|
||||||
|
if let Some(container) = parent.node_into_container() {
|
||||||
|
position.split_axis = Some(match container.split.get() {
|
||||||
|
ContainerSplit::Horizontal => PhaseAxis::Horizontal,
|
||||||
|
ContainerSplit::Vertical => PhaseAxis::Vertical,
|
||||||
|
});
|
||||||
|
if let Some(mono) = container.mono_child.get() {
|
||||||
|
position.parent_is_mono = true;
|
||||||
|
position.mono_active = mono.node.node_id() == data.node_id;
|
||||||
|
}
|
||||||
|
for (idx, child) in container.children.iter().enumerate() {
|
||||||
|
if child.node.node_id() == data.node_id {
|
||||||
|
position.sibling_index = Some(idx.min(u16::MAX as usize) as u16);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
position
|
||||||
|
}
|
||||||
|
|
||||||
fn tl_set_active(&self, active: bool) {
|
fn tl_set_active(&self, active: bool) {
|
||||||
let _ = active;
|
let _ = active;
|
||||||
}
|
}
|
||||||
|
|
@ -299,6 +387,11 @@ pub trait ToplevelNodeBase: Node {
|
||||||
fn tl_scanout_surface(&self) -> Option<Rc<WlSurface>> {
|
fn tl_scanout_surface(&self) -> Option<Rc<WlSurface>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn tl_restack_popups(&self) {
|
fn tl_restack_popups(&self) {
|
||||||
// nothing
|
// nothing
|
||||||
}
|
}
|
||||||
|
|
@ -339,6 +432,31 @@ pub trait ToplevelNodeBase: Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn populate_multiphase_ancestor_splits(
|
||||||
|
position: &mut MultiphaseHierarchyPosition,
|
||||||
|
mut parent: Option<Rc<dyn ContainingNode>>,
|
||||||
|
) {
|
||||||
|
let mut depth = 0u16;
|
||||||
|
while let Some(node) = parent {
|
||||||
|
let Some(toplevel) = node.clone().node_into_toplevel() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
depth = depth.saturating_add(1);
|
||||||
|
if let Some(container) = node.node_into_container() {
|
||||||
|
match container.split.get() {
|
||||||
|
ContainerSplit::Horizontal => {
|
||||||
|
position.nearest_horizontal_split_depth.get_or_insert(depth);
|
||||||
|
}
|
||||||
|
ContainerSplit::Vertical => {
|
||||||
|
position.nearest_vertical_split_depth.get_or_insert(depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent = toplevel.tl_data().parent.get();
|
||||||
|
}
|
||||||
|
position.depth = depth;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FullscreenedData {
|
pub struct FullscreenedData {
|
||||||
pub placeholder: Rc<PlaceholderNode>,
|
pub placeholder: Rc<PlaceholderNode>,
|
||||||
pub workspace: Rc<WorkspaceNode>,
|
pub workspace: Rc<WorkspaceNode>,
|
||||||
|
|
@ -377,6 +495,13 @@ impl ToplevelType {
|
||||||
ToplevelType::XWindow { .. } => window::X_WINDOW,
|
ToplevelType::XWindow { .. } => window::X_WINDOW,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_app_window(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
ToplevelType::XdgToplevel(_) | ToplevelType::XWindow(_)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ToplevelData {
|
pub struct ToplevelData {
|
||||||
|
|
@ -399,8 +524,10 @@ pub struct ToplevelData {
|
||||||
pub title: RefCell<String>,
|
pub title: RefCell<String>,
|
||||||
pub parent: CloneCell<Option<Rc<dyn ContainingNode>>>,
|
pub parent: CloneCell<Option<Rc<dyn ContainingNode>>>,
|
||||||
pub mapped_during_iteration: Cell<u64>,
|
pub mapped_during_iteration: Cell<u64>,
|
||||||
|
pub spawn_in_pending: Cell<bool>,
|
||||||
pub pos: Cell<Rect>,
|
pub pos: Cell<Rect>,
|
||||||
pub desired_extents: Cell<Rect>,
|
pub desired_extents: Cell<Rect>,
|
||||||
|
pub layout_animation_position: Cell<MultiphaseHierarchyPosition>,
|
||||||
pub seat_state: NodeSeatState,
|
pub seat_state: NodeSeatState,
|
||||||
pub wants_attention: Cell<bool>,
|
pub wants_attention: Cell<bool>,
|
||||||
pub requested_attention: Cell<bool>,
|
pub requested_attention: Cell<bool>,
|
||||||
|
|
@ -462,8 +589,10 @@ impl ToplevelData {
|
||||||
title: RefCell::new(title),
|
title: RefCell::new(title),
|
||||||
parent: Default::default(),
|
parent: Default::default(),
|
||||||
mapped_during_iteration: Cell::new(0),
|
mapped_during_iteration: Cell::new(0),
|
||||||
|
spawn_in_pending: Cell::new(false),
|
||||||
pos: Default::default(),
|
pos: Default::default(),
|
||||||
desired_extents: Default::default(),
|
desired_extents: Default::default(),
|
||||||
|
layout_animation_position: Default::default(),
|
||||||
seat_state: Default::default(),
|
seat_state: Default::default(),
|
||||||
wants_attention: Cell::new(false),
|
wants_attention: Cell::new(false),
|
||||||
requested_attention: Cell::new(false),
|
requested_attention: Cell::new(false),
|
||||||
|
|
@ -935,6 +1064,62 @@ impl ToplevelData {
|
||||||
self.mapped_during_iteration.get() == self.state.eng.iteration()
|
self.mapped_during_iteration.get() == self.state.eng.iteration()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn queue_spawn_out(&self, node: &dyn ToplevelNode, retained: Option<Rc<RetainedToplevel>>) {
|
||||||
|
if !self.kind.is_app_window()
|
||||||
|
|| !self.visible.get()
|
||||||
|
|| self.is_fullscreen.get()
|
||||||
|
|| node.node_is_container()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(retained) = retained else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let bw = self.state.theme.sizes.border_width.get().max(0);
|
||||||
|
let now = self.state.now_nsec();
|
||||||
|
let (outer, frame_inset, layer) = if self.parent_is_float.get() {
|
||||||
|
let Some(float) = self.float.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
(
|
||||||
|
self.state
|
||||||
|
.animations
|
||||||
|
.visual_rect(float.node_id(), float.position.get(), now),
|
||||||
|
bw,
|
||||||
|
RetainedExitLayer::Floating,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let body =
|
||||||
|
self.state
|
||||||
|
.animations
|
||||||
|
.visual_rect(self.node_id, node.node_absolute_position(), now);
|
||||||
|
if body.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.state.theme.sizes.gap.get() != 0 {
|
||||||
|
(
|
||||||
|
Rect::new_sized_saturating(
|
||||||
|
body.x1() - bw,
|
||||||
|
body.y1() - bw,
|
||||||
|
body.width() + 2 * bw,
|
||||||
|
body.height() + 2 * bw,
|
||||||
|
),
|
||||||
|
bw,
|
||||||
|
RetainedExitLayer::Tiled,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(body, 0, RetainedExitLayer::Tiled)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.state.clone().queue_spawn_out_animation(
|
||||||
|
outer,
|
||||||
|
frame_inset,
|
||||||
|
retained,
|
||||||
|
self.active(),
|
||||||
|
layer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_content_type(&self, content_type: Option<ContentType>) {
|
pub fn set_content_type(&self, content_type: Option<ContentType>) {
|
||||||
if self.content_type.replace(content_type) != content_type {
|
if self.content_type.replace(content_type) != content_type {
|
||||||
self.property_changed(TL_CHANGED_CONTENT_TY);
|
self.property_changed(TL_CHANGED_CONTENT_TY);
|
||||||
|
|
@ -1043,6 +1228,26 @@ pub fn toplevel_create_split(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, axis:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn float_outer_for_body(state: &State, body: Rect) -> Rect {
|
||||||
|
let bw = state.theme.sizes.border_width.get();
|
||||||
|
Rect::new_sized_saturating(
|
||||||
|
body.x1() - bw,
|
||||||
|
body.y1() - bw,
|
||||||
|
body.width() + 2 * bw,
|
||||||
|
body.height() + 2 * bw,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn float_body_for_outer(state: &State, outer: Rect) -> Rect {
|
||||||
|
let bw = state.theme.sizes.border_width.get();
|
||||||
|
Rect::new_sized_saturating(
|
||||||
|
outer.x1() + bw,
|
||||||
|
outer.y1() + bw,
|
||||||
|
outer.width() - 2 * bw,
|
||||||
|
outer.height() - 2 * bw,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floating: bool) {
|
pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floating: bool) {
|
||||||
let data = tl.tl_data();
|
let data = tl.tl_data();
|
||||||
if data.is_fullscreen.get() {
|
if data.is_fullscreen.get() {
|
||||||
|
|
@ -1059,9 +1264,19 @@ pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floati
|
||||||
parent.cnode_remove_child2(&*tl, true);
|
parent.cnode_remove_child2(&*tl, true);
|
||||||
state.map_tiled(tl);
|
state.map_tiled(tl);
|
||||||
} else if let Some(ws) = data.workspace.get() {
|
} else if let Some(ws) = data.workspace.get() {
|
||||||
|
let node_id = data.node_id;
|
||||||
|
let old_body =
|
||||||
|
state
|
||||||
|
.animations
|
||||||
|
.visual_rect(node_id, tl.node_absolute_position(), state.now_nsec());
|
||||||
|
let old_outer = float_outer_for_body(state, old_body);
|
||||||
parent.cnode_remove_child2(&*tl, true);
|
parent.cnode_remove_child2(&*tl, true);
|
||||||
let (width, height) = data.float_size(&ws);
|
let (width, height) = data.float_size(&ws);
|
||||||
state.map_floating(tl, width, height, &ws, None);
|
let floater = state.map_floating(tl, width, height, &ws, None);
|
||||||
|
let new_outer = floater.position.get();
|
||||||
|
let new_body = float_body_for_outer(state, new_outer);
|
||||||
|
state.queue_linear_layout_animation(floater.node_id(), old_outer, new_outer);
|
||||||
|
state.queue_linear_layout_animation(node_id, old_body, new_body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -197,10 +197,10 @@ impl WorkspaceNode {
|
||||||
}
|
}
|
||||||
self.pull_child_properties(&**container);
|
self.pull_child_properties(&**container);
|
||||||
let pos = self.position.get();
|
let pos = self.position.get();
|
||||||
container.clone().tl_change_extents(&pos);
|
|
||||||
container.tl_set_parent(self.clone());
|
container.tl_set_parent(self.clone());
|
||||||
container.tl_set_visible(self.container_visible());
|
container.tl_set_visible(self.container_visible());
|
||||||
self.container.set(Some(container.clone()));
|
self.container.set(Some(container.clone()));
|
||||||
|
container.clone().tl_change_extents(&pos);
|
||||||
self.state.damage(self.position.get());
|
self.state.damage(self.position.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2034,6 +2034,7 @@ impl Wm {
|
||||||
self.windows_by_surface_serial.remove(&serial);
|
self.windows_by_surface_serial.remove(&serial);
|
||||||
}
|
}
|
||||||
if let Some(window) = data.window.take() {
|
if let Some(window) = data.window.take() {
|
||||||
|
window.queue_spawn_out();
|
||||||
window.destroy();
|
window.destroy();
|
||||||
}
|
}
|
||||||
if let Some(parent) = data.parent.take() {
|
if let Some(parent) = data.parent.take() {
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,20 @@ pub struct UiDrag {
|
||||||
pub threshold: Option<i32>,
|
pub threshold: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Animations {
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
pub duration_ms: Option<u32>,
|
||||||
|
pub style: Option<String>,
|
||||||
|
pub curve: Option<AnimationCurveConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum AnimationCurveConfig {
|
||||||
|
Preset(String),
|
||||||
|
CubicBezier([f32; 4]),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum OutputMatch {
|
pub enum OutputMatch {
|
||||||
Any(Vec<OutputMatch>),
|
Any(Vec<OutputMatch>),
|
||||||
|
|
@ -567,6 +581,7 @@ pub struct Config {
|
||||||
pub tearing: Option<Tearing>,
|
pub tearing: Option<Tearing>,
|
||||||
pub libei: Libei,
|
pub libei: Libei,
|
||||||
pub ui_drag: UiDrag,
|
pub ui_drag: UiDrag,
|
||||||
|
pub animations: Animations,
|
||||||
pub xwayland: Option<Xwayland>,
|
pub xwayland: Option<Xwayland>,
|
||||||
pub color_management: Option<ColorManagement>,
|
pub color_management: Option<ColorManagement>,
|
||||||
pub float: Option<Float>,
|
pub float: Option<Float>,
|
||||||
|
|
@ -651,3 +666,26 @@ fn default_config_parses() {
|
||||||
let input = include_bytes!("default-config.toml");
|
let input = include_bytes!("default-config.toml");
|
||||||
parse_config(input, &Default::default(), |_| ()).unwrap();
|
parse_config(input, &Default::default(), |_| ()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_animation_curve_parses() {
|
||||||
|
let input = b"
|
||||||
|
[animations]
|
||||||
|
curve = [0.25, 0.1, 0.25, 1.0]
|
||||||
|
";
|
||||||
|
let config = parse_config(input, &Default::default(), |_| ()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
config.animations.curve,
|
||||||
|
Some(AnimationCurveConfig::CubicBezier([0.25, 0.1, 0.25, 1.0]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn animation_style_parses() {
|
||||||
|
let input = b"
|
||||||
|
[animations]
|
||||||
|
style = \"plain\"
|
||||||
|
";
|
||||||
|
let config = parse_config(input, &Default::default(), |_| ()).unwrap();
|
||||||
|
assert_eq!(config.animations.style.as_deref(), Some("plain"));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use {
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
mod actions;
|
mod actions;
|
||||||
|
mod animations;
|
||||||
mod capabilities;
|
mod capabilities;
|
||||||
mod clean_logs_older_than;
|
mod clean_logs_older_than;
|
||||||
mod client_match;
|
mod client_match;
|
||||||
|
|
|
||||||
99
toml-config/src/config/parsers/animations.rs
Normal file
99
toml-config/src/config/parsers/animations.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
config::{
|
||||||
|
AnimationCurveConfig, Animations,
|
||||||
|
context::Context,
|
||||||
|
extractor::{Extractor, ExtractorError, bol, n32, opt, recover, str, val},
|
||||||
|
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||||
|
},
|
||||||
|
toml::{
|
||||||
|
toml_span::{DespanExt, Span, Spanned, SpannedExt},
|
||||||
|
toml_value::Value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
indexmap::IndexMap,
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AnimationsParserError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Expected(#[from] UnexpectedDataType),
|
||||||
|
#[error(transparent)]
|
||||||
|
Extract(#[from] ExtractorError),
|
||||||
|
#[error("Expected animation curve to be a string or an array")]
|
||||||
|
CurveType,
|
||||||
|
#[error("Cubic-bezier animation curves must contain exactly four values")]
|
||||||
|
CubicBezierLen,
|
||||||
|
#[error("Cubic-bezier animation curve entries must be finite floats or integers")]
|
||||||
|
CubicBezierValue,
|
||||||
|
#[error("Cubic-bezier x control points must be between 0 and 1")]
|
||||||
|
CubicBezierXRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AnimationsParser<'a>(pub &'a Context<'a>);
|
||||||
|
|
||||||
|
impl Parser for AnimationsParser<'_> {
|
||||||
|
type Value = Animations;
|
||||||
|
type Error = AnimationsParserError;
|
||||||
|
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 (enabled, duration_ms, style, curve) = ext.extract((
|
||||||
|
recover(opt(bol("enabled"))),
|
||||||
|
recover(opt(n32("duration-ms"))),
|
||||||
|
recover(opt(str("style"))),
|
||||||
|
opt(val("curve")),
|
||||||
|
))?;
|
||||||
|
let curve = match curve {
|
||||||
|
Some(curve) => Some(parse_curve(curve)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
Ok(Animations {
|
||||||
|
enabled: enabled.despan(),
|
||||||
|
duration_ms: duration_ms.despan(),
|
||||||
|
style: style.despan().map(|style| style.to_string()),
|
||||||
|
curve,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_curve(
|
||||||
|
curve: Spanned<&Value>,
|
||||||
|
) -> Result<AnimationCurveConfig, Spanned<AnimationsParserError>> {
|
||||||
|
match curve.value {
|
||||||
|
Value::String(s) => Ok(AnimationCurveConfig::Preset(s.clone())),
|
||||||
|
Value::Array(values) => parse_cubic_bezier(curve.span, values),
|
||||||
|
_ => Err(AnimationsParserError::CurveType.spanned(curve.span)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cubic_bezier(
|
||||||
|
span: Span,
|
||||||
|
values: &[Spanned<Value>],
|
||||||
|
) -> Result<AnimationCurveConfig, Spanned<AnimationsParserError>> {
|
||||||
|
if values.len() != 4 {
|
||||||
|
return Err(AnimationsParserError::CubicBezierLen.spanned(span));
|
||||||
|
}
|
||||||
|
let mut points = [0.0; 4];
|
||||||
|
for (idx, value) in values.iter().enumerate() {
|
||||||
|
let f = match value.value {
|
||||||
|
Value::Float(f) => f,
|
||||||
|
Value::Integer(i) => i as f64,
|
||||||
|
_ => return Err(AnimationsParserError::CubicBezierValue.spanned(value.span)),
|
||||||
|
};
|
||||||
|
if !f.is_finite() {
|
||||||
|
return Err(AnimationsParserError::CubicBezierValue.spanned(value.span));
|
||||||
|
}
|
||||||
|
points[idx] = f as f32;
|
||||||
|
}
|
||||||
|
if !(0.0..=1.0).contains(&points[0]) || !(0.0..=1.0).contains(&points[2]) {
|
||||||
|
return Err(AnimationsParserError::CubicBezierXRange.spanned(span));
|
||||||
|
}
|
||||||
|
Ok(AnimationCurveConfig::CubicBezier(points))
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
config::{
|
config::{
|
||||||
Action, Config, Libei, Theme, UiDrag,
|
Action, Animations, Config, Libei, Theme, UiDrag,
|
||||||
context::Context,
|
context::Context,
|
||||||
extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val},
|
extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val},
|
||||||
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||||
parsers::{
|
parsers::{
|
||||||
action::ActionParser,
|
action::ActionParser,
|
||||||
actions::ActionsParser,
|
actions::ActionsParser,
|
||||||
|
animations::AnimationsParser,
|
||||||
clean_logs_older_than::CleanLogsOlderThanParser,
|
clean_logs_older_than::CleanLogsOlderThanParser,
|
||||||
client_rule::ClientRulesParser,
|
client_rule::ClientRulesParser,
|
||||||
color_management::ColorManagementParser,
|
color_management::ColorManagementParser,
|
||||||
|
|
@ -153,6 +154,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
fallback_output_mode_val,
|
fallback_output_mode_val,
|
||||||
clean_logs_older_than_val,
|
clean_logs_older_than_val,
|
||||||
mouse_follows_focus,
|
mouse_follows_focus,
|
||||||
|
animations_val,
|
||||||
),
|
),
|
||||||
) = ext.extract((
|
) = ext.extract((
|
||||||
(
|
(
|
||||||
|
|
@ -213,6 +215,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
opt(val("fallback-output-mode")),
|
opt(val("fallback-output-mode")),
|
||||||
opt(val("clean-logs-older-than")),
|
opt(val("clean-logs-older-than")),
|
||||||
recover(opt(bol("unstable-mouse-follows-focus"))),
|
recover(opt(bol("unstable-mouse-follows-focus"))),
|
||||||
|
opt(val("animations")),
|
||||||
),
|
),
|
||||||
))?;
|
))?;
|
||||||
let mut keymap = None;
|
let mut keymap = None;
|
||||||
|
|
@ -429,6 +432,15 @@ impl Parser for ConfigParser<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut animations = Animations::default();
|
||||||
|
if let Some(value) = animations_val {
|
||||||
|
match value.parse(&mut AnimationsParser(self.0)) {
|
||||||
|
Ok(v) => animations = v,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Could not parse animations setting: {}", self.0.error(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut xwayland = None;
|
let mut xwayland = None;
|
||||||
if let Some(value) = xwayland_val {
|
if let Some(value) = xwayland_val {
|
||||||
match value.parse(&mut XwaylandParser(self.0)) {
|
match value.parse(&mut XwaylandParser(self.0)) {
|
||||||
|
|
@ -587,6 +599,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
tearing,
|
tearing,
|
||||||
libei,
|
libei,
|
||||||
ui_drag,
|
ui_drag,
|
||||||
|
animations,
|
||||||
xwayland,
|
xwayland,
|
||||||
color_management,
|
color_management,
|
||||||
float,
|
float,
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ mod toml;
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
config::{
|
config::{
|
||||||
Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
|
Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
|
||||||
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch,
|
ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
|
||||||
SimpleCommand, Status, Theme, WindowRule, parse_config,
|
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config,
|
||||||
},
|
},
|
||||||
rules::{MatcherTemp, RuleMapper},
|
rules::{MatcherTemp, RuleMapper},
|
||||||
shortcuts::ModeState,
|
shortcuts::ModeState,
|
||||||
|
|
@ -23,7 +23,7 @@ use {
|
||||||
ahash::{AHashMap, AHashSet},
|
ahash::{AHashMap, AHashSet},
|
||||||
error_reporter::Report,
|
error_reporter::Report,
|
||||||
jay_config::{
|
jay_config::{
|
||||||
Axis,
|
AnimationCurve, AnimationStyle, Axis,
|
||||||
client::Client,
|
client::Client,
|
||||||
config, config_dir,
|
config, config_dir,
|
||||||
exec::{Command, set_env, unset_env},
|
exec::{Command, set_env, unset_env},
|
||||||
|
|
@ -37,8 +37,10 @@ use {
|
||||||
is_reload,
|
is_reload,
|
||||||
keyboard::Keymap,
|
keyboard::Keymap,
|
||||||
logging::{clean_logs_older_than, set_log_level},
|
logging::{clean_logs_older_than, set_log_level},
|
||||||
on_devices_enumerated, on_idle, on_unload, quit, reload, set_autotile,
|
on_devices_enumerated, on_idle, on_unload, quit, reload, set_animation_cubic_bezier,
|
||||||
set_color_management_enabled, set_corner_radius, set_default_workspace_capture,
|
set_animation_curve, set_animation_duration_ms, set_animation_style,
|
||||||
|
set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius,
|
||||||
|
set_default_workspace_capture,
|
||||||
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle,
|
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle,
|
||||||
set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar,
|
set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar,
|
||||||
set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled,
|
set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled,
|
||||||
|
|
@ -1649,6 +1651,38 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
||||||
if let Some(threshold) = config.ui_drag.threshold {
|
if let Some(threshold) = config.ui_drag.threshold {
|
||||||
set_ui_drag_threshold(threshold);
|
set_ui_drag_threshold(threshold);
|
||||||
}
|
}
|
||||||
|
set_animations_enabled(config.animations.enabled.unwrap_or(false));
|
||||||
|
set_animation_duration_ms(config.animations.duration_ms.unwrap_or(160));
|
||||||
|
match config.animations.style.as_deref().unwrap_or("multiphase") {
|
||||||
|
"plain" => set_animation_style(AnimationStyle::PLAIN),
|
||||||
|
"multiphase" => set_animation_style(AnimationStyle::MULTIPHASE),
|
||||||
|
style_name => log::warn!("Unknown animation style: {style_name}"),
|
||||||
|
}
|
||||||
|
match config
|
||||||
|
.animations
|
||||||
|
.curve
|
||||||
|
.unwrap_or_else(|| AnimationCurveConfig::Preset("ease-out".to_string()))
|
||||||
|
{
|
||||||
|
AnimationCurveConfig::Preset(curve_name) => {
|
||||||
|
let curve = match curve_name.as_str() {
|
||||||
|
"linear" => Some(AnimationCurve::LINEAR),
|
||||||
|
"ease" => Some(AnimationCurve::EASE),
|
||||||
|
"ease-in" => Some(AnimationCurve::EASE_IN),
|
||||||
|
"ease-out" => Some(AnimationCurve::EASE_OUT),
|
||||||
|
"ease-in-out" => Some(AnimationCurve::EASE_IN_OUT),
|
||||||
|
_ => {
|
||||||
|
log::warn!("Unknown animation curve: {curve_name}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(curve) = curve {
|
||||||
|
set_animation_curve(curve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimationCurveConfig::CubicBezier([x1, y1, x2, y2]) => {
|
||||||
|
set_animation_cubic_bezier(x1, y1, x2, y2);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(xwayland) = config.xwayland {
|
if let Some(xwayland) = config.xwayland {
|
||||||
if let Some(enabled) = xwayland.enabled {
|
if let Some(enabled) = xwayland.enabled {
|
||||||
set_x_wayland_enabled(enabled);
|
set_x_wayland_enabled(enabled);
|
||||||
|
|
|
||||||
|
|
@ -641,6 +641,61 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"AnimationCurve": {
|
||||||
|
"description": "Describes a window animation curve.\n",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "One of the supported curve presets.\n",
|
||||||
|
"enum": [
|
||||||
|
"linear",
|
||||||
|
"ease",
|
||||||
|
"ease-in",
|
||||||
|
"ease-out",
|
||||||
|
"ease-in-out"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"description": "A custom CSS-style cubic-bezier curve as four numbers:\n`x1`, `y1`, `x2`, and `y2`.\n\nThe implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must\nbe between `0` and `1`.\n",
|
||||||
|
"items": {
|
||||||
|
"type": "number",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"AnimationStyle": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Describes a tiled window movement animation style.\n",
|
||||||
|
"enum": [
|
||||||
|
"plain",
|
||||||
|
"multiphase"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Animations": {
|
||||||
|
"description": "Describes window animation settings.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n style = \"multiphase\"\n curve = [0.25, 0.1, 0.25, 1.0]\n ```\n",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enables or disables window animations.\n\nThe default is `false`.\n"
|
||||||
|
},
|
||||||
|
"duration-ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Sets the animation duration in milliseconds.\n\nThe default is `160`.\n"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"description": "Sets the animation style used for tiled window movement animations.\n\nThe default is `multiphase`.\n",
|
||||||
|
"$ref": "#/$defs/AnimationStyle"
|
||||||
|
},
|
||||||
|
"curve": {
|
||||||
|
"description": "Sets the animation curve.\n\nThe default is `ease-out`.\n",
|
||||||
|
"$ref": "#/$defs/AnimationCurve"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
"BarPosition": {
|
"BarPosition": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The position of the bar.",
|
"description": "The position of the bar.",
|
||||||
|
|
@ -1085,6 +1140,10 @@
|
||||||
"description": "Configures the ui-drag settings.\n\n- Example:\n\n ```toml\n ui-drag = { enabled = false, threshold = 20 }\n ```\n",
|
"description": "Configures the ui-drag settings.\n\n- Example:\n\n ```toml\n ui-drag = { enabled = false, threshold = 20 }\n ```\n",
|
||||||
"$ref": "#/$defs/UiDrag"
|
"$ref": "#/$defs/UiDrag"
|
||||||
},
|
},
|
||||||
|
"animations": {
|
||||||
|
"description": "Configures window animations.\n\nAnimations are disabled by default.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n style = \"multiphase\"\n curve = \"ease-out\"\n ```\n",
|
||||||
|
"$ref": "#/$defs/Animations"
|
||||||
|
},
|
||||||
"xwayland": {
|
"xwayland": {
|
||||||
"description": "Configures the Xwayland settings.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n",
|
"description": "Configures the Xwayland settings.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n",
|
||||||
"$ref": "#/$defs/Xwayland"
|
"$ref": "#/$defs/Xwayland"
|
||||||
|
|
|
||||||
|
|
@ -942,6 +942,125 @@ This table is a tagged union. The variant is determined by the `type` field. It
|
||||||
The numbers should be integers.
|
The numbers should be integers.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="types-AnimationCurve"></a>
|
||||||
|
### `AnimationCurve`
|
||||||
|
|
||||||
|
Describes a window animation curve.
|
||||||
|
|
||||||
|
Values of this type should have one of the following forms:
|
||||||
|
|
||||||
|
#### A string
|
||||||
|
|
||||||
|
One of the supported curve presets.
|
||||||
|
|
||||||
|
The string should have one of the following values:
|
||||||
|
|
||||||
|
- `linear`:
|
||||||
|
|
||||||
|
No easing.
|
||||||
|
|
||||||
|
- `ease`:
|
||||||
|
|
||||||
|
The CSS `ease` curve.
|
||||||
|
|
||||||
|
- `ease-in`:
|
||||||
|
|
||||||
|
The CSS `ease-in` curve.
|
||||||
|
|
||||||
|
- `ease-out`:
|
||||||
|
|
||||||
|
The CSS `ease-out` curve.
|
||||||
|
|
||||||
|
- `ease-in-out`:
|
||||||
|
|
||||||
|
The CSS `ease-in-out` curve.
|
||||||
|
|
||||||
|
|
||||||
|
#### An array
|
||||||
|
|
||||||
|
A custom CSS-style cubic-bezier curve as four numbers:
|
||||||
|
`x1`, `y1`, `x2`, and `y2`.
|
||||||
|
|
||||||
|
The implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must
|
||||||
|
be between `0` and `1`.
|
||||||
|
|
||||||
|
Each element of this array should be a number.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="types-AnimationStyle"></a>
|
||||||
|
### `AnimationStyle`
|
||||||
|
|
||||||
|
Describes a tiled window movement animation style.
|
||||||
|
|
||||||
|
Values of this type should be strings.
|
||||||
|
|
||||||
|
The string should have one of the following values:
|
||||||
|
|
||||||
|
- `plain`:
|
||||||
|
|
||||||
|
Uses a single interpolated movement from each window's current visual
|
||||||
|
rectangle to its destination rectangle.
|
||||||
|
|
||||||
|
- `multiphase`:
|
||||||
|
|
||||||
|
Uses the no-overlap multiphase planner for tiled window movement when a
|
||||||
|
supported plan exists.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="types-Animations"></a>
|
||||||
|
### `Animations`
|
||||||
|
|
||||||
|
Describes window animation settings.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[animations]
|
||||||
|
enabled = true
|
||||||
|
duration-ms = 160
|
||||||
|
style = "multiphase"
|
||||||
|
curve = [0.25, 0.1, 0.25, 1.0]
|
||||||
|
```
|
||||||
|
|
||||||
|
Values of this type should be tables.
|
||||||
|
|
||||||
|
The table has the following fields:
|
||||||
|
|
||||||
|
- `enabled` (optional):
|
||||||
|
|
||||||
|
Enables or disables window animations.
|
||||||
|
|
||||||
|
The default is `false`.
|
||||||
|
|
||||||
|
The value of this field should be a boolean.
|
||||||
|
|
||||||
|
- `duration-ms` (optional):
|
||||||
|
|
||||||
|
Sets the animation duration in milliseconds.
|
||||||
|
|
||||||
|
The default is `160`.
|
||||||
|
|
||||||
|
The value of this field should be a number.
|
||||||
|
|
||||||
|
The numbers should be integers.
|
||||||
|
|
||||||
|
- `style` (optional):
|
||||||
|
|
||||||
|
Sets the animation style used for tiled window movement animations.
|
||||||
|
|
||||||
|
The default is `multiphase`.
|
||||||
|
|
||||||
|
The value of this field should be a [AnimationStyle](#types-AnimationStyle).
|
||||||
|
|
||||||
|
- `curve` (optional):
|
||||||
|
|
||||||
|
Sets the animation curve.
|
||||||
|
|
||||||
|
The default is `ease-out`.
|
||||||
|
|
||||||
|
The value of this field should be a [AnimationCurve](#types-AnimationCurve).
|
||||||
|
|
||||||
|
|
||||||
<a name="types-BarPosition"></a>
|
<a name="types-BarPosition"></a>
|
||||||
### `BarPosition`
|
### `BarPosition`
|
||||||
|
|
||||||
|
|
@ -2169,6 +2288,24 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a [UiDrag](#types-UiDrag).
|
The value of this field should be a [UiDrag](#types-UiDrag).
|
||||||
|
|
||||||
|
- `animations` (optional):
|
||||||
|
|
||||||
|
Configures window animations.
|
||||||
|
|
||||||
|
Animations are disabled by default.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[animations]
|
||||||
|
enabled = true
|
||||||
|
duration-ms = 160
|
||||||
|
style = "multiphase"
|
||||||
|
curve = "ease-out"
|
||||||
|
```
|
||||||
|
|
||||||
|
The value of this field should be a [Animations](#types-Animations).
|
||||||
|
|
||||||
- `xwayland` (optional):
|
- `xwayland` (optional):
|
||||||
|
|
||||||
Configures the Xwayland settings.
|
Configures the Xwayland settings.
|
||||||
|
|
@ -5670,4 +5807,3 @@ 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).
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2942,6 +2942,23 @@ Config:
|
||||||
```toml
|
```toml
|
||||||
ui-drag = { enabled = false, threshold = 20 }
|
ui-drag = { enabled = false, threshold = 20 }
|
||||||
```
|
```
|
||||||
|
animations:
|
||||||
|
ref: Animations
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
Configures window animations.
|
||||||
|
|
||||||
|
Animations are disabled by default.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[animations]
|
||||||
|
enabled = true
|
||||||
|
duration-ms = 160
|
||||||
|
style = "multiphase"
|
||||||
|
curve = "ease-out"
|
||||||
|
```
|
||||||
xwayland:
|
xwayland:
|
||||||
ref: Xwayland
|
ref: Xwayland
|
||||||
required: false
|
required: false
|
||||||
|
|
@ -3655,6 +3672,97 @@ UiDrag:
|
||||||
The default is `10`.
|
The default is `10`.
|
||||||
|
|
||||||
|
|
||||||
|
Animations:
|
||||||
|
kind: table
|
||||||
|
description: |
|
||||||
|
Describes window animation settings.
|
||||||
|
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[animations]
|
||||||
|
enabled = true
|
||||||
|
duration-ms = 160
|
||||||
|
style = "multiphase"
|
||||||
|
curve = [0.25, 0.1, 0.25, 1.0]
|
||||||
|
```
|
||||||
|
fields:
|
||||||
|
enabled:
|
||||||
|
kind: boolean
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
Enables or disables window animations.
|
||||||
|
|
||||||
|
The default is `false`.
|
||||||
|
duration-ms:
|
||||||
|
kind: number
|
||||||
|
integer_only: true
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
Sets the animation duration in milliseconds.
|
||||||
|
|
||||||
|
The default is `160`.
|
||||||
|
style:
|
||||||
|
ref: AnimationStyle
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
Sets the animation style used for tiled window movement animations.
|
||||||
|
|
||||||
|
The default is `multiphase`.
|
||||||
|
curve:
|
||||||
|
ref: AnimationCurve
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
Sets the animation curve.
|
||||||
|
|
||||||
|
The default is `ease-out`.
|
||||||
|
|
||||||
|
|
||||||
|
AnimationStyle:
|
||||||
|
kind: string
|
||||||
|
description: |
|
||||||
|
Describes a tiled window movement animation style.
|
||||||
|
values:
|
||||||
|
- value: plain
|
||||||
|
description: |
|
||||||
|
Uses a single interpolated movement from each window's current visual
|
||||||
|
rectangle to its destination rectangle.
|
||||||
|
- value: multiphase
|
||||||
|
description: |
|
||||||
|
Uses the no-overlap multiphase planner for tiled window movement when a
|
||||||
|
supported plan exists.
|
||||||
|
|
||||||
|
|
||||||
|
AnimationCurve:
|
||||||
|
kind: variable
|
||||||
|
description: |
|
||||||
|
Describes a window animation curve.
|
||||||
|
variants:
|
||||||
|
- kind: string
|
||||||
|
description: |
|
||||||
|
One of the supported curve presets.
|
||||||
|
values:
|
||||||
|
- value: linear
|
||||||
|
description: No easing.
|
||||||
|
- value: ease
|
||||||
|
description: The CSS `ease` curve.
|
||||||
|
- value: ease-in
|
||||||
|
description: The CSS `ease-in` curve.
|
||||||
|
- value: ease-out
|
||||||
|
description: The CSS `ease-out` curve.
|
||||||
|
- value: ease-in-out
|
||||||
|
description: The CSS `ease-in-out` curve.
|
||||||
|
- kind: array
|
||||||
|
items:
|
||||||
|
kind: number
|
||||||
|
description: |
|
||||||
|
A custom CSS-style cubic-bezier curve as four numbers:
|
||||||
|
`x1`, `y1`, `x2`, and `y2`.
|
||||||
|
|
||||||
|
The implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must
|
||||||
|
be between `0` and `1`.
|
||||||
|
|
||||||
|
|
||||||
Xwayland:
|
Xwayland:
|
||||||
kind: table
|
kind: table
|
||||||
description: |
|
description: |
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue