aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/backend/tty.rs3
-rw-r--r--src/backend/winit.rs3
-rw-r--r--src/handlers/xdg_shell.rs33
-rw-r--r--src/layout/mod.rs56
-rw-r--r--src/layout/tile.rs358
-rw-r--r--src/layout/workspace.rs127
-rw-r--r--src/render_helpers/crossfade.rs156
-rw-r--r--src/render_helpers/mod.rs3
-rw-r--r--src/render_helpers/primary_gpu_pixel_shader_with_textures.rs423
-rw-r--r--src/render_helpers/resources.rs106
-rw-r--r--src/render_helpers/shaders/crossfade.frag31
-rw-r--r--src/render_helpers/shaders/mod.rs24
-rw-r--r--src/render_helpers/shaders/texture.vert25
-rw-r--r--src/window/mapped.rs91
14 files changed, 1368 insertions, 71 deletions
diff --git a/src/backend/tty.rs b/src/backend/tty.rs
index eedb47a2..0d2aaed2 100644
--- a/src/backend/tty.rs
+++ b/src/backend/tty.rs
@@ -59,7 +59,7 @@ use super::{IpcOutputMap, RenderResult};
use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::renderer::AsGlesRenderer;
-use crate::render_helpers::{shaders, RenderTarget};
+use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
@@ -490,6 +490,7 @@ impl Tty {
warn!("error binding wl-display in EGL: {err:?}");
}
+ resources::init(renderer.as_gles_renderer());
shaders::init(renderer.as_gles_renderer());
// Create the dmabuf global.
diff --git a/src/backend/winit.rs b/src/backend/winit.rs
index 0c5fc1fc..9e05822f 100644
--- a/src/backend/winit.rs
+++ b/src/backend/winit.rs
@@ -19,7 +19,7 @@ use smithay::reexports::winit::window::WindowBuilder;
use super::{IpcOutputMap, RenderResult};
use crate::niri::{Niri, RedrawState, State};
-use crate::render_helpers::{shaders, RenderTarget};
+use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit {
@@ -130,6 +130,7 @@ impl Winit {
warn!("error binding renderer wl_display: {err}");
}
+ resources::init(renderer);
shaders::init(renderer);
niri.add_output(self.output.clone(), None);
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index 85ce5915..ab9d7072 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -815,16 +815,34 @@ fn unconstrain_with_padding(
pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId {
add_pre_commit_hook::<State, _>(toplevel.wl_surface(), move |state, _dh, surface| {
+ let _span = tracy_client::span!("mapped toplevel pre-commit");
+
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
return;
};
- let got_unmapped = with_states(surface, |states| {
- let attrs = states.cached_state.current::<SurfaceAttributes>();
- matches!(attrs.buffer, Some(BufferAssignment::Removed))
+ let (got_unmapped, commit_serial) = with_states(surface, |states| {
+ let attrs = states.cached_state.pending::<SurfaceAttributes>();
+ let got_unmapped = matches!(attrs.buffer, Some(BufferAssignment::Removed));
+
+ let role = states
+ .data_map
+ .get::<XdgToplevelSurfaceData>()
+ .unwrap()
+ .lock()
+ .unwrap();
+
+ (got_unmapped, role.configure_serial)
});
+ let animate = if let Some(serial) = commit_serial {
+ mapped.should_animate_commit(serial)
+ } else {
+ error!("commit on a mapped surface without a configured serial");
+ false
+ };
+
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
mapped.render_and_store_snapshot(renderer);
@@ -832,6 +850,15 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
} else {
// The toplevel remains mapped; clear any cached render snapshot.
let _ = mapped.take_last_render();
+
+ if animate {
+ state.backend.with_primary_renderer(|renderer| {
+ mapped.store_animation_snapshot(renderer);
+ });
+
+ let window = mapped.window.clone();
+ state.niri.layout.prepare_for_resize_animation(&window);
+ }
}
})
}
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