feat: add window animations
This commit is contained in:
parent
eece44a59c
commit
2a079ed800
29 changed files with 6957 additions and 114 deletions
|
|
@ -1023,6 +1023,26 @@ 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_animation_style(&self, style: u32) {
|
||||
self.send(&ClientMessage::SetAnimationStyle { style });
|
||||
}
|
||||
|
||||
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
self.send(&ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 });
|
||||
}
|
||||
|
||||
pub fn set_color_management_enabled(&self, enabled: bool) {
|
||||
self.send(&ClientMessage::SetColorManagementEnabled { enabled });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -551,6 +551,24 @@ pub enum ClientMessage<'a> {
|
|||
SetUiDragThreshold {
|
||||
threshold: i32,
|
||||
},
|
||||
SetAnimationsEnabled {
|
||||
enabled: bool,
|
||||
},
|
||||
SetAnimationDurationMs {
|
||||
duration_ms: u32,
|
||||
},
|
||||
SetAnimationCurve {
|
||||
curve: u32,
|
||||
},
|
||||
SetAnimationStyle {
|
||||
style: u32,
|
||||
},
|
||||
SetAnimationCubicBezier {
|
||||
x1: f32,
|
||||
y1: f32,
|
||||
x2: f32,
|
||||
y2: f32,
|
||||
},
|
||||
SetXScalingMode {
|
||||
mode: XScalingMode,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -103,6 +103,27 @@ impl Axis {
|
|||
}
|
||||
}
|
||||
|
||||
/// The curve used for tiled window animations.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AnimationCurve(pub u32);
|
||||
|
||||
impl AnimationCurve {
|
||||
pub const LINEAR: Self = Self(0);
|
||||
pub const EASE: Self = Self(1);
|
||||
pub const EASE_IN: Self = Self(2);
|
||||
pub const EASE_OUT: Self = Self(3);
|
||||
pub const EASE_IN_OUT: Self = Self(4);
|
||||
}
|
||||
|
||||
/// The presentation style used for tiled window movement animations.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AnimationStyle(pub u32);
|
||||
|
||||
impl AnimationStyle {
|
||||
pub const PLAIN: Self = Self(0);
|
||||
pub const MULTIPHASE: Self = Self(1);
|
||||
}
|
||||
|
||||
/// Exits the compositor.
|
||||
pub fn quit() {
|
||||
get!().quit()
|
||||
|
|
@ -301,6 +322,42 @@ 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);
|
||||
}
|
||||
|
||||
/// Sets the presentation style used for tiled window movement animations.
|
||||
///
|
||||
/// The default is [`AnimationStyle::MULTIPHASE`].
|
||||
pub fn set_animation_style(style: AnimationStyle) {
|
||||
get!().set_animation_style(style.0);
|
||||
}
|
||||
|
||||
/// Sets a custom cubic-bezier curve used by tiled window animations.
|
||||
///
|
||||
/// `x1` and `x2` must be between `0.0` and `1.0`. The curve starts at `(0, 0)`
|
||||
/// and ends at `(1, 1)`.
|
||||
pub fn set_animation_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
get!().set_animation_cubic_bezier(x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
/// Enables or disables the color-management protocol.
|
||||
///
|
||||
/// The default is `false`.
|
||||
|
|
|
|||
1233
src/animation.rs
Normal file
1233
src/animation.rs
Normal file
File diff suppressed because it is too large
Load diff
3405
src/animation/multiphase.rs
Normal file
3405
src/animation/multiphase.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -363,6 +363,13 @@ 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(),
|
||||
layout_animation_curve_override: Default::default(),
|
||||
layout_animation_style_override: Default::default(),
|
||||
layout_animation_batch: Default::default(),
|
||||
suppress_animations_for_next_layout: Default::default(),
|
||||
toplevels: Default::default(),
|
||||
const_40hz_latch: Default::default(),
|
||||
tray_item_ids: Default::default(),
|
||||
|
|
|
|||
|
|
@ -658,17 +658,23 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.move_focused(direction.into());
|
||||
Ok(())
|
||||
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> {
|
||||
let window = self.get_window(window)?;
|
||||
if let Some(c) = toplevel_parent_container(&*window) {
|
||||
c.move_child(window, direction.into());
|
||||
}
|
||||
Ok(())
|
||||
self.state.with_layout_animations(|| {
|
||||
let window = self.get_window(window)?;
|
||||
if let Some(float) = window.tl_data().float.get() {
|
||||
float.move_by_direction(direction.into());
|
||||
} else if let Some(c) = toplevel_parent_container(&*window) {
|
||||
c.move_child(window, direction.into());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> {
|
||||
|
|
@ -986,6 +992,31 @@ 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_animation_style(&self, style: u32) {
|
||||
if !self.state.set_animation_style(style) {
|
||||
log::warn!("Ignoring invalid animation style");
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
if !self.state.set_animation_cubic_bezier(x1, y1, x2, y2) {
|
||||
log::warn!("Ignoring invalid animation cubic-bezier curve");
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_set_direct_scanout_enabled(
|
||||
&self,
|
||||
device: Option<DrmDevice>,
|
||||
|
|
@ -1732,9 +1763,11 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.set_mono(mono);
|
||||
Ok(())
|
||||
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> {
|
||||
|
|
@ -1748,11 +1781,13 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> {
|
||||
let window = self.get_window(window)?;
|
||||
if let Some(c) = toplevel_parent_container(&*window) {
|
||||
c.set_mono(mono.then_some(window.as_ref()));
|
||||
}
|
||||
Ok(())
|
||||
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> {
|
||||
|
|
@ -1767,15 +1802,19 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.set_split(axis.into());
|
||||
Ok(())
|
||||
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> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.toggle_tab();
|
||||
Ok(())
|
||||
self.state.with_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.toggle_tab();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_seat_make_group(
|
||||
|
|
@ -1784,27 +1823,35 @@ impl ConfigProxyHandler {
|
|||
axis: Axis,
|
||||
ephemeral: bool,
|
||||
) -> Result<(), CphError> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.make_group(axis.into(), ephemeral);
|
||||
Ok(())
|
||||
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> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.change_group_opposite();
|
||||
Ok(())
|
||||
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> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.equalize(recursive);
|
||||
Ok(())
|
||||
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> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.move_tab(right);
|
||||
Ok(())
|
||||
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> {
|
||||
|
|
@ -1819,11 +1866,13 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> {
|
||||
let window = self.get_window(window)?;
|
||||
if let Some(c) = toplevel_parent_container(&*window) {
|
||||
c.set_split(axis.into());
|
||||
}
|
||||
Ok(())
|
||||
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(
|
||||
|
|
@ -1963,9 +2012,11 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.set_floating(floating);
|
||||
Ok(())
|
||||
self.state.with_linear_layout_animations(|| {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.set_floating(floating);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> {
|
||||
|
|
@ -1977,9 +2028,11 @@ impl ConfigProxyHandler {
|
|||
}
|
||||
|
||||
fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> {
|
||||
let window = self.get_window(window)?;
|
||||
toplevel_set_floating(&self.state, window, floating);
|
||||
Ok(())
|
||||
self.state.with_linear_layout_animations(|| {
|
||||
let window = self.get_window(window)?;
|
||||
toplevel_set_floating(&self.state, window, floating);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_add_pollable(self: &Rc<Self>, fd: i32) -> Result<(), CphError> {
|
||||
|
|
@ -2729,8 +2782,10 @@ impl ConfigProxyHandler {
|
|||
dx2: i32,
|
||||
dy2: i32,
|
||||
) -> Result<(), CphError> {
|
||||
self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2);
|
||||
Ok(())
|
||||
self.state.with_layout_animations(|| {
|
||||
self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_window_exists(&self, window: Window) {
|
||||
|
|
@ -3207,6 +3262,17 @@ 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::SetAnimationStyle { style } => self.handle_set_animation_style(style),
|
||||
ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 } => {
|
||||
self.handle_set_animation_cubic_bezier(x1, y1, x2, y2)
|
||||
}
|
||||
ClientMessage::SetXScalingMode { mode } => self
|
||||
.handle_set_x_scaling_mode(mode)
|
||||
.wrn("set_x_scaling_mode")?,
|
||||
|
|
|
|||
|
|
@ -936,6 +936,9 @@ impl WlSeatGlobal {
|
|||
{
|
||||
c.move_child(tl, direction);
|
||||
self.maybe_schedule_warp_mouse_to_focus();
|
||||
} else if let Some(float) = data.float.get() {
|
||||
float.move_by_direction(direction);
|
||||
self.maybe_schedule_warp_mouse_to_focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -628,6 +628,11 @@ fn schedule_async_upload(
|
|||
{
|
||||
back_tex_opt = None;
|
||||
}
|
||||
if let Some(back_tex) = &back_tex_opt
|
||||
&& Rc::strong_count(back_tex) > 1
|
||||
{
|
||||
back_tex_opt = None;
|
||||
}
|
||||
let damage_full = || {
|
||||
back.damage.clear();
|
||||
back.damage.damage(slice::from_ref(&buf.rect));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use {
|
||||
crate::{
|
||||
ifs::wl_surface::{
|
||||
SurfaceExt, WlSurface, WlSurfaceError,
|
||||
PendingState, SurfaceExt, WlSurface, WlSurfaceError,
|
||||
x_surface::{xwayland_surface_v1::XwaylandSurfaceV1, xwindow::Xwindow},
|
||||
},
|
||||
leaks::Tracker,
|
||||
|
|
@ -30,6 +30,22 @@ impl SurfaceExt for XSurface {
|
|||
win.node_layer()
|
||||
}
|
||||
|
||||
fn before_apply_commit(
|
||||
self: Rc<Self>,
|
||||
pending: &mut PendingState,
|
||||
) -> Result<(), WlSurfaceError> {
|
||||
if pending
|
||||
.buffer
|
||||
.as_ref()
|
||||
.is_some_and(|buffer| buffer.is_none())
|
||||
&& self.surface.buffer.is_some()
|
||||
&& let Some(xwindow) = self.xwindow.get()
|
||||
{
|
||||
xwindow.queue_spawn_out();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn after_apply_commit(self: Rc<Self>) {
|
||||
if let Some(xwindow) = self.xwindow.get() {
|
||||
xwindow.map_status_changed();
|
||||
|
|
@ -45,6 +61,7 @@ impl SurfaceExt for XSurface {
|
|||
}
|
||||
self.surface.unset_ext();
|
||||
if let Some(xwindow) = self.xwindow.take() {
|
||||
xwindow.queue_spawn_out();
|
||||
xwindow.tl_destroy();
|
||||
xwindow.data.window.set(None);
|
||||
xwindow.data.surface_id.set(None);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use {
|
||||
crate::{
|
||||
animation::RetainedToplevel,
|
||||
client::Client,
|
||||
cursor::KnownCursor,
|
||||
fixed::Fixed,
|
||||
|
|
@ -252,6 +253,11 @@ impl Xwindow {
|
|||
self.x.surface.buffer.is_some() && self.data.info.mapped.get()
|
||||
}
|
||||
|
||||
pub fn queue_spawn_out(&self) {
|
||||
self.toplevel_data
|
||||
.queue_spawn_out(self, self.tl_animation_snapshot());
|
||||
}
|
||||
|
||||
fn map_change(&self) -> Change {
|
||||
match (self.may_be_mapped(), self.is_mapped()) {
|
||||
(true, false) => Change::Map,
|
||||
|
|
@ -274,6 +280,7 @@ impl Xwindow {
|
|||
match map_change {
|
||||
Change::None => return,
|
||||
Change::Unmap => {
|
||||
self.queue_spawn_out();
|
||||
self.data
|
||||
.info
|
||||
.pending_extents
|
||||
|
|
@ -514,6 +521,10 @@ impl ToplevelNodeBase for Xwindow {
|
|||
Some(self.x.surface.clone())
|
||||
}
|
||||
|
||||
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
|
||||
RetainedToplevel::capture_surface(&self.x.surface, (0, 0))
|
||||
}
|
||||
|
||||
fn tl_admits_children(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,6 +226,10 @@ pub trait XdgSurfaceExt: Debug {
|
|||
// nothing
|
||||
}
|
||||
|
||||
fn prepare_unmap(&self) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
fn extents_changed(&self) {
|
||||
// nothing
|
||||
}
|
||||
|
|
@ -664,6 +668,15 @@ impl SurfaceExt for XdgSurface {
|
|||
if let Some(serial) = pending.serial.take() {
|
||||
self.applied_serial.set(serial);
|
||||
}
|
||||
if pending
|
||||
.buffer
|
||||
.as_ref()
|
||||
.is_some_and(|buffer| buffer.is_none())
|
||||
&& self.surface.buffer.is_some()
|
||||
&& let Some(ext) = self.ext.get()
|
||||
{
|
||||
ext.prepare_unmap();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pub mod xdg_dialog_v1;
|
|||
|
||||
use {
|
||||
crate::{
|
||||
animation::RetainedToplevel,
|
||||
bugs,
|
||||
bugs::Bugs,
|
||||
client::{Client, ClientError},
|
||||
|
|
@ -259,6 +260,7 @@ impl XdgToplevelRequestHandler for XdgToplevel {
|
|||
type Error = XdgToplevelError;
|
||||
|
||||
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
|
||||
self.queue_spawn_out();
|
||||
self.tl_destroy();
|
||||
self.xdg.unset_ext();
|
||||
{
|
||||
|
|
@ -398,6 +400,11 @@ impl XdgToplevelRequestHandler for XdgToplevel {
|
|||
}
|
||||
|
||||
impl XdgToplevel {
|
||||
fn queue_spawn_out(&self) {
|
||||
self.toplevel_data
|
||||
.queue_spawn_out(self, self.tl_animation_snapshot());
|
||||
}
|
||||
|
||||
fn map(
|
||||
self: &Rc<Self>,
|
||||
parent: Option<&XdgToplevel>,
|
||||
|
|
@ -779,6 +786,11 @@ impl ToplevelNodeBase for XdgToplevel {
|
|||
Some(self.xdg.surface.clone())
|
||||
}
|
||||
|
||||
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
|
||||
let geo = self.xdg.geometry();
|
||||
RetainedToplevel::capture_surface(&self.xdg.surface, (-geo.x1(), -geo.y1()))
|
||||
}
|
||||
|
||||
fn tl_restack_popups(&self) {
|
||||
self.xdg.restack_popups();
|
||||
}
|
||||
|
|
@ -818,6 +830,10 @@ impl XdgSurfaceExt for XdgToplevel {
|
|||
self.after_commit(None);
|
||||
}
|
||||
|
||||
fn prepare_unmap(&self) {
|
||||
self.queue_spawn_out();
|
||||
}
|
||||
|
||||
fn extents_changed(&self) {
|
||||
self.toplevel_data.pos.set(self.xdg.extents.get());
|
||||
self.tl_extents_changed();
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ mod leaks;
|
|||
mod tracy;
|
||||
mod acceptor;
|
||||
mod allocator;
|
||||
mod animation;
|
||||
mod async_engine;
|
||||
mod backend;
|
||||
mod backends;
|
||||
|
|
|
|||
367
src/renderer.rs
367
src/renderer.rs
|
|
@ -1,7 +1,11 @@
|
|||
use {
|
||||
crate::{
|
||||
animation::{
|
||||
RetainedContent, RetainedExitFrame, RetainedExitLayer, RetainedSurface,
|
||||
RetainedToplevel,
|
||||
},
|
||||
cmm::cmm_render_intent::RenderIntent,
|
||||
gfx_api::{AcquireSync, AlphaMode, GfxApiOpt, ReleaseSync, SampleRect},
|
||||
gfx_api::{AcquireSync, AlphaMode, BufferResv, GfxApiOpt, ReleaseSync, SampleRect},
|
||||
ifs::wl_surface::{
|
||||
SurfaceBuffer, WlSurface,
|
||||
x_surface::xwindow::Xwindow,
|
||||
|
|
@ -14,8 +18,8 @@ use {
|
|||
state::State,
|
||||
theme::{Color, CornerRadius},
|
||||
tree::{
|
||||
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
|
||||
ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
||||
ContainerNode, DisplayNode, FloatNode, Node, OutputNode, PlaceholderNode, ToplevelData,
|
||||
ToplevelNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
||||
},
|
||||
},
|
||||
std::{ops::Deref, rc::Rc, slice},
|
||||
|
|
@ -200,14 +204,22 @@ impl Renderer<'_> {
|
|||
self.render_workspace(&ws, x, y);
|
||||
}
|
||||
}
|
||||
let now = self.state.now_nsec();
|
||||
let exit_frames = self.state.animations.exit_frames(now);
|
||||
self.render_exit_frames(&exit_frames, RetainedExitLayer::Tiled, &opos);
|
||||
macro_rules! render_stacked {
|
||||
($stack:expr) => {
|
||||
for stacked in $stack.iter() {
|
||||
if stacked.node_visible() {
|
||||
self.base.sync();
|
||||
let pos = stacked.node_absolute_position();
|
||||
if pos.intersects(&opos) {
|
||||
let (x, y) = opos.translate(pos.x1(), pos.y1());
|
||||
let visual = self.state.animations.visual_rect(
|
||||
stacked.node_id(),
|
||||
pos,
|
||||
self.state.now_nsec(),
|
||||
);
|
||||
if visual.intersects(&opos) {
|
||||
let (x, y) = opos.translate(visual.x1(), visual.y1());
|
||||
stacked.node_render(self, x, y, None);
|
||||
}
|
||||
}
|
||||
|
|
@ -215,6 +227,7 @@ impl Renderer<'_> {
|
|||
};
|
||||
}
|
||||
render_stacked!(self.state.root.stacked);
|
||||
self.render_exit_frames(&exit_frames, RetainedExitLayer::Floating, &opos);
|
||||
// Flush RoundedFillRect ops from container/float borders so they don't
|
||||
// sort after (and render on top of) layer-shell CopyTexture ops.
|
||||
self.base.sync();
|
||||
|
|
@ -453,6 +466,265 @@ 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())
|
||||
}
|
||||
|
||||
fn render_child_or_snapshot(
|
||||
&mut self,
|
||||
child: &Rc<dyn ToplevelNode>,
|
||||
x: i32,
|
||||
y: i32,
|
||||
bounds: Option<&Rect>,
|
||||
) {
|
||||
if let Some(retained) = self
|
||||
.state
|
||||
.animations
|
||||
.retained_snapshot(child.node_id(), self.state.now_nsec())
|
||||
{
|
||||
self.render_retained_toplevel(&retained, x, y, bounds);
|
||||
} else {
|
||||
child.node_render(self, x, y, bounds);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_retained_toplevel(
|
||||
&mut self,
|
||||
retained: &RetainedToplevel,
|
||||
x: i32,
|
||||
y: i32,
|
||||
bounds: Option<&Rect>,
|
||||
) {
|
||||
let (x, y) = self
|
||||
.base
|
||||
.scale_point(x + retained.offset.0, y + retained.offset.1);
|
||||
self.render_retained_surface_scaled(&retained.surface, x, y, None, bounds);
|
||||
}
|
||||
|
||||
fn render_exit_frames(
|
||||
&mut self,
|
||||
frames: &[RetainedExitFrame],
|
||||
layer: RetainedExitLayer,
|
||||
output_rect: &Rect,
|
||||
) {
|
||||
for frame in frames {
|
||||
if frame.layer != layer || !frame.rect.intersects(output_rect) {
|
||||
continue;
|
||||
}
|
||||
self.render_exit_frame(frame, output_rect);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_exit_frame(&mut self, frame: &RetainedExitFrame, output_rect: &Rect) {
|
||||
let (x, y) = output_rect.translate(frame.rect.x1(), frame.rect.y1());
|
||||
let inset = frame.frame_inset;
|
||||
if inset > 0 {
|
||||
let color = if frame.active {
|
||||
self.state.theme.colors.active_border.get()
|
||||
} else {
|
||||
self.state.theme.colors.border.get()
|
||||
};
|
||||
self.render_rounded_frame(
|
||||
Rect::new_sized_saturating(0, 0, frame.rect.width(), frame.rect.height()),
|
||||
&color,
|
||||
self.state.theme.corner_radius.get(),
|
||||
inset,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
}
|
||||
let body = Rect::new_sized_saturating(
|
||||
x + inset,
|
||||
y + inset,
|
||||
frame.rect.width() - 2 * inset,
|
||||
frame.rect.height() - 2 * inset,
|
||||
);
|
||||
if body.is_empty() {
|
||||
return;
|
||||
}
|
||||
if inset > 0 && !self.state.theme.corner_radius.get().is_zero() {
|
||||
let inner_cr = self.scale_corner_radius(
|
||||
self.state
|
||||
.theme
|
||||
.corner_radius
|
||||
.get()
|
||||
.expanded_by(-(inset as f32)),
|
||||
);
|
||||
self.corner_radius = Some(inner_cr);
|
||||
}
|
||||
self.render_window_body_background(body);
|
||||
let bounds = self.base.scale_rect(body);
|
||||
self.stretch = if frame.source_body_size != body.size() {
|
||||
Some(self.base.scale_point(body.width(), body.height()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.render_retained_toplevel(&frame.retained, body.x1(), body.y1(), Some(&bounds));
|
||||
self.stretch = None;
|
||||
self.corner_radius = None;
|
||||
}
|
||||
|
||||
fn render_window_body_background(&mut self, body: Rect) {
|
||||
if body.is_empty() {
|
||||
return;
|
||||
}
|
||||
let color = self.state.theme.colors.background.get();
|
||||
let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||
let srgb = &srgb_srgb.linear;
|
||||
let perceptual = RenderIntent::Perceptual;
|
||||
self.base.sync();
|
||||
if let Some(cr) = self.corner_radius
|
||||
&& !cr.is_zero()
|
||||
{
|
||||
self.base
|
||||
.fill_rounded_rect(body, &color, None, srgb, perceptual, cr, 0.0);
|
||||
} else {
|
||||
let bounds = self.base.scale_rect(body);
|
||||
self.base
|
||||
.fill_scaled_boxes(slice::from_ref(&bounds), &color, None, srgb, perceptual);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_retained_surface_scaled(
|
||||
&mut self,
|
||||
retained: &RetainedSurface,
|
||||
x: i32,
|
||||
y: i32,
|
||||
pos_rel: Option<(i32, i32)>,
|
||||
bounds: Option<&Rect>,
|
||||
) {
|
||||
let stretch = self.stretch.take();
|
||||
let corner_radius = self.corner_radius.take();
|
||||
let mut size = retained.size;
|
||||
if let Some((x_rel, y_rel)) = pos_rel {
|
||||
let (x, y) = self.base.scale_point(x_rel, y_rel);
|
||||
let (w, h) = self.base.scale_point(x_rel + size.0, y_rel + size.1);
|
||||
size = (w - x, h - y);
|
||||
} else {
|
||||
size = self.base.scale_point(size.0, size.1);
|
||||
}
|
||||
let mut stretched_source = None;
|
||||
if let Some(s) = stretch {
|
||||
if let RetainedContent::Texture { source, .. } = &retained.content {
|
||||
let mut source = *source;
|
||||
if size.0 > 0 && size.1 > 0 {
|
||||
let sx = s.0 as f32 / size.0 as f32;
|
||||
let sy = s.1 as f32 / size.1 as f32;
|
||||
source.x2 *= sx;
|
||||
source.y2 *= sy;
|
||||
}
|
||||
stretched_source = Some(source);
|
||||
}
|
||||
size = s;
|
||||
}
|
||||
for child in &retained.below {
|
||||
let (x1, y1) = self.base.scale_point(child.offset.0, child.offset.1);
|
||||
self.render_retained_surface_scaled(child, x + x1, y + y1, Some(child.offset), bounds);
|
||||
}
|
||||
self.corner_radius = corner_radius;
|
||||
self.render_retained_content(retained, stretched_source, x, y, size, bounds);
|
||||
for child in &retained.above {
|
||||
let (x1, y1) = self.base.scale_point(child.offset.0, child.offset.1);
|
||||
self.render_retained_surface_scaled(child, x + x1, y + y1, Some(child.offset), bounds);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_retained_content(
|
||||
&mut self,
|
||||
retained: &RetainedSurface,
|
||||
stretched_source: Option<SampleRect>,
|
||||
x: i32,
|
||||
y: i32,
|
||||
size: (i32, i32),
|
||||
bounds: Option<&Rect>,
|
||||
) {
|
||||
let corner_radius = self.corner_radius.take();
|
||||
match &retained.content {
|
||||
RetainedContent::Texture {
|
||||
texture,
|
||||
buffer,
|
||||
source,
|
||||
alpha,
|
||||
color_description,
|
||||
render_intent,
|
||||
alpha_mode,
|
||||
opaque,
|
||||
} => {
|
||||
let source = stretched_source.unwrap_or(*source);
|
||||
if let Some(cr) = corner_radius {
|
||||
self.base.render_rounded_texture(
|
||||
texture,
|
||||
*alpha,
|
||||
x,
|
||||
y,
|
||||
Some(source),
|
||||
Some(size),
|
||||
self.base.scale,
|
||||
bounds,
|
||||
Some(buffer.clone() as Rc<dyn BufferResv>),
|
||||
AcquireSync::Unnecessary,
|
||||
buffer.release_sync,
|
||||
color_description,
|
||||
*render_intent,
|
||||
*alpha_mode,
|
||||
cr,
|
||||
);
|
||||
} else {
|
||||
self.base.render_texture(
|
||||
texture,
|
||||
*alpha,
|
||||
x,
|
||||
y,
|
||||
Some(source),
|
||||
Some(size),
|
||||
self.base.scale,
|
||||
bounds,
|
||||
Some(buffer.clone() as Rc<dyn BufferResv>),
|
||||
AcquireSync::Unnecessary,
|
||||
buffer.release_sync,
|
||||
*opaque,
|
||||
color_description,
|
||||
*render_intent,
|
||||
*alpha_mode,
|
||||
);
|
||||
}
|
||||
}
|
||||
RetainedContent::Color {
|
||||
color,
|
||||
alpha,
|
||||
color_description,
|
||||
render_intent,
|
||||
} => {
|
||||
if let Some(rect) = Rect::new_sized(x, y, size.0, size.1) {
|
||||
let rect = match bounds {
|
||||
None => rect,
|
||||
Some(bounds) => rect.intersect(*bounds),
|
||||
};
|
||||
if !rect.is_empty() {
|
||||
self.base.sync();
|
||||
self.base.fill_scaled_boxes(
|
||||
&[rect],
|
||||
color,
|
||||
*alpha,
|
||||
&color_description.linear,
|
||||
*render_intent,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) {
|
||||
self.render_container_decorations(container, x, y);
|
||||
|
||||
|
|
@ -465,6 +737,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 +749,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,14 +764,17 @@ impl Renderer<'_> {
|
|||
);
|
||||
}
|
||||
}
|
||||
let body = 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()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let body = visual_mb.move_(x, y);
|
||||
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
|
||||
};
|
||||
if self.state.theme.sizes.gap.get() != 0 && !child.node.node_is_container() {
|
||||
let cr = self.state.theme.corner_radius.get();
|
||||
if !cr.is_zero() {
|
||||
|
|
@ -507,9 +783,16 @@ impl Renderer<'_> {
|
|||
self.corner_radius = Some(inner_cr);
|
||||
}
|
||||
}
|
||||
child
|
||||
.node
|
||||
.node_render(self, x + content.x1(), y + content.y1(), Some(&body));
|
||||
if !child.node.node_is_container() {
|
||||
self.render_window_body_background(body);
|
||||
}
|
||||
let body = self.base.scale_rect(body);
|
||||
self.render_child_or_snapshot(
|
||||
&child.node,
|
||||
x + content.x1(),
|
||||
y + content.y1(),
|
||||
Some(&body),
|
||||
);
|
||||
self.stretch = None;
|
||||
self.corner_radius = None;
|
||||
} else {
|
||||
|
|
@ -524,10 +807,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 +830,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()))
|
||||
|
|
@ -556,10 +842,16 @@ impl Renderer<'_> {
|
|||
self.corner_radius = Some(inner_cr);
|
||||
}
|
||||
let body = body.move_(x, y);
|
||||
if !child.node.node_is_container() {
|
||||
self.render_window_body_background(body);
|
||||
}
|
||||
let body = self.base.scale_rect(body);
|
||||
child
|
||||
.node
|
||||
.node_render(self, x + content.x1(), y + content.y1(), Some(&body));
|
||||
self.render_child_or_snapshot(
|
||||
&child.node,
|
||||
x + content.x1(),
|
||||
y + content.y1(),
|
||||
Some(&body),
|
||||
);
|
||||
self.stretch = None;
|
||||
self.corner_radius = None;
|
||||
}
|
||||
|
|
@ -793,6 +1085,10 @@ impl Renderer<'_> {
|
|||
_ => return,
|
||||
};
|
||||
let pos = floating.position.get();
|
||||
let visual =
|
||||
self.state
|
||||
.animations
|
||||
.visual_rect(floating.node_id(), pos, self.state.now_nsec());
|
||||
let theme = &self.state.theme;
|
||||
let bw = theme.sizes.border_width.get();
|
||||
let bc = if floating.active.get() {
|
||||
|
|
@ -801,16 +1097,27 @@ impl Renderer<'_> {
|
|||
theme.colors.border.get()
|
||||
};
|
||||
let cr = theme.corner_radius.get();
|
||||
let outer = Rect::new_sized_saturating(0, 0, pos.width(), pos.height());
|
||||
let outer = Rect::new_sized_saturating(0, 0, visual.width(), visual.height());
|
||||
self.render_rounded_frame(outer, &bc, cr, bw, x, y);
|
||||
let body =
|
||||
Rect::new_sized_saturating(x + bw, y + bw, pos.width() - 2 * bw, pos.height() - 2 * bw);
|
||||
let body = Rect::new_sized_saturating(
|
||||
x + bw,
|
||||
y + bw,
|
||||
visual.width() - 2 * bw,
|
||||
visual.height() - 2 * bw,
|
||||
);
|
||||
let scissor_body = self.base.scale_rect(body);
|
||||
self.stretch = if pos.width() != visual.width() || pos.height() != visual.height() {
|
||||
Some(self.base.scale_point(body.width(), body.height()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !cr.is_zero() {
|
||||
let inner_cr = self.scale_corner_radius(cr.expanded_by(-(bw as f32)));
|
||||
self.corner_radius = Some(inner_cr);
|
||||
}
|
||||
child.node_render(self, body.x1(), body.y1(), Some(&scissor_body));
|
||||
self.render_window_body_background(body);
|
||||
self.render_child_or_snapshot(&child, body.x1(), body.y1(), Some(&scissor_body));
|
||||
self.stretch = None;
|
||||
self.corner_radius = None;
|
||||
}
|
||||
|
||||
|
|
|
|||
886
src/state.rs
886
src/state.rs
|
|
@ -2,6 +2,17 @@ use {
|
|||
crate::{
|
||||
acceptor::Acceptor,
|
||||
allocator::BufferObject,
|
||||
animation::{
|
||||
AnimationCurve, AnimationState, AnimationStyle, AnimationTick, RetainedExitLayer,
|
||||
RetainedToplevel,
|
||||
expand_damage_rect,
|
||||
multiphase::{
|
||||
MultiphasePhase, MultiphasePlan, MultiphasePlanFailure, MultiphaseRequest,
|
||||
MultiphaseWindow, MultiphaseWindowHierarchy,
|
||||
partition_motion_groups, plan_no_overlap_with_diagnostics, validate_phase_paths,
|
||||
},
|
||||
spawn_in_start_rect,
|
||||
},
|
||||
async_engine::{AsyncEngine, SpawnedFuture},
|
||||
backend::{
|
||||
Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice,
|
||||
|
|
@ -103,11 +114,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::{
|
||||
|
|
@ -155,6 +165,98 @@ use {
|
|||
uapi::{OwnedFd, c},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct LayoutAnimationCandidate {
|
||||
node_id: NodeId,
|
||||
old: Rect,
|
||||
new: Rect,
|
||||
curve: AnimationCurve,
|
||||
style: AnimationStyle,
|
||||
hierarchy: MultiphaseWindowHierarchy,
|
||||
}
|
||||
|
||||
fn coalesce_layout_animation_candidates(
|
||||
candidates: Vec<LayoutAnimationCandidate>,
|
||||
) -> Vec<LayoutAnimationCandidate> {
|
||||
let mut merged: Vec<LayoutAnimationCandidate> = vec![];
|
||||
for candidate in candidates {
|
||||
if let Some(existing) = merged
|
||||
.iter_mut()
|
||||
.find(|existing| existing.node_id == candidate.node_id)
|
||||
{
|
||||
existing.new = candidate.new;
|
||||
existing.curve = candidate.curve;
|
||||
existing.style = candidate.style;
|
||||
existing.hierarchy = MultiphaseWindowHierarchy::new(
|
||||
existing.hierarchy.source,
|
||||
candidate.hierarchy.target,
|
||||
);
|
||||
} else {
|
||||
merged.push(candidate);
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
fn layout_animation_group_uses_plain(
|
||||
candidates: &[LayoutAnimationCandidate],
|
||||
group: &[usize],
|
||||
) -> bool {
|
||||
group
|
||||
.iter()
|
||||
.any(|&idx| candidates[idx].style == AnimationStyle::Plain)
|
||||
}
|
||||
|
||||
fn bridged_retarget_plan(
|
||||
request: &MultiphaseRequest,
|
||||
candidates: &[LayoutAnimationCandidate],
|
||||
group: &[usize],
|
||||
bridge_paths: &[Vec<(Rect, Rect)>],
|
||||
bridge_phase_count: usize,
|
||||
follow_phases: &[MultiphasePhase],
|
||||
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||
let mut paths = vec![];
|
||||
for (group_pos, &idx) in group.iter().enumerate() {
|
||||
let candidate = &candidates[idx];
|
||||
let window = request.windows[group_pos];
|
||||
let Some(bridge_path) = bridge_paths.get(group_pos) else {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
};
|
||||
let mut path = bridge_path.clone();
|
||||
let mut current = path
|
||||
.last()
|
||||
.map(|(_, to)| *to)
|
||||
.unwrap_or(window.from);
|
||||
while path.len() < bridge_phase_count {
|
||||
path.push((current, current));
|
||||
}
|
||||
if current != candidate.old {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
for phase in follow_phases {
|
||||
match phase
|
||||
.steps
|
||||
.iter()
|
||||
.find(|step| step.node_id == candidate.node_id)
|
||||
{
|
||||
Some(step) => {
|
||||
if step.from != current {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
path.push((step.from, step.to));
|
||||
current = step.to;
|
||||
}
|
||||
None => path.push((current, current)),
|
||||
}
|
||||
}
|
||||
if current != window.to {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
paths.push(path);
|
||||
}
|
||||
validate_phase_paths(request, &paths)
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub pid: c::pid_t,
|
||||
pub kb_ctx: KbvmContext,
|
||||
|
|
@ -265,6 +367,13 @@ 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 layout_animation_curve_override: Cell<Option<AnimationCurve>>,
|
||||
pub layout_animation_style_override: Cell<Option<AnimationStyle>>,
|
||||
pub(crate) layout_animation_batch: RefCell<Option<Vec<LayoutAnimationCandidate>>>,
|
||||
pub suppress_animations_for_next_layout: Cell<bool>,
|
||||
pub toplevels: CopyHashMap<ToplevelIdentifier, Weak<dyn ToplevelNode>>,
|
||||
pub const_40hz_latch: EventSource<dyn LatchListener>,
|
||||
pub tray_item_ids: TrayItemIds,
|
||||
|
|
@ -816,7 +925,14 @@ impl State {
|
|||
|
||||
pub fn map_tiled(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
|
||||
let seat = self.seat_queue.last();
|
||||
self.do_map_tiled(seat.as_deref(), node.clone());
|
||||
let animate_new_app_map = node.tl_data().parent.is_none()
|
||||
&& node.tl_data().kind.is_app_window()
|
||||
&& !node.tl_data().visible.get();
|
||||
if animate_new_app_map {
|
||||
self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone()));
|
||||
} else {
|
||||
self.do_map_tiled(seat.as_deref(), node.clone());
|
||||
}
|
||||
self.focus_after_map(node, seat.as_deref());
|
||||
}
|
||||
|
||||
|
|
@ -851,7 +967,7 @@ impl State {
|
|||
mut height: i32,
|
||||
workspace: &Rc<WorkspaceNode>,
|
||||
abs_pos: Option<(i32, i32)>,
|
||||
) {
|
||||
) -> Rc<FloatNode> {
|
||||
width += 2 * self.theme.sizes.border_width.get();
|
||||
height +=
|
||||
2 * self.theme.sizes.border_width.get() + self.theme.title_plus_underline_height();
|
||||
|
|
@ -882,8 +998,9 @@ impl State {
|
|||
}
|
||||
Rect::new_sized_saturating(x1, y1, width, height)
|
||||
};
|
||||
FloatNode::new(self, workspace, position, node.clone());
|
||||
let float = FloatNode::new(self, workspace, position, node.clone());
|
||||
self.focus_after_map(node, self.seat_queue.last().as_deref());
|
||||
float
|
||||
}
|
||||
|
||||
fn focus_after_map(&self, node: Rc<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
|
||||
|
|
@ -1126,6 +1243,12 @@ 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.layout_animation_curve_override.set(None);
|
||||
self.layout_animation_style_override.set(None);
|
||||
self.suppress_animations_for_next_layout.set(false);
|
||||
self.render_ctx_watchers.clear();
|
||||
self.workspace_watchers.clear();
|
||||
self.toplevel_lists.clear();
|
||||
|
|
@ -1496,6 +1619,532 @@ impl State {
|
|||
self.eng.now().msec()
|
||||
}
|
||||
|
||||
pub fn queue_tiled_animation(
|
||||
self: &Rc<Self>,
|
||||
node_id: NodeId,
|
||||
old: Rect,
|
||||
new: Rect,
|
||||
) {
|
||||
let curve = self
|
||||
.layout_animation_curve_override
|
||||
.get()
|
||||
.unwrap_or_else(|| self.animations.curve.get());
|
||||
self.queue_layout_animation(
|
||||
node_id,
|
||||
old,
|
||||
new,
|
||||
curve,
|
||||
MultiphaseWindowHierarchy::default(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn queue_tiled_animation_with_hierarchy(
|
||||
self: &Rc<Self>,
|
||||
node_id: NodeId,
|
||||
old: Rect,
|
||||
new: Rect,
|
||||
hierarchy: MultiphaseWindowHierarchy,
|
||||
) {
|
||||
let curve = self
|
||||
.layout_animation_curve_override
|
||||
.get()
|
||||
.unwrap_or_else(|| self.animations.curve.get());
|
||||
self.queue_layout_animation(node_id, old, new, curve, hierarchy);
|
||||
}
|
||||
|
||||
pub fn queue_linear_layout_animation(
|
||||
self: &Rc<Self>,
|
||||
node_id: NodeId,
|
||||
old: Rect,
|
||||
new: Rect,
|
||||
) {
|
||||
self.queue_layout_animation(
|
||||
node_id,
|
||||
old,
|
||||
new,
|
||||
AnimationCurve::Linear,
|
||||
MultiphaseWindowHierarchy::default(),
|
||||
);
|
||||
}
|
||||
|
||||
fn queue_layout_animation(
|
||||
self: &Rc<Self>,
|
||||
node_id: NodeId,
|
||||
old: Rect,
|
||||
new: Rect,
|
||||
curve: AnimationCurve,
|
||||
hierarchy: MultiphaseWindowHierarchy,
|
||||
) {
|
||||
if !self.animations.enabled.get()
|
||||
|| !self.layout_animations_active.get()
|
||||
|| self.suppress_animations_for_next_layout.get()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (old_output, old_scale) = {
|
||||
let (x, y) = old.center();
|
||||
let (output, _, _) = self.find_closest_output(x, y);
|
||||
(output.id, output.global.persistent.scale.get())
|
||||
};
|
||||
let (new_output, new_scale) = {
|
||||
let (x, y) = new.center();
|
||||
let (output, _, _) = self.find_closest_output(x, y);
|
||||
(output.id, output.global.persistent.scale.get())
|
||||
};
|
||||
if old_output != new_output || old_scale != new_scale {
|
||||
return;
|
||||
}
|
||||
let candidate = LayoutAnimationCandidate {
|
||||
node_id,
|
||||
old,
|
||||
new,
|
||||
curve,
|
||||
style: self
|
||||
.layout_animation_style_override
|
||||
.get()
|
||||
.unwrap_or_else(|| self.animations.style.get()),
|
||||
hierarchy,
|
||||
};
|
||||
if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() {
|
||||
batch.push(candidate);
|
||||
return;
|
||||
}
|
||||
self.start_layout_animation_candidate(candidate, self.now_nsec());
|
||||
}
|
||||
|
||||
fn start_layout_animation_candidate(
|
||||
self: &Rc<Self>,
|
||||
candidate: LayoutAnimationCandidate,
|
||||
now_nsec: u64,
|
||||
) {
|
||||
let started = self.animations.set_target(
|
||||
candidate.node_id,
|
||||
candidate.old,
|
||||
candidate.new,
|
||||
None,
|
||||
now_nsec,
|
||||
self.animations.duration_ms.get(),
|
||||
candidate.curve,
|
||||
);
|
||||
if started {
|
||||
self.damage(expand_damage_rect(
|
||||
candidate.old.union(candidate.new),
|
||||
self.theme.sizes.border_width.get().max(0),
|
||||
));
|
||||
self.ensure_animation_tick();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn begin_layout_animation_batch(&self) {
|
||||
self.layout_animation_batch
|
||||
.borrow_mut()
|
||||
.get_or_insert_with(Vec::new);
|
||||
}
|
||||
|
||||
pub fn finish_layout_animation_batch(self: &Rc<Self>) {
|
||||
let Some(candidates) = self.layout_animation_batch.borrow_mut().take() else {
|
||||
return;
|
||||
};
|
||||
let candidates = coalesce_layout_animation_candidates(candidates);
|
||||
if candidates.is_empty() {
|
||||
return;
|
||||
}
|
||||
let now = self.now_nsec();
|
||||
let windows: Vec<_> = candidates
|
||||
.iter()
|
||||
.map(|candidate| {
|
||||
MultiphaseWindow::with_hierarchy(
|
||||
candidate.node_id,
|
||||
self.animations
|
||||
.visual_rect(candidate.node_id, candidate.old, now),
|
||||
candidate.new,
|
||||
candidate.hierarchy,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for group in partition_motion_groups(&windows, self.layout_animation_clearance()) {
|
||||
if layout_animation_group_uses_plain(&candidates, &group) {
|
||||
for idx in group {
|
||||
self.start_layout_animation_candidate(candidates[idx].clone(), now);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) {
|
||||
continue;
|
||||
}
|
||||
for idx in group {
|
||||
self.start_layout_animation_candidate(candidates[idx].clone(), now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_animation_clearance(&self) -> i32 {
|
||||
let border = self.theme.sizes.border_width.get().max(0);
|
||||
let gap = self.theme.sizes.gap.get().max(0);
|
||||
if gap == 0 { border } else { gap + 2 * border }
|
||||
}
|
||||
|
||||
fn start_multiphase_layout_animation(
|
||||
self: &Rc<Self>,
|
||||
candidates: &[LayoutAnimationCandidate],
|
||||
windows: &[MultiphaseWindow],
|
||||
group: &[usize],
|
||||
now_nsec: u64,
|
||||
) -> bool {
|
||||
let request_windows: Vec<_> = group.iter().map(|&idx| windows[idx]).collect();
|
||||
let Some(first) = request_windows.first() else {
|
||||
return false;
|
||||
};
|
||||
let mut bounds = first.from.union(first.to);
|
||||
for window in &request_windows[1..] {
|
||||
bounds = bounds.union(window.from).union(window.to);
|
||||
}
|
||||
let request = MultiphaseRequest {
|
||||
bounds,
|
||||
windows: request_windows,
|
||||
clearance: self.layout_animation_clearance(),
|
||||
};
|
||||
if self.start_existing_phased_retarget(candidates, windows, group, &request, now_nsec) {
|
||||
return true;
|
||||
}
|
||||
if self.start_bridged_phased_retarget(candidates, windows, group, &request, now_nsec) {
|
||||
return true;
|
||||
}
|
||||
let plan = match plan_no_overlap_with_diagnostics(&request) {
|
||||
Ok(plan) => plan,
|
||||
Err(diagnostic) => {
|
||||
log::debug!(
|
||||
"falling back to plain layout animation for group {:?}: {:?}",
|
||||
group,
|
||||
diagnostic
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
||||
}
|
||||
|
||||
fn start_existing_phased_retarget(
|
||||
self: &Rc<Self>,
|
||||
candidates: &[LayoutAnimationCandidate],
|
||||
windows: &[MultiphaseWindow],
|
||||
group: &[usize],
|
||||
request: &MultiphaseRequest,
|
||||
now_nsec: u64,
|
||||
) -> bool {
|
||||
let mut paths = vec![];
|
||||
for &idx in group {
|
||||
let candidate = &candidates[idx];
|
||||
let window = windows[idx];
|
||||
let Some(path) =
|
||||
self.animations
|
||||
.phased_route_to(candidate.node_id, window.to, now_nsec)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
paths.push(path);
|
||||
}
|
||||
let plan = match validate_phase_paths(request, &paths) {
|
||||
Ok(plan) => plan,
|
||||
Err(error) => {
|
||||
log::debug!(
|
||||
"existing phased retarget rejected for group {:?}: {:?}",
|
||||
group,
|
||||
error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
log::debug!("retargeting active phased animation for group {:?}", group);
|
||||
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
||||
}
|
||||
|
||||
fn start_bridged_phased_retarget(
|
||||
self: &Rc<Self>,
|
||||
candidates: &[LayoutAnimationCandidate],
|
||||
windows: &[MultiphaseWindow],
|
||||
group: &[usize],
|
||||
request: &MultiphaseRequest,
|
||||
now_nsec: u64,
|
||||
) -> bool {
|
||||
let mut bridge_paths = vec![];
|
||||
let mut bridge_phase_count = 0;
|
||||
let mut has_bridge = false;
|
||||
for &idx in group {
|
||||
let candidate = &candidates[idx];
|
||||
let window = windows[idx];
|
||||
if window.from == candidate.old {
|
||||
bridge_paths.push(vec![]);
|
||||
continue;
|
||||
}
|
||||
let Some(path) =
|
||||
self.animations
|
||||
.phased_route_to(candidate.node_id, candidate.old, now_nsec)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if !path.is_empty() {
|
||||
has_bridge = true;
|
||||
bridge_phase_count = bridge_phase_count.max(path.len());
|
||||
}
|
||||
bridge_paths.push(path);
|
||||
}
|
||||
if !has_bridge {
|
||||
return false;
|
||||
}
|
||||
|
||||
let settled_windows: Vec<_> = group
|
||||
.iter()
|
||||
.map(|&idx| {
|
||||
let candidate = &candidates[idx];
|
||||
MultiphaseWindow::with_hierarchy(
|
||||
candidate.node_id,
|
||||
candidate.old,
|
||||
candidate.new,
|
||||
candidate.hierarchy,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let Some(first) = settled_windows.first() else {
|
||||
return false;
|
||||
};
|
||||
let mut bounds = first.from.union(first.to);
|
||||
for window in &settled_windows[1..] {
|
||||
bounds = bounds.union(window.from).union(window.to);
|
||||
}
|
||||
let settled_request = MultiphaseRequest {
|
||||
bounds,
|
||||
windows: settled_windows,
|
||||
clearance: self.layout_animation_clearance(),
|
||||
};
|
||||
let follow_plan = match plan_no_overlap_with_diagnostics(&settled_request) {
|
||||
Ok(plan) => plan,
|
||||
Err(diagnostic) => {
|
||||
log::debug!(
|
||||
"bridged phased retarget follow-up rejected for group {:?}: {:?}",
|
||||
group,
|
||||
diagnostic
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let plan = match bridged_retarget_plan(
|
||||
request,
|
||||
candidates,
|
||||
group,
|
||||
&bridge_paths,
|
||||
bridge_phase_count,
|
||||
&follow_plan.phases,
|
||||
) {
|
||||
Ok(plan) => plan,
|
||||
Err(error) => {
|
||||
log::debug!(
|
||||
"bridged phased retarget rejected for group {:?}: {:?}",
|
||||
group,
|
||||
error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
log::debug!("bridging active phased animation for group {:?}", group);
|
||||
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
|
||||
}
|
||||
|
||||
fn start_multiphase_plan(
|
||||
self: &Rc<Self>,
|
||||
candidates: &[LayoutAnimationCandidate],
|
||||
windows: &[MultiphaseWindow],
|
||||
group: &[usize],
|
||||
plan_phases: &[crate::animation::multiphase::MultiphasePhase],
|
||||
now_nsec: u64,
|
||||
) -> bool {
|
||||
if plan_phases.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let mut entries = vec![];
|
||||
for &idx in group {
|
||||
let candidate = &candidates[idx];
|
||||
let window = windows[idx];
|
||||
let mut current = window.from;
|
||||
let mut damage = current.union(window.to);
|
||||
let mut phases = vec![];
|
||||
for phase in plan_phases {
|
||||
match phase
|
||||
.steps
|
||||
.iter()
|
||||
.find(|step| step.node_id == candidate.node_id)
|
||||
{
|
||||
Some(step) => {
|
||||
phases.push((step.from, step.to));
|
||||
damage = damage.union(step.from).union(step.to);
|
||||
current = step.to;
|
||||
}
|
||||
None => phases.push((current, current)),
|
||||
}
|
||||
}
|
||||
if current != window.to {
|
||||
return false;
|
||||
}
|
||||
entries.push((candidate.clone(), phases, damage));
|
||||
}
|
||||
let mut started_any = false;
|
||||
for (candidate, phases, damage) in entries {
|
||||
if self.animations.set_phased_target(
|
||||
candidate.node_id,
|
||||
phases,
|
||||
None,
|
||||
now_nsec,
|
||||
self.animations.duration_ms.get(),
|
||||
candidate.curve,
|
||||
) {
|
||||
started_any = true;
|
||||
self.damage(expand_damage_rect(
|
||||
damage,
|
||||
self.theme.sizes.border_width.get().max(0),
|
||||
));
|
||||
}
|
||||
}
|
||||
if started_any {
|
||||
self.ensure_animation_tick();
|
||||
}
|
||||
started_any
|
||||
}
|
||||
|
||||
pub fn queue_spawn_in_animation(
|
||||
self: &Rc<Self>,
|
||||
node_id: NodeId,
|
||||
target: Rect,
|
||||
) {
|
||||
if !self.animations.enabled.get() || target.is_empty() {
|
||||
return;
|
||||
}
|
||||
let start = spawn_in_start_rect(target);
|
||||
let now = self.now_nsec();
|
||||
let started = self.animations.set_spawn_in(
|
||||
node_id,
|
||||
target,
|
||||
None,
|
||||
now,
|
||||
self.animations.duration_ms.get(),
|
||||
self.animations.curve.get(),
|
||||
);
|
||||
if started {
|
||||
self.damage(expand_damage_rect(
|
||||
start.union(target),
|
||||
self.theme.sizes.border_width.get().max(0),
|
||||
));
|
||||
self.ensure_animation_tick();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queue_spawn_out_animation(
|
||||
self: &Rc<Self>,
|
||||
from: Rect,
|
||||
frame_inset: i32,
|
||||
retained: Rc<RetainedToplevel>,
|
||||
active: bool,
|
||||
layer: RetainedExitLayer,
|
||||
) {
|
||||
if !self.animations.enabled.get() || from.is_empty() {
|
||||
return;
|
||||
}
|
||||
let now = self.now_nsec();
|
||||
let started = self.animations.set_spawn_out(
|
||||
from,
|
||||
frame_inset,
|
||||
retained,
|
||||
active,
|
||||
layer,
|
||||
now,
|
||||
self.animations.duration_ms.get(),
|
||||
self.animations.curve.get(),
|
||||
);
|
||||
if started {
|
||||
self.damage(expand_damage_rect(
|
||||
from,
|
||||
self.theme.sizes.border_width.get().max(0),
|
||||
));
|
||||
self.ensure_animation_tick();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_animations_enabled(&self, enabled: bool) {
|
||||
if self.animations.enabled.replace(enabled) && !enabled {
|
||||
self.animations.clear();
|
||||
self.damage(self.root.extents.get());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_animation_duration_ms(&self, duration_ms: u32) {
|
||||
self.animations.duration_ms.set(duration_ms);
|
||||
}
|
||||
|
||||
pub fn set_animation_curve(&self, curve: u32) {
|
||||
self.animations
|
||||
.curve
|
||||
.set(AnimationCurve::from_config(curve));
|
||||
}
|
||||
|
||||
pub fn set_animation_style(&self, style: u32) -> bool {
|
||||
let Some(style) = AnimationStyle::from_config(style) else {
|
||||
return false;
|
||||
};
|
||||
self.animations.style.set(style);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
|
||||
let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else {
|
||||
return false;
|
||||
};
|
||||
self.animations.curve.set(curve);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn with_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
let prev_requested = self.layout_animations_requested.replace(true);
|
||||
let prev_active = self.layout_animations_active.replace(true);
|
||||
let res = f();
|
||||
self.layout_animations_requested.set(prev_requested);
|
||||
self.layout_animations_active.set(prev_active);
|
||||
res
|
||||
}
|
||||
|
||||
pub fn with_linear_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
let prev_requested = self.layout_animations_requested.replace(true);
|
||||
let prev_active = self.layout_animations_active.replace(true);
|
||||
let prev_curve = self
|
||||
.layout_animation_curve_override
|
||||
.replace(Some(AnimationCurve::Linear));
|
||||
let prev_style = self
|
||||
.layout_animation_style_override
|
||||
.replace(Some(AnimationStyle::Plain));
|
||||
let res = f();
|
||||
self.layout_animations_requested.set(prev_requested);
|
||||
self.layout_animations_active.set(prev_active);
|
||||
self.layout_animation_curve_override.set(prev_curve);
|
||||
self.layout_animation_style_override.set(prev_style);
|
||||
res
|
||||
}
|
||||
|
||||
fn ensure_animation_tick(self: &Rc<Self>) {
|
||||
if self.animations.tick_is_active() {
|
||||
return;
|
||||
}
|
||||
let outputs: Vec<_> = self.root.outputs.lock().values().cloned().collect();
|
||||
if outputs.is_empty() {
|
||||
return;
|
||||
}
|
||||
let tick = Rc::new_cyclic(|weak| AnimationTick::new(self, weak));
|
||||
for output in &outputs {
|
||||
tick.attach(output);
|
||||
}
|
||||
self.animations.set_tick(tick);
|
||||
for output in &outputs {
|
||||
self.damage(output.global.pos.get());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_extents_changed(&self) {
|
||||
self.root.update_extents();
|
||||
for seat in self.globals.seats.lock().values() {
|
||||
|
|
@ -2024,6 +2673,227 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::*,
|
||||
crate::animation::multiphase::MultiphaseHierarchyPosition,
|
||||
};
|
||||
|
||||
fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect {
|
||||
Rect::new_saturating(x1, y1, x2, y2)
|
||||
}
|
||||
|
||||
fn hierarchy(
|
||||
source: MultiphaseHierarchyPosition,
|
||||
target: MultiphaseHierarchyPosition,
|
||||
) -> MultiphaseWindowHierarchy {
|
||||
MultiphaseWindowHierarchy::new(source, target)
|
||||
}
|
||||
|
||||
fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate {
|
||||
candidate_rects(
|
||||
node_id,
|
||||
rect(0, 0, 100, 100),
|
||||
rect(100, 0, 200, 100),
|
||||
style,
|
||||
)
|
||||
}
|
||||
|
||||
fn candidate_rects(
|
||||
node_id: u32,
|
||||
old: Rect,
|
||||
new: Rect,
|
||||
style: AnimationStyle,
|
||||
) -> LayoutAnimationCandidate {
|
||||
LayoutAnimationCandidate {
|
||||
node_id: NodeId(node_id),
|
||||
old,
|
||||
new,
|
||||
curve: AnimationCurve::Linear,
|
||||
style,
|
||||
hierarchy: MultiphaseWindowHierarchy::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_style_candidate_forces_group_plain() {
|
||||
let candidates = vec![
|
||||
candidate(1, AnimationStyle::Multiphase),
|
||||
candidate(2, AnimationStyle::Plain),
|
||||
];
|
||||
|
||||
assert!(!layout_animation_group_uses_plain(&candidates, &[0]));
|
||||
assert!(layout_animation_group_uses_plain(&candidates, &[0, 1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridged_retarget_handles_second_rotation_interrupt() {
|
||||
let a_left = rect(0, 0, 100, 100);
|
||||
let c_mid = rect(100, 0, 200, 100);
|
||||
let c_left = a_left;
|
||||
let a_mid = c_mid;
|
||||
let c_current = rect(150, 50, 250, 100);
|
||||
let c_mid_lane = rect(100, 50, 200, 100);
|
||||
let candidates = vec![
|
||||
candidate_rects(1, a_left, a_mid, AnimationStyle::Multiphase),
|
||||
candidate_rects(3, c_mid, c_left, AnimationStyle::Multiphase),
|
||||
];
|
||||
let request = MultiphaseRequest {
|
||||
bounds: rect(0, 0, 250, 100),
|
||||
windows: vec![
|
||||
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
|
||||
MultiphaseWindow::new(NodeId(3), c_current, c_left),
|
||||
],
|
||||
clearance: 0,
|
||||
};
|
||||
let settled_request = MultiphaseRequest {
|
||||
bounds: rect(0, 0, 200, 100),
|
||||
windows: vec![
|
||||
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
|
||||
MultiphaseWindow::new(NodeId(3), c_mid, c_left),
|
||||
],
|
||||
clearance: 0,
|
||||
};
|
||||
let follow_plan = plan_no_overlap_with_diagnostics(&settled_request).unwrap();
|
||||
let bridge_paths = vec![vec![], vec![(c_current, c_mid_lane), (c_mid_lane, c_mid)]];
|
||||
|
||||
let plan = bridged_retarget_plan(
|
||||
&request,
|
||||
&candidates,
|
||||
&[0, 1],
|
||||
&bridge_paths,
|
||||
2,
|
||||
&follow_plan.phases,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(plan
|
||||
.phases
|
||||
.iter()
|
||||
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(1))));
|
||||
assert!(plan
|
||||
.phases
|
||||
.iter()
|
||||
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(3))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_animation_candidates_coalesce_duplicate_nodes() {
|
||||
let source = MultiphaseHierarchyPosition {
|
||||
parent: Some(NodeId(10)),
|
||||
depth: 2,
|
||||
sibling_index: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
let intermediate = MultiphaseHierarchyPosition {
|
||||
parent: Some(NodeId(11)),
|
||||
depth: 1,
|
||||
sibling_index: Some(0),
|
||||
..Default::default()
|
||||
};
|
||||
let target = MultiphaseHierarchyPosition {
|
||||
parent: Some(NodeId(12)),
|
||||
depth: 0,
|
||||
sibling_index: Some(2),
|
||||
..Default::default()
|
||||
};
|
||||
let second_source = MultiphaseHierarchyPosition {
|
||||
parent: Some(NodeId(20)),
|
||||
depth: 1,
|
||||
sibling_index: Some(0),
|
||||
..Default::default()
|
||||
};
|
||||
let second_target = MultiphaseHierarchyPosition {
|
||||
parent: Some(NodeId(20)),
|
||||
depth: 1,
|
||||
sibling_index: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let candidates = vec![
|
||||
LayoutAnimationCandidate {
|
||||
node_id: NodeId(1),
|
||||
old: rect(0, 0, 100, 100),
|
||||
new: rect(0, 0, 80, 100),
|
||||
curve: AnimationCurve::Linear,
|
||||
style: AnimationStyle::Multiphase,
|
||||
hierarchy: hierarchy(source, intermediate),
|
||||
},
|
||||
LayoutAnimationCandidate {
|
||||
node_id: NodeId(2),
|
||||
old: rect(100, 0, 200, 100),
|
||||
new: rect(120, 0, 220, 100),
|
||||
curve: AnimationCurve::Linear,
|
||||
style: AnimationStyle::Multiphase,
|
||||
hierarchy: hierarchy(second_source, second_target),
|
||||
},
|
||||
LayoutAnimationCandidate {
|
||||
node_id: NodeId(1),
|
||||
old: rect(0, 0, 80, 100),
|
||||
new: rect(0, 0, 60, 100),
|
||||
curve: AnimationCurve::from_config(4),
|
||||
style: AnimationStyle::Plain,
|
||||
hierarchy: hierarchy(intermediate, target),
|
||||
},
|
||||
];
|
||||
|
||||
let merged = coalesce_layout_animation_candidates(candidates);
|
||||
|
||||
assert_eq!(merged.len(), 2);
|
||||
assert_eq!(merged[0].node_id, NodeId(1));
|
||||
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
|
||||
assert_eq!(merged[0].new, rect(0, 0, 60, 100));
|
||||
assert_eq!(merged[0].curve, AnimationCurve::from_config(4));
|
||||
assert_eq!(merged[0].style, AnimationStyle::Plain);
|
||||
assert_eq!(merged[0].hierarchy, hierarchy(source, target));
|
||||
assert_eq!(merged[1].node_id, NodeId(2));
|
||||
assert_eq!(merged[1].old, rect(100, 0, 200, 100));
|
||||
assert_eq!(merged[1].new, rect(120, 0, 220, 100));
|
||||
assert_eq!(merged[1].hierarchy, hierarchy(second_source, second_target));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_animation_candidates_keep_coalesced_layout_noops() {
|
||||
let hierarchy = MultiphaseWindowHierarchy::default();
|
||||
let candidates = vec![
|
||||
LayoutAnimationCandidate {
|
||||
node_id: NodeId(1),
|
||||
old: rect(0, 0, 100, 100),
|
||||
new: rect(0, 0, 80, 100),
|
||||
curve: AnimationCurve::Linear,
|
||||
style: AnimationStyle::Multiphase,
|
||||
hierarchy,
|
||||
},
|
||||
LayoutAnimationCandidate {
|
||||
node_id: NodeId(1),
|
||||
old: rect(0, 0, 80, 100),
|
||||
new: rect(0, 0, 100, 100),
|
||||
curve: AnimationCurve::Linear,
|
||||
style: AnimationStyle::Plain,
|
||||
hierarchy,
|
||||
},
|
||||
LayoutAnimationCandidate {
|
||||
node_id: NodeId(2),
|
||||
old: rect(100, 0, 200, 100),
|
||||
new: rect(120, 0, 220, 100),
|
||||
curve: AnimationCurve::Linear,
|
||||
style: AnimationStyle::Multiphase,
|
||||
hierarchy,
|
||||
},
|
||||
];
|
||||
|
||||
let merged = coalesce_layout_animation_candidates(candidates);
|
||||
|
||||
assert_eq!(merged.len(), 2);
|
||||
assert_eq!(merged[0].node_id, NodeId(1));
|
||||
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
|
||||
assert_eq!(merged[0].new, rect(0, 0, 100, 100));
|
||||
assert_eq!(merged[0].style, AnimationStyle::Plain);
|
||||
assert_eq!(merged[1].node_id, NodeId(2));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ShmScreencopyError {
|
||||
#[error("There is no render context")]
|
||||
|
|
|
|||
|
|
@ -131,6 +131,8 @@ pub struct ContainerNode {
|
|||
pub content_height: Cell<i32>,
|
||||
pub sum_factors: Cell<f64>,
|
||||
pub layout_scheduled: Cell<bool>,
|
||||
animate_next_layout: Cell<bool>,
|
||||
pub mono_transition_animation_pending: Cell<bool>,
|
||||
compute_render_positions_scheduled: Cell<bool>,
|
||||
num_children: NumCell<usize>,
|
||||
pub children: LinkedList<ContainerChild>,
|
||||
|
|
@ -238,6 +240,8 @@ impl ContainerNode {
|
|||
content_height: Cell::new(0),
|
||||
sum_factors: Cell::new(1.0),
|
||||
layout_scheduled: Cell::new(false),
|
||||
animate_next_layout: Cell::new(false),
|
||||
mono_transition_animation_pending: Cell::new(false),
|
||||
compute_render_positions_scheduled: Cell::new(false),
|
||||
num_children: NumCell::new(1),
|
||||
children,
|
||||
|
|
@ -436,6 +440,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());
|
||||
}
|
||||
|
|
@ -467,6 +475,7 @@ impl ContainerNode {
|
|||
fn perform_layout(self: &Rc<Self>) {
|
||||
self.layout_scheduled.set(false);
|
||||
if self.num_children.get() == 0 {
|
||||
self.mono_transition_animation_pending.set(false);
|
||||
return;
|
||||
}
|
||||
if let Some(child) = self.mono_child.get() {
|
||||
|
|
@ -484,6 +493,7 @@ impl ContainerNode {
|
|||
self.damage();
|
||||
}
|
||||
}
|
||||
self.mono_transition_animation_pending.set(false);
|
||||
}
|
||||
|
||||
fn perform_mono_layout(self: &Rc<Self>, child: &ContainerChild) {
|
||||
|
|
@ -656,6 +666,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();
|
||||
}
|
||||
}
|
||||
|
|
@ -816,6 +827,7 @@ impl ContainerNode {
|
|||
}
|
||||
}
|
||||
self.mono_child.set(child.clone());
|
||||
self.mono_transition_animation_pending.set(true);
|
||||
if child.is_some() {
|
||||
self.rebuild_tab_bar();
|
||||
} else {
|
||||
|
|
@ -1759,10 +1771,42 @@ enum SeatOpKind {
|
|||
|
||||
pub async fn container_layout(state: Rc<State>) {
|
||||
loop {
|
||||
let container = state.pending_container_layout.pop().await;
|
||||
if container.layout_scheduled.get() {
|
||||
container.perform_layout();
|
||||
let first = state.pending_container_layout.pop().await;
|
||||
let mut containers = vec![first];
|
||||
while let Some(container) = state.pending_container_layout.try_pop() {
|
||||
containers.push(container);
|
||||
}
|
||||
let mut animated = vec![];
|
||||
let mut immediate = vec![];
|
||||
for container in containers {
|
||||
if !container.layout_scheduled.get() {
|
||||
continue;
|
||||
}
|
||||
let animate = container.animate_next_layout.replace(false)
|
||||
&& !state.suppress_animations_for_next_layout.get();
|
||||
if animate {
|
||||
animated.push(container);
|
||||
} else {
|
||||
immediate.push(container);
|
||||
}
|
||||
}
|
||||
if !animated.is_empty() {
|
||||
let prev_active = state.layout_animations_active.replace(true);
|
||||
state.begin_layout_animation_batch();
|
||||
for container in animated {
|
||||
container.perform_layout();
|
||||
}
|
||||
state.finish_layout_animation_batch();
|
||||
state.layout_animations_active.set(prev_active);
|
||||
}
|
||||
if !immediate.is_empty() {
|
||||
let prev_active = state.layout_animations_active.replace(false);
|
||||
for container in immediate {
|
||||
container.perform_layout();
|
||||
}
|
||||
state.layout_animations_active.set(prev_active);
|
||||
}
|
||||
state.suppress_animations_for_next_layout.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2259,6 +2303,11 @@ impl ContainingNode for ContainerNode {
|
|||
}
|
||||
// log::info!("cnode_remove_child2");
|
||||
self.rebuild_tab_bar();
|
||||
if self.state.animations.enabled.get()
|
||||
&& !self.state.suppress_animations_for_next_layout.get()
|
||||
{
|
||||
self.animate_next_layout.set(true);
|
||||
}
|
||||
self.schedule_layout();
|
||||
self.cancel_seat_ops();
|
||||
self.child_removed.trigger();
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ use {
|
|||
};
|
||||
|
||||
tree_id!(FloatNodeId);
|
||||
|
||||
const COMMAND_MOVE_DELTA: i32 = 100;
|
||||
|
||||
pub struct FloatNode {
|
||||
pub id: FloatNodeId,
|
||||
pub state: Rc<State>,
|
||||
|
|
@ -153,6 +156,13 @@ impl FloatNode {
|
|||
_ => return,
|
||||
};
|
||||
let pos = self.position.get();
|
||||
let spawn_in_pending = {
|
||||
let data = child.tl_data();
|
||||
data.spawn_in_pending.get() && data.kind.is_app_window() && !data.is_fullscreen.get()
|
||||
};
|
||||
if spawn_in_pending && self.visible.get() {
|
||||
self.state.queue_spawn_in_animation(self.id.into(), pos);
|
||||
}
|
||||
let theme = &self.state.theme;
|
||||
let bw = theme.sizes.border_width.get();
|
||||
let cpos = Rect::new_sized_saturating(
|
||||
|
|
@ -363,6 +373,50 @@ impl FloatNode {
|
|||
y2 += y1 - pos.y1();
|
||||
}
|
||||
let new_pos = Rect::new_saturating(x1, y1, x2, y2);
|
||||
self.set_position(new_pos);
|
||||
}
|
||||
|
||||
pub fn move_by_direction(self: &Rc<Self>, direction: Direction) {
|
||||
let (dx, dy) = match direction {
|
||||
Direction::Left => (-COMMAND_MOVE_DELTA, 0),
|
||||
Direction::Down => (0, COMMAND_MOVE_DELTA),
|
||||
Direction::Up => (0, -COMMAND_MOVE_DELTA),
|
||||
Direction::Right => (COMMAND_MOVE_DELTA, 0),
|
||||
Direction::Unspecified => return,
|
||||
};
|
||||
self.set_position(self.position.get().move_(dx, dy));
|
||||
}
|
||||
|
||||
fn body_for_outer(&self, outer: Rect) -> Rect {
|
||||
let bw = self.state.theme.sizes.border_width.get();
|
||||
Rect::new_sized_saturating(
|
||||
outer.x1() + bw,
|
||||
outer.y1() + bw,
|
||||
outer.width() - 2 * bw,
|
||||
outer.height() - 2 * bw,
|
||||
)
|
||||
}
|
||||
|
||||
fn queue_position_animation(&self, old_pos: Rect, new_pos: Rect) {
|
||||
self.state
|
||||
.clone()
|
||||
.queue_tiled_animation(self.id.into(), old_pos, new_pos);
|
||||
let Some(child) = self.child.get() else {
|
||||
return;
|
||||
};
|
||||
self.state.clone().queue_tiled_animation(
|
||||
child.node_id(),
|
||||
self.body_for_outer(old_pos),
|
||||
self.body_for_outer(new_pos),
|
||||
);
|
||||
}
|
||||
|
||||
fn set_position(self: &Rc<Self>, new_pos: Rect) {
|
||||
let pos = self.position.get();
|
||||
if new_pos == pos {
|
||||
return;
|
||||
}
|
||||
self.queue_position_animation(pos, new_pos);
|
||||
self.position.set(new_pos);
|
||||
if self.visible.get() {
|
||||
self.state.damage(pos);
|
||||
|
|
@ -791,13 +845,7 @@ impl ContainingNode for FloatNode {
|
|||
let bw = theme.sizes.border_width.get();
|
||||
let (x, y) = (x - bw, y - bw);
|
||||
let pos = self.position.get();
|
||||
if pos.position() != (x, y) {
|
||||
let new_pos = pos.at_point(x, y);
|
||||
self.position.set(new_pos);
|
||||
self.state.damage(pos);
|
||||
self.state.damage(new_pos);
|
||||
self.schedule_layout();
|
||||
}
|
||||
self.set_position(pos.at_point(x, y));
|
||||
}
|
||||
|
||||
fn cnode_resize_child(
|
||||
|
|
@ -828,14 +876,7 @@ impl ContainingNode for FloatNode {
|
|||
y2 = (v + bw).max(y1 + bw + bw);
|
||||
}
|
||||
let new_pos = Rect::new_saturating(x1, y1, x2, y2);
|
||||
if new_pos != pos {
|
||||
self.position.set(new_pos);
|
||||
if self.visible.get() {
|
||||
self.state.damage(pos);
|
||||
self.state.damage(new_pos);
|
||||
}
|
||||
self.schedule_layout();
|
||||
}
|
||||
self.set_position(new_pos);
|
||||
}
|
||||
|
||||
fn cnode_pinned(&self) -> bool {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
use {
|
||||
crate::{
|
||||
animation::{
|
||||
RetainedExitLayer, RetainedToplevel,
|
||||
multiphase::{
|
||||
MultiphaseHierarchyPosition, MultiphaseHierarchyTransition,
|
||||
MultiphaseWindowHierarchy, PhaseAxis,
|
||||
},
|
||||
},
|
||||
client::{Client, ClientId},
|
||||
criteria::{
|
||||
CritDestroyListener, CritMatcherId,
|
||||
|
|
@ -117,6 +124,7 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
|
|||
let parent_was_none = data.parent.set(Some(parent.clone())).is_none();
|
||||
if parent_was_none {
|
||||
data.mapped_during_iteration.set(data.state.eng.iteration());
|
||||
data.spawn_in_pending.set(data.kind.is_app_window());
|
||||
data.property_changed(TL_CHANGED_NEW);
|
||||
}
|
||||
let was_floating = data.parent_is_float.get();
|
||||
|
|
@ -184,6 +192,57 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
|
|||
fn tl_change_extents(self: Rc<Self>, rect: &Rect) {
|
||||
let data = self.tl_data();
|
||||
let prev = data.desired_extents.replace(*rect);
|
||||
let target_hierarchy = self.tl_multiphase_hierarchy_position();
|
||||
let hierarchy = MultiphaseWindowHierarchy::new(
|
||||
data.layout_animation_position.replace(target_hierarchy),
|
||||
target_hierarchy,
|
||||
);
|
||||
let spawn_in_pending = data.spawn_in_pending.get();
|
||||
let spawn_in_eligible = spawn_in_pending
|
||||
&& !rect.is_empty()
|
||||
&& data.visible.get()
|
||||
&& !data.is_fullscreen.get()
|
||||
&& data.kind.is_app_window()
|
||||
&& !self.node_is_container();
|
||||
let parent_container = data
|
||||
.parent
|
||||
.get()
|
||||
.and_then(|parent| parent.node_into_container());
|
||||
let parent_is_mono = parent_container
|
||||
.as_ref()
|
||||
.is_some_and(|container| container.mono_child.is_some());
|
||||
let parent_mono_transition = parent_container
|
||||
.as_ref()
|
||||
.is_some_and(|container| container.mono_transition_animation_pending.get());
|
||||
let active_mono_boundary = matches!(
|
||||
hierarchy.transition,
|
||||
MultiphaseHierarchyTransition::EnteringMono
|
||||
| MultiphaseHierarchyTransition::ExitingMono
|
||||
) && parent_mono_transition
|
||||
&& (hierarchy.source.mono_active || hierarchy.target.mono_active);
|
||||
if prev != *rect
|
||||
&& !prev.is_empty()
|
||||
&& !rect.is_empty()
|
||||
&& data.visible.get()
|
||||
&& !data.parent_is_float.get()
|
||||
&& !self.node_is_container()
|
||||
&& (!parent_is_mono || active_mono_boundary)
|
||||
{
|
||||
data.state.clone().queue_tiled_animation_with_hierarchy(
|
||||
data.node_id,
|
||||
prev,
|
||||
*rect,
|
||||
hierarchy,
|
||||
);
|
||||
}
|
||||
if spawn_in_eligible {
|
||||
data.state
|
||||
.clone()
|
||||
.queue_spawn_in_animation(data.node_id, *rect);
|
||||
}
|
||||
if spawn_in_eligible {
|
||||
data.spawn_in_pending.set(false);
|
||||
}
|
||||
if prev.size() != rect.size() {
|
||||
for sc in data.jay_screencasts.lock().values() {
|
||||
sc.schedule_realloc_or_reconfigure();
|
||||
|
|
@ -275,6 +334,35 @@ pub trait ToplevelNodeBase: Node {
|
|||
true
|
||||
}
|
||||
|
||||
fn tl_multiphase_hierarchy_position(&self) -> MultiphaseHierarchyPosition {
|
||||
let data = self.tl_data();
|
||||
let Some(parent) = data.parent.get() else {
|
||||
return Default::default();
|
||||
};
|
||||
let mut position = MultiphaseHierarchyPosition {
|
||||
parent: Some(parent.node_id()),
|
||||
..Default::default()
|
||||
};
|
||||
populate_multiphase_ancestor_splits(&mut position, Some(parent.clone()));
|
||||
if let Some(container) = parent.node_into_container() {
|
||||
position.split_axis = Some(match container.split.get() {
|
||||
ContainerSplit::Horizontal => PhaseAxis::Horizontal,
|
||||
ContainerSplit::Vertical => PhaseAxis::Vertical,
|
||||
});
|
||||
if let Some(mono) = container.mono_child.get() {
|
||||
position.parent_is_mono = true;
|
||||
position.mono_active = mono.node.node_id() == data.node_id;
|
||||
}
|
||||
for (idx, child) in container.children.iter().enumerate() {
|
||||
if child.node.node_id() == data.node_id {
|
||||
position.sibling_index = Some(idx.min(u16::MAX as usize) as u16);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
position
|
||||
}
|
||||
|
||||
fn tl_set_active(&self, active: bool) {
|
||||
let _ = active;
|
||||
}
|
||||
|
|
@ -299,6 +387,11 @@ pub trait ToplevelNodeBase: Node {
|
|||
fn tl_scanout_surface(&self) -> Option<Rc<WlSurface>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn tl_restack_popups(&self) {
|
||||
// nothing
|
||||
}
|
||||
|
|
@ -339,6 +432,31 @@ pub trait ToplevelNodeBase: Node {
|
|||
}
|
||||
}
|
||||
|
||||
fn populate_multiphase_ancestor_splits(
|
||||
position: &mut MultiphaseHierarchyPosition,
|
||||
mut parent: Option<Rc<dyn ContainingNode>>,
|
||||
) {
|
||||
let mut depth = 0u16;
|
||||
while let Some(node) = parent {
|
||||
let Some(toplevel) = node.clone().node_into_toplevel() else {
|
||||
break;
|
||||
};
|
||||
depth = depth.saturating_add(1);
|
||||
if let Some(container) = node.node_into_container() {
|
||||
match container.split.get() {
|
||||
ContainerSplit::Horizontal => {
|
||||
position.nearest_horizontal_split_depth.get_or_insert(depth);
|
||||
}
|
||||
ContainerSplit::Vertical => {
|
||||
position.nearest_vertical_split_depth.get_or_insert(depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
parent = toplevel.tl_data().parent.get();
|
||||
}
|
||||
position.depth = depth;
|
||||
}
|
||||
|
||||
pub struct FullscreenedData {
|
||||
pub placeholder: Rc<PlaceholderNode>,
|
||||
pub workspace: Rc<WorkspaceNode>,
|
||||
|
|
@ -377,6 +495,13 @@ impl ToplevelType {
|
|||
ToplevelType::XWindow { .. } => window::X_WINDOW,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_app_window(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ToplevelType::XdgToplevel(_) | ToplevelType::XWindow(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToplevelData {
|
||||
|
|
@ -399,8 +524,10 @@ pub struct ToplevelData {
|
|||
pub title: RefCell<String>,
|
||||
pub parent: CloneCell<Option<Rc<dyn ContainingNode>>>,
|
||||
pub mapped_during_iteration: Cell<u64>,
|
||||
pub spawn_in_pending: Cell<bool>,
|
||||
pub pos: Cell<Rect>,
|
||||
pub desired_extents: Cell<Rect>,
|
||||
pub layout_animation_position: Cell<MultiphaseHierarchyPosition>,
|
||||
pub seat_state: NodeSeatState,
|
||||
pub wants_attention: Cell<bool>,
|
||||
pub requested_attention: Cell<bool>,
|
||||
|
|
@ -462,8 +589,10 @@ impl ToplevelData {
|
|||
title: RefCell::new(title),
|
||||
parent: Default::default(),
|
||||
mapped_during_iteration: Cell::new(0),
|
||||
spawn_in_pending: Cell::new(false),
|
||||
pos: Default::default(),
|
||||
desired_extents: Default::default(),
|
||||
layout_animation_position: Default::default(),
|
||||
seat_state: Default::default(),
|
||||
wants_attention: Cell::new(false),
|
||||
requested_attention: Cell::new(false),
|
||||
|
|
@ -935,6 +1064,62 @@ impl ToplevelData {
|
|||
self.mapped_during_iteration.get() == self.state.eng.iteration()
|
||||
}
|
||||
|
||||
pub fn queue_spawn_out(&self, node: &dyn ToplevelNode, retained: Option<Rc<RetainedToplevel>>) {
|
||||
if !self.kind.is_app_window()
|
||||
|| !self.visible.get()
|
||||
|| self.is_fullscreen.get()
|
||||
|| node.node_is_container()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(retained) = retained else {
|
||||
return;
|
||||
};
|
||||
let bw = self.state.theme.sizes.border_width.get().max(0);
|
||||
let now = self.state.now_nsec();
|
||||
let (outer, frame_inset, layer) = if self.parent_is_float.get() {
|
||||
let Some(float) = self.float.get() else {
|
||||
return;
|
||||
};
|
||||
(
|
||||
self.state
|
||||
.animations
|
||||
.visual_rect(float.node_id(), float.position.get(), now),
|
||||
bw,
|
||||
RetainedExitLayer::Floating,
|
||||
)
|
||||
} else {
|
||||
let body =
|
||||
self.state
|
||||
.animations
|
||||
.visual_rect(self.node_id, node.node_absolute_position(), now);
|
||||
if body.is_empty() {
|
||||
return;
|
||||
}
|
||||
if self.state.theme.sizes.gap.get() != 0 {
|
||||
(
|
||||
Rect::new_sized_saturating(
|
||||
body.x1() - bw,
|
||||
body.y1() - bw,
|
||||
body.width() + 2 * bw,
|
||||
body.height() + 2 * bw,
|
||||
),
|
||||
bw,
|
||||
RetainedExitLayer::Tiled,
|
||||
)
|
||||
} else {
|
||||
(body, 0, RetainedExitLayer::Tiled)
|
||||
}
|
||||
};
|
||||
self.state.clone().queue_spawn_out_animation(
|
||||
outer,
|
||||
frame_inset,
|
||||
retained,
|
||||
self.active(),
|
||||
layer,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_content_type(&self, content_type: Option<ContentType>) {
|
||||
if self.content_type.replace(content_type) != content_type {
|
||||
self.property_changed(TL_CHANGED_CONTENT_TY);
|
||||
|
|
@ -1043,6 +1228,26 @@ pub fn toplevel_create_split(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, axis:
|
|||
}
|
||||
}
|
||||
|
||||
fn float_outer_for_body(state: &State, body: Rect) -> Rect {
|
||||
let bw = state.theme.sizes.border_width.get();
|
||||
Rect::new_sized_saturating(
|
||||
body.x1() - bw,
|
||||
body.y1() - bw,
|
||||
body.width() + 2 * bw,
|
||||
body.height() + 2 * bw,
|
||||
)
|
||||
}
|
||||
|
||||
fn float_body_for_outer(state: &State, outer: Rect) -> Rect {
|
||||
let bw = state.theme.sizes.border_width.get();
|
||||
Rect::new_sized_saturating(
|
||||
outer.x1() + bw,
|
||||
outer.y1() + bw,
|
||||
outer.width() - 2 * bw,
|
||||
outer.height() - 2 * bw,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floating: bool) {
|
||||
let data = tl.tl_data();
|
||||
if data.is_fullscreen.get() {
|
||||
|
|
@ -1059,9 +1264,19 @@ pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floati
|
|||
parent.cnode_remove_child2(&*tl, true);
|
||||
state.map_tiled(tl);
|
||||
} else if let Some(ws) = data.workspace.get() {
|
||||
let node_id = data.node_id;
|
||||
let old_body =
|
||||
state
|
||||
.animations
|
||||
.visual_rect(node_id, tl.node_absolute_position(), state.now_nsec());
|
||||
let old_outer = float_outer_for_body(state, old_body);
|
||||
parent.cnode_remove_child2(&*tl, true);
|
||||
let (width, height) = data.float_size(&ws);
|
||||
state.map_floating(tl, width, height, &ws, None);
|
||||
let floater = state.map_floating(tl, width, height, &ws, None);
|
||||
let new_outer = floater.position.get();
|
||||
let new_body = float_body_for_outer(state, new_outer);
|
||||
state.queue_linear_layout_animation(floater.node_id(), old_outer, new_outer);
|
||||
state.queue_linear_layout_animation(node_id, old_body, new_body);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -197,10 +197,10 @@ impl WorkspaceNode {
|
|||
}
|
||||
self.pull_child_properties(&**container);
|
||||
let pos = self.position.get();
|
||||
container.clone().tl_change_extents(&pos);
|
||||
container.tl_set_parent(self.clone());
|
||||
container.tl_set_visible(self.container_visible());
|
||||
self.container.set(Some(container.clone()));
|
||||
container.clone().tl_change_extents(&pos);
|
||||
self.state.damage(self.position.get());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2034,6 +2034,7 @@ impl Wm {
|
|||
self.windows_by_surface_serial.remove(&serial);
|
||||
}
|
||||
if let Some(window) = data.window.take() {
|
||||
window.queue_spawn_out();
|
||||
window.destroy();
|
||||
}
|
||||
if let Some(parent) = data.parent.take() {
|
||||
|
|
|
|||
|
|
@ -266,6 +266,20 @@ pub struct UiDrag {
|
|||
pub threshold: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Animations {
|
||||
pub enabled: Option<bool>,
|
||||
pub duration_ms: Option<u32>,
|
||||
pub style: Option<String>,
|
||||
pub curve: Option<AnimationCurveConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum AnimationCurveConfig {
|
||||
Preset(String),
|
||||
CubicBezier([f32; 4]),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OutputMatch {
|
||||
Any(Vec<OutputMatch>),
|
||||
|
|
@ -569,6 +583,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>,
|
||||
|
|
@ -653,3 +668,26 @@ fn default_config_parses() {
|
|||
let input = include_bytes!("default-config.toml");
|
||||
parse_config(input, &Default::default(), |_| ()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_animation_curve_parses() {
|
||||
let input = b"
|
||||
[animations]
|
||||
curve = [0.25, 0.1, 0.25, 1.0]
|
||||
";
|
||||
let config = parse_config(input, &Default::default(), |_| ()).unwrap();
|
||||
assert_eq!(
|
||||
config.animations.curve,
|
||||
Some(AnimationCurveConfig::CubicBezier([0.25, 0.1, 0.25, 1.0]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animation_style_parses() {
|
||||
let input = b"
|
||||
[animations]
|
||||
style = \"plain\"
|
||||
";
|
||||
let config = parse_config(input, &Default::default(), |_| ()).unwrap();
|
||||
assert_eq!(config.animations.style.as_deref(), Some("plain"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use {
|
|||
|
||||
pub mod action;
|
||||
mod actions;
|
||||
mod animations;
|
||||
mod capabilities;
|
||||
mod clean_logs_older_than;
|
||||
mod client_match;
|
||||
|
|
|
|||
99
toml-config/src/config/parsers/animations.rs
Normal file
99
toml-config/src/config/parsers/animations.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use {
|
||||
crate::{
|
||||
config::{
|
||||
AnimationCurveConfig, Animations,
|
||||
context::Context,
|
||||
extractor::{Extractor, ExtractorError, bol, n32, opt, recover, str, val},
|
||||
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||
},
|
||||
toml::{
|
||||
toml_span::{DespanExt, Span, Spanned, SpannedExt},
|
||||
toml_value::Value,
|
||||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AnimationsParserError {
|
||||
#[error(transparent)]
|
||||
Expected(#[from] UnexpectedDataType),
|
||||
#[error(transparent)]
|
||||
Extract(#[from] ExtractorError),
|
||||
#[error("Expected animation curve to be a string or an array")]
|
||||
CurveType,
|
||||
#[error("Cubic-bezier animation curves must contain exactly four values")]
|
||||
CubicBezierLen,
|
||||
#[error("Cubic-bezier animation curve entries must be finite floats or integers")]
|
||||
CubicBezierValue,
|
||||
#[error("Cubic-bezier x control points must be between 0 and 1")]
|
||||
CubicBezierXRange,
|
||||
}
|
||||
|
||||
pub struct AnimationsParser<'a>(pub &'a Context<'a>);
|
||||
|
||||
impl Parser for AnimationsParser<'_> {
|
||||
type Value = Animations;
|
||||
type Error = AnimationsParserError;
|
||||
const EXPECTED: &'static [DataType] = &[DataType::Table];
|
||||
|
||||
fn parse_table(
|
||||
&mut self,
|
||||
span: Span,
|
||||
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||
) -> ParseResult<Self> {
|
||||
let mut ext = Extractor::new(self.0, span, table);
|
||||
let (enabled, duration_ms, style, curve) = ext.extract((
|
||||
recover(opt(bol("enabled"))),
|
||||
recover(opt(n32("duration-ms"))),
|
||||
recover(opt(str("style"))),
|
||||
opt(val("curve")),
|
||||
))?;
|
||||
let curve = match curve {
|
||||
Some(curve) => Some(parse_curve(curve)?),
|
||||
None => None,
|
||||
};
|
||||
Ok(Animations {
|
||||
enabled: enabled.despan(),
|
||||
duration_ms: duration_ms.despan(),
|
||||
style: style.despan().map(|style| style.to_string()),
|
||||
curve,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_curve(
|
||||
curve: Spanned<&Value>,
|
||||
) -> Result<AnimationCurveConfig, Spanned<AnimationsParserError>> {
|
||||
match curve.value {
|
||||
Value::String(s) => Ok(AnimationCurveConfig::Preset(s.clone())),
|
||||
Value::Array(values) => parse_cubic_bezier(curve.span, values),
|
||||
_ => Err(AnimationsParserError::CurveType.spanned(curve.span)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cubic_bezier(
|
||||
span: Span,
|
||||
values: &[Spanned<Value>],
|
||||
) -> Result<AnimationCurveConfig, Spanned<AnimationsParserError>> {
|
||||
if values.len() != 4 {
|
||||
return Err(AnimationsParserError::CubicBezierLen.spanned(span));
|
||||
}
|
||||
let mut points = [0.0; 4];
|
||||
for (idx, value) in values.iter().enumerate() {
|
||||
let f = match value.value {
|
||||
Value::Float(f) => f,
|
||||
Value::Integer(i) => i as f64,
|
||||
_ => return Err(AnimationsParserError::CubicBezierValue.spanned(value.span)),
|
||||
};
|
||||
if !f.is_finite() {
|
||||
return Err(AnimationsParserError::CubicBezierValue.spanned(value.span));
|
||||
}
|
||||
points[idx] = f as f32;
|
||||
}
|
||||
if !(0.0..=1.0).contains(&points[0]) || !(0.0..=1.0).contains(&points[2]) {
|
||||
return Err(AnimationsParserError::CubicBezierXRange.spanned(span));
|
||||
}
|
||||
Ok(AnimationCurveConfig::CubicBezier(points))
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
use {
|
||||
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;
|
||||
|
|
@ -433,6 +436,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)) {
|
||||
|
|
@ -593,6 +605,7 @@ impl Parser for ConfigParser<'_> {
|
|||
tearing,
|
||||
libei,
|
||||
ui_drag,
|
||||
animations,
|
||||
xwayland,
|
||||
color_management,
|
||||
float,
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ mod toml;
|
|||
use {
|
||||
crate::{
|
||||
config::{
|
||||
Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
|
||||
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch,
|
||||
SimpleCommand, Status, Theme, WindowRule, parse_config,
|
||||
Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
|
||||
ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
|
||||
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config,
|
||||
},
|
||||
rules::{MatcherTemp, RuleMapper},
|
||||
shortcuts::ModeState,
|
||||
|
|
@ -23,7 +23,7 @@ use {
|
|||
ahash::{AHashMap, AHashSet},
|
||||
error_reporter::Report,
|
||||
jay_config::{
|
||||
Axis,
|
||||
AnimationCurve, AnimationStyle, Axis,
|
||||
client::Client,
|
||||
config, config_dir,
|
||||
exec::{Command, set_env, unset_env},
|
||||
|
|
@ -37,8 +37,10 @@ use {
|
|||
is_reload,
|
||||
keyboard::Keymap,
|
||||
logging::{clean_logs_older_than, set_log_level},
|
||||
on_devices_enumerated, on_idle, on_unload, quit, reload, set_autotile,
|
||||
set_color_management_enabled, set_corner_radius, set_default_workspace_capture,
|
||||
on_devices_enumerated, on_idle, on_unload, quit, reload, set_animation_cubic_bezier,
|
||||
set_animation_curve, set_animation_duration_ms, set_animation_style,
|
||||
set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius,
|
||||
set_default_workspace_capture,
|
||||
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle,
|
||||
set_idle_grace_period, set_key_press_enables_dpms, set_middle_click_paste_enabled,
|
||||
set_mouse_move_enables_dpms, set_show_bar, set_show_float_pin_icon, set_show_titles,
|
||||
|
|
@ -1649,6 +1651,38 @@ 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));
|
||||
match config.animations.style.as_deref().unwrap_or("multiphase") {
|
||||
"plain" => set_animation_style(AnimationStyle::PLAIN),
|
||||
"multiphase" => set_animation_style(AnimationStyle::MULTIPHASE),
|
||||
style_name => log::warn!("Unknown animation style: {style_name}"),
|
||||
}
|
||||
match config
|
||||
.animations
|
||||
.curve
|
||||
.unwrap_or_else(|| AnimationCurveConfig::Preset("ease-out".to_string()))
|
||||
{
|
||||
AnimationCurveConfig::Preset(curve_name) => {
|
||||
let curve = match curve_name.as_str() {
|
||||
"linear" => Some(AnimationCurve::LINEAR),
|
||||
"ease" => Some(AnimationCurve::EASE),
|
||||
"ease-in" => Some(AnimationCurve::EASE_IN),
|
||||
"ease-out" => Some(AnimationCurve::EASE_OUT),
|
||||
"ease-in-out" => Some(AnimationCurve::EASE_IN_OUT),
|
||||
_ => {
|
||||
log::warn!("Unknown animation curve: {curve_name}");
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(curve) = curve {
|
||||
set_animation_curve(curve);
|
||||
}
|
||||
}
|
||||
AnimationCurveConfig::CubicBezier([x1, y1, x2, y2]) => {
|
||||
set_animation_cubic_bezier(x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
if let Some(xwayland) = config.xwayland {
|
||||
if let Some(enabled) = xwayland.enabled {
|
||||
set_x_wayland_enabled(enabled);
|
||||
|
|
|
|||
|
|
@ -641,6 +641,61 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"AnimationCurve": {
|
||||
"description": "Describes a window animation curve.\n",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "One of the supported curve presets.\n",
|
||||
"enum": [
|
||||
"linear",
|
||||
"ease",
|
||||
"ease-in",
|
||||
"ease-out",
|
||||
"ease-in-out"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"description": "A custom CSS-style cubic-bezier curve as four numbers:\n`x1`, `y1`, `x2`, and `y2`.\n\nThe implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must\nbe between `0` and `1`.\n",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"AnimationStyle": {
|
||||
"type": "string",
|
||||
"description": "Describes a tiled window movement animation style.\n",
|
||||
"enum": [
|
||||
"plain",
|
||||
"multiphase"
|
||||
]
|
||||
},
|
||||
"Animations": {
|
||||
"description": "Describes window animation settings.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n style = \"multiphase\"\n curve = [0.25, 0.1, 0.25, 1.0]\n ```\n",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enables or disables window animations.\n\nThe default is `false`.\n"
|
||||
},
|
||||
"duration-ms": {
|
||||
"type": "integer",
|
||||
"description": "Sets the animation duration in milliseconds.\n\nThe default is `160`.\n"
|
||||
},
|
||||
"style": {
|
||||
"description": "Sets the animation style used for tiled window movement animations.\n\nThe default is `multiphase`.\n",
|
||||
"$ref": "#/$defs/AnimationStyle"
|
||||
},
|
||||
"curve": {
|
||||
"description": "Sets the animation curve.\n\nThe default is `ease-out`.\n",
|
||||
"$ref": "#/$defs/AnimationCurve"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"BarPosition": {
|
||||
"type": "string",
|
||||
"description": "The position of the bar.",
|
||||
|
|
@ -1085,6 +1140,10 @@
|
|||
"description": "Configures the ui-drag settings.\n\n- Example:\n\n ```toml\n ui-drag = { enabled = false, threshold = 20 }\n ```\n",
|
||||
"$ref": "#/$defs/UiDrag"
|
||||
},
|
||||
"animations": {
|
||||
"description": "Configures window animations.\n\nAnimations are disabled by default.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n style = \"multiphase\"\n curve = \"ease-out\"\n ```\n",
|
||||
"$ref": "#/$defs/Animations"
|
||||
},
|
||||
"xwayland": {
|
||||
"description": "Configures the Xwayland settings.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n",
|
||||
"$ref": "#/$defs/Xwayland"
|
||||
|
|
|
|||
|
|
@ -942,6 +942,125 @@ This table is a tagged union. The variant is determined by the `type` field. It
|
|||
The numbers should be integers.
|
||||
|
||||
|
||||
<a name="types-AnimationCurve"></a>
|
||||
### `AnimationCurve`
|
||||
|
||||
Describes a window animation curve.
|
||||
|
||||
Values of this type should have one of the following forms:
|
||||
|
||||
#### A string
|
||||
|
||||
One of the supported curve presets.
|
||||
|
||||
The string should have one of the following values:
|
||||
|
||||
- `linear`:
|
||||
|
||||
No easing.
|
||||
|
||||
- `ease`:
|
||||
|
||||
The CSS `ease` curve.
|
||||
|
||||
- `ease-in`:
|
||||
|
||||
The CSS `ease-in` curve.
|
||||
|
||||
- `ease-out`:
|
||||
|
||||
The CSS `ease-out` curve.
|
||||
|
||||
- `ease-in-out`:
|
||||
|
||||
The CSS `ease-in-out` curve.
|
||||
|
||||
|
||||
#### An array
|
||||
|
||||
A custom CSS-style cubic-bezier curve as four numbers:
|
||||
`x1`, `y1`, `x2`, and `y2`.
|
||||
|
||||
The implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must
|
||||
be between `0` and `1`.
|
||||
|
||||
Each element of this array should be a number.
|
||||
|
||||
|
||||
<a name="types-AnimationStyle"></a>
|
||||
### `AnimationStyle`
|
||||
|
||||
Describes a tiled window movement animation style.
|
||||
|
||||
Values of this type should be strings.
|
||||
|
||||
The string should have one of the following values:
|
||||
|
||||
- `plain`:
|
||||
|
||||
Uses a single interpolated movement from each window's current visual
|
||||
rectangle to its destination rectangle.
|
||||
|
||||
- `multiphase`:
|
||||
|
||||
Uses the no-overlap multiphase planner for tiled window movement when a
|
||||
supported plan exists.
|
||||
|
||||
|
||||
<a name="types-Animations"></a>
|
||||
### `Animations`
|
||||
|
||||
Describes window animation settings.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[animations]
|
||||
enabled = true
|
||||
duration-ms = 160
|
||||
style = "multiphase"
|
||||
curve = [0.25, 0.1, 0.25, 1.0]
|
||||
```
|
||||
|
||||
Values of this type should be tables.
|
||||
|
||||
The table has the following fields:
|
||||
|
||||
- `enabled` (optional):
|
||||
|
||||
Enables or disables window animations.
|
||||
|
||||
The default is `false`.
|
||||
|
||||
The value of this field should be a boolean.
|
||||
|
||||
- `duration-ms` (optional):
|
||||
|
||||
Sets the animation duration in milliseconds.
|
||||
|
||||
The default is `160`.
|
||||
|
||||
The value of this field should be a number.
|
||||
|
||||
The numbers should be integers.
|
||||
|
||||
- `style` (optional):
|
||||
|
||||
Sets the animation style used for tiled window movement animations.
|
||||
|
||||
The default is `multiphase`.
|
||||
|
||||
The value of this field should be a [AnimationStyle](#types-AnimationStyle).
|
||||
|
||||
- `curve` (optional):
|
||||
|
||||
Sets the animation curve.
|
||||
|
||||
The default is `ease-out`.
|
||||
|
||||
The value of this field should be a [AnimationCurve](#types-AnimationCurve).
|
||||
|
||||
|
||||
<a name="types-BarPosition"></a>
|
||||
### `BarPosition`
|
||||
|
||||
|
|
@ -2169,6 +2288,24 @@ The table has the following fields:
|
|||
|
||||
The value of this field should be a [UiDrag](#types-UiDrag).
|
||||
|
||||
- `animations` (optional):
|
||||
|
||||
Configures window animations.
|
||||
|
||||
Animations are disabled by default.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[animations]
|
||||
enabled = true
|
||||
duration-ms = 160
|
||||
style = "multiphase"
|
||||
curve = "ease-out"
|
||||
```
|
||||
|
||||
The value of this field should be a [Animations](#types-Animations).
|
||||
|
||||
- `xwayland` (optional):
|
||||
|
||||
Configures the Xwayland settings.
|
||||
|
|
@ -5670,4 +5807,3 @@ The table has the following fields:
|
|||
|
||||
The value of this field should be a [XScalingMode](#types-XScalingMode).
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2942,6 +2942,23 @@ Config:
|
|||
```toml
|
||||
ui-drag = { enabled = false, threshold = 20 }
|
||||
```
|
||||
animations:
|
||||
ref: Animations
|
||||
required: false
|
||||
description: |
|
||||
Configures window animations.
|
||||
|
||||
Animations are disabled by default.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[animations]
|
||||
enabled = true
|
||||
duration-ms = 160
|
||||
style = "multiphase"
|
||||
curve = "ease-out"
|
||||
```
|
||||
xwayland:
|
||||
ref: Xwayland
|
||||
required: false
|
||||
|
|
@ -3655,6 +3672,97 @@ UiDrag:
|
|||
The default is `10`.
|
||||
|
||||
|
||||
Animations:
|
||||
kind: table
|
||||
description: |
|
||||
Describes window animation settings.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[animations]
|
||||
enabled = true
|
||||
duration-ms = 160
|
||||
style = "multiphase"
|
||||
curve = [0.25, 0.1, 0.25, 1.0]
|
||||
```
|
||||
fields:
|
||||
enabled:
|
||||
kind: boolean
|
||||
required: false
|
||||
description: |
|
||||
Enables or disables window animations.
|
||||
|
||||
The default is `false`.
|
||||
duration-ms:
|
||||
kind: number
|
||||
integer_only: true
|
||||
required: false
|
||||
description: |
|
||||
Sets the animation duration in milliseconds.
|
||||
|
||||
The default is `160`.
|
||||
style:
|
||||
ref: AnimationStyle
|
||||
required: false
|
||||
description: |
|
||||
Sets the animation style used for tiled window movement animations.
|
||||
|
||||
The default is `multiphase`.
|
||||
curve:
|
||||
ref: AnimationCurve
|
||||
required: false
|
||||
description: |
|
||||
Sets the animation curve.
|
||||
|
||||
The default is `ease-out`.
|
||||
|
||||
|
||||
AnimationStyle:
|
||||
kind: string
|
||||
description: |
|
||||
Describes a tiled window movement animation style.
|
||||
values:
|
||||
- value: plain
|
||||
description: |
|
||||
Uses a single interpolated movement from each window's current visual
|
||||
rectangle to its destination rectangle.
|
||||
- value: multiphase
|
||||
description: |
|
||||
Uses the no-overlap multiphase planner for tiled window movement when a
|
||||
supported plan exists.
|
||||
|
||||
|
||||
AnimationCurve:
|
||||
kind: variable
|
||||
description: |
|
||||
Describes a window animation curve.
|
||||
variants:
|
||||
- kind: string
|
||||
description: |
|
||||
One of the supported curve presets.
|
||||
values:
|
||||
- value: linear
|
||||
description: No easing.
|
||||
- value: ease
|
||||
description: The CSS `ease` curve.
|
||||
- value: ease-in
|
||||
description: The CSS `ease-in` curve.
|
||||
- value: ease-out
|
||||
description: The CSS `ease-out` curve.
|
||||
- value: ease-in-out
|
||||
description: The CSS `ease-in-out` curve.
|
||||
- kind: array
|
||||
items:
|
||||
kind: number
|
||||
description: |
|
||||
A custom CSS-style cubic-bezier curve as four numbers:
|
||||
`x1`, `y1`, `x2`, and `y2`.
|
||||
|
||||
The implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must
|
||||
be between `0` and `1`.
|
||||
|
||||
|
||||
Xwayland:
|
||||
kind: table
|
||||
description: |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue