1
0
Fork 0
forked from wry/wry

Add animation style toggle

This commit is contained in:
atagen 2026-05-27 22:52:06 +10:00
parent 02222d5189
commit e7f9a5cb09
15 changed files with 238 additions and 20 deletions

View file

@ -6,11 +6,11 @@ be handled deliberately.
## Accepted Decisions ## Accepted Decisions
- The first landed slice is linear interpolation only, disabled by default. - The first landed slice is plain interpolation only, disabled by default.
- Animation is presentation-only. Logical layout, input hit testing, focus, and - Animation is presentation-only. Logical layout, input hit testing, focus, and
Wayland configure state use final geometry immediately. Wayland configure state use final geometry immediately.
- Pointer drag and resize initiated by the mouse or tablet do not animate. - Pointer drag and resize initiated by the mouse or tablet do not animate.
- Linear animations restart only for windows whose destination changes. Other - Plain animations restart only for windows whose destination changes. Other
in-flight windows keep their existing timelines. in-flight windows keep their existing timelines.
- Spawn-in uses scale and position for newly mapped tiled and floating app - Spawn-in uses scale and position for newly mapped tiled and floating app
windows. Layer-shell, overlay, override-redirect, and fullscreen surfaces do windows. Layer-shell, overlay, override-redirect, and fullscreen surfaces do
@ -19,7 +19,7 @@ be handled deliberately.
destroy. destroy.
- Command-driven tile-to-float and float-to-tile transitions may animate. - Command-driven tile-to-float and float-to-tile transitions may animate.
Protocol drag/drop paths do not. Protocol drag/drop paths do not.
- The no-overlap multiphase system is a separate phase after the linear path is - The no-overlap multiphase system is a separate phase after the plain path is
working and testable. working and testable.
- Content freezing will use retained per-surface texture references, not a full - Content freezing will use retained per-surface texture references, not a full
offscreen snapshot as the default design. offscreen snapshot as the default design.
@ -34,7 +34,7 @@ be handled deliberately.
below roughly one quarter of the relevant full size. The implementation may below roughly one quarter of the relevant full size. The implementation may
enforce a conservative sanity minimum, and pathological cases may fall back. enforce a conservative sanity minimum, and pathological cases may fall back.
- If the no-overlap planner cannot produce a legal sequence, only the affected - If the no-overlap planner cannot produce a legal sequence, only the affected
group should fall back to linear animation. This is expected to be rare for group should fall back to plain animation. This is expected to be rare for
valid tiling layouts. valid tiling layouts.
- When entering mono mode, the active child should animate to the mono geometry. - When entering mono mode, the active child should animate to the mono geometry.
Inactive siblings may snap invisible. Floats may overlap normally and do not Inactive siblings may snap invisible. Floats may overlap normally and do not
@ -285,6 +285,7 @@ Phase 1 should expose a disabled-by-default setting for:
- enabled/disabled - enabled/disabled
- duration - duration
- style: `plain` or `multiphase`
- curve preset or cubic bezier - curve preset or cubic bezier
Initial TOML shape: Initial TOML shape:
@ -293,6 +294,7 @@ Initial TOML shape:
[animations] [animations]
enabled = false enabled = false
duration-ms = 160 duration-ms = 160
style = "multiphase"
curve = "ease-out" curve = "ease-out"
# or: # or:
curve = [0.25, 0.1, 0.25, 1.0] curve = [0.25, 0.1, 0.25, 1.0]

View file

