1
0
Fork 0
forked from wry/wry

feat: add window animations

This commit is contained in:
atagen 2026-05-21 15:20:46 +10:00 committed by kossLAN
parent eece44a59c
commit 2a079ed800
No known key found for this signature in database
29 changed files with 6957 additions and 114 deletions

View file

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

View file

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

View file

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

View file

@ -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());
}