Add linear tiled window animations
This commit is contained in:
parent
a29937ebe8
commit
3540cdc4be
17 changed files with 913 additions and 64 deletions
200
docs/window-animations-plan.md
Normal file
200
docs/window-animations-plan.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# 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 linear 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.
|
||||
- Linear animations restart only for windows whose destination changes. Other
|
||||
in-flight windows keep their existing timelines.
|
||||
- Spawn-in uses scale and position. Spawn-out requires retained visual content
|
||||
and is deferred until the freezing layer exists.
|
||||
- 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 linear path is
|
||||
working and testable.
|
||||
- Content freezing will use retained per-surface texture references, not a full
|
||||
offscreen snapshot as the default design.
|
||||
- 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.
|
||||
|
||||
## 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, tile-to-float, float-to-tile, and spawn-in are
|
||||
deferred until after tiled reflow is validated.
|
||||
- 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.
|
||||
- No spawn-out.
|
||||
- No content freezing.
|
||||
- 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
|
||||
|
||||
## Phase 2: Retained Texture Freezing
|
||||
|
||||
Goal: freeze visual contents during movement and enable spawn-out.
|
||||
|
||||
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.
|
||||
|
||||
Open design work:
|
||||
|
||||
- Exact lifetime model for retained `SurfaceBuffer` / `GfxTexture` references.
|
||||
- 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.
|
||||
|
||||
## Phase 3: Multiphase No-Overlap Animations
|
||||
|
||||
Goal: implement Wry's staged no-overlap planner while preserving the rule that
|
||||
windows never overlap.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
Tests:
|
||||
|
||||
- horizontal swaps shrink, move, then grow without overlap
|
||||
- extraction from a stack creates space before moving the extracted window
|
||||
- nested containers do not produce simultaneous cross-axis motion
|
||||
- interruption restarts only affected phase groups
|
||||
- reversing direction produces equivalent motion in reverse
|
||||
- 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
|
||||
- curve preset or cubic bezier
|
||||
|
||||
Initial TOML shape:
|
||||
|
||||
```toml
|
||||
[animations]
|
||||
enabled = false
|
||||
duration-ms = 160
|
||||
curve = "ease-out"
|
||||
```
|
||||
|
||||
Bezier curves should be analyzed at configuration time and stored in a form that
|
||||
is cheap to evaluate during rendering.
|
||||
|
||||
## 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.
|
||||
|
|
@ -1023,6 +1023,18 @@ impl ConfigClient {
|
|||
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_color_management_enabled(&self, enabled: bool) {
|
||||
self.send(&ClientMessage::SetColorManagementEnabled { enabled });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -545,6 +545,15 @@ pub enum ClientMessage<'a> {
|
|||
SetUiDragThreshold {
|
||||
threshold: i32,
|
||||
},
|
||||
SetAnimationsEnabled {
|
||||
enabled: bool,
|
||||
},
|
||||
SetAnimationDurationMs {
|
||||
duration_ms: u32,
|
||||
},
|
||||
SetAnimationCurve {
|
||||
curve: u32,
|
||||
},
|
||||
SetXScalingMode {
|
||||
mode: XScalingMode,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -103,6 +103,18 @@ 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);
|
||||
}
|
||||
|
||||
/// Exits the compositor.
|
||||
pub fn quit() {
|
||||
get!().quit()
|
||||
|
|
@ -287,6 +299,27 @@ pub fn set_ui_drag_threshold(threshold: i32) {
|
|||
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);
|
||||
}
|
||||
|
||||
/// Enables or disables the color-management protocol.
|
||||
///
|
||||
/// The default is `false`.
|
||||
|
|
|
|||
315
src/animation.rs
Normal file
315
src/animation.rs
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
use {
|
||||
crate::{
|
||||
rect::Rect,
|
||||
state::State,
|
||||
tree::{LatchListener, NodeId, OutputNode},
|
||||
utils::{clonecell::CloneCell, event_listener::EventListener},
|
||||
},
|
||||
ahash::AHashMap,
|
||||
std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::{Rc, Weak},
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_DURATION_MS: u32 = 160;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AnimationCurve {
|
||||
Linear,
|
||||
Ease,
|
||||
EaseIn,
|
||||
EaseOut,
|
||||
EaseInOut,
|
||||
}
|
||||
|
||||
impl AnimationCurve {
|
||||
pub fn from_config(value: u32) -> Self {
|
||||
match value {
|
||||
0 => Self::Linear,
|
||||
1 => Self::Ease,
|
||||
2 => Self::EaseIn,
|
||||
4 => Self::EaseInOut,
|
||||
_ => Self::EaseOut,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample(self, t: f64) -> f64 {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
match self {
|
||||
Self::Linear => t,
|
||||
Self::Ease => cubic_bezier(0.25, 0.1, 0.25, 1.0, t),
|
||||
Self::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0, t),
|
||||
Self::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0, t),
|
||||
Self::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0, t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnimationState {
|
||||
pub enabled: Cell<bool>,
|
||||
pub duration_ms: Cell<u32>,
|
||||
pub curve: Cell<AnimationCurve>,
|
||||
windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
|
||||
tick: CloneCell<Option<Rc<AnimationTick>>>,
|
||||
}
|
||||
|
||||
impl Default for AnimationState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: Cell::new(false),
|
||||
duration_ms: Cell::new(DEFAULT_DURATION_MS),
|
||||
curve: Cell::new(AnimationCurve::EaseOut),
|
||||
windows: Default::default(),
|
||||
tick: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationState {
|
||||
pub fn clear(&self) {
|
||||
self.windows.borrow_mut().clear();
|
||||
if let Some(tick) = self.tick.take() {
|
||||
tick.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_target(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
old: Rect,
|
||||
new: Rect,
|
||||
now_nsec: u64,
|
||||
duration_ms: u32,
|
||||
curve: AnimationCurve,
|
||||
) -> bool {
|
||||
if old == new || old.is_empty() || new.is_empty() || duration_ms == 0 {
|
||||
self.windows.borrow_mut().remove(&node_id);
|
||||
return false;
|
||||
}
|
||||
let duration_nsec = duration_ms as u64 * 1_000_000;
|
||||
let mut windows = self.windows.borrow_mut();
|
||||
let from = match windows.get(&node_id) {
|
||||
Some(anim) if anim.to == new => return false,
|
||||
Some(anim) => anim.rect_at(now_nsec),
|
||||
None => old,
|
||||
};
|
||||
if from == new {
|
||||
windows.remove(&node_id);
|
||||
return false;
|
||||
}
|
||||
windows.insert(
|
||||
node_id,
|
||||
WindowAnimation {
|
||||
from,
|
||||
to: new,
|
||||
start_nsec: now_nsec,
|
||||
duration_nsec,
|
||||
curve,
|
||||
last_damage: from,
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect {
|
||||
let windows = self.windows.borrow();
|
||||
match windows.get(&node_id) {
|
||||
Some(anim) if !anim.done(now_nsec) => anim.rect_at(now_nsec),
|
||||
_ => layout,
|
||||
}
|
||||
}
|
||||
|
||||
fn damage_active(&self, state: &State, now_nsec: u64) -> bool {
|
||||
let mut damages = vec![];
|
||||
let mut any_active = false;
|
||||
{
|
||||
let mut windows = self.windows.borrow_mut();
|
||||
windows.retain(|_, anim| {
|
||||
let current = anim.rect_at(now_nsec);
|
||||
let damage = anim.last_damage.union(current).union(anim.to);
|
||||
damages.push(expand_damage_rect(
|
||||
damage,
|
||||
state.theme.sizes.border_width.get().max(0),
|
||||
));
|
||||
anim.last_damage = current;
|
||||
let active = !anim.done(now_nsec);
|
||||
any_active |= active;
|
||||
active
|
||||
});
|
||||
}
|
||||
for damage in damages {
|
||||
state.damage(damage);
|
||||
}
|
||||
any_active
|
||||
}
|
||||
|
||||
pub(crate) fn tick_is_active(&self) -> bool {
|
||||
self.tick.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn set_tick(&self, tick: Rc<AnimationTick>) {
|
||||
self.tick.set(Some(tick));
|
||||
}
|
||||
|
||||
pub(crate) fn clear_tick(&self) {
|
||||
self.tick.take();
|
||||
}
|
||||
}
|
||||
|
||||
struct WindowAnimation {
|
||||
from: Rect,
|
||||
to: Rect,
|
||||
start_nsec: u64,
|
||||
duration_nsec: u64,
|
||||
curve: AnimationCurve,
|
||||
last_damage: Rect,
|
||||
}
|
||||
|
||||
impl WindowAnimation {
|
||||
fn done(&self, now_nsec: u64) -> bool {
|
||||
now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec
|
||||
}
|
||||
|
||||
fn rect_at(&self, now_nsec: u64) -> Rect {
|
||||
if self.duration_nsec == 0 {
|
||||
return self.to;
|
||||
}
|
||||
let elapsed = now_nsec.saturating_sub(self.start_nsec);
|
||||
let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
|
||||
let t = self.curve.sample(t);
|
||||
lerp_rect(self.from, self.to, t)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnimationTick {
|
||||
state: Weak<State>,
|
||||
slf: Weak<dyn LatchListener>,
|
||||
latch_listeners: RefCell<Vec<EventListener<dyn LatchListener>>>,
|
||||
}
|
||||
|
||||
impl AnimationTick {
|
||||
pub fn new(state: &Rc<State>, slf: &Weak<Self>) -> Self {
|
||||
let slf: Weak<dyn LatchListener> = slf.clone();
|
||||
Self {
|
||||
state: Rc::downgrade(state),
|
||||
slf,
|
||||
latch_listeners: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach(&self, output: &OutputNode) {
|
||||
let listener = EventListener::new(self.slf.clone());
|
||||
listener.attach(&output.latch_event);
|
||||
self.latch_listeners.borrow_mut().push(listener);
|
||||
}
|
||||
|
||||
pub fn detach(&self) {
|
||||
for listener in self.latch_listeners.borrow_mut().drain(..) {
|
||||
listener.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LatchListener for AnimationTick {
|
||||
fn after_latch(self: Rc<Self>, _on: &OutputNode, _tearing: bool) {
|
||||
let Some(state) = self.state.upgrade() else {
|
||||
self.detach();
|
||||
return;
|
||||
};
|
||||
let active = state.animations.damage_active(&state, state.now_nsec());
|
||||
if !active {
|
||||
self.detach();
|
||||
state.animations.clear_tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp_rect(from: Rect, to: Rect, t: f64) -> Rect {
|
||||
fn lerp(from: i32, to: i32, t: f64) -> i32 {
|
||||
(from as f64 + (to as f64 - from as f64) * t).round() as i32
|
||||
}
|
||||
Rect::new_saturating(
|
||||
lerp(from.x1(), to.x1(), t),
|
||||
lerp(from.y1(), to.y1(), t),
|
||||
lerp(from.x2(), to.x2(), t),
|
||||
lerp(from.y2(), to.y2(), t),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn expand_damage_rect(rect: Rect, width: i32) -> Rect {
|
||||
Rect::new_saturating(
|
||||
rect.x1().saturating_sub(width),
|
||||
rect.y1().saturating_sub(width),
|
||||
rect.x2().saturating_add(width),
|
||||
rect.y2().saturating_add(width),
|
||||
)
|
||||
}
|
||||
|
||||
fn cubic_bezier(x1: f64, y1: f64, x2: f64, y2: f64, x: f64) -> f64 {
|
||||
fn bezier(a: f64, b: f64, t: f64) -> f64 {
|
||||
let inv = 1.0 - t;
|
||||
3.0 * inv * inv * t * a + 3.0 * inv * t * t * b + t * t * t
|
||||
}
|
||||
let mut lo = 0.0;
|
||||
let mut hi = 1.0;
|
||||
let mut t = x;
|
||||
for _ in 0..12 {
|
||||
let bx = bezier(x1, x2, t);
|
||||
if (bx - x).abs() < 0.000_001 {
|
||||
break;
|
||||
}
|
||||
if bx < x {
|
||||
lo = t;
|
||||
} else {
|
||||
hi = t;
|
||||
}
|
||||
t = (lo + hi) * 0.5;
|
||||
}
|
||||
bezier(y1, y2, t)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn linear_rect_interpolation_is_symmetric() {
|
||||
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
||||
let b = Rect::new_sized_saturating(100, 40, 200, 80);
|
||||
assert_eq!(lerp_rect(a, b, 0.25), lerp_rect(b, a, 0.75));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_target_does_not_restart() {
|
||||
let state = AnimationState::default();
|
||||
let id = NodeId(1);
|
||||
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
||||
let b = Rect::new_sized_saturating(100, 0, 100, 100);
|
||||
assert!(state.set_target(id, a, b, 0, 160, AnimationCurve::Linear));
|
||||
assert!(!state.set_target(id, a, b, 80_000_000, 160, AnimationCurve::Linear));
|
||||
assert_eq!(
|
||||
state.visual_rect(id, b, 80_000_000),
|
||||
Rect::new_sized_saturating(50, 0, 100, 100)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_target_restarts_from_current_visual_rect() {
|
||||
let state = AnimationState::default();
|
||||
let id = NodeId(1);
|
||||
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
||||
let b = Rect::new_sized_saturating(100, 0, 100, 100);
|
||||
let c = Rect::new_sized_saturating(200, 0, 100, 100);
|
||||
assert!(state.set_target(id, a, b, 0, 160, AnimationCurve::Linear));
|
||||
assert!(state.set_target(id, a, c, 80_000_000, 160, AnimationCurve::Linear));
|
||||
assert_eq!(
|
||||
state.visual_rect(id, c, 80_000_000),
|
||||
Rect::new_sized_saturating(50, 0, 100, 100)
|
||||
);
|
||||
assert_eq!(
|
||||
state.visual_rect(id, c, 160_000_000),
|
||||
Rect::new_sized_saturating(125, 0, 100, 100)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -360,6 +360,10 @@ fn start_compositor2(
|
|||
cpu_worker,
|
||||
ui_drag_enabled: Cell::new(true),
|
||||
ui_drag_threshold_squared: Cell::new(10),
|
||||
animations: Default::default(),
|
||||
layout_animations_requested: Default::default(),
|
||||
layout_animations_active: Default::default(),
|
||||
suppress_animations_for_next_layout: Default::default(),
|
||||
toplevels: Default::default(),
|
||||
const_40hz_latch: Default::default(),
|
||||
tray_item_ids: Default::default(),
|
||||
|
|
|
|||
|
|
@ -658,17 +658,21 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.move_focused(direction.into());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let window = self.get_window(window)?;
|
||||
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> {
|
||||
|
|
@ -986,6 +990,19 @@ impl ConfigProxyHandler {
|
|||
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_direct_scanout_enabled(
|
||||
&self,
|
||||
device: Option<DrmDevice>,
|
||||
|
|
@ -1724,9 +1741,11 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.set_mono(mono);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> {
|
||||
|
|
@ -1740,11 +1759,13 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let window = self.get_window(window)?;
|
||||
if let Some(c) = toplevel_parent_container(&*window) {
|
||||
c.set_mono(mono.then_some(window.as_ref()));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> {
|
||||
|
|
@ -1759,15 +1780,19 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.set_split(axis.into());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.toggle_tab();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_seat_make_group(
|
||||
|
|
@ -1776,27 +1801,35 @@ impl ConfigProxyHandler {
|
|||
axis: Axis,
|
||||
ephemeral: bool,
|
||||
) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.make_group(axis.into(), ephemeral);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.change_group_opposite();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.equalize(recursive);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.move_tab(right);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> {
|
||||
|
|
@ -1811,11 +1844,13 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
let window = self.get_window(window)?;
|
||||
if let Some(c) = toplevel_parent_container(&*window) {
|
||||
c.set_split(axis.into());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_add_shortcut(
|
||||
|
|
@ -2721,8 +2756,10 @@ impl ConfigProxyHandler {
|
|||
dx2: i32,
|
||||
dy2: i32,
|
||||
) -> Result<(), CphError> {
|
||||
self.state.with_layout_animations(|| {
|
||||
self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_window_exists(&self, window: Window) {
|
||||
|
|
@ -3193,6 +3230,13 @@ impl ConfigProxyHandler {
|
|||
ClientMessage::SetUiDragThreshold { 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::SetXScalingMode { mode } => self
|
||||
.handle_set_x_scaling_mode(mode)
|
||||
.wrn("set_x_scaling_mode")?,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ mod leaks;
|
|||
mod tracy;
|
||||
mod acceptor;
|
||||
mod allocator;
|
||||
mod animation;
|
||||
mod async_engine;
|
||||
mod backend;
|
||||
mod backends;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use {
|
|||
theme::{Color, CornerRadius},
|
||||
tree::{
|
||||
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
|
||||
ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
||||
ToplevelNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
||||
},
|
||||
},
|
||||
std::{ops::Deref, rc::Rc, slice},
|
||||
|
|
@ -453,6 +453,20 @@ impl Renderer<'_> {
|
|||
.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())
|
||||
}
|
||||
|
||||
pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) {
|
||||
self.render_container_decorations(container, x, y);
|
||||
|
||||
|
|
@ -465,6 +479,7 @@ impl Renderer<'_> {
|
|||
}
|
||||
}
|
||||
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 {
|
||||
let bw = self.state.theme.sizes.border_width.get();
|
||||
let border_color = self.state.theme.colors.border.get();
|
||||
|
|
@ -476,10 +491,10 @@ impl Renderer<'_> {
|
|||
};
|
||||
if !child.node.node_is_container() {
|
||||
let frame = Rect::new_sized_saturating(
|
||||
mb.x1() - bw,
|
||||
mb.y1() - bw,
|
||||
mb.width() + 2 * bw,
|
||||
mb.height() + 2 * bw,
|
||||
visual_mb.x1() - bw,
|
||||
visual_mb.y1() - bw,
|
||||
visual_mb.width() + 2 * bw,
|
||||
visual_mb.height() + 2 * bw,
|
||||
);
|
||||
self.render_rounded_frame(
|
||||
frame,
|
||||
|
|
@ -491,11 +506,15 @@ 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.mono_content.get();
|
||||
self.stretch = if content.width() != mb.width() || content.height() != mb.height() {
|
||||
Some(self.base.scale_point(mb.width(), mb.height()))
|
||||
let content = container
|
||||
.mono_content
|
||||
.get()
|
||||
.at_point(visual_mb.x1(), visual_mb.y1());
|
||||
self.stretch =
|
||||
if content.width() != visual_mb.width() || content.height() != visual_mb.height() {
|
||||
Some(self.base.scale_point(visual_mb.width(), visual_mb.height()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -524,10 +543,13 @@ impl Renderer<'_> {
|
|||
};
|
||||
let cr = self.state.theme.corner_radius.get();
|
||||
for child in container.children.iter() {
|
||||
let body = child.body.get();
|
||||
if body.x1() >= container.width.get() || body.y1() >= container.height.get() {
|
||||
let layout_body = child.body.get();
|
||||
if layout_body.x1() >= container.width.get()
|
||||
|| layout_body.y1() >= container.height.get()
|
||||
{
|
||||
break;
|
||||
}
|
||||
let body = self.presentation_child_body(container, &child.node, layout_body);
|
||||
if gap != 0 {
|
||||
let c = if child.border_color_is_focused.get() {
|
||||
&focused_border_color
|
||||
|
|
@ -544,7 +566,7 @@ impl Renderer<'_> {
|
|||
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 =
|
||||
if content.width() != body.width() || content.height() != body.height() {
|
||||
Some(self.base.scale_point(body.width(), body.height()))
|
||||
|
|
|
|||
100
src/state.rs
100
src/state.rs
|
|
@ -2,6 +2,7 @@ use {
|
|||
crate::{
|
||||
acceptor::Acceptor,
|
||||
allocator::BufferObject,
|
||||
animation::{AnimationCurve, AnimationState, AnimationTick, expand_damage_rect},
|
||||
async_engine::{AsyncEngine, SpawnedFuture},
|
||||
backend::{
|
||||
Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice,
|
||||
|
|
@ -102,11 +103,10 @@ use {
|
|||
time::Time,
|
||||
tree::{
|
||||
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
|
||||
FoundNode, LatchListener, Node, NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode,
|
||||
TearingMode, TileState, ToplevelData, ToplevelIdentifier, ToplevelNode,
|
||||
ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder, WorkspaceNode,
|
||||
WorkspaceNodeId,
|
||||
WsMoveConfig, generic_node_visitor, move_ws_to_output,
|
||||
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
|
||||
PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier,
|
||||
ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder,
|
||||
WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output,
|
||||
},
|
||||
udmabuf::UdmabufHolder,
|
||||
utils::{
|
||||
|
|
@ -264,6 +264,10 @@ pub struct State {
|
|||
pub cpu_worker: Rc<CpuWorker>,
|
||||
pub ui_drag_enabled: Cell<bool>,
|
||||
pub ui_drag_threshold_squared: Cell<i32>,
|
||||
pub animations: AnimationState,
|
||||
pub layout_animations_requested: Cell<bool>,
|
||||
pub layout_animations_active: Cell<bool>,
|
||||
pub suppress_animations_for_next_layout: Cell<bool>,
|
||||
pub toplevels: CopyHashMap<ToplevelIdentifier, Weak<dyn ToplevelNode>>,
|
||||
pub const_40hz_latch: EventSource<dyn LatchListener>,
|
||||
pub tray_item_ids: TrayItemIds,
|
||||
|
|
@ -1115,6 +1119,10 @@ impl State {
|
|||
self.pending_screencast_reallocs_or_reconfigures.clear();
|
||||
self.pending_placeholder_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.suppress_animations_for_next_layout.set(false);
|
||||
self.render_ctx_watchers.clear();
|
||||
self.workspace_watchers.clear();
|
||||
self.toplevel_lists.clear();
|
||||
|
|
@ -1461,6 +1469,88 @@ impl State {
|
|||
self.eng.now().msec()
|
||||
}
|
||||
|
||||
pub fn queue_tiled_animation(self: &Rc<Self>, node_id: NodeId, old: Rect, new: Rect) {
|
||||
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 now = self.now_nsec();
|
||||
let started = self.animations.set_target(
|
||||
node_id,
|
||||
old,
|
||||
new,
|
||||
now,
|
||||
self.animations.duration_ms.get(),
|
||||
self.animations.curve.get(),
|
||||
);
|
||||
if started {
|
||||
self.damage(expand_damage_rect(
|
||||
old.union(new),
|
||||
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 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
|
||||
}
|
||||
|
||||
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) {
|
||||
self.root.update_extents();
|
||||
for seat in self.globals.seats.lock().values() {
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ pub struct ContainerNode {
|
|||
pub content_height: Cell<i32>,
|
||||
pub sum_factors: Cell<f64>,
|
||||
pub layout_scheduled: Cell<bool>,
|
||||
animate_next_layout: Cell<bool>,
|
||||
compute_render_positions_scheduled: Cell<bool>,
|
||||
num_children: NumCell<usize>,
|
||||
pub children: LinkedList<ContainerChild>,
|
||||
|
|
@ -238,6 +239,7 @@ impl ContainerNode {
|
|||
content_height: Cell::new(0),
|
||||
sum_factors: Cell::new(1.0),
|
||||
layout_scheduled: Cell::new(false),
|
||||
animate_next_layout: Cell::new(false),
|
||||
compute_render_positions_scheduled: Cell::new(false),
|
||||
num_children: NumCell::new(1),
|
||||
children,
|
||||
|
|
@ -436,6 +438,10 @@ impl ContainerNode {
|
|||
}
|
||||
|
||||
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) {
|
||||
self.state.pending_container_layout.push(self.clone());
|
||||
}
|
||||
|
|
@ -656,6 +662,7 @@ impl ContainerNode {
|
|||
op.child.factor.set(child_factor);
|
||||
self.sum_factors.set(sum_factors);
|
||||
// log::info!("pointer_move");
|
||||
self.state.suppress_animations_for_next_layout.set(true);
|
||||
self.schedule_layout_immediate();
|
||||
}
|
||||
}
|
||||
|
|
@ -1761,8 +1768,13 @@ pub async fn container_layout(state: Rc<State>) {
|
|||
loop {
|
||||
let container = state.pending_container_layout.pop().await;
|
||||
if container.layout_scheduled.get() {
|
||||
let animate = container.animate_next_layout.replace(false)
|
||||
&& !state.suppress_animations_for_next_layout.get();
|
||||
let prev_active = state.layout_animations_active.replace(animate);
|
||||
container.perform_layout();
|
||||
state.layout_animations_active.set(prev_active);
|
||||
}
|
||||
state.suppress_animations_for_next_layout.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -184,6 +184,23 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
|
|||
fn tl_change_extents(self: Rc<Self>, rect: &Rect) {
|
||||
let data = self.tl_data();
|
||||
let prev = data.desired_extents.replace(*rect);
|
||||
let parent_is_mono = data
|
||||
.parent
|
||||
.get()
|
||||
.and_then(|parent| parent.node_into_container())
|
||||
.is_some_and(|container| container.mono_child.is_some());
|
||||
if prev != *rect
|
||||
&& !prev.is_empty()
|
||||
&& !rect.is_empty()
|
||||
&& data.visible.get()
|
||||
&& !data.parent_is_float.get()
|
||||
&& !self.node_is_container()
|
||||
&& !parent_is_mono
|
||||
{
|
||||
data.state
|
||||
.clone()
|
||||
.queue_tiled_animation(data.node_id, prev, *rect);
|
||||
}
|
||||
if prev.size() != rect.size() {
|
||||
for sc in data.jay_screencasts.lock().values() {
|
||||
sc.schedule_realloc_or_reconfigure();
|
||||
|
|
|
|||
|
|
@ -266,6 +266,13 @@ pub struct UiDrag {
|
|||
pub threshold: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Animations {
|
||||
pub enabled: Option<bool>,
|
||||
pub duration_ms: Option<u32>,
|
||||
pub curve: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OutputMatch {
|
||||
Any(Vec<OutputMatch>),
|
||||
|
|
@ -567,6 +574,7 @@ pub struct Config {
|
|||
pub tearing: Option<Tearing>,
|
||||
pub libei: Libei,
|
||||
pub ui_drag: UiDrag,
|
||||
pub animations: Animations,
|
||||
pub xwayland: Option<Xwayland>,
|
||||
pub color_management: Option<ColorManagement>,
|
||||
pub float: Option<Float>,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use {
|
|||
|
||||
pub mod action;
|
||||
mod actions;
|
||||
mod animations;
|
||||
mod capabilities;
|
||||
mod clean_logs_older_than;
|
||||
mod client_match;
|
||||
|
|
|
|||
50
toml-config/src/config/parsers/animations.rs
Normal file
50
toml-config/src/config/parsers/animations.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use {
|
||||
crate::{
|
||||
config::{
|
||||
Animations,
|
||||
context::Context,
|
||||
extractor::{Extractor, ExtractorError, bol, n32, opt, recover, str},
|
||||
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||
},
|
||||
toml::{
|
||||
toml_span::{DespanExt, Span, Spanned},
|
||||
toml_value::Value,
|
||||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AnimationsParserError {
|
||||
#[error(transparent)]
|
||||
Expected(#[from] UnexpectedDataType),
|
||||
#[error(transparent)]
|
||||
Extract(#[from] ExtractorError),
|
||||
}
|
||||
|
||||
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, curve) = ext.extract((
|
||||
recover(opt(bol("enabled"))),
|
||||
recover(opt(n32("duration-ms"))),
|
||||
recover(opt(str("curve"))),
|
||||
))?;
|
||||
Ok(Animations {
|
||||
enabled: enabled.despan(),
|
||||
duration_ms: duration_ms.despan(),
|
||||
curve: curve.despan().map(|s| s.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
use {
|
||||
crate::{
|
||||
config::{
|
||||
Action, Config, Libei, Theme, UiDrag,
|
||||
Action, Animations, Config, Libei, Theme, UiDrag,
|
||||
context::Context,
|
||||
extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val},
|
||||
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||
parsers::{
|
||||
action::ActionParser,
|
||||
actions::ActionsParser,
|
||||
animations::AnimationsParser,
|
||||
clean_logs_older_than::CleanLogsOlderThanParser,
|
||||
client_rule::ClientRulesParser,
|
||||
color_management::ColorManagementParser,
|
||||
|
|
@ -153,6 +154,7 @@ impl Parser for ConfigParser<'_> {
|
|||
fallback_output_mode_val,
|
||||
clean_logs_older_than_val,
|
||||
mouse_follows_focus,
|
||||
animations_val,
|
||||
),
|
||||
) = ext.extract((
|
||||
(
|
||||
|
|
@ -213,6 +215,7 @@ impl Parser for ConfigParser<'_> {
|
|||
opt(val("fallback-output-mode")),
|
||||
opt(val("clean-logs-older-than")),
|
||||
recover(opt(bol("unstable-mouse-follows-focus"))),
|
||||
opt(val("animations")),
|
||||
),
|
||||
))?;
|
||||
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;
|
||||
if let Some(value) = xwayland_val {
|
||||
match value.parse(&mut XwaylandParser(self.0)) {
|
||||
|
|
@ -587,6 +599,7 @@ impl Parser for ConfigParser<'_> {
|
|||
tearing,
|
||||
libei,
|
||||
ui_drag,
|
||||
animations,
|
||||
xwayland,
|
||||
color_management,
|
||||
float,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use {
|
|||
ahash::{AHashMap, AHashSet},
|
||||
error_reporter::Report,
|
||||
jay_config::{
|
||||
Axis,
|
||||
AnimationCurve, Axis,
|
||||
client::Client,
|
||||
config, config_dir,
|
||||
exec::{Command, set_env, unset_env},
|
||||
|
|
@ -37,7 +37,8 @@ use {
|
|||
is_reload,
|
||||
keyboard::Keymap,
|
||||
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_curve,
|
||||
set_animation_duration_ms, set_animations_enabled, set_autotile,
|
||||
set_color_management_enabled, set_corner_radius, set_default_workspace_capture,
|
||||
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle,
|
||||
set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar,
|
||||
|
|
@ -1649,6 +1650,23 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
|
|||
if let Some(threshold) = config.ui_drag.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));
|
||||
let curve_name = config.animations.curve.as_deref().unwrap_or("ease-out");
|
||||
let curve = match curve_name {
|
||||
"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);
|
||||
}
|
||||
if let Some(xwayland) = config.xwayland {
|
||||
if let Some(enabled) = xwayland.enabled {
|
||||
set_x_wayland_enabled(enabled);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue