1
0
Fork 0
forked from wry/wry

Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
3540cdc4be Add linear tiled window animations 2026-05-21 15:20:46 +10:00
17 changed files with 913 additions and 64 deletions

View 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.

View file

@ -1023,6 +1023,18 @@ 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_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 });
} }

View file

@ -545,6 +545,15 @@ pub enum ClientMessage<'a> {
SetUiDragThreshold { SetUiDragThreshold {
threshold: i32, threshold: i32,
}, },
SetAnimationsEnabled {
enabled: bool,
},
SetAnimationDurationMs {
duration_ms: u32,
},
SetAnimationCurve {
curve: u32,
},
SetXScalingMode { SetXScalingMode {
mode: XScalingMode, mode: XScalingMode,
}, },

View file

@ -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. /// Exits the compositor.
pub fn quit() { pub fn quit() {
get!().quit() get!().quit()
@ -287,6 +299,27 @@ 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);
}
/// Enables or disables the color-management protocol. /// Enables or disables the color-management protocol.
/// ///
/// The default is `false`. /// The default is `false`.

315
src/animation.rs Normal file
View 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)
);
}
}

View file

@ -360,6 +360,10 @@ 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(),
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(),

View file

@ -658,17 +658,21 @@ 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(c) = toplevel_parent_container(&*window) {
} c.move_child(window, direction.into());
Ok(()) }
Ok(())
})
} }
fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> { 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)); 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( fn handle_set_direct_scanout_enabled(
&self, &self,
device: Option<DrmDevice>, device: Option<DrmDevice>,
@ -1724,9 +1741,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 +1759,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 +1780,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 +1801,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 +1844,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(
@ -2721,8 +2756,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 +3230,13 @@ 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::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")?,

View file

@ -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;

View file

@ -15,7 +15,7 @@ use {
theme::{Color, CornerRadius}, theme::{Color, CornerRadius},
tree::{ tree::{
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData, ContainerNode, DisplayNode, FloatNode, 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},
@ -453,6 +453,20 @@ 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())
}
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 +479,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 +491,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 +506,18 @@ impl Renderer<'_> {
); );
} }
} }
let body = mb.move_(x, y); let body = visual_mb.move_(x, y);
let body = self.base.scale_rect(body); let body = self.base.scale_rect(body);
let content = container.mono_content.get(); let content = container
self.stretch = if content.width() != mb.width() || content.height() != mb.height() { .mono_content
Some(self.base.scale_point(mb.width(), mb.height())) .get()
} else { .at_point(visual_mb.x1(), visual_mb.y1());
None 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
};
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() {
@ -524,10 +543,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 +566,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()))

View file

@ -2,6 +2,7 @@ use {
crate::{ crate::{
acceptor::Acceptor, acceptor::Acceptor,
allocator::BufferObject, allocator::BufferObject,
animation::{AnimationCurve, AnimationState, AnimationTick, expand_damage_rect},
async_engine::{AsyncEngine, SpawnedFuture}, async_engine::{AsyncEngine, SpawnedFuture},
backend::{ backend::{
Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice, Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice,
@ -102,11 +103,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::{
@ -264,6 +264,10 @@ 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 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,
@ -1115,6 +1119,10 @@ 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.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 +1469,88 @@ 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) {
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) { 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() {

View file

@ -131,6 +131,7 @@ 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>,
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 +239,7 @@ 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),
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 +438,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());
} }
@ -656,6 +662,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();
} }
} }
@ -1761,8 +1768,13 @@ pub async fn container_layout(state: Rc<State>) {
loop { loop {
let container = state.pending_container_layout.pop().await; let container = state.pending_container_layout.pop().await;
if container.layout_scheduled.get() { 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(); container.perform_layout();
state.layout_animations_active.set(prev_active);
} }
state.suppress_animations_for_next_layout.set(false);
} }
} }

View file

@ -184,6 +184,23 @@ 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 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() { 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();

View file

@ -266,6 +266,13 @@ 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 curve: Option<String>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum OutputMatch { pub enum OutputMatch {
Any(Vec<OutputMatch>), Any(Vec<OutputMatch>),
@ -567,6 +574,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>,

View file

@ -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;

View 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()),
})
}
}

View file

@ -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,

View file

@ -23,7 +23,7 @@ use {
ahash::{AHashMap, AHashSet}, ahash::{AHashMap, AHashSet},
error_reporter::Report, error_reporter::Report,
jay_config::{ jay_config::{
Axis, AnimationCurve, Axis,
client::Client, client::Client,
config, config_dir, config, config_dir,
exec::{Command, set_env, unset_env}, exec::{Command, set_env, unset_env},
@ -37,7 +37,8 @@ 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_curve,
set_animation_duration_ms, set_animations_enabled, set_autotile,
set_color_management_enabled, set_corner_radius, set_default_workspace_capture, 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,
@ -1649,6 +1650,23 @@ 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));
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(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);