@ -24,9 +24,24 @@ Relevant internal config hooks:
- `SetAnimationsEnabled` - `SetAnimationsEnabled`
- `SetAnimationDurationMs` - `SetAnimationDurationMs`
- `SetAnimationStyle`
- `SetAnimationCurve` - `SetAnimationCurve`
- `SetAnimationCubicBezier` - `SetAnimationCubicBezier`
TOML example:
```toml
[animations]
enabled = true
duration-ms = 600
style = "multiphase"
curve = "ease-out"
```
Set `style = "plain"` to force ordinary one-step movement interpolation while
keeping the configured curve. `curve = "linear"` only changes easing; it does
not select the plain animation style.
Current curve IDs in code: Current curve IDs in code:
- `0`: linear - `0`: linear
@ -37,19 +52,20 @@ Current curve IDs in code:
## Enabling Multiphase Tests ## Enabling Multiphase Tests
There is currently no separate user-facing multiphase toggle. To exercise the To exercise the multiphase planner:
multiphase planner:
1. Enable animations with `SetAnimationsEnabled`. 1. Enable animations with `SetAnimationsEnabled`.
2. Set a slow duration with `SetAnimationDurationMs`, around `400-700ms`. 2. Set a slow duration with `SetAnimationDurationMs`, around `400-700ms`.
3. Use tiled layout commands that are wired through `State::with_layout_animations`. 3. Select `style = "multiphase"` in TOML, or call `SetAnimationStyle` with
4. Use layouts where at least two tiled windows change geometry in the same `AnimationStyle::MULTIPHASE`.
4. Use tiled layout commands that are wired through `State::with_layout_animations`.
5. Use layouts where at least two tiled windows change geometry in the same
container layout batch. container layout batch.
The compositor then attempts multiphase planning automatically when the batched The compositor then attempts multiphase planning automatically when the batched
layout pass completes. If the planner proves a legal no-overlap sequence, that layout pass completes. If the planner proves a legal no-overlap sequence, that
group uses phased animation. If it cannot prove one, only that motion group falls group uses phased animation. If it cannot prove one, only that motion group falls
back to ordinary linear animation. back to ordinary plain animation.
Good command families for multiphase testing: Good command families for multiphase testing:
@ -64,7 +80,7 @@ Good command families for multiphase testing:
These paths should not be used as evidence of multiphase behavior: These paths should not be used as evidence of multiphase behavior:
- tile-to-float and float-to-tile, which deliberately use linear animation - tile-to-float and float-to-tile, which deliberately use plain animation
- command-driven floating move/resize, which may animate but can overlap - command-driven floating move/resize, which may animate but can overlap
- pointer or tablet drag/resize, which should not animate - pointer or tablet drag/resize, which should not animate
- spawn-in and spawn-out, which are single-phase and use the configured curve - spawn-in and spawn-out, which are single-phase and use the configured curve
@ -73,7 +89,7 @@ These paths should not be used as evidence of multiphase behavior:
Useful debug signal: Useful debug signal:
- `falling back to linear layout animation for group ...` means the group entered - `falling back to plain layout animation for group ...` means the group entered
the multiphase gate but the planner rejected it. That is acceptable for the multiphase gate but the planner rejected it. That is acceptable for
unsupported patterns, but unexpected for the supported swap/extraction cases unsupported patterns, but unexpected for the supported swap/extraction cases
below. below.

View file

@ -1035,6 +1035,10 @@ impl ConfigClient {
self.send(&ClientMessage::SetAnimationCurve { curve }); self.send(&ClientMessage::SetAnimationCurve { curve });
} }
pub fn set_animation_style(&self, style: u32) {
self.send(&ClientMessage::SetAnimationStyle { style });
}
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) { pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) {
self.send(&ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 }); self.send(&ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 });
} }

View file

