From 02eccf7762bf01cca0bf066d769e2533e2d5a3d2 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Sat, 8 Feb 2025 15:24:02 +0300 Subject: layout: Fix/add animations around tabbed columns --- src/layout/scrolling.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++- src/layout/tile.rs | 48 +++++++++++++++++++++++++++--- src/layout/workspace.rs | 8 ++++- 3 files changed, 127 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 55ddfc1b..59681cdf 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -865,6 +865,19 @@ impl ScrollingSpace { } } + let target_column = &mut self.columns[col_idx]; + if target_column.display_mode == ColumnDisplay::Tabbed { + if target_column.active_tile_idx == tile_idx { + // Fade out the previously active tile. + let tile = &mut target_column.tiles[prev_active_tile_idx]; + tile.animate_alpha(1., 0., self.options.animations.window_movement.0); + } else { + // Fade out when adding into a tabbed column into the background. + let tile = &mut target_column.tiles[tile_idx]; + tile.animate_alpha(1., 0., self.options.animations.window_movement.0); + } + } + // Adding a wider window into a column increases its width now (even if the window will // shrink later). Move the columns to account for this. let offset = self.column_x(col_idx + 1) - prev_next_x; @@ -1004,6 +1017,8 @@ impl ScrollingSpace { let column = &mut self.columns[column_idx]; let prev_width = self.data[column_idx].width; + let movement_config = anim_config.unwrap_or(self.options.animations.window_movement.0); + // Animate movement of other tiles. // FIXME: tiles can move by X too, in a centered or resizing layout with one window smaller // than the others. @@ -1012,6 +1027,12 @@ impl ScrollingSpace { tile.animate_move_y_from(offset_y); } + if column.display_mode == ColumnDisplay::Tabbed && tile_idx != column.active_tile_idx { + // Fade in when removing background tab from a tabbed column. + let tile = &mut column.tiles[tile_idx]; + tile.animate_alpha(0., 1., movement_config); + } + let tile = column.tiles.remove(tile_idx); column.data.remove(tile_idx); @@ -1036,6 +1057,7 @@ impl ScrollingSpace { is_floating: false, }; + #[allow(clippy::comparison_chain)] // What do you even want here? if tile_idx < column.active_tile_idx { // A tile above was removed; preserve the current position. column.active_tile_idx -= 1; @@ -1044,6 +1066,9 @@ impl ScrollingSpace { if tile_idx == column.tiles.len() { // The bottom tile was removed and it was active, update active idx to remain valid. column.activate_idx(tile_idx - 1); + } else { + // Ensure the newly active tile animates to opaque. + column.tiles[tile_idx].ensure_alpha_animates_to_1(); } } @@ -1052,7 +1077,6 @@ impl ScrollingSpace { let offset = prev_width - column.width(); // Animate movement of the other columns. - let movement_config = anim_config.unwrap_or(self.options.animations.window_movement.0); if self.active_column_idx <= column_idx { for col in &mut self.columns[column_idx + 1..] { col.animate_move_from_with_config(offset, movement_config); @@ -1980,6 +2004,7 @@ impl ScrollingSpace { // Animations self.columns[target_column_idx].tiles[target_tile_idx] .animate_move_from(source_pt - target_pt); + self.columns[target_column_idx].tiles[target_tile_idx].ensure_alpha_animates_to_1(); // FIXME: this stop_move_animations() causes the target tile animation to "reset" when // swapping. It's here as a workaround to stop the unwanted animation of moving the source @@ -1989,6 +2014,7 @@ impl ScrollingSpace { self.columns[source_column_idx].tiles[source_tile_idx].stop_move_animations(); self.columns[source_column_idx].tiles[source_tile_idx] .animate_move_from(target_pt - source_pt); + self.columns[source_column_idx].tiles[source_tile_idx].ensure_alpha_animates_to_1(); self.activate_column(target_column_idx); } @@ -2602,6 +2628,12 @@ impl ScrollingSpace { let focus_ring = focus_ring && first; first = false; + // In the scrolling layout, we currently use visible only for hidden tabs in the + // tabbed mode. We want to animate their opacity when going in and out of tabbed + // mode, so we don't want to apply "visible" immediately. However, "visible" is + // also used for input handling, and there we *do* want to apply it immediately. + // So, let's just selectively ignore "visible" here when animating alpha. + let visible = visible || tile.alpha_animation.is_some(); if !visible { continue; } @@ -3521,6 +3553,8 @@ impl Column { self.active_tile_idx = idx; + self.tiles[idx].ensure_alpha_animates_to_1(); + true } @@ -4258,6 +4292,47 @@ impl Column { return; } + // Animate the movement. + // + // We're doing some shortcuts here because we know that currently normal vs. tabbed can + // only cause a vertical shift + a shift to the origin. + // + // Doing it this way to avoid storing all tile positions in a vector. If more display modes + // are added it might be simpler to just collect everything into a smallvec. + let prev_origin = self.tiles_origin(); + self.display_mode = display; + let new_origin = self.tiles_origin(); + let origin_delta = prev_origin - new_origin; + + // When need to walk the tiles in the normal display mode to get the right offsets. + self.display_mode = ColumnDisplay::Normal; + for (tile, pos) in self.tiles_mut() { + let mut y_delta = pos.y - prev_origin.y; + + // Invert the Y motion when transitioning *to* normal display mode. + if display == ColumnDisplay::Normal { + y_delta *= -1.; + } + + let mut delta = origin_delta; + delta.y += y_delta; + tile.animate_move_from(delta); + } + + // Animate the opacity. + for (idx, tile) in self.tiles.iter_mut().enumerate() { + let is_active = idx == self.active_tile_idx; + if !is_active { + let (from, to) = if display == ColumnDisplay::Tabbed { + (1., 0.) + } else { + (0., 1.) + }; + tile.animate_alpha(from, to, self.options.animations.window_movement.0); + } + } + + // Now switch the display mode for real. self.display_mode = display; self.update_tile_sizes(true); } diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 7a49f5ed..7877457b 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -84,6 +84,9 @@ pub struct Tile { /// The animation of a tile visually moving vertically. move_y_animation: Option, + /// The animation of the tile's opacity. + pub(super) alpha_animation: Option, + /// Offset during the initial interactive move rubberband. pub(super) interactive_move_offset: Point, @@ -168,6 +171,7 @@ impl Tile { resize_animation: None, move_x_animation: None, move_y_animation: None, + alpha_animation: None, interactive_move_offset: Point::from((0., 0.)), unmap_snapshot: None, rounded_corner_damage: Default::default(), @@ -301,6 +305,12 @@ impl Tile { self.move_y_animation = None; } } + + if let Some(alpha) = &mut self.alpha_animation { + if alpha.is_done() { + self.alpha_animation = None; + } + } } pub fn are_animations_ongoing(&self) -> bool { @@ -308,10 +318,12 @@ impl Tile { || self.resize_animation.is_some() || self.move_x_animation.is_some() || self.move_y_animation.is_some() + || self.alpha_animation.is_some() } pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle) { let rules = self.window.rules(); + let alpha = self.tile_alpha(); let draw_border_with_background = rules .draw_border_with_background @@ -336,7 +348,7 @@ impl Tile { ), radius, self.scale, - 1., + alpha, ); let radius = if self.is_fullscreen { @@ -351,7 +363,7 @@ impl Tile { is_active, radius, self.scale, - 1., + alpha, ); let draw_focus_ring_with_background = if self.effective_border_width().is_some() { @@ -367,7 +379,7 @@ impl Tile { view_rect, radius, self.scale, - 1., + alpha, ); } @@ -452,6 +464,31 @@ impl Tile { self.move_y_animation = None; } + pub fn animate_alpha(&mut self, from: f64, to: f64, config: niri_config::Animation) { + let from = from.clamp(0., 1.); + let to = to.clamp(0., 1.); + let current = self.alpha_animation.take().map(|anim| anim.clamped_value()); + let current = current.unwrap_or(from); + self.alpha_animation = Some(Animation::new(self.clock.clone(), current, to, 0., config)); + } + + pub fn ensure_alpha_animates_to_1(&mut self) { + if let Some(anim) = &self.alpha_animation { + if anim.to() != 1. { + // Cancel animation instead of starting a new one because the user likely wants to + // see the tile right away. + self.alpha_animation = None; + } + } + } + + /// Opacity that applies to both window and decorations. + fn tile_alpha(&self) -> f32 { + self.alpha_animation + .as_ref() + .map_or(1., |anim| anim.clamped_value()) as f32 + } + pub fn window(&self) -> &W { &self.window } @@ -740,6 +777,9 @@ impl Tile { self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.) }; + let tile_alpha = self.tile_alpha(); + let win_alpha = win_alpha * tile_alpha; + let window_loc = self.window_loc(); let window_size = self.window_size().to_f64(); let animated_window_size = self.animated_window_size(); @@ -921,7 +961,7 @@ impl Tile { SolidColorRenderElement::from_buffer( &self.fullscreen_backdrop, location, - 1., + tile_alpha, Kind::Unspecified, ) .into() diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 9fd57baa..b5392644 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -1688,7 +1688,7 @@ impl Workspace { ); } - for (tile, tile_pos, _visible) in self.tiles_with_render_positions() { + for (tile, tile_pos, visible) in self.tiles_with_render_positions() { if Some(tile.window().id()) != move_win_id { assert_eq!(tile.interactive_move_offset, Point::from((0., 0.))); } @@ -1698,6 +1698,12 @@ impl Workspace { // Tile positions must be rounded to physical pixels. assert_abs_diff_eq!(tile_pos.x, rounded_pos.x, epsilon = 1e-5); assert_abs_diff_eq!(tile_pos.y, rounded_pos.y, epsilon = 1e-5); + + if let Some(anim) = &tile.alpha_animation { + if visible { + assert_eq!(anim.to(), 1., "visible tiles can animate alpha only to 1"); + } + } } } } -- cgit