aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-04-09 22:37:10 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-04-09 23:42:01 +0400
commitdd011f1012e10b1e3a1dbe100cb603a457bba12a (patch)
tree2fb853be8bbe1ee5afdf3dea4974768874a5e793
parent301a2c06613c76d2c16a85ab21ad132e5618454b (diff)
downloadniri-dd011f1012e10b1e3a1dbe100cb603a457bba12a.tar.gz
niri-dd011f1012e10b1e3a1dbe100cb603a457bba12a.tar.bz2
niri-dd011f1012e10b1e3a1dbe100cb603a457bba12a.zip
Implement window closing animations
-rw-r--r--niri-config/src/lib.rs13
-rw-r--r--niri-visual-tests/src/test_window.rs10
-rw-r--r--src/handlers/compositor.rs13
-rw-r--r--src/handlers/xdg_shell.rs6
-rw-r--r--src/layout/closing_window.rs177
-rw-r--r--src/layout/mod.rs47
-rw-r--r--src/layout/tile.rs73
-rw-r--r--src/layout/workspace.rs124
-rw-r--r--src/render_helpers/mod.rs25
-rw-r--r--src/render_helpers/surface.rs116
-rw-r--r--src/window/mapped.rs79
-rw-r--r--wiki/Configuration:-Animations.md24
12 files changed, 683 insertions, 24 deletions
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<LayoutElementSnapshotRenderElements> {
+ RenderSnapshot::default()
+ }
+
fn request_size(&self, size: Size<i32, Logical>) {
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<GlesTexture>,
+
+ /// Blocked-out contents of the window.
+ blocked_out_buffer: TextureBuffer<GlesTexture>,
+
+ /// Where the window should be blocked out from.
+ block_out_from: Option<BlockOutFrom>,
+
+ /// Center of the window geometry.
+ center: Point<i32, Logical>,
+
+ /// Position in the workspace.
+ pos: Point<i32, Logical>,
+
+ /// How much the buffer should be offset.
+ buffer_offset: Point<i32, Logical>,
+
+ /// How much the blocked-out buffer should be offset.
+ blocked_out_buffer_offset: Point<i32, Logical>,
+
+ /// 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<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
+ }
+}
+
+impl ClosingWindow {
+ #[allow(clippy::too_many_arguments)]
+ pub fn new<E: RenderElement<GlesRenderer>>(
+ renderer: &mut GlesRenderer,
+ snapshot: RenderSnapshot<E>,
+ scale: i32,
+ center: Point<i32, Logical>,
+ pos: Point<i32, Logical>,
+ anim: Animation,
+ starting_alpha: f32,
+ starting_scale: f64,
+ ) -> anyhow::Result<Self> {
+ let _span = tracy_client::span!("ClosingWindow::new");
+
+ let mut render_to_buffer = |elements: Vec<E>| -> 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<f64>,
+ 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<LayoutElementRenderElement<R>>;
+ fn take_last_render(&self) -> RenderSnapshot<LayoutElementSnapshotRenderElements>;
+
fn request_size(&self, size: Size<i32, Logical>);
fn request_fullscreen(&self, size: Size<i32, Logical>);
fn min_size(&self) -> Size<i32, Logical>;
@@ -1750,6 +1762,35 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ 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<LayoutElementSnapshotRenderElements> {
+ RenderSnapshot::default()
+ }
+
fn request_size(&self, size: Size<i32, Logical>) {
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<LayoutElementSnapshotRenderElements>,
+ FocusRing = FocusRingRenderElement,
+ SolidColor = SolidColorRenderElement,
+ }
+}
+
impl<W: LayoutElement> Tile<W> {
pub fn new(window: W, options: Rc<Options>) -> Self {
Self {
@@ -129,6 +142,10 @@ impl<W: LayoutElement> Tile<W> {
));
}
+ pub fn open_animation(&self) -> &Option<Animation> {
+ &self.open_animation
+ }
+
pub fn window(&self) -> &W {
&self.window
}
@@ -399,4 +416,54 @@ impl<W: LayoutElement> Tile<W> {
None.into_iter().chain(Some(elements).into_iter().flatten())
}
}
+
+ pub fn take_snapshot_for_close_anim(
+ &self,
+ renderer: &mut GlesRenderer,
+ scale: Scale<f64>,
+ view_size: Size<i32, Logical>,
+ ) -> RenderSnapshot<TileSnapshotRenderElement> {
+ 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<W: LayoutElement> {
/// The value is the view offset that the previous column had before, to restore it.
activate_prev_column_on_removal: Option<i32>,
+ /// Windows in the closing animation.
+ closing_windows: Vec<ClosingWindow>,
+
/// Configurable properties of the layout.
pub options: Rc<Options>,
@@ -100,6 +105,7 @@ impl WorkspaceId {
niri_render_elements! {
WorkspaceRenderElement<R> => {
Tile = TileRenderElement<R>,
+ ClosingWindow = ClosingWindowRenderElement,
}
}
@@ -243,6 +249,7 @@ impl<W: LayoutElement> Workspace<W> {
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<W: LayoutElement> Workspace<W> {
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<W: LayoutElement> Workspace<W> {
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<Options>) {
@@ -897,6 +912,97 @@ impl<W: LayoutElement> Workspace<W> {
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<W: LayoutElement> Workspace<W> {
renderer: &mut R,
target: RenderTarget,
) -> Vec<WorkspaceRenderElement<R>> {
- 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<W: LayoutElement> Workspace<W> {
.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<E> {
+ /// Contents for a normal render.
+ pub contents: Vec<E>,
+
+ /// Blocked-out contents.
+ pub blocked_out_contents: Vec<E>,
+
+ /// Where the contents were blocked out from at the time of the snapshot.
+ pub block_out_from: Option<BlockOutFrom>,
+}
+
+impl<E> Default for RenderSnapshot<E> {
+ 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<i32, Physical>,
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<R: NiriRenderer>(
+ renderer: &mut R,
+ surface: &WlSurface,
+ location: Point<f64, Physical>,
+ offset: Point<f64, Physical>,
+ scale: Scale<f64>,
+ alpha: f32,
+ kind: Kind,
+ elements: &mut Vec<LayoutElementRenderElement<R>>,
+ storage: &mut Option<&mut Vec<LayoutElementSnapshotRenderElements>>,
+) {
+ 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::<RendererSurfaceStateUserData>();
+
+ 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::<RendererSurfaceStateUserData>();
+
+ 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::<GlesRenderer>(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};