aboutsummaryrefslogtreecommitdiff
path: root/src/layout/tile.rs
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/tile.rs
parent4fd9300bdb07e90c26df28461f9bd6591c3d1d41 (diff)
downloadniri-71be19b234d58f4ec447e921633506beb81a52c0.tar.gz
niri-71be19b234d58f4ec447e921633506beb81a52c0.tar.bz2
niri-71be19b234d58f4ec447e921633506beb81a52c0.zip
Implement window resize animations
Diffstat (limited to 'src/layout/tile.rs')
-rw-r--r--src/layout/tile.rs358
1 files changed, 334 insertions, 24 deletions
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
+ }
+ }
+ })
+ }
+ }
+}