aboutsummaryrefslogtreecommitdiff
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
parent4fd9300bdb07e90c26df28461f9bd6591c3d1d41 (diff)
downloadniri-71be19b234d58f4ec447e921633506beb81a52c0.tar.gz
niri-71be19b234d58f4ec447e921633506beb81a52c0.tar.bz2
niri-71be19b234d58f4ec447e921633506beb81a52c0.zip
Implement window resize animations
-rw-r--r--Cargo.lock4
-rw-r--r--niri-config/src/lib.rs14
-rw-r--r--niri-visual-tests/src/cases/tile.rs9
-rw-r--r--niri-visual-tests/src/cases/window.rs14
-rw-r--r--niri-visual-tests/src/smithay_view.rs3
-rw-r--r--niri-visual-tests/src/test_window.rs16
-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
-rw-r--r--wiki/Configuration:-Animations.md21
21 files changed, 1432 insertions, 88 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9eecafa9..f5c20301 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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),