diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-04-13 11:07:23 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-04-13 11:07:23 +0400 |
| commit | 71be19b234d58f4ec447e921633506beb81a52c0 (patch) | |
| tree | a6b69d57a3e2edc9d8dac4c969548f227cbf341a | |
| parent | 4fd9300bdb07e90c26df28461f9bd6591c3d1d41 (diff) | |
| download | niri-71be19b234d58f4ec447e921633506beb81a52c0.tar.gz niri-71be19b234d58f4ec447e921633506beb81a52c0.tar.bz2 niri-71be19b234d58f4ec447e921633506beb81a52c0.zip | |
Implement window resize animations
| -rw-r--r-- | Cargo.lock | 4 | ||||
| -rw-r--r-- | niri-config/src/lib.rs | 14 | ||||
| -rw-r--r-- | niri-visual-tests/src/cases/tile.rs | 9 | ||||
| -rw-r--r-- | niri-visual-tests/src/cases/window.rs | 14 | ||||
| -rw-r--r-- | niri-visual-tests/src/smithay_view.rs | 3 | ||||
| -rw-r--r-- | niri-visual-tests/src/test_window.rs | 16 | ||||
| -rw-r--r-- | src/backend/tty.rs | 3 | ||||
| -rw-r--r-- | src/backend/winit.rs | 3 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 33 | ||||
| -rw-r--r-- | src/layout/mod.rs | 56 | ||||
| -rw-r--r-- | src/layout/tile.rs | 358 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 127 | ||||
| -rw-r--r-- | src/render_helpers/crossfade.rs | 156 | ||||
| -rw-r--r-- | src/render_helpers/mod.rs | 3 | ||||
| -rw-r--r-- | src/render_helpers/primary_gpu_pixel_shader_with_textures.rs | 423 | ||||
| -rw-r--r-- | src/render_helpers/resources.rs | 106 | ||||
| -rw-r--r-- | src/render_helpers/shaders/crossfade.frag | 31 | ||||
| -rw-r--r-- | src/render_helpers/shaders/mod.rs | 24 | ||||
| -rw-r--r-- | src/render_helpers/shaders/texture.vert | 25 | ||||
| -rw-r--r-- | src/window/mapped.rs | 91 | ||||
| -rw-r--r-- | wiki/Configuration:-Animations.md | 21 |
21 files changed, 1432 insertions, 88 deletions
@@ -3115,7 +3115,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay" version = "0.3.0" -source = "git+https://github.com/Smithay/smithay.git#e237b077bd922e17849eff91ba05853c7a68f958" +source = "git+https://github.com/Smithay/smithay.git#c5e9a697e41f50dc56b918d9ef1e3d2e52c84ac0" dependencies = [ "appendlist", "bitflags 2.5.0", @@ -3187,7 +3187,7 @@ dependencies = [ [[package]] name = "smithay-drm-extras" version = "0.1.0" -source = "git+https://github.com/Smithay/smithay.git#e237b077bd922e17849eff91ba05853c7a68f958" +source = "git+https://github.com/Smithay/smithay.git#c5e9a697e41f50dc56b918d9ef1e3d2e52c84ac0" dependencies = [ "drm", "edid-rs", diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index e1ad0b2b..59cf01cf 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -489,6 +489,8 @@ pub struct Animations { pub window_open: Animation, #[knuffel(child, default = Animation::default_window_close())] pub window_close: Animation, + #[knuffel(child, default = Animation::default_window_resize())] + pub window_resize: Animation, #[knuffel(child, default = Animation::default_config_notification_open_close())] pub config_notification_open_close: Animation, } @@ -503,6 +505,7 @@ impl Default for Animations { window_movement: Animation::default_window_movement(), window_open: Animation::default_window_open(), window_close: Animation::default_window_close(), + window_resize: Animation::default_window_resize(), config_notification_open_close: Animation::default_config_notification_open_close(), } } @@ -592,6 +595,17 @@ impl Animation { }), } } + + pub const fn default_window_resize() -> Self { + Self { + off: false, + kind: AnimationKind::Spring(SpringParams { + damping_ratio: 1., + stiffness: 800, + epsilon: 0.0001, + }), + } + } } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs index 5b2f7cb8..93aa38a0 100644 --- a/niri-visual-tests/src/cases/tile.rs +++ b/niri-visual-tests/src/cases/tile.rs @@ -20,7 +20,7 @@ impl Tile { pub fn freeform(size: Size<i32, Logical>) -> Self { let window = TestWindow::freeform(0); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size); + rv.tile.request_tile_size(size, false); rv.window.communicate(); rv } @@ -28,7 +28,7 @@ impl Tile { pub fn fixed_size(size: Size<i32, Logical>) -> Self { let window = TestWindow::fixed_size(0); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size); + rv.tile.request_tile_size(size, false); rv.window.communicate(); rv } @@ -37,7 +37,7 @@ impl Tile { let window = TestWindow::fixed_size(0); window.set_csd_shadow_width(64); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size); + rv.tile.request_tile_size(size, false); rv.window.communicate(); rv } @@ -84,7 +84,8 @@ impl Tile { impl TestCase for Tile { fn resize(&mut self, width: i32, height: i32) { - self.tile.request_tile_size(Size::from((width, height))); + self.tile + .request_tile_size(Size::from((width, height)), false); self.window.communicate(); } diff --git a/niri-visual-tests/src/cases/window.rs b/niri-visual-tests/src/cases/window.rs index 3cd4c0b8..f19ec5ba 100644 --- a/niri-visual-tests/src/cases/window.rs +++ b/niri-visual-tests/src/cases/window.rs @@ -13,23 +13,23 @@ pub struct Window { impl Window { pub fn freeform(size: Size<i32, Logical>) -> Self { - let window = TestWindow::freeform(0); - window.request_size(size); + let mut window = TestWindow::freeform(0); + window.request_size(size, false); window.communicate(); Self { window } } pub fn fixed_size(size: Size<i32, Logical>) -> Self { - let window = TestWindow::fixed_size(0); - window.request_size(size); + let mut window = TestWindow::fixed_size(0); + window.request_size(size, false); window.communicate(); Self { window } } pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self { - let window = TestWindow::fixed_size(0); + let mut window = TestWindow::fixed_size(0); window.set_csd_shadow_width(64); - window.request_size(size); + window.request_size(size, false); window.communicate(); Self { window } } @@ -37,7 +37,7 @@ impl Window { impl TestCase for Window { fn resize(&mut self, width: i32, height: i32) { - self.window.request_size(Size::from((width, height))); + self.window.request_size(Size::from((width, height)), false); self.window.communicate(); } diff --git a/niri-visual-tests/src/smithay_view.rs b/niri-visual-tests/src/smithay_view.rs index 0dcd3935..91a8929a 100644 --- a/niri-visual-tests/src/smithay_view.rs +++ b/niri-visual-tests/src/smithay_view.rs @@ -11,7 +11,7 @@ mod imp { use anyhow::{ensure, Context}; use gtk::gdk; use gtk::prelude::*; - use niri::render_helpers::shaders; + use niri::render_helpers::{resources, shaders}; use niri::utils::get_monotonic_time; use smithay::backend::egl::ffi::egl; use smithay::backend::egl::EGLContext; @@ -194,6 +194,7 @@ mod imp { let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities) .context("error creating GlesRenderer")?; + resources::init(&mut renderer); shaders::init(&mut renderer); Ok(renderer) diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs index 9b8b80a2..ce325534 100644 --- a/niri-visual-tests/src/test_window.rs +++ b/niri-visual-tests/src/test_window.rs @@ -2,7 +2,9 @@ use std::cell::RefCell; use std::cmp::{max, min}; use std::rc::Rc; -use niri::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot}; +use niri::layout::{ + AnimationSnapshot, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, +}; use niri::render_helpers::renderer::NiriRenderer; use niri::render_helpers::{RenderSnapshot, RenderTarget}; use niri::window::ResolvedWindowRules; @@ -177,7 +179,7 @@ impl LayoutElement for TestWindow { RenderSnapshot::default() } - fn request_size(&self, size: Size<i32, Logical>) { + fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) { self.inner.borrow_mut().requested_size = Some(size); self.inner.borrow_mut().pending_fullscreen = false; } @@ -214,7 +216,7 @@ impl LayoutElement for TestWindow { 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 @@ -230,4 +232,12 @@ impl LayoutElement for TestWindow { static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty(); &EMPTY } + + fn animation_snapshot(&self) -> Option<&AnimationSnapshot> { + None + } + + fn take_animation_snapshot(&mut self) -> Option<AnimationSnapshot> { + None + } } 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), |
