From dd011f1012e10b1e3a1dbe100cb603a457bba12a Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 9 Apr 2024 22:37:10 +0400 Subject: Implement window closing animations --- niri-config/src/lib.rs | 13 +++ niri-visual-tests/src/test_window.rs | 10 +- src/handlers/compositor.rs | 13 ++- src/handlers/xdg_shell.rs | 6 ++ src/layout/closing_window.rs | 177 +++++++++++++++++++++++++++++++++++ src/layout/mod.rs | 47 +++++++++- src/layout/tile.rs | 73 ++++++++++++++- src/layout/workspace.rs | 124 ++++++++++++++++++++++-- src/render_helpers/mod.rs | 25 +++++ src/render_helpers/surface.rs | 116 +++++++++++++++++++++++ src/window/mapped.rs | 79 ++++++++++++++-- wiki/Configuration:-Animations.md | 24 ++++- 12 files changed, 683 insertions(+), 24 deletions(-) create mode 100644 src/layout/closing_window.rs create mode 100644 src/render_helpers/surface.rs diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index e51602cc..e1ad0b2b 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -487,6 +487,8 @@ pub struct Animations { pub window_movement: Animation, #[knuffel(child, default = Animation::default_window_open())] pub window_open: Animation, + #[knuffel(child, default = Animation::default_window_close())] + pub window_close: Animation, #[knuffel(child, default = Animation::default_config_notification_open_close())] pub config_notification_open_close: Animation, } @@ -500,6 +502,7 @@ impl Default for Animations { horizontal_view_movement: Animation::default_horizontal_view_movement(), window_movement: Animation::default_window_movement(), window_open: Animation::default_window_open(), + window_close: Animation::default_window_close(), config_notification_open_close: Animation::default_config_notification_open_close(), } } @@ -579,6 +582,16 @@ impl Animation { }), } } + + pub const fn default_window_close() -> Self { + Self { + off: false, + kind: AnimationKind::Easing(EasingParams { + duration_ms: Some(150), + curve: Some(AnimationCurve::EaseOutQuad), + }), + } + } } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs index 6d028db9..8dc33b88 100644 --- a/niri-visual-tests/src/test_window.rs +++ b/niri-visual-tests/src/test_window.rs @@ -2,9 +2,11 @@ use std::cell::RefCell; use std::cmp::{max, min}; use std::rc::Rc; -use niri::layout::{LayoutElement, LayoutElementRenderElement}; +use niri::layout::{ + LayoutElement, LayoutElementRenderElement, LayoutElementSnapshotRenderElements, +}; use niri::render_helpers::renderer::NiriRenderer; -use niri::render_helpers::RenderTarget; +use niri::render_helpers::{RenderSnapshot, RenderTarget}; use niri::window::ResolvedWindowRules; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::{Id, Kind}; @@ -173,6 +175,10 @@ impl LayoutElement for TestWindow { ] } + fn take_last_render(&self) -> RenderSnapshot { + RenderSnapshot::default() + } + fn request_size(&self, size: Size) { self.inner.borrow_mut().requested_size = Some(size); self.inner.borrow_mut().pending_fullscreen = false; diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index 9b164865..2e657151 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -187,8 +187,6 @@ impl CompositorHandler for State { let window = mapped.window.clone(); let output = output.clone(); - window.on_commit(); - // This is a commit of a previously-mapped toplevel. let is_mapped = with_renderer_surface_state(surface, |state| state.buffer().is_some()) @@ -197,6 +195,17 @@ impl CompositorHandler for State { false }); + // Must start the close animation before window.on_commit(). + if !is_mapped { + self.backend.with_primary_renderer(|renderer| { + self.niri + .layout + .start_close_animation_for_window(renderer, &window); + }); + } + + window.on_commit(); + if !is_mapped { // The toplevel got unmapped. // diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index c31c6404..2cbaad25 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -382,6 +382,12 @@ impl XdgShellHandler for State { let window = mapped.window.clone(); let output = output.clone(); + self.backend.with_primary_renderer(|renderer| { + self.niri + .layout + .start_close_animation_for_window(renderer, &window); + }); + let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window); let was_active = active_window == Some(&window); diff --git a/src/layout/closing_window.rs b/src/layout/closing_window.rs new file mode 100644 index 00000000..ec565ebf --- /dev/null +++ b/src/layout/closing_window.rs @@ -0,0 +1,177 @@ +use std::time::Duration; + +use anyhow::Context as _; +use niri_config::BlockOutFrom; +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement}; +use smithay::backend::renderer::element::utils::{ + Relocate, RelocateRenderElement, RescaleRenderElement, +}; +use smithay::backend::renderer::element::{Kind, RenderElement}; +use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; +use smithay::utils::{Logical, Point, Scale, Transform}; + +use crate::animation::Animation; +use crate::niri_render_elements; +use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; +use crate::render_helpers::{render_to_texture, RenderSnapshot, RenderTarget}; + +#[derive(Debug)] +pub struct ClosingWindow { + /// Contents of the window. + buffer: TextureBuffer, + + /// Blocked-out contents of the window. + blocked_out_buffer: TextureBuffer, + + /// Where the window should be blocked out from. + block_out_from: Option, + + /// Center of the window geometry. + center: Point, + + /// Position in the workspace. + pos: Point, + + /// How much the buffer should be offset. + buffer_offset: Point, + + /// How much the blocked-out buffer should be offset. + blocked_out_buffer_offset: Point, + + /// The closing animation. + anim: Animation, + + /// Alpha the animation should start from. + starting_alpha: f32, + + /// Scale the animation should start from. + starting_scale: f64, +} + +niri_render_elements! { + ClosingWindowRenderElement => { + Texture = RelocateRenderElement>, + } +} + +impl ClosingWindow { + #[allow(clippy::too_many_arguments)] + pub fn new>( + renderer: &mut GlesRenderer, + snapshot: RenderSnapshot, + scale: i32, + center: Point, + pos: Point, + anim: Animation, + starting_alpha: f32, + starting_scale: f64, + ) -> anyhow::Result { + let _span = tracy_client::span!("ClosingWindow::new"); + + let mut render_to_buffer = |elements: Vec| -> anyhow::Result<_> { + let geo = elements + .iter() + .map(|ele| ele.geometry(Scale::from(f64::from(scale)))) + .reduce(|a, b| a.merge(b)) + .unwrap_or_default(); + + let elements = elements.iter().rev().map(|ele| { + RelocateRenderElement::from_element( + ele, + (-geo.loc.x, -geo.loc.y), + Relocate::Relative, + ) + }); + let offset = geo.loc.to_logical(scale); + + let (texture, _sync_point) = render_to_texture( + renderer, + geo.size, + Scale::from(scale as f64), + Transform::Normal, + Fourcc::Abgr8888, + elements, + ) + .context("error rendering to texture")?; + + let buffer = + TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None); + Ok((buffer, offset)) + }; + + let (buffer, buffer_offset) = + render_to_buffer(snapshot.contents).context("error rendering contents")?; + let (blocked_out_buffer, blocked_out_buffer_offset) = + render_to_buffer(snapshot.blocked_out_contents) + .context("error rendering blocked-out contents")?; + + Ok(Self { + buffer, + blocked_out_buffer, + block_out_from: snapshot.block_out_from, + center, + pos, + buffer_offset, + blocked_out_buffer_offset, + anim, + starting_alpha, + starting_scale, + }) + } + + pub fn advance_animations(&mut self, current_time: Duration) { + self.anim.set_current_time(current_time); + } + + pub fn are_animations_ongoing(&self) -> bool { + !self.anim.is_done() + } + + pub fn render( + &self, + view_pos: i32, + scale: Scale, + target: RenderTarget, + ) -> ClosingWindowRenderElement { + let val = self.anim.value(); + + let block_out = match self.block_out_from { + None => false, + Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast, + Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output, + }; + let (buffer, offset) = if block_out { + (&self.blocked_out_buffer, self.blocked_out_buffer_offset) + } else { + (&self.buffer, self.buffer_offset) + }; + + let elem = TextureRenderElement::from_texture_buffer( + Point::from((0., 0.)), + buffer, + Some(val.clamp(0., 1.) as f32 * self.starting_alpha), + None, + None, + Kind::Unspecified, + ); + + let elem = PrimaryGpuTextureRenderElement(elem); + + let elem = RescaleRenderElement::from_element( + elem, + (self.center - offset).to_physical_precise_round(scale), + ((val / 5. + 0.8) * self.starting_scale).max(0.), + ); + + let mut location = self.pos + offset; + location.x -= view_pos; + let elem = RelocateRenderElement::from_element( + elem, + location.to_physical_precise_round(scale), + Relocate::Relative, + ); + + elem.into() + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 9db7b140..18870025 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -39,6 +39,7 @@ use niri_ipc::SizeChange; use smithay::backend::renderer::element::solid::SolidColorRenderElement; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::Id; +use smithay::backend::renderer::gles::GlesRenderer; use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Point, Scale, Size, Transform}; @@ -47,11 +48,13 @@ use self::monitor::Monitor; pub use self::monitor::MonitorRenderElement; use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Workspace}; use crate::niri_render_elements; +use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::renderer::NiriRenderer; -use crate::render_helpers::RenderTarget; +use crate::render_helpers::{RenderSnapshot, RenderTarget}; use crate::utils::output_size; use crate::window::ResolvedWindowRules; +pub mod closing_window; pub mod focus_ring; pub mod monitor; pub mod tile; @@ -64,6 +67,13 @@ niri_render_elements! { } } +niri_render_elements! { + LayoutElementSnapshotRenderElements => { + Texture = PrimaryGpuTextureRenderElement, + SolidColor = SolidColorRenderElement, + } +} + pub trait LayoutElement { /// Type that can be used as a unique ID of this element. type Id: PartialEq; @@ -100,6 +110,8 @@ pub trait LayoutElement { target: RenderTarget, ) -> Vec>; + fn take_last_render(&self) -> RenderSnapshot; + fn request_size(&self, size: Size); fn request_fullscreen(&self, size: Size); fn min_size(&self) -> Size; @@ -1750,6 +1762,35 @@ impl Layout { } } + pub fn start_close_animation_for_window( + &mut self, + renderer: &mut GlesRenderer, + window: &W::Id, + ) { + let _span = tracy_client::span!("Layout::start_close_animation_for_window"); + + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + if ws.has_window(window) { + ws.start_close_animation_for_window(renderer, window); + return; + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(window) { + ws.start_close_animation_for_window(renderer, window); + return; + } + } + } + } + } + pub fn refresh(&mut self) { let _span = tracy_client::span!("Layout::refresh"); @@ -1889,6 +1930,10 @@ mod tests { vec![] } + fn take_last_render(&self) -> RenderSnapshot { + RenderSnapshot::default() + } + fn request_size(&self, size: Size) { self.0.requested_size.set(Some(size)); self.0.pending_fullscreen.set(false); diff --git a/src/layout/tile.rs b/src/layout/tile.rs index ed71d8a0..fb825184 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -3,17 +3,22 @@ use std::rc::Rc; use std::time::Duration; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; -use smithay::backend::renderer::element::utils::RescaleRenderElement; +use smithay::backend::renderer::element::utils::{ + Relocate, RelocateRenderElement, RescaleRenderElement, +}; use smithay::backend::renderer::element::{Element, Kind}; +use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Point, Rectangle, Scale, Size}; use super::focus_ring::{FocusRing, FocusRingRenderElement}; -use super::{LayoutElement, LayoutElementRenderElement, Options}; +use super::{ + LayoutElement, LayoutElementRenderElement, LayoutElementSnapshotRenderElements, Options, +}; use crate::animation::Animation; use crate::niri_render_elements; use crate::render_helpers::offscreen::OffscreenRenderElement; use crate::render_helpers::renderer::NiriRenderer; -use crate::render_helpers::RenderTarget; +use crate::render_helpers::{RenderSnapshot, RenderTarget}; /// Toplevel window with decorations. #[derive(Debug)] @@ -58,6 +63,14 @@ niri_render_elements! { } } +niri_render_elements! { + TileSnapshotRenderElement => { + LayoutElement = RelocateRenderElement, + FocusRing = FocusRingRenderElement, + SolidColor = SolidColorRenderElement, + } +} + impl Tile { pub fn new(window: W, options: Rc) -> Self { Self { @@ -129,6 +142,10 @@ impl Tile { )); } + pub fn open_animation(&self) -> &Option { + &self.open_animation + } + pub fn window(&self) -> &W { &self.window } @@ -399,4 +416,54 @@ impl Tile { None.into_iter().chain(Some(elements).into_iter().flatten()) } } + + pub fn take_snapshot_for_close_anim( + &self, + renderer: &mut GlesRenderer, + scale: Scale, + view_size: Size, + ) -> RenderSnapshot { + let snapshot = self.window.take_last_render(); + if snapshot.contents.is_empty() { + return RenderSnapshot::default(); + } + + let mut process = |contents| { + let mut rv = vec![]; + + let buf_pos = + (self.window_loc() + self.window.buf_loc()).to_physical_precise_round(scale); + for elem in contents { + let elem = RelocateRenderElement::from_element(elem, buf_pos, Relocate::Relative); + rv.push(elem.into()); + } + + if let Some(width) = self.effective_border_width() { + rv.extend( + self.border + .render(renderer, Point::from((width, width)), scale, view_size) + .map(Into::into), + ); + } + + if self.is_fullscreen { + let elem = SolidColorRenderElement::from_buffer( + &self.fullscreen_backdrop, + Point::from((0, 0)), + scale, + 1., + Kind::Unspecified, + ); + rv.push(elem.into()); + } + + rv + }; + + RenderSnapshot { + contents: process(snapshot.contents), + blocked_out_contents: process(snapshot.blocked_out_contents), + block_out_from: snapshot.block_out_from, + } + } } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index ab23a1f0..61cf6e79 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -5,6 +5,7 @@ use std::time::Duration; use niri_config::{CenterFocusedColumn, PresetWidth, Struts}; use niri_ipc::SizeChange; +use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::{layer_map_for_output, Window}; use smithay::output::Output; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; @@ -12,6 +13,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Point, Rectangle, Scale, Size}; use smithay::wayland::compositor::send_surface_state; +use super::closing_window::{ClosingWindow, ClosingWindowRenderElement}; use super::tile::{Tile, TileRenderElement}; use super::{LayoutElement, Options}; use crate::animation::Animation; @@ -76,6 +78,9 @@ pub struct Workspace { /// The value is the view offset that the previous column had before, to restore it. activate_prev_column_on_removal: Option, + /// Windows in the closing animation. + closing_windows: Vec, + /// Configurable properties of the layout. pub options: Rc, @@ -100,6 +105,7 @@ impl WorkspaceId { niri_render_elements! { WorkspaceRenderElement => { Tile = TileRenderElement, + ClosingWindow = ClosingWindowRenderElement, } } @@ -243,6 +249,7 @@ impl Workspace { view_offset: 0, view_offset_adj: None, activate_prev_column_on_removal: None, + closing_windows: vec![], options, id: WorkspaceId::next(), } @@ -259,6 +266,7 @@ impl Workspace { view_offset: 0, view_offset_adj: None, activate_prev_column_on_removal: None, + closing_windows: vec![], options, id: WorkspaceId::next(), } @@ -283,10 +291,17 @@ impl Workspace { let is_active = is_active && col_idx == self.active_column_idx; col.advance_animations(current_time, is_active); } + + self.closing_windows.retain_mut(|closing| { + closing.advance_animations(current_time); + closing.are_animations_ongoing() + }); } pub fn are_animations_ongoing(&self) -> bool { - self.view_offset_adj.is_some() || self.columns.iter().any(Column::are_animations_ongoing) + self.view_offset_adj.is_some() + || self.columns.iter().any(Column::are_animations_ongoing) + || !self.closing_windows.is_empty() } pub fn update_config(&mut self, options: Rc) { @@ -897,6 +912,97 @@ impl Workspace { self.activate_column(column_idx); } + pub fn start_close_animation_for_window( + &mut self, + renderer: &mut GlesRenderer, + window: &W::Id, + ) { + let (tile, mut tile_pos) = self + .tiles_in_render_order() + .find(|(tile, _)| tile.window().id() == window) + .unwrap(); + + // FIXME: workspaces should probably cache their last used scale so they can be correctly + // rendered even with no outputs connected. + let output_scale = self + .output + .as_ref() + .map(|o| Scale::from(o.current_scale().fractional_scale())) + .unwrap_or(Scale::from(1.)); + + let snapshot = tile.take_snapshot_for_close_anim(renderer, output_scale, self.view_size); + if snapshot.contents.is_empty() { + return; + }; + + let col_idx = self + .columns + .iter() + .position(|col| col.contains(window)) + .unwrap(); + + let removing_last = self.columns[col_idx].tiles.len() == 1; + let offset = self.column_x(col_idx + 1) - self.column_x(col_idx); + + let mut center = Point::from((0, 0)); + center.x += tile.tile_size().w / 2; + center.y += tile.tile_size().h / 2; + + tile_pos.x += self.view_pos(); + + if col_idx < self.active_column_idx && removing_last { + tile_pos.x -= offset; + } + + // FIXME: this is a bit cursed since it's relying on Tile's internal details. + let (starting_alpha, starting_scale) = if let Some(anim) = tile.open_animation() { + let val = anim.value(); + (val.clamp(0., 1.) as f32, (val / 2. + 0.5).max(0.)) + } else { + (1., 1.) + }; + + let anim = Animation::new( + 1., + 0., + 0., + self.options.animations.window_close, + niri_config::Animation::default_window_close(), + ); + + let res = ClosingWindow::new( + renderer, + snapshot, + output_scale.x as i32, + center, + tile_pos, + anim, + starting_alpha, + starting_scale, + ); + match res { + Ok(closing) => { + self.closing_windows.push(closing); + } + Err(err) => { + warn!("error creating a closing window animation: {err:?}"); + } + } + + // Also move the other columns. + if removing_last { + if self.active_column_idx <= col_idx { + for col in &mut self.columns[col_idx + 1..] { + col.animate_move_from(offset); + } + } else { + for col in &mut self.columns[..col_idx] { + col.animate_move_from(-offset); + } + } + } + } + #[cfg(test)] pub fn verify_invariants(&self) { assert!(self.view_size.w > 0); @@ -1354,10 +1460,6 @@ impl Workspace { renderer: &mut R, target: RenderTarget, ) -> Vec> { - if self.columns.is_empty() { - return vec![]; - } - // FIXME: workspaces should probably cache their last used scale so they can be correctly // rendered even with no outputs connected. let output_scale = self @@ -1367,8 +1469,18 @@ impl Workspace { .unwrap_or(Scale::from(1.)); let mut rv = vec![]; - let mut first = true; + // Draw the closing windows on top. + let view_pos = self.view_pos(); + for closing in &self.closing_windows { + rv.push(closing.render(view_pos, output_scale, target).into()); + } + + if self.columns.is_empty() { + return rv; + } + + let mut first = true; for (tile, tile_pos) in self.tiles_in_render_order() { // For the active tile (which comes first), draw the focus ring. let focus_ring = first; diff --git a/src/render_helpers/mod.rs b/src/render_helpers/mod.rs index 238ad8ec..c6c9fed1 100644 --- a/src/render_helpers/mod.rs +++ b/src/render_helpers/mod.rs @@ -1,6 +1,7 @@ use std::ptr; use anyhow::{ensure, Context}; +use niri_config::BlockOutFrom; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture}; @@ -18,6 +19,7 @@ pub mod primary_gpu_texture; pub mod render_elements; pub mod renderer; pub mod shaders; +pub mod surface; /// What we're rendering for. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -30,6 +32,29 @@ pub enum RenderTarget { ScreenCapture, } +/// Snapshot of a render. +#[derive(Debug)] +pub struct RenderSnapshot { + /// Contents for a normal render. + pub contents: Vec, + + /// Blocked-out contents. + pub blocked_out_contents: Vec, + + /// Where the contents were blocked out from at the time of the snapshot. + pub block_out_from: Option, +} + +impl Default for RenderSnapshot { + fn default() -> Self { + Self { + contents: Default::default(), + blocked_out_contents: Default::default(), + block_out_from: Default::default(), + } + } +} + pub fn render_to_texture( renderer: &mut GlesRenderer, size: Size, diff --git a/src/render_helpers/surface.rs b/src/render_helpers/surface.rs new file mode 100644 index 00000000..ae7ffc32 --- /dev/null +++ b/src/render_helpers/surface.rs @@ -0,0 +1,116 @@ +use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; +use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement}; +use smithay::backend::renderer::element::Kind; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::backend::renderer::utils::{import_surface, RendererSurfaceStateUserData}; +use smithay::backend::renderer::Renderer as _; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Physical, Point, Scale}; +use smithay::wayland::compositor::{with_surface_tree_downward, TraversalAction}; + +use super::primary_gpu_texture::PrimaryGpuTextureRenderElement; +use super::renderer::NiriRenderer; +use crate::layout::{LayoutElementRenderElement, LayoutElementSnapshotRenderElements}; + +/// Renders elements from a surface tree, as well as saves them as textures into `storage`. +/// +/// Saved textures are based at (0, 0) to facilitate later offscreening. This is why the location +/// argument is split into `location` and `offset`: the former is ignored for saved textures, but +/// the latter isn't (for things like popups). +#[allow(clippy::too_many_arguments)] +pub fn render_and_save_from_surface_tree( + renderer: &mut R, + surface: &WlSurface, + location: Point, + offset: Point, + scale: Scale, + alpha: f32, + kind: Kind, + elements: &mut Vec>, + storage: &mut Option<&mut Vec>, +) { + let _span = tracy_client::span!("render_and_save_from_surface_tree"); + + let base_pos = location; + + with_surface_tree_downward( + surface, + location + offset, + |_, states, location| { + let mut location = *location; + let data = states.data_map.get::(); + + if let Some(data) = data { + let data = &*data.borrow(); + + if let Some(view) = data.view() { + location += view.offset.to_f64().to_physical(scale); + TraversalAction::DoChildren(location) + } else { + TraversalAction::SkipChildren + } + } else { + TraversalAction::SkipChildren + } + }, + |surface, states, location| { + let mut location = *location; + let data = states.data_map.get::(); + + if let Some(data) = data { + if let Some(view) = data.borrow().view() { + location += view.offset.to_f64().to_physical(scale); + } else { + return; + } + + let elem = match WaylandSurfaceRenderElement::from_surface( + renderer, surface, states, location, alpha, kind, + ) { + Ok(elem) => elem, + Err(err) => { + warn!("failed to import surface: {err:?}"); + return; + } + }; + + elements.push(elem.into()); + + if let Some(storage) = storage { + let renderer = renderer.as_gles_renderer(); + // FIXME (possibly in Smithay): this causes a re-upload for shm textures. + if let Err(err) = import_surface(renderer, states) { + warn!("failed to import surface: {err:?}"); + return; + } + + let data = data.borrow(); + let view = data.view().unwrap(); + let Some(texture) = data.texture::(renderer.id()) else { + return; + }; + + let buffer = TextureBuffer::from_texture( + renderer, + texture.clone(), + data.buffer_scale(), + data.buffer_transform(), + None, + ); + + let elem = TextureRenderElement::from_texture_buffer( + location - base_pos, + &buffer, + Some(alpha), + Some(view.src), + Some(view.dst), + kind, + ); + + storage.push(PrimaryGpuTextureRenderElement(elem).into()); + } + } + }, + |_, _, _| true, + ); +} diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 8a4b2473..7210f8b0 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -3,9 +3,9 @@ use std::cmp::{max, min}; use niri_config::{BlockOutFrom, WindowRule}; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; -use smithay::backend::renderer::element::{AsRenderElements as _, Id, Kind}; +use smithay::backend::renderer::element::{Id, Kind}; use smithay::desktop::space::SpaceElement as _; -use smithay::desktop::Window; +use smithay::desktop::{PopupManager, Window}; use smithay::output::Output; use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; @@ -15,10 +15,13 @@ use smithay::wayland::compositor::{send_surface_state, with_states}; use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface}; use super::{ResolvedWindowRules, WindowRef}; -use crate::layout::{LayoutElement, LayoutElementRenderElement}; +use crate::layout::{ + LayoutElement, LayoutElementRenderElement, LayoutElementSnapshotRenderElements, +}; use crate::niri::WindowOffscreenId; use crate::render_helpers::renderer::NiriRenderer; -use crate::render_helpers::RenderTarget; +use crate::render_helpers::surface::render_and_save_from_surface_tree; +use crate::render_helpers::{RenderSnapshot, RenderTarget}; #[derive(Debug)] pub struct Mapped { @@ -38,6 +41,9 @@ pub struct Mapped { /// Buffer to draw instead of the window when it should be blocked out. block_out_buffer: RefCell, + + /// Snapshot of the last render for use in the close animation. + last_render: RefCell>, } impl Mapped { @@ -48,6 +54,7 @@ impl Mapped { need_to_recompute_rules: false, is_focused: false, block_out_buffer: RefCell::new(SolidColorBuffer::new((0, 0), [0., 0., 0., 1.])), + last_render: RefCell::new(RenderSnapshot::default()), } } @@ -124,9 +131,10 @@ impl LayoutElement for Mapped { Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output, }; + let mut buffer = self.block_out_buffer.borrow_mut(); + buffer.resize(self.window.geometry().size); + if block_out { - let mut buffer = self.block_out_buffer.borrow_mut(); - buffer.resize(self.window.geometry().size); let elem = SolidColorRenderElement::from_buffer( &buffer, location.to_physical_precise_round(scale), @@ -137,15 +145,68 @@ impl LayoutElement for Mapped { vec![elem.into()] } else { let buf_pos = location - self.window.geometry().loc; - self.window.render_elements( + let buf_pos = buf_pos.to_physical_precise_round(scale); + + let mut elements = vec![]; + + // If we're rendering for output, save into last_render. + let mut last_render = self.last_render.borrow_mut(); + // FIXME: when preview-render is active, last render contents will never update. + let mut storage = if target == RenderTarget::Output { + last_render.contents.clear(); + last_render.block_out_from = self.rules.block_out_from; + last_render.blocked_out_contents = vec![SolidColorRenderElement::from_buffer( + &buffer, + (0, 0), + scale, + alpha, + Kind::Unspecified, + ) + .into()]; + + Some(&mut last_render.contents) + } else { + None + }; + + let surface = self.toplevel().wl_surface(); + for (popup, popup_offset) in PopupManager::popups_for_surface(surface) { + let offset = (self.window.geometry().loc + popup_offset - popup.geometry().loc) + .to_physical_precise_round(scale); + + render_and_save_from_surface_tree( + renderer, + popup.wl_surface(), + buf_pos, + offset, + scale, + alpha, + Kind::Unspecified, + &mut elements, + &mut storage, + ); + } + + render_and_save_from_surface_tree( renderer, - buf_pos.to_physical_precise_round(scale), + surface, + buf_pos, + Point::from((0., 0.)), scale, alpha, - ) + Kind::Unspecified, + &mut elements, + &mut storage, + ); + + elements } } + fn take_last_render(&self) -> RenderSnapshot { + self.last_render.take() + } + fn request_size(&self, size: Size) { self.toplevel().with_pending_state(|state| { state.size = Some(size); diff --git a/wiki/Configuration:-Animations.md b/wiki/Configuration:-Animations.md index ada9c9ca..31aff0f3 100644 --- a/wiki/Configuration:-Animations.md +++ b/wiki/Configuration:-Animations.md @@ -33,6 +33,11 @@ animations { curve "ease-out-expo" } + window-close { + duration-ms 150 + curve "ease-out-quad" + } + config-notification-open-close { spring damping-ratio=0.6 stiffness=1000 epsilon=0.001 } @@ -149,7 +154,7 @@ Window movement animations, currently cover only horizontal column movement. This animation runs on actions like `move-column-left` and `move-column-right` to move the windows themselves. It can sometimes run together with the `horizontal-view-movement` animation, if the camera also moves. -Since 0.1.5, this is also the animation that moves windows out of the way upon window opening. +Since 0.1.5, this is also the animation that moves windows out of the way upon window opening and closing. ``` animations { @@ -161,6 +166,8 @@ animations { #### `window-open` +Since: 0.1.5 + Window opening animation. This one uses an easing type by default. @@ -174,6 +181,21 @@ animations { } ``` +#### `window-close` + +Window closing animation. + +This one uses an easing type by default. + +``` +animations { + window-open { + duration-ms 150 + curve "ease-out-quad" + } +} +``` + #### `config-notification-open-close` The open/close animation of the config parse error and new default config notifications. -- cgit