aboutsummaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-04-13 11:07:23 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-04-13 11:07:23 +0400
commit71be19b234d58f4ec447e921633506beb81a52c0 (patch)
treea6b69d57a3e2edc9d8dac4c969548f227cbf341a /src/layout
parent4fd9300bdb07e90c26df28461f9bd6591c3d1d41 (diff)
downloadniri-71be19b234d58f4ec447e921633506beb81a52c0.tar.gz
niri-71be19b234d58f4ec447e921633506beb81a52c0.tar.bz2
niri-71be19b234d58f4ec447e921633506beb81a52c0.zip
Implement window resize animations
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/mod.rs56
-rw-r--r--src/layout/tile.rs358
-rw-r--r--src/layout/workspace.rs127
3 files changed, 490 insertions, 51 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 073ae804..efcc8167 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -60,6 +60,9 @@ pub mod monitor;
pub mod tile;
pub mod workspace;
+/// Size changes up to this many pixels don't animate.
+pub const RESIZE_ANIMATION_THRESHOLD: i32 = 10;
+
niri_render_elements! {
LayoutElementRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
@@ -70,6 +73,15 @@ niri_render_elements! {
pub type LayoutElementRenderSnapshot =
RenderSnapshot<BakedBuffer<TextureBuffer<GlesTexture>>, BakedBuffer<SolidColorBuffer>>;
+/// Snapshot of an element for animation.
+#[derive(Debug)]
+pub struct AnimationSnapshot {
+ /// Snapshot of the render.
+ pub render: LayoutElementRenderSnapshot,
+ /// Visual size of the element at the point of the snapshot.
+ pub size: Size<i32, Logical>,
+}
+
pub trait LayoutElement {
/// Type that can be used as a unique ID of this element.
type Id: PartialEq;
@@ -108,7 +120,7 @@ pub trait LayoutElement {
fn take_last_render(&self) -> LayoutElementRenderSnapshot;
- fn request_size(&self, size: Size<i32, Logical>);
+ fn request_size(&mut self, size: Size<i32, Logical>, animate: bool);
fn request_fullscreen(&self, size: Size<i32, Logical>);
fn min_size(&self) -> Size<i32, Logical>;
fn max_size(&self) -> Size<i32, Logical>;
@@ -121,7 +133,7 @@ pub trait LayoutElement {
fn set_activated(&mut self, active: bool);
fn set_bounds(&self, bounds: Size<i32, Logical>);
- fn send_pending_configure(&self);
+ fn send_pending_configure(&mut self);
/// Whether the element is currently fullscreen.
///
@@ -137,6 +149,9 @@ pub trait LayoutElement {
/// Runs periodic clean-up tasks.
fn refresh(&self);
+
+ fn animation_snapshot(&self) -> Option<&AnimationSnapshot>;
+ fn take_animation_snapshot(&mut self) -> Option<AnimationSnapshot>;
}
#[derive(Debug)]
@@ -1787,6 +1802,31 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ pub fn prepare_for_resize_animation(&mut self, window: &W::Id) {
+ let _span = tracy_client::span!("Layout::prepare_for_resize_animation");
+
+ match &mut self.monitor_set {
+ MonitorSet::Normal { monitors, .. } => {
+ for mon in monitors {
+ for ws in &mut mon.workspaces {
+ if ws.has_window(window) {
+ ws.prepare_for_resize_animation(window);
+ return;
+ }
+ }
+ }
+ }
+ MonitorSet::NoOutputs { workspaces, .. } => {
+ for ws in workspaces {
+ if ws.has_window(window) {
+ ws.prepare_for_resize_animation(window);
+ return;
+ }
+ }
+ }
+ }
+ }
+
pub fn refresh(&mut self) {
let _span = tracy_client::span!("Layout::refresh");
@@ -1930,7 +1970,7 @@ mod tests {
RenderSnapshot::default()
}
- fn request_size(&self, size: Size<i32, Logical>) {
+ fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
self.0.requested_size.set(Some(size));
self.0.pending_fullscreen.set(false);
}
@@ -1967,7 +2007,7 @@ mod tests {
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
- fn send_pending_configure(&self) {}
+ fn send_pending_configure(&mut self) {}
fn is_fullscreen(&self) -> bool {
false
@@ -1983,6 +2023,14 @@ mod tests {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
+
+ fn animation_snapshot(&self) -> Option<&AnimationSnapshot> {
+ None
+ }
+
+ fn take_animation_snapshot(&mut self) -> Option<AnimationSnapshot> {
+ None
+ }
}
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
diff --git a/src/layout/tile.rs b/src/layout/tile.rs
index 1fcf4f1b..578de86a 100644
--- a/src/layout/tile.rs
+++ b/src/layout/tile.rs
@@ -1,21 +1,31 @@
+use std::cell::OnceCell;
use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
+use niri_config::BlockOutFrom;
+use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::utils::RescaleRenderElement;
use smithay::backend::renderer::element::{Element, Kind};
-use smithay::backend::renderer::gles::GlesRenderer;
-use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
+use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
+use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
-use super::{LayoutElement, LayoutElementRenderElement, Options};
+use super::{
+ AnimationSnapshot, LayoutElement, LayoutElementRenderElement, Options,
+ RESIZE_ANIMATION_THRESHOLD,
+};
use crate::animation::Animation;
use crate::niri_render_elements;
+use crate::render_helpers::crossfade::CrossfadeRenderElement;
use crate::render_helpers::offscreen::OffscreenRenderElement;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
-use crate::render_helpers::{RenderSnapshot, RenderTarget, ToRenderElement};
+use crate::render_helpers::shaders::Shaders;
+use crate::render_helpers::{
+ render_to_encompassing_texture, RenderSnapshot, RenderTarget, ToRenderElement,
+};
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -47,6 +57,12 @@ pub struct Tile<W: LayoutElement> {
/// The animation upon opening a window.
open_animation: Option<Animation>,
+ /// The animation of the window resizing.
+ resize_animation: Option<ResizeAnimation>,
+
+ /// The animation of a tile visually moving.
+ move_animation: Option<MoveAnimation>,
+
/// Configurable properties of the layout.
options: Rc<Options>,
}
@@ -57,17 +73,41 @@ niri_render_elements! {
FocusRing = FocusRingRenderElement,
SolidColor = SolidColorRenderElement,
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
+ Crossfade = CrossfadeRenderElement,
}
}
niri_render_elements! {
- TileSnapshotRenderElement => {
+ TileSnapshotContentsRenderElement => {
Texture = PrimaryGpuTextureRenderElement,
+ SolidColor = SolidColorRenderElement,
+ }
+}
+
+niri_render_elements! {
+ TileSnapshotRenderElement => {
+ Contents = RescaleRenderElement<TileSnapshotContentsRenderElement>,
FocusRing = FocusRingRenderElement,
SolidColor = SolidColorRenderElement,
}
}
+#[derive(Debug)]
+struct ResizeAnimation {
+ anim: Animation,
+ size_from: Size<i32, Logical>,
+ snapshot: AnimationSnapshot,
+ /// Snapshot rendered into a texture (happens lazily).
+ snapshot_texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
+ snapshot_blocked_out_texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
+}
+
+#[derive(Debug)]
+struct MoveAnimation {
+ anim: Animation,
+ from: Point<i32, Logical>,
+}
+
impl<W: LayoutElement> Tile<W> {
pub fn new(window: W, options: Rc<Options>) -> Self {
Self {
@@ -78,6 +118,8 @@ impl<W: LayoutElement> Tile<W> {
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
fullscreen_size: Default::default(),
open_animation: None,
+ resize_animation: None,
+ move_animation: None,
options,
}
}
@@ -93,16 +135,76 @@ impl<W: LayoutElement> Tile<W> {
if self.fullscreen_size != Size::from((0, 0)) {
self.is_fullscreen = self.window.is_fullscreen();
}
+
+ if let Some(animate_from) = self.window.take_animation_snapshot() {
+ let size_from = if let Some(resize) = self.resize_animation.take() {
+ // Compute like in animated_window_size(), but using the snapshot geometry (since
+ // the current one is already overwritten).
+ let mut size = animate_from.size;
+
+ let val = resize.anim.value();
+ let size_from = resize.size_from;
+
+ size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32;
+ size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32;
+
+ size
+ } else {
+ animate_from.size
+ };
+
+ let change = self.window.size().to_point() - size_from.to_point();
+ let change = max(change.x.abs(), change.y.abs());
+ if change > RESIZE_ANIMATION_THRESHOLD {
+ let anim = Animation::new(
+ 0.,
+ 1.,
+ 0.,
+ self.options.animations.window_resize,
+ niri_config::Animation::default_window_resize(),
+ );
+ self.resize_animation = Some(ResizeAnimation {
+ anim,
+ size_from,
+ snapshot: animate_from,
+ snapshot_texture: OnceCell::new(),
+ snapshot_blocked_out_texture: OnceCell::new(),
+ });
+ } else {
+ self.resize_animation = None;
+ }
+ }
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
+ if let Some(anim) = &mut self.open_animation {
+ anim.set_current_time(current_time);
+ if anim.is_done() {
+ self.open_animation = None;
+ }
+ }
+
+ if let Some(resize) = &mut self.resize_animation {
+ resize.anim.set_current_time(current_time);
+ if resize.anim.is_done() {
+ self.resize_animation = None;
+ }
+ }
+
+ if let Some(move_) = &mut self.move_animation {
+ move_.anim.set_current_time(current_time);
+ if move_.anim.is_done() {
+ self.move_animation = None;
+ }
+ }
+
let draw_border_with_background = self
.window
.rules()
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
self.border
- .update(self.window.size(), !draw_border_with_background);
+ .update(self.animated_window_size(), !draw_border_with_background);
self.border.set_active(is_active);
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
@@ -111,22 +213,24 @@ impl<W: LayoutElement> Tile<W> {
draw_border_with_background
};
self.focus_ring
- .update(self.tile_size(), !draw_focus_ring_with_background);
+ .update(self.animated_tile_size(), !draw_focus_ring_with_background);
self.focus_ring.set_active(is_active);
-
- match &mut self.open_animation {
- Some(anim) => {
- anim.set_current_time(current_time);
- if anim.is_done() {
- self.open_animation = None;
- }
- }
- None => (),
- }
}
pub fn are_animations_ongoing(&self) -> bool {
self.open_animation.is_some()
+ || self.resize_animation.is_some()
+ || self.move_animation.is_some()
+ }
+
+ pub fn render_offset(&self) -> Point<i32, Logical> {
+ let mut offset = Point::from((0., 0.));
+
+ if let Some(move_) = &self.move_animation {
+ offset += move_.from.to_f64().upscale(move_.anim.value());
+ }
+
+ offset.to_i32_round()
}
pub fn start_open_animation(&mut self) {
@@ -143,6 +247,24 @@ impl<W: LayoutElement> Tile<W> {
&self.open_animation
}
+ pub fn resize_animation(&self) -> Option<&Animation> {
+ self.resize_animation.as_ref().map(|resize| &resize.anim)
+ }
+
+ pub fn animate_move_from_with_config(
+ &mut self,
+ from: Point<i32, Logical>,
+ config: niri_config::Animation,
+ default: niri_config::Animation,
+ ) {
+ let current_offset = self.render_offset();
+
+ self.move_animation = Some(MoveAnimation {
+ anim: Animation::new(1., 0., 0., config, default),
+ from: from + current_offset,
+ });
+ }
+
pub fn window(&self) -> &W {
&self.window
}
@@ -217,6 +339,39 @@ impl<W: LayoutElement> Tile<W> {
self.window.size()
}
+ fn animated_window_size(&self) -> Size<i32, Logical> {
+ let mut size = self.window.size();
+
+ if let Some(resize) = &self.resize_animation {
+ let val = resize.anim.value();
+ let size_from = resize.size_from;
+
+ size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32;
+ size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32;
+ }
+
+ size
+ }
+
+ fn animated_tile_size(&self) -> Size<i32, Logical> {
+ let mut size = self.animated_window_size();
+
+ if self.is_fullscreen {
+ // Normally we'd just return the fullscreen size here, but this makes things a bit
+ // nicer if a fullscreen window is bigger than the fullscreen size for some reason.
+ size.w = max(size.w, self.fullscreen_size.w);
+ size.h = max(size.h, self.fullscreen_size.h);
+ return size;
+ }
+
+ if let Some(width) = self.effective_border_width() {
+ size.w = size.w.saturating_add(width * 2);
+ size.h = size.h.saturating_add(width * 2);
+ }
+
+ size
+ }
+
pub fn buf_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
loc += self.window_loc();
@@ -234,7 +389,7 @@ impl<W: LayoutElement> Tile<W> {
activation_region.to_f64().contains(point)
}
- pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>) {
+ pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>, animate: bool) {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
@@ -242,7 +397,7 @@ impl<W: LayoutElement> Tile<W> {
size.h = max(1, size.h - width * 2);
}
- self.window.request_size(size);
+ self.window.request_size(size, animate);
}
pub fn tile_width_for_window_width(&self, size: i32) -> i32 {
@@ -330,11 +485,85 @@ impl<W: LayoutElement> Tile<W> {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
- let rv = self
- .window
- .render(renderer, location + self.window_loc(), scale, alpha, target)
+ let window_loc = self.window_loc();
+ let window_size = self.window_size();
+ let animated_window_size = self.animated_window_size();
+ let window_render_loc = location + window_loc;
+ let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
+
+ let gles_renderer = renderer.as_gles_renderer();
+
+ // If we're resizing, try to render a crossfade, or a fallback.
+ let mut crossfade = None;
+ let mut crossfade_fallback = None;
+
+ if let Some(resize) = &self.resize_animation {
+ if Shaders::get(gles_renderer).crossfade.is_some() {
+ if let Some(texture_from) = resize.rendered_texture(gles_renderer, scale, target) {
+ let window_elements =
+ self.window
+ .render(gles_renderer, Point::from((0, 0)), scale, 1., target);
+ let current = render_to_encompassing_texture(
+ gles_renderer,
+ scale,
+ Transform::Normal,
+ Fourcc::Abgr8888,
+ &window_elements,
+ )
+ .map_err(|err| warn!("error rendering window to texture: {err:?}"))
+ .ok();
+
+ if let Some((texture_current, _sync_point, texture_current_geo)) = current {
+ let elem = CrossfadeRenderElement::new(
+ gles_renderer,
+ area,
+ scale,
+ texture_from.clone(),
+ resize.snapshot.size,
+ (texture_current, texture_current_geo),
+ window_size,
+ resize.anim.clamped_value().clamp(0., 1.) as f32,
+ alpha,
+ )
+ .expect("we checked the crossfade shader above");
+ self.window
+ .set_offscreen_element_id(Some(elem.id().clone()));
+ crossfade = Some(elem.into());
+ }
+ }
+ }
+
+ if crossfade.is_none() {
+ let fallback_buffer = SolidColorBuffer::new(area.size, [1., 0., 0., 1.]);
+ crossfade_fallback = Some(
+ SolidColorRenderElement::from_buffer(
+ &fallback_buffer,
+ area.loc.to_physical_precise_round(scale),
+ scale,
+ alpha,
+ Kind::Unspecified,
+ )
+ .into(),
+ );
+ self.window.set_offscreen_element_id(None);
+ }
+ }
+
+ // If we're not resizing, render the window itself.
+ let mut window = None;
+ if crossfade.is_none() && crossfade_fallback.is_none() {
+ window = Some(
+ self.window
+ .render(renderer, window_render_loc, scale, alpha, target)
+ .into_iter()
+ .map(Into::into),
+ );
+ }
+
+ let rv = crossfade
.into_iter()
- .map(Into::into);
+ .chain(crossfade_fallback)
+ .chain(window.into_iter().flatten());
let elem = self.effective_border_width().map(|width| {
self.border
@@ -422,7 +651,7 @@ impl<W: LayoutElement> Tile<W> {
contents: Vec<C>,
) -> Vec<TileSnapshotRenderElement>
where
- E: Into<TileSnapshotRenderElement>,
+ E: Into<TileSnapshotContentsRenderElement>,
C: ToRenderElement<RenderElement = E>,
{
let alpha = if self.is_fullscreen {
@@ -431,10 +660,18 @@ impl<W: LayoutElement> Tile<W> {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
+ let window_size = self.window_size();
+ let animated_window_size = self.animated_window_size();
+ let animated_scale = animated_window_size.to_f64() / window_size.to_f64();
+
let mut rv = vec![];
for baked in contents {
let elem = baked.to_render_element(self.window_loc(), scale, alpha, Kind::Unspecified);
+ let elem: TileSnapshotContentsRenderElement = elem.into();
+
+ let origin = self.window_loc().to_physical_precise_round(scale);
+ let elem = RescaleRenderElement::from_element(elem, origin, animated_scale);
rv.push(elem.into());
}
@@ -483,3 +720,76 @@ impl<W: LayoutElement> Tile<W> {
}
}
}
+
+impl ResizeAnimation {
+ fn rendered_texture(
+ &self,
+ renderer: &mut GlesRenderer,
+ scale: Scale<f64>,
+ target: RenderTarget,
+ ) -> &Option<(GlesTexture, Rectangle<i32, Physical>)> {
+ let block_out = match self.snapshot.render.block_out_from {
+ None => false,
+ Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
+ Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
+ };
+
+ if block_out {
+ self.snapshot_blocked_out_texture.get_or_init(|| {
+ let _span = tracy_client::span!("ResizeAnimation::rendered_texture");
+
+ let elements: Vec<_> = self
+ .snapshot
+ .render
+ .blocked_out_contents
+ .iter()
+ .map(|baked| {
+ baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified)
+ })
+ .collect();
+
+ match render_to_encompassing_texture(
+ renderer,
+ scale,
+ Transform::Normal,
+ Fourcc::Abgr8888,
+ &elements,
+ ) {
+ Ok((texture, _sync_point, geo)) => Some((texture, geo)),
+ Err(err) => {
+ warn!("error rendering snapshot to texture: {err:?}");
+ None
+ }
+ }
+ })
+ } else {
+ self.snapshot_texture.get_or_init(|| {
+ let _span = tracy_client::span!("ResizeAnimation::rendered_texture");
+
+ let elements: Vec<_> = self
+ .snapshot
+ .render
+ .contents
+ .iter()
+ .map(|baked| {
+ baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified)
+ })
+ .collect();
+
+ match render_to_encompassing_texture(
+ renderer,
+ scale,
+ Transform::Normal,
+ Fourcc::Abgr8888,
+ &elements,
+ ) {
+ Ok((texture, _sync_point, geo)) => Some((texture, geo)),
+ Err(err) => {
+ warn!("error rendering snapshot to texture: {err:?}");
+ None
+ }
+ }
+ })
+ }
+ }
+}
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index d5735bbd..202867af 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -190,6 +190,9 @@ pub struct Column<W: LayoutElement> {
/// Animation of the render offset during window swapping.
move_animation: Option<Animation>,
+ /// Width right before a resize animation on one of the windows of the column.
+ pub width_before_resize: Option<i32>,
+
/// Latest known view size for this column's workspace.
view_size: Size<i32, Logical>,
@@ -815,7 +818,7 @@ impl<W: LayoutElement> Workspace<W> {
}
column.active_tile_idx = min(column.active_tile_idx, column.tiles.len() - 1);
- column.update_tile_sizes();
+ column.update_tile_sizes(false);
window
}
@@ -879,16 +882,48 @@ impl<W: LayoutElement> Workspace<W> {
}
pub fn update_window(&mut self, window: &W::Id) {
- let (idx, column) = self
+ let (col_idx, column) = self
.columns
.iter_mut()
.enumerate()
.find(|(_, col)| col.contains(window))
.unwrap();
+ let tile_idx = column
+ .tiles
+ .iter()
+ .position(|tile| tile.window().id() == window)
+ .unwrap();
+
+ let offset = column
+ .width_before_resize
+ .take()
+ .map_or(0, |prev| prev - column.width());
+
column.update_window(window);
- column.update_tile_sizes();
+ column.update_tile_sizes(false);
- if idx == self.active_column_idx
+ // Move other columns in tandem with resizing.
+ if column.tiles[tile_idx].resize_animation().is_some() && offset != 0 {
+ if self.active_column_idx <= col_idx {
+ for col in &mut self.columns[col_idx + 1..] {
+ col.animate_move_from_with_config(
+ offset,
+ self.options.animations.window_resize,
+ niri_config::Animation::default_window_resize(),
+ );
+ }
+ } else {
+ for col in &mut self.columns[..col_idx] {
+ col.animate_move_from_with_config(
+ -offset,
+ self.options.animations.window_resize,
+ niri_config::Animation::default_window_resize(),
+ );
+ }
+ }
+ }
+
+ if col_idx == self.active_column_idx
&& !matches!(self.view_offset_adj, Some(ViewOffsetAdjustment::Gesture(_)))
{
// We might need to move the view to ensure the resized window is still visible.
@@ -896,7 +931,7 @@ impl<W: LayoutElement> Workspace<W> {
// FIXME: we will want to skip the animation in some cases here to make continuously
// resizing windows not look janky.
- self.animate_view_offset_to_column(current_x, idx, None);
+ self.animate_view_offset_to_column(current_x, col_idx, None);
}
}
@@ -1005,6 +1040,16 @@ impl<W: LayoutElement> Workspace<W> {
}
}
+ pub fn prepare_for_resize_animation(&mut self, window: &W::Id) {
+ let column = self
+ .columns
+ .iter_mut()
+ .find(|col| col.contains(window))
+ .unwrap();
+
+ column.width_before_resize = Some(column.width());
+ }
+
#[cfg(test)]
pub fn verify_invariants(&self) {
assert!(self.view_size.w > 0);
@@ -1265,8 +1310,9 @@ impl<W: LayoutElement> Workspace<W> {
// Start with the active window since it's drawn on top.
let col = &self.columns[self.active_column_idx];
let tile = &col.tiles[col.active_tile_idx];
- let tile_pos =
- Point::from((-self.view_offset, col.tile_y(col.active_tile_idx))) + col.render_offset();
+ let tile_pos = Point::from((-self.view_offset, col.tile_y(col.active_tile_idx)))
+ + col.render_offset()
+ + tile.render_offset();
let first = iter::once((tile, tile_pos));
// Next, the rest of the tiles in the active column, since it should be drawn on top as a
@@ -1280,7 +1326,9 @@ impl<W: LayoutElement> Workspace<W> {
return None;
}
- let tile_pos = Point::from((-self.view_offset, y)) + col.render_offset();
+ let tile_pos = Point::from((-self.view_offset, y))
+ + col.render_offset()
+ + tile.render_offset();
Some((tile, tile_pos))
});
@@ -1305,7 +1353,7 @@ impl<W: LayoutElement> Workspace<W> {
})
.flat_map(move |(col, x)| {
zip(&col.tiles, col.tile_ys()).map(move |(tile, y)| {
- let tile_pos = Point::from((x, y)) + col.render_offset();
+ let tile_pos = Point::from((x, y)) + col.render_offset() + tile.render_offset();
(tile, tile_pos)
})
});
@@ -1409,7 +1457,7 @@ impl<W: LayoutElement> Workspace<W> {
let window = col.tiles.remove(tile_idx).into_window();
col.heights.remove(tile_idx);
col.active_tile_idx = min(col.active_tile_idx, col.tiles.len() - 1);
- col.update_tile_sizes();
+ col.update_tile_sizes(false);
let width = col.width;
let is_full_width = col.is_full_width;
@@ -1744,6 +1792,7 @@ impl<W: LayoutElement> Column<W> {
is_full_width,
is_fullscreen: false,
move_animation: None,
+ width_before_resize: None,
view_size,
working_area,
options,
@@ -1768,7 +1817,7 @@ impl<W: LayoutElement> Column<W> {
self.view_size = size;
self.working_area = working_area;
- self.update_tile_sizes();
+ self.update_tile_sizes(false);
}
fn update_config(&mut self, options: Rc<Options>) {
@@ -1798,14 +1847,14 @@ impl<W: LayoutElement> Column<W> {
self.options = options;
if update_sizes {
- self.update_tile_sizes();
+ self.update_tile_sizes(false);
}
}
fn set_width(&mut self, width: ColumnWidth) {
self.width = width;
self.is_full_width = false;
- self.update_tile_sizes();
+ self.update_tile_sizes(true);
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
@@ -1840,14 +1889,27 @@ impl<W: LayoutElement> Column<W> {
}
pub fn animate_move_from(&mut self, from_x_offset: i32) {
+ self.animate_move_from_with_config(
+ from_x_offset,
+ self.options.animations.window_movement,
+ niri_config::Animation::default_window_movement(),
+ );
+ }
+
+ pub fn animate_move_from_with_config(
+ &mut self,
+ from_x_offset: i32,
+ config: niri_config::Animation,
+ default: niri_config::Animation,
+ ) {
let current_offset = self.move_animation.as_ref().map_or(0., Animation::value);
self.move_animation = Some(Animation::new(
f64::from(from_x_offset) + current_offset,
0.,
0.,
- self.options.animations.window_movement,
- niri_config::Animation::default_window_movement(),
+ config,
+ default,
));
}
@@ -1875,19 +1937,38 @@ impl<W: LayoutElement> Column<W> {
self.is_fullscreen = false;
self.tiles.push(tile);
self.heights.push(WindowHeight::Auto);
- self.update_tile_sizes();
+ self.update_tile_sizes(false);
}
fn update_window(&mut self, window: &W::Id) {
- let tile = self
+ let (tile_idx, tile) = self
.tiles
.iter_mut()
- .find(|tile| tile.window().id() == window)
+ .enumerate()
+ .find(|(_, tile)| tile.window().id() == window)
.unwrap();
+
+ let height = tile.window().size().h;
+ let offset = tile
+ .window()
+ .animation_snapshot()
+ .map_or(0, |from| from.size.h - height);
+
tile.update_window();
+
+ // Move windows below in tandem with resizing.
+ if tile.resize_animation().is_some() && offset != 0 {
+ for tile in &mut self.tiles[tile_idx + 1..] {
+ tile.animate_move_from_with_config(
+ Point::from((0, offset)),
+ self.options.animations.window_resize,
+ niri_config::Animation::default_window_resize(),
+ );
+ }
+ }
}
- fn update_tile_sizes(&mut self) {
+ fn update_tile_sizes(&mut self, animate: bool) {
if self.is_fullscreen {
self.tiles[0].request_fullscreen(self.view_size);
return;
@@ -2040,7 +2121,7 @@ impl<W: LayoutElement> Column<W> {
};
let size = Size::from((width, height));
- tile.request_tile_size(size);
+ tile.request_tile_size(size, animate);
}
}
@@ -2127,7 +2208,7 @@ impl<W: LayoutElement> Column<W> {
fn toggle_full_width(&mut self) {
self.is_full_width = !self.is_full_width;
- self.update_tile_sizes();
+ self.update_tile_sizes(true);
}
fn set_column_width(&mut self, change: SizeChange) {
@@ -2226,13 +2307,13 @@ impl<W: LayoutElement> Column<W> {
}
self.heights[self.active_tile_idx] = WindowHeight::Fixed(window_height.clamp(1, MAX_PX));
- self.update_tile_sizes();
+ self.update_tile_sizes(true);
}
fn set_fullscreen(&mut self, is_fullscreen: bool) {
assert_eq!(self.tiles.len(), 1);
self.is_fullscreen = is_fullscreen;
- self.update_tile_sizes();
+ self.update_tile_sizes(false);
}
pub fn window_y(&self, tile_idx: usize) -> i32 {