@ -554,6 +554,9 @@ pub enum ClientMessage<'a> {
SetAnimationCurve { SetAnimationCurve {
curve: u32, curve: u32,
}, },
SetAnimationStyle {
style: u32,
},
SetAnimationCubicBezier { SetAnimationCubicBezier {
x1: f32, x1: f32,
y1: f32, y1: f32,

View file

@ -115,6 +115,15 @@ impl AnimationCurve {
pub const EASE_IN_OUT: Self = Self(4); pub const EASE_IN_OUT: Self = Self(4);
} }
/// The presentation style used for tiled window movement animations.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct AnimationStyle(pub u32);
impl AnimationStyle {
pub const PLAIN: Self = Self(0);
pub const MULTIPHASE: Self = Self(1);
}
/// Exits the compositor. /// Exits the compositor.
pub fn quit() { pub fn quit() {
get!().quit() get!().quit()
@ -320,6 +329,13 @@ pub fn set_animation_curve(curve: AnimationCurve) {
get!().set_animation_curve(curve.0); get!().set_animation_curve(curve.0);
} }
/// Sets the presentation style used for tiled window movement animations.
///
/// The default is [`AnimationStyle::MULTIPHASE`].
pub fn set_animation_style(style: AnimationStyle) {
get!().set_animation_style(style.0);
}
/// Sets a custom cubic-bezier curve used by tiled window animations. /// Sets a custom cubic-bezier curve used by tiled window animations.
/// ///
/// `x1` and `x2` must be between `0.0` and `1.0`. The curve starts at `(0, 0)` /// `x1` and `x2` must be between `0.0` and `1.0`. The curve starts at `(0, 0)`

View file

@ -30,6 +30,22 @@ pub enum AnimationCurve {
Piecewise(PiecewiseCurve), Piecewise(PiecewiseCurve),
} }
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum AnimationStyle {
Plain,
Multiphase,
}
impl AnimationStyle {
pub fn from_config(value: u32) -> Option<Self> {
match value {
0 => Some(Self::Plain),
1 => Some(Self::Multiphase),
_ => None,
}
}
}
impl AnimationCurve { impl AnimationCurve {
pub fn from_config(value: u32) -> Self { pub fn from_config(value: u32) -> Self {
match value { match value {
@ -129,6 +145,7 @@ pub struct AnimationState {
pub enabled: Cell<bool>, pub enabled: Cell<bool>,
pub duration_ms: Cell<u32>, pub duration_ms: Cell<u32>,
pub curve: Cell<AnimationCurve>, pub curve: Cell<AnimationCurve>,
pub style: Cell<AnimationStyle>,
windows: RefCell<AHashMap<NodeId, WindowAnimation>>, windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
phased: RefCell<AHashMap<NodeId, PhasedWindowAnimation>>, phased: RefCell<AHashMap<NodeId, PhasedWindowAnimation>>,
exits: RefCell<Vec<ExitAnimation>>, exits: RefCell<Vec<ExitAnimation>>,
@ -267,6 +284,7 @@ impl Default for AnimationState {
enabled: Cell::new(false), enabled: Cell::new(false),
duration_ms: Cell::new(DEFAULT_DURATION_MS), duration_ms: Cell::new(DEFAULT_DURATION_MS),
curve: Cell::new(AnimationCurve::from_config(3)), curve: Cell::new(AnimationCurve::from_config(3)),
style: Cell::new(AnimationStyle::Multiphase),
windows: Default::default(), windows: Default::default(),
phased: Default::default(), phased: Default::default(),
exits: Default::default(), exits: Default::default(),

View file

@ -364,6 +364,7 @@ fn start_compositor2(
layout_animations_requested: Default::default(), layout_animations_requested: Default::default(),
layout_animations_active: Default::default(), layout_animations_active: Default::default(),
layout_animation_curve_override: Default::default(), layout_animation_curve_override: Default::default(),
layout_animation_style_override: Default::default(),
layout_animation_batch: Default::default(), layout_animation_batch: Default::default(),
suppress_animations_for_next_layout: Default::default(), suppress_animations_for_next_layout: Default::default(),
toplevels: Default::default(), toplevels: Default::default(),

View file

@ -1005,6 +1005,12 @@ impl ConfigProxyHandler {
self.state.set_animation_curve(curve); self.state.set_animation_curve(curve);
} }
fn handle_set_animation_style(&self, style: u32) {
if !self.state.set_animation_style(style) {
log::warn!("Ignoring invalid animation style");
}
}
fn handle_set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) { fn handle_set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) {
if !self.state.set_animation_cubic_bezier(x1, y1, x2, y2) { if !self.state.set_animation_cubic_bezier(x1, y1, x2, y2) {
log::warn!("Ignoring invalid animation cubic-bezier curve"); log::warn!("Ignoring invalid animation cubic-bezier curve");
@ -3249,6 +3255,7 @@ impl ConfigProxyHandler {
self.handle_set_animation_duration_ms(duration_ms) self.handle_set_animation_duration_ms(duration_ms)
} }
ClientMessage::SetAnimationCurve { curve } => self.handle_set_animation_curve(curve), ClientMessage::SetAnimationCurve { curve } => self.handle_set_animation_curve(curve),
ClientMessage::SetAnimationStyle { style } => self.handle_set_animation_style(style),
ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 } => { ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 } => {
self.handle_set_animation_cubic_bezier(x1, y1, x2, y2) self.handle_set_animation_cubic_bezier(x1, y1, x2, y2)
} }

View file

@ -3,7 +3,8 @@ use {
acceptor::Acceptor, acceptor::Acceptor,
allocator::BufferObject, allocator::BufferObject,
animation::{ animation::{
AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel, AnimationCurve, AnimationState, AnimationStyle, AnimationTick, RetainedExitLayer,
RetainedToplevel,
expand_damage_rect, expand_damage_rect,
multiphase::{ multiphase::{
MultiphaseRequest, MultiphaseWindow, MultiphaseWindowHierarchy, MultiphaseRequest, MultiphaseWindow, MultiphaseWindowHierarchy,
@ -168,6 +169,7 @@ pub(crate) struct LayoutAnimationCandidate {
old: Rect, old: Rect,
new: Rect, new: Rect,
curve: AnimationCurve, curve: AnimationCurve,
style: AnimationStyle,
hierarchy: MultiphaseWindowHierarchy, hierarchy: MultiphaseWindowHierarchy,
} }
@ -182,6 +184,7 @@ fn coalesce_layout_animation_candidates(
{ {
existing.new = candidate.new; existing.new = candidate.new;
existing.curve = candidate.curve; existing.curve = candidate.curve;
existing.style = candidate.style;
existing.hierarchy = MultiphaseWindowHierarchy::new( existing.hierarchy = MultiphaseWindowHierarchy::new(
existing.hierarchy.source, existing.hierarchy.source,
candidate.hierarchy.target, candidate.hierarchy.target,
@ -193,6 +196,15 @@ fn coalesce_layout_animation_candidates(
merged merged
} }
fn layout_animation_group_uses_plain(
candidates: &[LayoutAnimationCandidate],
group: &[usize],
) -> bool {
group
.iter()
.any(|&idx| candidates[idx].style == AnimationStyle::Plain)
}
pub struct State { pub struct State {
pub pid: c::pid_t, pub pid: c::pid_t,
pub kb_ctx: KbvmContext, pub kb_ctx: KbvmContext,
@ -307,6 +319,7 @@ pub struct State {
pub layout_animations_requested: Cell<bool>, pub layout_animations_requested: Cell<bool>,
pub layout_animations_active: Cell<bool>, pub layout_animations_active: Cell<bool>,
pub layout_animation_curve_override: Cell<Option<AnimationCurve>>, pub layout_animation_curve_override: Cell<Option<AnimationCurve>>,
pub layout_animation_style_override: Cell<Option<AnimationStyle>>,
pub(crate) layout_animation_batch: RefCell<Option<Vec<LayoutAnimationCandidate>>>, pub(crate) layout_animation_batch: RefCell<Option<Vec<LayoutAnimationCandidate>>>,
pub suppress_animations_for_next_layout: Cell<bool>, pub suppress_animations_for_next_layout: Cell<bool>,
pub toplevels: CopyHashMap<ToplevelIdentifier, Weak<dyn ToplevelNode>>, pub toplevels: CopyHashMap<ToplevelIdentifier, Weak<dyn ToplevelNode>>,
@ -1172,6 +1185,7 @@ impl State {
self.layout_animations_requested.set(false); self.layout_animations_requested.set(false);
self.layout_animations_active.set(false); self.layout_animations_active.set(false);
self.layout_animation_curve_override.set(None); self.layout_animation_curve_override.set(None);
self.layout_animation_style_override.set(None);
self.suppress_animations_for_next_layout.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();
@ -1599,6 +1613,10 @@ impl State {
old, old,
new, new,
curve, curve,
style: self
.layout_animation_style_override
.get()
.unwrap_or_else(|| self.animations.style.get()),
hierarchy, hierarchy,
}; };
if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() { if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() {
@ -1659,6 +1677,12 @@ impl State {
}) })
.collect(); .collect();
for group in partition_motion_groups(&windows, self.layout_animation_clearance()) { for group in partition_motion_groups(&windows, self.layout_animation_clearance()) {
if layout_animation_group_uses_plain(&candidates, &group) {
for idx in group {
self.start_layout_animation_candidate(candidates[idx].clone(), now);
}
continue;
}
if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) { if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) {
continue; continue;
} }
@ -1701,7 +1725,7 @@ impl State {
Ok(plan) => plan, Ok(plan) => plan,
Err(diagnostic) => { Err(diagnostic) => {
log::debug!( log::debug!(
"falling back to linear layout animation for group {:?}: {:?}", "falling back to plain layout animation for group {:?}: {:?}",
group, group,
diagnostic diagnostic
); );
@ -1881,6 +1905,14 @@ impl State {
.set(AnimationCurve::from_config(curve)); .set(AnimationCurve::from_config(curve));
} }
pub fn set_animation_style(&self, style: u32) -> bool {
let Some(style) = AnimationStyle::from_config(style) else {
return false;
};
self.animations.style.set(style);
true
}
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool { pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else { let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else {
return false; return false;
@ -1904,10 +1936,14 @@ impl State {
let prev_curve = self let prev_curve = self
.layout_animation_curve_override .layout_animation_curve_override
.replace(Some(AnimationCurve::Linear)); .replace(Some(AnimationCurve::Linear));
let prev_style = self
.layout_animation_style_override
.replace(Some(AnimationStyle::Plain));
let res = f(); let res = f();
self.layout_animations_requested.set(prev_requested); self.layout_animations_requested.set(prev_requested);
self.layout_animations_active.set(prev_active); self.layout_animations_active.set(prev_active);
self.layout_animation_curve_override.set(prev_curve); self.layout_animation_curve_override.set(prev_curve);
self.layout_animation_style_override.set(prev_style);
res res
} }
@ -2475,6 +2511,28 @@ mod tests {
MultiphaseWindowHierarchy::new(source, target) MultiphaseWindowHierarchy::new(source, target)
} }
fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate {
LayoutAnimationCandidate {
node_id: NodeId(node_id),
old: rect(0, 0, 100, 100),
new: rect(100, 0, 200, 100),
curve: AnimationCurve::Linear,
style,
hierarchy: MultiphaseWindowHierarchy::default(),
}
}
#[test]
fn plain_style_candidate_forces_group_plain() {
let candidates = vec![
candidate(1, AnimationStyle::Multiphase),
candidate(2, AnimationStyle::Plain),
];
assert!(!layout_animation_group_uses_plain(&candidates, &[0]));
assert!(layout_animation_group_uses_plain(&candidates, &[0, 1]));
}
#[test] #[test]
fn layout_animation_candidates_coalesce_duplicate_nodes() { fn layout_animation_candidates_coalesce_duplicate_nodes() {
let source = MultiphaseHierarchyPosition { let source = MultiphaseHierarchyPosition {
@ -2514,6 +2572,7 @@ mod tests {
old: rect(0, 0, 100, 100), old: rect(0, 0, 100, 100),
new: rect(0, 0, 80, 100), new: rect(0, 0, 80, 100),
curve: AnimationCurve::Linear, curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy: hierarchy(source, intermediate), hierarchy: hierarchy(source, intermediate),
}, },
LayoutAnimationCandidate { LayoutAnimationCandidate {
@ -2521,6 +2580,7 @@ mod tests {
old: rect(100, 0, 200, 100), old: rect(100, 0, 200, 100),
new: rect(120, 0, 220, 100), new: rect(120, 0, 220, 100),
curve: AnimationCurve::Linear, curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy: hierarchy(second_source, second_target), hierarchy: hierarchy(second_source, second_target),
}, },
LayoutAnimationCandidate { LayoutAnimationCandidate {
@ -2528,6 +2588,7 @@ mod tests {
old: rect(0, 0, 80, 100), old: rect(0, 0, 80, 100),
new: rect(0, 0, 60, 100), new: rect(0, 0, 60, 100),
curve: AnimationCurve::from_config(4), curve: AnimationCurve::from_config(4),
style: AnimationStyle::Plain,
hierarchy: hierarchy(intermediate, target), hierarchy: hierarchy(intermediate, target),
}, },
]; ];
@ -2539,6 +2600,7 @@ mod tests {
assert_eq!(merged[0].old, rect(0, 0, 100, 100)); assert_eq!(merged[0].old, rect(0, 0, 100, 100));
assert_eq!(merged[0].new, rect(0, 0, 60, 100)); assert_eq!(merged[0].new, rect(0, 0, 60, 100));
assert_eq!(merged[0].curve, AnimationCurve::from_config(4)); assert_eq!(merged[0].curve, AnimationCurve::from_config(4));
assert_eq!(merged[0].style, AnimationStyle::Plain);
assert_eq!(merged[0].hierarchy, hierarchy(source, target)); assert_eq!(merged[0].hierarchy, hierarchy(source, target));
assert_eq!(merged[1].node_id, NodeId(2)); assert_eq!(merged[1].node_id, NodeId(2));
assert_eq!(merged[1].old, rect(100, 0, 200, 100)); assert_eq!(merged[1].old, rect(100, 0, 200, 100));
@ -2555,6 +2617,7 @@ mod tests {
old: rect(0, 0, 100, 100), old: rect(0, 0, 100, 100),
new: rect(0, 0, 80, 100), new: rect(0, 0, 80, 100),
curve: AnimationCurve::Linear, curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy, hierarchy,
}, },
LayoutAnimationCandidate { LayoutAnimationCandidate {
@ -2562,6 +2625,7 @@ mod tests {
old: rect(0, 0, 80, 100), old: rect(0, 0, 80, 100),
new: rect(0, 0, 100, 100), new: rect(0, 0, 100, 100),
curve: AnimationCurve::Linear, curve: AnimationCurve::Linear,
style: AnimationStyle::Plain,
hierarchy, hierarchy,
}, },
LayoutAnimationCandidate { LayoutAnimationCandidate {
@ -2569,6 +2633,7 @@ mod tests {
old: rect(100, 0, 200, 100), old: rect(100, 0, 200, 100),
new: rect(120, 0, 220, 100), new: rect(120, 0, 220, 100),
curve: AnimationCurve::Linear, curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy, hierarchy,
}, },
]; ];
@ -2579,6 +2644,7 @@ mod tests {
assert_eq!(merged[0].node_id, NodeId(1)); assert_eq!(merged[0].node_id, NodeId(1));
assert_eq!(merged[0].old, rect(0, 0, 100, 100)); assert_eq!(merged[0].old, rect(0, 0, 100, 100));
assert_eq!(merged[0].new, rect(0, 0, 100, 100)); assert_eq!(merged[0].new, rect(0, 0, 100, 100));
assert_eq!(merged[0].style, AnimationStyle::Plain);
assert_eq!(merged[1].node_id, NodeId(2)); assert_eq!(merged[1].node_id, NodeId(2));
} }
} }

View file

@ -270,6 +270,7 @@ pub struct UiDrag {
pub struct Animations { pub struct Animations {
pub enabled: Option<bool>, pub enabled: Option<bool>,
pub duration_ms: Option<u32>, pub duration_ms: Option<u32>,
pub style: Option<String>,
pub curve: Option<AnimationCurveConfig>, pub curve: Option<AnimationCurveConfig>,
} }
@ -678,3 +679,13 @@ fn custom_animation_curve_parses() {
Some(AnimationCurveConfig::CubicBezier([0.25, 0.1, 0.25, 1.0])) Some(AnimationCurveConfig::CubicBezier([0.25, 0.1, 0.25, 1.0]))
); );
} }
#[test]
fn animation_style_parses() {
let input = b"
[animations]
style = \"plain\"
";
let config = parse_config(input, &Default::default(), |_| ()).unwrap();
assert_eq!(config.animations.style.as_deref(), Some("plain"));
}

View file

@ -3,7 +3,7 @@ use {
config::{ config::{
AnimationCurveConfig, Animations, AnimationCurveConfig, Animations,
context::Context, context::Context,
extractor::{Extractor, ExtractorError, bol, n32, opt, recover, val}, extractor::{Extractor, ExtractorError, bol, n32, opt, recover, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
}, },
toml::{ toml::{
@ -44,9 +44,10 @@ impl Parser for AnimationsParser<'_> {
table: &IndexMap<Spanned<String>, Spanned<Value>>, table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> { ) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table); let mut ext = Extractor::new(self.0, span, table);
let (enabled, duration_ms, curve) = ext.extract(( let (enabled, duration_ms, style, curve) = ext.extract((
recover(opt(bol("enabled"))), recover(opt(bol("enabled"))),
recover(opt(n32("duration-ms"))), recover(opt(n32("duration-ms"))),
recover(opt(str("style"))),
opt(val("curve")), opt(val("curve")),
))?; ))?;
let curve = match curve { let curve = match curve {
@ -56,6 +57,7 @@ impl Parser for AnimationsParser<'_> {
Ok(Animations { Ok(Animations {
enabled: enabled.despan(), enabled: enabled.despan(),
duration_ms: duration_ms.despan(), duration_ms: duration_ms.despan(),
style: style.despan().map(|style| style.to_string()),
curve, curve,
}) })
} }

View file

@ -23,7 +23,7 @@ use {
ahash::{AHashMap, AHashSet}, ahash::{AHashMap, AHashSet},
error_reporter::Report, error_reporter::Report,
jay_config::{ jay_config::{
AnimationCurve, Axis, AnimationCurve, AnimationStyle, Axis,
client::Client, client::Client,
config, config_dir, config, config_dir,
exec::{Command, set_env, unset_env}, exec::{Command, set_env, unset_env},
@ -38,8 +38,9 @@ use {
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_animation_cubic_bezier, on_devices_enumerated, on_idle, on_unload, quit, reload, set_animation_cubic_bezier,
set_animation_curve, set_animation_duration_ms, set_animations_enabled, set_autotile, set_animation_curve, set_animation_duration_ms, set_animation_style,
set_color_management_enabled, set_corner_radius, set_default_workspace_capture, set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius,
set_default_workspace_capture,
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle, set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle,
set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar, set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar,
set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled, set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled,
@ -1652,6 +1653,11 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
} }
set_animations_enabled(config.animations.enabled.unwrap_or(false)); set_animations_enabled(config.animations.enabled.unwrap_or(false));
set_animation_duration_ms(config.animations.duration_ms.unwrap_or(160)); set_animation_duration_ms(config.animations.duration_ms.unwrap_or(160));
match config.animations.style.as_deref().unwrap_or("multiphase") {
"plain" => set_animation_style(AnimationStyle::PLAIN),
"multiphase" => set_animation_style(AnimationStyle::MULTIPHASE),
style_name => log::warn!("Unknown animation style: {style_name}"),
}
match config match config
.animations .animations
.curve .curve

View file

@ -665,8 +665,16 @@
} }
] ]
}, },
"AnimationStyle": {
"type": "string",
"description": "Describes a tiled window movement animation style.\n",
"enum": [
"plain",
"multiphase"
]
},
"Animations": { "Animations": {
"description": "Describes window animation settings.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n curve = [0.25, 0.1, 0.25, 1.0]\n ```\n", "description": "Describes window animation settings.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n style = \"multiphase\"\n curve = [0.25, 0.1, 0.25, 1.0]\n ```\n",
"type": "object", "type": "object",
"properties": { "properties": {
"enabled": { "enabled": {
@ -677,6 +685,10 @@
"type": "integer", "type": "integer",
"description": "Sets the animation duration in milliseconds.\n\nThe default is `160`.\n" "description": "Sets the animation duration in milliseconds.\n\nThe default is `160`.\n"
}, },
"style": {
"description": "Sets the animation style used for tiled window movement animations.\n\nThe default is `multiphase`.\n",
"$ref": "#/$defs/AnimationStyle"
},
"curve": { "curve": {
"description": "Sets the animation curve.\n\nThe default is `ease-out`.\n", "description": "Sets the animation curve.\n\nThe default is `ease-out`.\n",
"$ref": "#/$defs/AnimationCurve" "$ref": "#/$defs/AnimationCurve"
@ -1129,7 +1141,7 @@
"$ref": "#/$defs/UiDrag" "$ref": "#/$defs/UiDrag"
}, },
"animations": { "animations": {
"description": "Configures window animations.\n\nAnimations are disabled by default.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n curve = \"ease-out\"\n ```\n", "description": "Configures window animations.\n\nAnimations are disabled by default.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n style = \"multiphase\"\n curve = \"ease-out\"\n ```\n",
"$ref": "#/$defs/Animations" "$ref": "#/$defs/Animations"
}, },
"xwayland": { "xwayland": {

View file

@ -987,6 +987,26 @@ be between `0` and `1`.
Each element of this array should be a number. Each element of this array should be a number.
<a name="types-AnimationStyle"></a>
### `AnimationStyle`
Describes a tiled window movement animation style.
Values of this type should be strings.
The string should have one of the following values:
- `plain`:
Uses a single interpolated movement from each window's current visual
rectangle to its destination rectangle.
- `multiphase`:
Uses the no-overlap multiphase planner for tiled window movement when a
supported plan exists.
<a name="types-Animations"></a> <a name="types-Animations"></a>
### `Animations` ### `Animations`
@ -998,6 +1018,7 @@ Describes window animation settings.
[animations] [animations]
enabled = true enabled = true
duration-ms = 160 duration-ms = 160
style = "multiphase"
curve = [0.25, 0.1, 0.25, 1.0] curve = [0.25, 0.1, 0.25, 1.0]
``` ```
@ -1023,6 +1044,14 @@ The table has the following fields:
The numbers should be integers. The numbers should be integers.
- `style` (optional):
Sets the animation style used for tiled window movement animations.
The default is `multiphase`.
The value of this field should be a [AnimationStyle](#types-AnimationStyle).
- `curve` (optional): - `curve` (optional):
Sets the animation curve. Sets the animation curve.
@ -2271,6 +2300,7 @@ The table has the following fields:
[animations] [animations]
enabled = true enabled = true
duration-ms = 160 duration-ms = 160
style = "multiphase"
curve = "ease-out" curve = "ease-out"
``` ```

View file

@ -2956,6 +2956,7 @@ Config:
[animations] [animations]
enabled = true enabled = true
duration-ms = 160 duration-ms = 160
style = "multiphase"
curve = "ease-out" curve = "ease-out"
``` ```
xwayland: xwayland:
@ -3682,6 +3683,7 @@ Animations:
[animations] [animations]
enabled = true enabled = true
duration-ms = 160 duration-ms = 160
style = "multiphase"
curve = [0.25, 0.1, 0.25, 1.0] curve = [0.25, 0.1, 0.25, 1.0]
``` ```
fields: fields:
@ -3700,6 +3702,13 @@ Animations:
Sets the animation duration in milliseconds. Sets the animation duration in milliseconds.
The default is `160`. The default is `160`.
style:
ref: AnimationStyle
required: false
description: |
Sets the animation style used for tiled window movement animations.
The default is `multiphase`.
curve: curve:
ref: AnimationCurve ref: AnimationCurve
required: false required: false
@ -3709,6 +3718,21 @@ Animations:
The default is `ease-out`. The default is `ease-out`.
AnimationStyle:
kind: string
description: |
Describes a tiled window movement animation style.
values:
- value: plain
description: |
Uses a single interpolated movement from each window's current visual
rectangle to its destination rectangle.
- value: multiphase
description: |
Uses the no-overlap multiphase planner for tiled window movement when a
supported plan exists.
AnimationCurve: AnimationCurve:
kind: variable kind: variable
description: | description: |