Add retained spawn-out animations
This commit is contained in:
parent
d0cc5dc3c7
commit
fa5c28ca3d
9 changed files with 331 additions and 8 deletions
|
|
@ -14,8 +14,9 @@ be handled deliberately.
|
|||
in-flight windows keep their existing timelines.
|
||||
- Spawn-in uses scale and position for newly mapped tiled and floating app
|
||||
windows. Layer-shell, overlay, override-redirect, and fullscreen surfaces do
|
||||
not use this path. Spawn-out requires retained visual content after the live
|
||||
node is gone and remains deferred.
|
||||
not use this path. Spawn-out uses retained visual content after the live node
|
||||
is gone, when a stable retained surface tree can be captured before unmap or
|
||||
destroy.
|
||||
- Command-driven tile-to-float and float-to-tile transitions may animate.
|
||||
Protocol drag/drop paths do not.
|
||||
- The no-overlap multiphase system is a separate phase after the linear path is
|
||||
|
|
@ -90,7 +91,8 @@ Initial scope:
|
|||
- Live client buffers are rendered in Phase 1. Retained content freezing is
|
||||
deferred, but animated windows must still be clipped to their presentation
|
||||
bounds and must preserve the existing stretch behavior for undersized contents.
|
||||
- No spawn-out.
|
||||
- Spawn-out is retained-content-only. If the surface cannot be retained safely
|
||||
the window snaps out instead of animating an empty frame.
|
||||
- No multiphase no-overlap planner.
|
||||
|
||||
Tests:
|
||||
|
|
@ -116,6 +118,9 @@ Initial retained-record implementation status:
|
|||
for both tiled windows and floating child contents.
|
||||
- Tile-to-float and float-to-tile transitions retain GPU/dmabuf-backed child
|
||||
contents while the presentation geometry changes.
|
||||
- Spawn-out captures retained app-window contents before XDG/Xwayland unmap or
|
||||
destroy, then renders a detached shrinking presentation record until the
|
||||
animation completes.
|
||||
- Retained records hold both `GfxTexture` and `SurfaceBuffer` references so the
|
||||
existing buffer release/sync path remains authoritative.
|
||||
- Single-pixel buffers can be retained as color records.
|
||||
|
|
|
|||
117
src/animation.rs
117
src/animation.rs
|
|
@ -57,6 +57,7 @@ pub struct AnimationState {
|
|||
pub duration_ms: Cell<u32>,
|
||||
pub curve: Cell<AnimationCurve>,
|
||||
windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
|
||||
exits: RefCell<Vec<ExitAnimation>>,
|
||||
tick: CloneCell<Option<Rc<AnimationTick>>>,
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +93,21 @@ pub enum RetainedContent {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum RetainedExitLayer {
|
||||
Tiled,
|
||||
Floating,
|
||||
}
|
||||
|
||||
pub struct RetainedExitFrame {
|
||||
pub rect: Rect,
|
||||
pub retained: Rc<RetainedToplevel>,
|
||||
pub frame_inset: i32,
|
||||
pub source_body_size: (i32, i32),
|
||||
pub active: bool,
|
||||
pub layer: RetainedExitLayer,
|
||||
}
|
||||
|
||||
impl RetainedToplevel {
|
||||
pub fn capture_surface(surface: &WlSurface, offset: (i32, i32)) -> Option<Rc<Self>> {
|
||||
Some(Rc::new(Self {
|
||||
|
|
@ -173,6 +189,7 @@ impl Default for AnimationState {
|
|||
duration_ms: Cell::new(DEFAULT_DURATION_MS),
|
||||
curve: Cell::new(AnimationCurve::EaseOut),
|
||||
windows: Default::default(),
|
||||
exits: Default::default(),
|
||||
tick: Default::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -181,6 +198,7 @@ impl Default for AnimationState {
|
|||
impl AnimationState {
|
||||
pub fn clear(&self) {
|
||||
self.windows.borrow_mut().clear();
|
||||
self.exits.borrow_mut().clear();
|
||||
if let Some(tick) = self.tick.take() {
|
||||
tick.detach();
|
||||
}
|
||||
|
|
@ -246,6 +264,42 @@ impl AnimationState {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn set_spawn_out(
|
||||
&self,
|
||||
from: Rect,
|
||||
frame_inset: i32,
|
||||
retained: Rc<RetainedToplevel>,
|
||||
active: bool,
|
||||
layer: RetainedExitLayer,
|
||||
now_nsec: u64,
|
||||
duration_ms: u32,
|
||||
) -> bool {
|
||||
if from.is_empty() || duration_ms == 0 {
|
||||
return false;
|
||||
}
|
||||
let to = spawn_in_start_rect(from);
|
||||
if to == from || to.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let source_body_size = body_size_for_frame(from, frame_inset);
|
||||
if source_body_size.0 <= 0 || source_body_size.1 <= 0 {
|
||||
return false;
|
||||
}
|
||||
self.exits.borrow_mut().push(ExitAnimation {
|
||||
from,
|
||||
to,
|
||||
start_nsec: now_nsec,
|
||||
duration_nsec: duration_ms as u64 * 1_000_000,
|
||||
last_damage: from,
|
||||
retained,
|
||||
frame_inset,
|
||||
source_body_size,
|
||||
active,
|
||||
layer,
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect {
|
||||
let windows = self.windows.borrow();
|
||||
match windows.get(&node_id) {
|
||||
|
|
@ -266,6 +320,22 @@ impl AnimationState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exit_frames(&self, now_nsec: u64) -> Vec<RetainedExitFrame> {
|
||||
self.exits
|
||||
.borrow()
|
||||
.iter()
|
||||
.filter(|exit| !exit.done(now_nsec))
|
||||
.map(|exit| RetainedExitFrame {
|
||||
rect: exit.rect_at(now_nsec),
|
||||
retained: exit.retained.clone(),
|
||||
frame_inset: exit.frame_inset,
|
||||
source_body_size: exit.source_body_size,
|
||||
active: exit.active,
|
||||
layer: exit.layer,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn damage_active(&self, state: &State, now_nsec: u64) -> bool {
|
||||
let mut damages = vec![];
|
||||
let mut any_active = false;
|
||||
|
|
@ -283,6 +353,18 @@ impl AnimationState {
|
|||
any_active |= active;
|
||||
active
|
||||
});
|
||||
self.exits.borrow_mut().retain_mut(|exit| {
|
||||
let current = exit.rect_at(now_nsec);
|
||||
let damage = exit.last_damage.union(current).union(exit.to);
|
||||
damages.push(expand_damage_rect(
|
||||
damage,
|
||||
state.theme.sizes.border_width.get().max(0),
|
||||
));
|
||||
exit.last_damage = current;
|
||||
let active = !exit.done(now_nsec);
|
||||
any_active |= active;
|
||||
active
|
||||
});
|
||||
}
|
||||
for damage in damages {
|
||||
state.damage(damage);
|
||||
|
|
@ -329,6 +411,34 @@ impl WindowAnimation {
|
|||
}
|
||||
}
|
||||
|
||||
struct ExitAnimation {
|
||||
from: Rect,
|
||||
to: Rect,
|
||||
start_nsec: u64,
|
||||
duration_nsec: u64,
|
||||
last_damage: Rect,
|
||||
retained: Rc<RetainedToplevel>,
|
||||
frame_inset: i32,
|
||||
source_body_size: (i32, i32),
|
||||
active: bool,
|
||||
layer: RetainedExitLayer,
|
||||
}
|
||||
|
||||
impl ExitAnimation {
|
||||
fn done(&self, now_nsec: u64) -> bool {
|
||||
now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec
|
||||
}
|
||||
|
||||
fn rect_at(&self, now_nsec: u64) -> Rect {
|
||||
if self.duration_nsec == 0 {
|
||||
return self.to;
|
||||
}
|
||||
let elapsed = now_nsec.saturating_sub(self.start_nsec);
|
||||
let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
|
||||
lerp_rect(self.from, self.to, t)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnimationTick {
|
||||
state: Weak<State>,
|
||||
slf: Weak<dyn LatchListener>,
|
||||
|
|
@ -389,6 +499,13 @@ pub(crate) fn spawn_in_start_rect(target: Rect) -> Rect {
|
|||
)
|
||||
}
|
||||
|
||||
fn body_size_for_frame(rect: Rect, frame_inset: i32) -> (i32, i32) {
|
||||
(
|
||||
rect.width().saturating_sub(2 * frame_inset),
|
||||
rect.height().saturating_sub(2 * frame_inset),
|
||||
)
|
||||
}
|
||||
|
||||
fn lerp_rect(from: Rect, to: Rect, t: f64) -> Rect {
|
||||
fn lerp(from: i32, to: i32, t: f64) -> i32 {
|
||||
(from as f64 + (to as f64 - from as f64) * t).round() as i32
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -253,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,
|
||||
|
|
@ -275,6 +280,7 @@ impl Xwindow {
|
|||
match map_change {
|
||||
Change::None => return,
|
||||
Change::Unmap => {
|
||||
self.queue_spawn_out();
|
||||
self.data
|
||||
.info
|
||||
.pending_extents
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,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();
|
||||
{
|
||||
|
|
@ -399,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>,
|
||||
|
|
@ -824,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();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
use {
|
||||
crate::{
|
||||
animation::{RetainedContent, RetainedSurface, RetainedToplevel},
|
||||
animation::{
|
||||
RetainedContent, RetainedExitFrame, RetainedExitLayer, RetainedSurface,
|
||||
RetainedToplevel,
|
||||
},
|
||||
cmm::cmm_render_intent::RenderIntent,
|
||||
gfx_api::{AcquireSync, AlphaMode, BufferResv, GfxApiOpt, ReleaseSync, SampleRect},
|
||||
ifs::wl_surface::{
|
||||
|
|
@ -201,6 +204,9 @@ 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() {
|
||||
|
|
@ -221,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();
|
||||
|
|
@ -504,6 +511,68 @@ impl Renderer<'_> {
|
|||
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;
|
||||
}
|
||||
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
|
||||
};
|
||||
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_retained_toplevel(&frame.retained, body.x1(), body.y1(), Some(&bounds));
|
||||
self.stretch = None;
|
||||
self.corner_radius = None;
|
||||
}
|
||||
|
||||
fn render_retained_surface_scaled(
|
||||
&mut self,
|
||||
retained: &RetainedSurface,
|
||||
|
|
|
|||
34
src/state.rs
34
src/state.rs
|
|
@ -3,8 +3,8 @@ use {
|
|||
acceptor::Acceptor,
|
||||
allocator::BufferObject,
|
||||
animation::{
|
||||
AnimationCurve, AnimationState, AnimationTick, RetainedToplevel, expand_damage_rect,
|
||||
spawn_in_start_rect,
|
||||
AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel,
|
||||
expand_damage_rect, spawn_in_start_rect,
|
||||
},
|
||||
async_engine::{AsyncEngine, SpawnedFuture},
|
||||
backend::{
|
||||
|
|
@ -1572,6 +1572,36 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use {
|
||||
crate::{
|
||||
animation::RetainedToplevel,
|
||||
animation::{RetainedExitLayer, RetainedToplevel},
|
||||
client::{Client, ClientId},
|
||||
criteria::{
|
||||
CritDestroyListener, CritMatcherId,
|
||||
|
|
@ -988,6 +988,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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue