feat: add window animations
This commit is contained in:
parent
eece44a59c
commit
2a079ed800
29 changed files with 6957 additions and 114 deletions
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue