From e75c6b2333a951503ff7fbf011ffbb0b57c96a25 Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Mon, 23 Feb 2026 00:58:42 +0530 Subject: [PATCH 01/13] config boilerplate --- niri-config/src/animations.rs | 102 ++++++++++++++++++++++++++++++ src/backend/tty.rs | 4 +- src/backend/winit.rs | 4 +- src/niri.rs | 24 ++++++- src/render_helpers/shaders/mod.rs | 48 ++++++++++++-- 5 files changed, 172 insertions(+), 10 deletions(-) diff --git a/niri-config/src/animations.rs b/niri-config/src/animations.rs index 346b62517a..f4a4f7ff7e 100644 --- a/niri-config/src/animations.rs +++ b/niri-config/src/animations.rs @@ -11,6 +11,8 @@ pub struct Animations { pub workspace_switch: WorkspaceSwitchAnim, pub window_open: WindowOpenAnim, pub window_close: WindowCloseAnim, + pub layer_open: LayerOpenAnim, + pub layer_close: LayerCloseAnim, pub horizontal_view_movement: HorizontalViewMovementAnim, pub window_movement: WindowMovementAnim, pub window_resize: WindowResizeAnim, @@ -31,6 +33,8 @@ impl Default for Animations { window_movement: Default::default(), window_open: Default::default(), window_close: Default::default(), + layer_open: Default::default(), + layer_close: Default::default(), window_resize: Default::default(), config_notification_open_close: Default::default(), exit_confirmation_open_close: Default::default(), @@ -56,6 +60,10 @@ pub struct AnimationsPart { #[knuffel(child)] pub window_close: Option, #[knuffel(child)] + pub layer_open: Option, + #[knuffel(child)] + pub layer_close: Option, + #[knuffel(child)] pub horizontal_view_movement: Option, #[knuffel(child)] pub window_movement: Option, @@ -193,6 +201,48 @@ impl Default for WindowCloseAnim { } } +#[derive(Debug, Clone, PartialEq)] +pub struct LayerOpenAnim { + pub anim: Animation, + pub custom_shader: Option, +} + +impl Default for LayerOpenAnim { + fn default() -> Self { + Self { + anim: Animation { + off: false, + kind: Kind::Easing(EasingParams { + duration_ms: 150, + curve: Curve::EaseOutExpo, + }), + }, + custom_shader: None, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LayerCloseAnim { + pub anim: Animation, + pub custom_shader: Option, +} + +impl Default for LayerCloseAnim { + fn default() -> Self { + Self { + anim: Animation { + off: false, + kind: Kind::Easing(EasingParams { + duration_ms: 150, + curve: Curve::EaseOutQuad, + }), + }, + custom_shader: None, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub struct HorizontalViewMovementAnim(pub Animation); @@ -449,6 +499,58 @@ where } } +impl knuffel::Decode for LayerOpenAnim +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + let default = Self::default().anim; + let mut custom_shader = None; + let anim = Animation::decode_node(node, ctx, default, |child, ctx| { + if &**child.node_name == "custom-shader" { + custom_shader = parse_arg_node("custom-shader", child, ctx)?; + Ok(true) + } else { + Ok(false) + } + })?; + + Ok(Self { + anim, + custom_shader, + }) + } +} + +impl knuffel::Decode for LayerCloseAnim +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + let default = Self::default().anim; + let mut custom_shader = None; + let anim = Animation::decode_node(node, ctx, default, |child, ctx| { + if &**child.node_name == "custom-shader" { + custom_shader = parse_arg_node("custom-shader", child, ctx)?; + Ok(true) + } else { + Ok(false) + } + })?; + + Ok(Self { + anim, + custom_shader, + }) + } +} + impl knuffel::Decode for ConfigNotificationOpenCloseAnim where S: knuffel::traits::ErrorSpan, diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 7b52b272dd..c0505acdb5 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -821,10 +821,10 @@ impl Tty { shaders::set_custom_resize_program(gles_renderer, Some(src)); } if let Some(src) = config.animations.window_close.custom_shader.as_deref() { - shaders::set_custom_close_program(gles_renderer, Some(src)); + shaders::set_custom_window_close_program(gles_renderer, Some(src)); } if let Some(src) = config.animations.window_open.custom_shader.as_deref() { - shaders::set_custom_open_program(gles_renderer, Some(src)); + shaders::set_custom_window_open_program(gles_renderer, Some(src)); } drop(config); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index beace1dce5..53a42b0d7b 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -155,10 +155,10 @@ impl Winit { shaders::set_custom_resize_program(renderer, Some(src)); } if let Some(src) = config.animations.window_close.custom_shader.as_deref() { - shaders::set_custom_close_program(renderer, Some(src)); + shaders::set_custom_window_close_program(renderer, Some(src)); } if let Some(src) = config.animations.window_open.custom_shader.as_deref() { - shaders::set_custom_open_program(renderer, Some(src)); + shaders::set_custom_window_open_program(renderer, Some(src)); } drop(config); diff --git a/src/niri.rs b/src/niri.rs index d84c390abf..c245c6a38e 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -1528,6 +1528,26 @@ impl State { layer_rules_changed = true; } + if config.animations.layer_close.custom_shader + != old_config.animations.layer_close.custom_shader + { + let src = config.animations.layer_close.custom_shader.as_deref(); + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_layer_close_program(renderer, src); + }); + shaders_changed = true; + } + + if config.animations.layer_open.custom_shader + != old_config.animations.layer_open.custom_shader + { + let src = config.animations.layer_open.custom_shader.as_deref(); + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_layer_open_program(renderer, src); + }); + shaders_changed = true; + } + if config.animations.window_resize.custom_shader != old_config.animations.window_resize.custom_shader { @@ -1543,7 +1563,7 @@ impl State { { let src = config.animations.window_close.custom_shader.as_deref(); self.backend.with_primary_renderer(|renderer| { - shaders::set_custom_close_program(renderer, src); + shaders::set_custom_window_close_program(renderer, src); }); shaders_changed = true; } @@ -1553,7 +1573,7 @@ impl State { { let src = config.animations.window_open.custom_shader.as_deref(); self.backend.with_primary_renderer(|renderer| { - shaders::set_custom_open_program(renderer, src); + shaders::set_custom_window_open_program(renderer, src); }); shaders_changed = true; } diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index 6fccce7f59..590a77f22b 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -246,12 +246,12 @@ fn compile_close_program( ) } -pub fn set_custom_close_program(renderer: &mut GlesRenderer, src: Option<&str>) { +pub fn set_custom_window_close_program(renderer: &mut GlesRenderer, src: Option<&str>) { let program = if let Some(src) = src { match compile_close_program(renderer, src) { Ok(program) => Some(program), Err(err) => { - warn!("error compiling custom close shader: {err:?}"); + warn!("error compiling custom window close shader: {err:?}"); return; } } @@ -289,12 +289,12 @@ fn compile_open_program( ) } -pub fn set_custom_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { +pub fn set_custom_window_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { let program = if let Some(src) = src { match compile_open_program(renderer, src) { Ok(program) => Some(program), Err(err) => { - warn!("error compiling custom open shader: {err:?}"); + warn!("error compiling custom window open shader: {err:?}"); return; } } @@ -309,6 +309,46 @@ pub fn set_custom_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { } } +pub fn set_custom_layer_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_open_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom layer open shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_open_program(program) { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer open shader: {err:?}"); + } + } +} + +pub fn set_custom_layer_close_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_close_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom layer close shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_close_program(program) { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer close shader: {err:?}"); + } + } +} + pub fn mat3_uniform(name: &str, mat: Mat3) -> Uniform<'_> { Uniform::new( name, From 2867907fd1f328572afcf234c71b11dded7af6c3 Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Mon, 23 Feb 2026 01:12:34 +0530 Subject: [PATCH 02/13] layer open/close modules in layer module --- src/layer/closing_layer.rs | 216 +++++++++++++++++++++++++++++++++++++ src/layer/mod.rs | 2 + src/layer/opening_layer.rs | 142 ++++++++++++++++++++++++ 3 files changed, 360 insertions(+) create mode 100644 src/layer/closing_layer.rs create mode 100644 src/layer/opening_layer.rs diff --git a/src/layer/closing_layer.rs b/src/layer/closing_layer.rs new file mode 100644 index 0000000000..82dd2698e9 --- /dev/null +++ b/src/layer/closing_layer.rs @@ -0,0 +1,216 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use anyhow::Context as _; +use glam::{Mat3, Vec2}; +use niri_config::BlockOutFrom; +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::element::utils::{ + Relocate, RelocateRenderElement, RescaleRenderElement, +}; +use smithay::backend::renderer::element::{Kind, RenderElement}; +use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform}; +use smithay::backend::renderer::Texture; +use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform}; + +use crate::animation::Animation; +use crate::niri_render_elements; +use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; +use crate::render_helpers::shader_element::ShaderRenderElement; +use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders}; +use crate::render_helpers::snapshot::RenderSnapshot; +use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement}; +use crate::render_helpers::{render_to_encompassing_texture, RenderTarget}; + +#[derive(Debug)] +pub struct ClosingLayer { + /// 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, + + /// Size of the window geometry. + geo_size: Size, + + /// Position in the workspace. + pos: Point, + + /// How much the texture should be offset. + buffer_offset: Point, + + /// How much the blocked-out texture should be offset. + blocked_out_buffer_offset: Point, + + /// The closing animation. + anim: Animation, + + /// Random seed for the shader. + random_seed: f32, +} + +niri_render_elements! { + ClosingLayerRenderElement => { + Texture = RelocateRenderElement>, + Shader = ShaderRenderElement, + } +} + +impl ClosingLayer { + pub fn new>( + renderer: &mut GlesRenderer, + snapshot: RenderSnapshot, + scale: Scale, + geo_size: Size, + pos: Point, + anim: Animation, + ) -> anyhow::Result { + let _span = tracy_client::span!("ClosingWindow::new"); + + let mut render_to_texture = |elements: Vec| -> anyhow::Result<_> { + let (texture, _sync_point, geo) = render_to_encompassing_texture( + renderer, + scale, + Transform::Normal, + Fourcc::Abgr8888, + &elements, + ) + .context("error rendering to texture")?; + + let buffer = TextureBuffer::from_texture( + renderer, + texture, + scale, + Transform::Normal, + Vec::new(), + ); + + let offset = geo.loc.to_f64().to_logical(scale); + + Ok((buffer, offset)) + }; + + let (buffer, buffer_offset) = + render_to_texture(snapshot.contents).context("error rendering contents")?; + let (blocked_out_buffer, blocked_out_buffer_offset) = + render_to_texture(snapshot.blocked_out_contents) + .context("error rendering blocked-out contents")?; + + Ok(Self { + buffer, + blocked_out_buffer, + block_out_from: snapshot.block_out_from, + geo_size, + pos, + buffer_offset, + blocked_out_buffer_offset, + anim, + random_seed: fastrand::f32(), + }) + } + + pub fn advance_animations(&mut self) { + // We don't need to do anything here since the animation is time-based, but we still want to + // call this to trigger the end of the animation when it finishes. + self.anim.value(); + } + + pub fn are_animations_ongoing(&self) -> bool { + !self.anim.is_done() + } + + pub fn render( + &self, + renderer: &mut GlesRenderer, + view_rect: Rectangle, + scale: Scale, + target: RenderTarget, + ) -> ClosingLayerRenderElement { + let (buffer, offset) = if target.should_block_out(self.block_out_from) { + (&self.blocked_out_buffer, self.blocked_out_buffer_offset) + } else { + (&self.buffer, self.buffer_offset) + }; + + let anim = &self.anim; + + let progress = anim.value(); + let clamped_progress = anim.clamped_value().clamp(0., 1.); + + if Shaders::get(renderer).program(ProgramType::Close).is_some() { + let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32); + let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32); + + // Round to physical pixels relative to the view position. This is similar to what + // happens when rendering normal windows. + let relative = self.pos - view_rect.loc; + let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale); + + let geo_loc = Vec2::new(pos.x as f32, pos.y as f32); + let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32); + + let input_to_geo = Mat3::from_scale(area_size / geo_size) + * Mat3::from_translation((area_loc - geo_loc) / area_size); + + let tex_scale = self.buffer.texture_scale(); + let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32); + let tex_loc = Vec2::new(offset.x as f32, offset.y as f32); + let tex_size = self.buffer.texture().size(); + let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale; + + let geo_to_tex = + Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); + + return ShaderRenderElement::new( + ProgramType::Close, + view_rect.size, + None, + scale.x as f32, + 1., + Rc::new([ + mat3_uniform("niri_input_to_geo", input_to_geo), + Uniform::new("niri_geo_size", geo_size.to_array()), + mat3_uniform("niri_geo_to_tex", geo_to_tex), + Uniform::new("niri_progress", progress as f32), + Uniform::new("niri_clamped_progress", clamped_progress as f32), + Uniform::new("niri_random_seed", self.random_seed), + ]), + HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]), + Kind::Unspecified, + ) + .with_location(Point::from((0., 0.))) + .into(); + } + + let elem = TextureRenderElement::from_texture_buffer( + buffer.clone(), + Point::from((0., 0.)), + 1. - clamped_progress as f32, + None, + None, + Kind::Unspecified, + ); + + let elem = PrimaryGpuTextureRenderElement(elem); + + let center = self.geo_size.to_point().downscale(2.); + let elem = RescaleRenderElement::from_element( + elem, + (center - offset).to_physical_precise_round(scale), + ((1. - clamped_progress) / 5. + 0.8).max(0.), + ); + + let mut location = self.pos + offset; + location.x -= view_rect.loc.x; + let elem = RelocateRenderElement::from_element( + elem, + location.to_physical_precise_round(scale), + Relocate::Relative, + ); + + elem.into() + } +} diff --git a/src/layer/mod.rs b/src/layer/mod.rs index b74b56088e..4d60bc33f1 100644 --- a/src/layer/mod.rs +++ b/src/layer/mod.rs @@ -4,6 +4,8 @@ use niri_config::{BlockOutFrom, CornerRadius, ShadowRule}; use smithay::desktop::LayerSurface; pub mod mapped; +pub mod closing_layer; +pub mod opening_layer; pub use mapped::MappedLayer; /// Rules fully resolved for a layer-shell surface. diff --git a/src/layer/opening_layer.rs b/src/layer/opening_layer.rs new file mode 100644 index 0000000000..27483dd3ac --- /dev/null +++ b/src/layer/opening_layer.rs @@ -0,0 +1,142 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use anyhow::Context as _; +use glam::{Mat3, Vec2}; +use smithay::backend::renderer::element::utils::{ + Relocate, RelocateRenderElement, RescaleRenderElement, +}; +use smithay::backend::renderer::element::{Element as _, Kind, RenderElement}; +use smithay::backend::renderer::gles::{GlesRenderer, Uniform}; +use smithay::backend::renderer::Texture; +use smithay::utils::{Logical, Point, Rectangle, Scale, Size}; + +use crate::animation::Animation; +use crate::niri_render_elements; +use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenData, OffscreenRenderElement}; +use crate::render_helpers::shader_element::ShaderRenderElement; +use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders}; + +#[derive(Debug)] +pub struct OpenAnimation { + anim: Animation, + random_seed: f32, + buffer: OffscreenBuffer, +} + +niri_render_elements! { + OpeningLayerRenderElement => { + Offscreen = RelocateRenderElement>, + Shader = ShaderRenderElement, + } +} + +impl OpenAnimation { + pub fn new(anim: Animation) -> Self { + Self { + anim, + random_seed: fastrand::f32(), + buffer: OffscreenBuffer::default(), + } + } + + pub fn is_done(&self) -> bool { + self.anim.is_done() + } + + // We can't depend on view_rect here, because the result of window opening can be snapshot and + // then rendered elsewhere. + pub fn render( + &self, + renderer: &mut GlesRenderer, + elements: &[impl RenderElement], + geo_size: Size, + location: Point, + scale: Scale, + alpha: f32, + ) -> anyhow::Result<(OpeningLayerRenderElement, OffscreenData)> { + let progress = self.anim.value(); + let clamped_progress = self.anim.clamped_value().clamp(0., 1.); + + let (elem, _sync_point, mut data) = self + .buffer + .render(renderer, scale, elements) + .context("error rendering to offscreen buffer")?; + + if Shaders::get(renderer).program(ProgramType::Open).is_some() { + // OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we + // can assume that below. + let offset = elem.offset(); + let texture = elem.texture(); + let texture_size = elem.logical_size(); + + let mut area = Rectangle::new(location + offset, texture_size); + + // Expand the area a bit to allow for more varied effects. + let mut target_size = area.size.upscale(1.5); + target_size.w = f64::max(area.size.w + 1000., target_size.w); + target_size.h = f64::max(area.size.h + 1000., target_size.h); + let diff = (target_size.to_point() - area.size.to_point()).downscale(2.); + let diff = diff.to_physical_precise_round(scale).to_logical(scale); + area.loc -= diff; + area.size += diff.upscale(2.).to_size(); + + let area_loc = Vec2::new(area.loc.x as f32, area.loc.y as f32); + let area_size = Vec2::new(area.size.w as f32, area.size.h as f32); + + let geo_loc = Vec2::new(location.x as f32, location.y as f32); + let geo_size = Vec2::new(geo_size.w as f32, geo_size.h as f32); + + let input_to_geo = Mat3::from_scale(area_size / geo_size) + * Mat3::from_translation((area_loc - geo_loc) / area_size); + + let tex_scale = Vec2::new(scale.x as f32, scale.y as f32); + let tex_loc = Vec2::new(offset.x as f32, offset.y as f32); + let tex_size = Vec2::new(texture.width() as f32, texture.height() as f32) / tex_scale; + + let geo_to_tex = + Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); + + let elem = ShaderRenderElement::new( + ProgramType::Open, + area.size, + None, + scale.x as f32, + alpha, + Rc::new([ + mat3_uniform("niri_input_to_geo", input_to_geo), + Uniform::new("niri_geo_size", geo_size.to_array()), + mat3_uniform("niri_geo_to_tex", geo_to_tex), + Uniform::new("niri_progress", progress as f32), + Uniform::new("niri_clamped_progress", clamped_progress as f32), + Uniform::new("niri_random_seed", self.random_seed), + ]), + HashMap::from([(String::from("niri_tex"), texture.clone())]), + Kind::Unspecified, + ) + .with_location(area.loc); + + // We're drawing the shader, not the offscreen itself. + data.id = elem.id().clone(); + + return Ok((elem.into(), data)); + } + + let elem = elem.with_alpha(clamped_progress as f32 * alpha); + + let center = geo_size.to_point().downscale(2.); + let elem = RescaleRenderElement::from_element( + elem, + center.to_physical_precise_round(scale), + (progress / 2. + 0.5).max(0.), + ); + + let elem = RelocateRenderElement::from_element( + elem, + location.to_physical_precise_round(scale), + Relocate::Relative, + ); + + Ok((elem.into(), data)) + } +} From 337d8bbc6d2256b8b9e415d3e7f40d6c000387ed Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Mon, 23 Feb 2026 02:10:51 +0530 Subject: [PATCH 03/13] initial wiring --- src/handlers/layer_shell.rs | 25 ++++++++++++- src/layer/mapped.rs | 75 ++++++++++++++++++++++++++++++++++++- src/niri.rs | 4 ++ 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index e0f1084f5e..14de36b431 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -3,6 +3,7 @@ use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurf use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::wayland::compositor::SurfaceAttributes; use smithay::wayland::compositor::{get_parent, with_states}; use smithay::wayland::shell::wlr_layer::{ self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler, @@ -126,7 +127,7 @@ impl State { let output_size = output_size(&output); let scale = output.current_scale().fractional_scale(); - let mapped = MappedLayer::new( + let mut mapped = MappedLayer::new( layer.clone(), rules, output_size, @@ -135,6 +136,9 @@ impl State { &config, ); + // Mark for animation when buffer arrives + mapped.has_pending_open_animation = true; + let prev = self .niri .mapped_layer_surfaces @@ -143,6 +147,25 @@ impl State { error!("MappedLayer was present for an unmapped surface"); } } + // In src/handlers/layer_shell.rs, after line 144 + if was_unmapped && is_mapped(surface) { + // Check if this is the first buffer attachment + if with_states(surface, |states| { + states + .cached_state + .get::() + .current() + .buffer + .is_some() + }) { + if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) { + if mapped.has_pending_open_animation { + mapped.start_open_animation(&self.niri.config.borrow().animations); + mapped.has_pending_open_animation = false; + } + } + } + } // Give focus to newly mapped on-demand surfaces. Some launchers like lxqt-runner rely // on this behavior. While this behavior doesn't make much sense for other clients like diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index c59baf13f1..3e836bc778 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -2,16 +2,19 @@ use niri_config::utils::MergeWith as _; use niri_config::{Config, LayerRule}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::Kind; +use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::{LayerSurface, PopupManager}; use smithay::utils::{Logical, Point, Scale, Size}; use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer}; use super::ResolvedLayerRules; -use crate::animation::Clock; +use crate::animation::{Animation, Clock}; +use crate::layer::opening_layer::OpenAnimation; use crate::layout::shadow::Shadow; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::shadow::ShadowRenderElement; +use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::surface::push_elements_from_surface_tree; use crate::render_helpers::RenderTarget; @@ -37,6 +40,15 @@ pub struct MappedLayer { /// Scale of the output the layer surface is on (and rounds its sizes to). scale: f64, + /// Whether there is an ongoing open animation that needs to be advanced and rendered. + pub has_pending_open_animation: bool, + + /// The animation upon opening a layer. + open_animation: Option, + + /// The animation upon closing a layer. + unmap_snapshot: Option, + /// Clock for driving animations. clock: Clock, } @@ -49,6 +61,11 @@ niri_render_elements! { } } +pub type LayerSurfaceRenderSnapshot = RenderSnapshot< + LayerSurfaceRenderElement, + LayerSurfaceRenderElement, +>; + impl MappedLayer { pub fn new( surface: LayerSurface, @@ -70,6 +87,9 @@ impl MappedLayer { view_size, scale, shadow: Shadow::new(shadow_config), + has_pending_open_animation: false, + open_animation: None, + unmap_snapshot: None, clock, } } @@ -105,6 +125,59 @@ impl MappedLayer { .update_render_elements(size, true, radius, self.scale, 1.); } + pub fn store_unmap_snapshot(&mut self, renderer: &mut GlesRenderer) { + let _span = tracy_client::span!("MappedLayer::store_unmap_snapshot"); + let mut contents = Vec::new(); + self.render_normal( + renderer, + Point::from((0., 0.)), + RenderTarget::Output, + &mut |elem| contents.push(elem), + ); + + // A bit of a hack to render blocked out as for screencast, but I think it's fine here. + let mut blocked_out_contents = Vec::new(); + self.render_normal( + renderer, + Point::from((0., 0.)), + RenderTarget::Screencast, + &mut |elem| blocked_out_contents.push(elem), + ); + + let size = self.surface.cached_state().size.to_f64(); + + self.unmap_snapshot = Some(LayerSurfaceRenderSnapshot { + contents, + blocked_out_contents, + block_out_from: self.rules.block_out_from, + size, + texture: Default::default(), + blocked_out_texture: Default::default(), + }) + } + + pub fn take_unmap_snapshot(&mut self) -> Option { + self.unmap_snapshot.take() + } + + pub fn advance_animations(&mut self) { + if let Some(open_anim) = &self.open_animation { + if open_anim.is_done() { + self.open_animation = None; + } + } + } + + pub fn start_open_animation(&mut self, config: &niri_config::Animations) { + self.open_animation = Some(OpenAnimation::new(Animation::new( + self.clock.clone(), + 0., + 1., + 0., + config.layer_open.anim, + ))); + } + pub fn are_animations_ongoing(&self) -> bool { self.rules.baba_is_float } diff --git a/src/niri.rs b/src/niri.rs index c245c6a38e..29a08b83e1 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -239,6 +239,9 @@ pub struct Niri { /// Extra data for mapped layer surfaces. pub mapped_layer_surfaces: HashMap, + /// Layers in the closing animation + pub closing_layers: Vec, + // Cached root surface for every surface, so that we can access it in destroyed() where the // normal get_parent() is cleared out. pub root_surface: HashMap, @@ -2461,6 +2464,7 @@ impl Niri { unmapped_windows: HashMap::new(), unmapped_layer_surfaces: HashSet::new(), mapped_layer_surfaces: HashMap::new(), + closing_layers: Vec::new(), root_surface: HashMap::new(), dmabuf_pre_commit_hook: HashMap::new(), blocker_cleared_tx, From 2488a2ebb332e65b816c65d76f3e392835dd9e96 Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Mon, 23 Feb 2026 02:32:00 +0530 Subject: [PATCH 04/13] more config boilerplate --- niri-config/src/animations.rs | 2 ++ src/backend/tty.rs | 6 ++++ src/backend/winit.rs | 6 ++++ src/render_helpers/shaders/mod.rs | 54 ++++++++++++++++++++++--------- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/niri-config/src/animations.rs b/niri-config/src/animations.rs index f4a4f7ff7e..27b86cac54 100644 --- a/niri-config/src/animations.rs +++ b/niri-config/src/animations.rs @@ -97,6 +97,8 @@ impl MergeWith for Animations { workspace_switch, window_open, window_close, + layer_open, + layer_close, horizontal_view_movement, window_movement, window_resize, diff --git a/src/backend/tty.rs b/src/backend/tty.rs index c0505acdb5..ead059cd0e 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -826,6 +826,12 @@ impl Tty { if let Some(src) = config.animations.window_open.custom_shader.as_deref() { shaders::set_custom_window_open_program(gles_renderer, Some(src)); } + if let Some(src) = config.animations.layer_close.custom_shader.as_deref() { + shaders::set_custom_layer_close_program(gles_renderer, Some(src)); + } + if let Some(src) = config.animations.layer_open.custom_shader.as_deref() { + shaders::set_custom_layer_open_program(gles_renderer, Some(src)); + } drop(config); niri.update_shaders(); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 53a42b0d7b..90632b8fcc 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -160,6 +160,12 @@ impl Winit { if let Some(src) = config.animations.window_open.custom_shader.as_deref() { shaders::set_custom_window_open_program(renderer, Some(src)); } + if let Some(src) = config.animations.layer_close.custom_shader.as_deref() { + shaders::set_custom_layer_close_program(renderer, Some(src)); + } + if let Some(src) = config.animations.layer_open.custom_shader.as_deref() { + shaders::set_custom_layer_open_program(renderer, Some(src)); + } drop(config); niri.update_shaders(); diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index 590a77f22b..c244023f23 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -16,8 +16,10 @@ pub struct Shaders { pub resize: Option, pub gradient_fade: Option, pub custom_resize: RefCell>, - pub custom_close: RefCell>, - pub custom_open: RefCell>, + pub custom_window_close: RefCell>, + pub custom_window_open: RefCell>, + pub custom_layer_close: RefCell>, + pub custom_layer_open: RefCell>, } #[derive(Debug, Clone, Copy)] @@ -27,6 +29,8 @@ pub enum ProgramType { Resize, Close, Open, + LayerClose, + LayerOpen, } impl Shaders { @@ -114,8 +118,10 @@ impl Shaders { resize, gradient_fade, custom_resize: RefCell::new(None), - custom_close: RefCell::new(None), - custom_open: RefCell::new(None), + custom_window_close: RefCell::new(None), + custom_window_open: RefCell::new(None), + custom_layer_close: RefCell::new(None), + custom_layer_open: RefCell::new(None), } } @@ -139,18 +145,32 @@ impl Shaders { self.custom_resize.replace(program) } - pub fn replace_custom_close_program( + pub fn replace_custom_window_close_program( &self, program: Option, ) -> Option { - self.custom_close.replace(program) + self.custom_window_close.replace(program) } - pub fn replace_custom_open_program( + pub fn replace_custom_window_open_program( &self, program: Option, ) -> Option { - self.custom_open.replace(program) + self.custom_window_open.replace(program) + } + + pub fn replace_custom_layer_close_program( + &self, + program: Option, + ) -> Option { + self.custom_layer_close.replace(program) + } + + pub fn replace_custom_layer_open_program( + &self, + program: Option, + ) -> Option { + self.custom_layer_open.replace(program) } pub fn program(&self, program: ProgramType) -> Option { @@ -162,8 +182,10 @@ impl Shaders { .borrow() .clone() .or_else(|| self.resize.clone()), - ProgramType::Close => self.custom_close.borrow().clone(), - ProgramType::Open => self.custom_open.borrow().clone(), + ProgramType::Close => self.custom_window_close.borrow().clone(), + ProgramType::Open => self.custom_window_open.borrow().clone(), + ProgramType::LayerClose => self.custom_layer_close.borrow().clone(), + ProgramType::LayerOpen => self.custom_layer_open.borrow().clone(), } } } @@ -259,9 +281,9 @@ pub fn set_custom_window_close_program(renderer: &mut GlesRenderer, src: Option< None }; - if let Some(prev) = Shaders::get(renderer).replace_custom_close_program(program) { + if let Some(prev) = Shaders::get(renderer).replace_custom_window_close_program(program) { if let Err(err) = prev.destroy(renderer) { - warn!("error destroying previous custom close shader: {err:?}"); + warn!("error destroying previous custom window close shader: {err:?}"); } } } @@ -302,9 +324,9 @@ pub fn set_custom_window_open_program(renderer: &mut GlesRenderer, src: Option<& None }; - if let Some(prev) = Shaders::get(renderer).replace_custom_open_program(program) { + if let Some(prev) = Shaders::get(renderer).replace_custom_window_open_program(program) { if let Err(err) = prev.destroy(renderer) { - warn!("error destroying previous custom open shader: {err:?}"); + warn!("error destroying previous custom window open shader: {err:?}"); } } } @@ -322,7 +344,7 @@ pub fn set_custom_layer_open_program(renderer: &mut GlesRenderer, src: Option<&s None }; - if let Some(prev) = Shaders::get(renderer).replace_custom_open_program(program) { + if let Some(prev) = Shaders::get(renderer).replace_custom_layer_open_program(program) { if let Err(err) = prev.destroy(renderer) { warn!("error destroying previous custom layer open shader: {err:?}"); } @@ -342,7 +364,7 @@ pub fn set_custom_layer_close_program(renderer: &mut GlesRenderer, src: Option<& None }; - if let Some(prev) = Shaders::get(renderer).replace_custom_close_program(program) { + if let Some(prev) = Shaders::get(renderer).replace_custom_layer_close_program(program) { if let Err(err) = prev.destroy(renderer) { warn!("error destroying previous custom layer close shader: {err:?}"); } From dd38651eb398ef67799c6a03500d82a582695584 Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Mon, 23 Feb 2026 02:44:28 +0530 Subject: [PATCH 05/13] full wiring --- src/handlers/layer_shell.rs | 129 ++++++++++++++++++++++++++++-------- src/layer/closing_layer.rs | 7 +- src/layer/mapped.rs | 50 +++++++++++++- src/layer/opening_layer.rs | 7 +- src/niri.rs | 46 ++++++++++++- 5 files changed, 202 insertions(+), 37 deletions(-) diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index 14de36b431..c9b0063066 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -3,6 +3,7 @@ use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurf use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Logical, Rectangle, Scale}; use smithay::wayland::compositor::SurfaceAttributes; use smithay::wayland::compositor::{get_parent, with_states}; use smithay::wayland::shell::wlr_layer::{ @@ -11,8 +12,10 @@ use smithay::wayland::shell::wlr_layer::{ }; use smithay::wayland::shell::xdg::PopupSurface; +use crate::animation::Animation; +use crate::layer::closing_layer::ClosingLayer; use crate::layer::{MappedLayer, ResolvedLayerRules}; -use crate::niri::State; +use crate::niri::{ClosingLayerState, State}; use crate::utils::{is_mapped, output_size, send_scale_transform}; impl WlrLayerShellHandler for State { @@ -51,17 +54,27 @@ impl WlrLayerShellHandler for State { let wl_surface = surface.wl_surface(); self.niri.unmapped_layer_surfaces.remove(wl_surface); - let output = if let Some((output, mut map, layer)) = - self.niri.layout.outputs().find_map(|o| { - let map = layer_map_for_output(o); - let layer = map - .layers() - .find(|&layer| layer.layer_surface() == &surface) - .cloned(); - layer.map(|layer| (o.clone(), map, layer)) - }) { + let found = self.niri.layout.outputs().find_map(|o| { + let map = layer_map_for_output(o); + let layer = map + .layers() + .find(|&layer| layer.layer_surface() == &surface) + .cloned()?; + Some((o.clone(), layer)) + }); + + let output = if let Some((output, layer)) = found { + let mut map = layer_map_for_output(&output); + let geo = map.layer_geometry(&layer); + + if let Some(mapped) = self.niri.mapped_layer_surfaces.remove(&layer) { + if let Some(geo) = geo { + self.start_close_animation_for_layer(&output, &layer, geo, mapped); + } + } + map.unmap_layer(&layer); - self.niri.mapped_layer_surfaces.remove(&layer); + drop(map); Some(output) } else { None @@ -147,26 +160,33 @@ impl State { error!("MappedLayer was present for an unmapped surface"); } } - // In src/handlers/layer_shell.rs, after line 144 - if was_unmapped && is_mapped(surface) { - // Check if this is the first buffer attachment - if with_states(surface, |states| { - states - .cached_state - .get::() - .current() - .buffer - .is_some() - }) { - if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) { - if mapped.has_pending_open_animation { - mapped.start_open_animation(&self.niri.config.borrow().animations); - mapped.has_pending_open_animation = false; - } + + // Layer surfaces may map before they have renderable content; defer open animation + // until we see the first buffer attachment. + if with_states(surface, |states| { + states + .cached_state + .get::() + .current() + .buffer + .is_some() + }) { + if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) { + if mapped.has_pending_open_animation { + mapped.start_open_animation(&self.niri.config.borrow().animations); + mapped.has_pending_open_animation = false; } } } + // Keep a fresh snapshot while the surface is mapped, so close animation still has + // contents on null-buffer unmap commits. + if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) { + self.backend.with_primary_renderer(|renderer| { + mapped.store_unmap_snapshot(renderer); + }); + } + // Give focus to newly mapped on-demand surfaces. Some launchers like lxqt-runner rely // on this behavior. While this behavior doesn't make much sense for other clients like // panels, the consensus seems to be that it's not a big deal since panels generally @@ -189,7 +209,12 @@ impl State { } } else { // The surface is unmapped. - if self.niri.mapped_layer_surfaces.remove(layer).is_some() { + let geo = map.layer_geometry(layer); + if let Some(mapped) = self.niri.mapped_layer_surfaces.remove(layer) { + if let Some(geo) = geo { + self.start_close_animation_for_layer(&output, layer, geo, mapped); + } + // A mapped surface got unmapped via a null commit. Now it needs to do a new // initial commit again. self.niri.unmapped_layer_surfaces.insert(surface.clone()); @@ -226,4 +251,52 @@ impl State { true } + + fn start_close_animation_for_layer( + &mut self, + output: &Output, + layer: &LayerSurface, + geo: Rectangle, + mut mapped: MappedLayer, + ) { + let anim_config = self.niri.config.borrow().animations.layer_close.anim; + let scale = Scale::from(output.current_scale().fractional_scale()); + + self.backend.with_primary_renderer(|renderer| { + let snapshot = mapped.take_unmap_snapshot().or_else(|| { + mapped.store_unmap_snapshot(renderer); + mapped.take_unmap_snapshot() + }); + + let Some(snapshot) = snapshot else { + warn!("error starting layer close animation: missing layer snapshot"); + return; + }; + + if snapshot.contents.is_empty() && snapshot.blocked_out_contents.is_empty() { + warn!("error starting layer close animation: layer snapshot is empty"); + return; + } + + let anim = Animation::new(self.niri.clock.clone(), 0., 1., 0., anim_config); + let res = ClosingLayer::new( + renderer, + snapshot, + scale, + geo.size.to_f64(), + geo.loc.to_f64(), + anim, + ); + + match res { + Ok(animation) => self.niri.closing_layers.push(ClosingLayerState { + output: output.clone(), + layer: layer.layer(), + for_backdrop: mapped.place_within_backdrop(), + animation, + }), + Err(err) => warn!("error starting layer close animation: {err:?}"), + } + }); + } } diff --git a/src/layer/closing_layer.rs b/src/layer/closing_layer.rs index 82dd2698e9..c71732e443 100644 --- a/src/layer/closing_layer.rs +++ b/src/layer/closing_layer.rs @@ -140,7 +140,10 @@ impl ClosingLayer { let progress = anim.value(); let clamped_progress = anim.clamped_value().clamp(0., 1.); - if Shaders::get(renderer).program(ProgramType::Close).is_some() { + if Shaders::get(renderer) + .program(ProgramType::LayerClose) + .is_some() + { let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32); let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32); @@ -165,7 +168,7 @@ impl ClosingLayer { Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); return ShaderRenderElement::new( - ProgramType::Close, + ProgramType::LayerClose, view_rect.size, None, scale.x as f32, diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index 3e836bc778..37e64b57f2 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -9,7 +9,9 @@ use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer}; use super::ResolvedLayerRules; use crate::animation::{Animation, Clock}; +use crate::layer::closing_layer::ClosingLayerRenderElement; use crate::layer::opening_layer::OpenAnimation; +use crate::layer::opening_layer::OpeningLayerRenderElement; use crate::layout::shadow::Shadow; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; @@ -58,6 +60,8 @@ niri_render_elements! { Wayland = WaylandSurfaceRenderElement, SolidColor = SolidColorRenderElement, Shadow = ShadowRenderElement, + Opening = OpeningLayerRenderElement, + Closing = ClosingLayerRenderElement, } } @@ -128,7 +132,7 @@ impl MappedLayer { pub fn store_unmap_snapshot(&mut self, renderer: &mut GlesRenderer) { let _span = tracy_client::span!("MappedLayer::store_unmap_snapshot"); let mut contents = Vec::new(); - self.render_normal( + self.render_normal_inner( renderer, Point::from((0., 0.)), RenderTarget::Output, @@ -137,7 +141,7 @@ impl MappedLayer { // A bit of a hack to render blocked out as for screencast, but I think it's fine here. let mut blocked_out_contents = Vec::new(); - self.render_normal( + self.render_normal_inner( renderer, Point::from((0., 0.)), RenderTarget::Screencast, @@ -180,6 +184,10 @@ impl MappedLayer { pub fn are_animations_ongoing(&self) -> bool { self.rules.baba_is_float + || self + .open_animation + .as_ref() + .is_some_and(|open| !open.is_done()) } pub fn surface(&self) -> &LayerSurface { @@ -239,6 +247,44 @@ impl MappedLayer { let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.); let location = location + self.bob_offset(); + let mut pushed = false; + if let Some(open) = &self.open_animation { + let renderer = renderer.as_gles_renderer(); + let mut elements = Vec::new(); + self.render_normal_inner( + renderer, + Point::from((0., 0.)), + target, + &mut |elem| elements.push(elem), + ); + + let geo_size = self.surface.cached_state().size.to_f64(); + match open.render(renderer, &elements, geo_size, location, scale, alpha) { + Ok((elem, _)) => { + push(elem.into()); + pushed = true; + } + Err(err) => { + warn!("error rendering layer opening animation: {err:?}"); + } + } + } + + if !pushed { + self.render_normal_inner(renderer, location, target, push); + } + } + + fn render_normal_inner( + &self, + renderer: &mut R, + location: Point, + target: RenderTarget, + push: &mut dyn FnMut(LayerSurfaceRenderElement), + ) { + let scale = Scale::from(self.scale); + let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.); + if target.should_block_out(self.rules.block_out_from) { // Round to physical pixels. let location = location.to_physical_precise_round(scale).to_logical(scale); diff --git a/src/layer/opening_layer.rs b/src/layer/opening_layer.rs index 27483dd3ac..558c3a57bd 100644 --- a/src/layer/opening_layer.rs +++ b/src/layer/opening_layer.rs @@ -63,7 +63,10 @@ impl OpenAnimation { .render(renderer, scale, elements) .context("error rendering to offscreen buffer")?; - if Shaders::get(renderer).program(ProgramType::Open).is_some() { + if Shaders::get(renderer) + .program(ProgramType::LayerOpen) + .is_some() + { // OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we // can assume that below. let offset = elem.offset(); @@ -98,7 +101,7 @@ impl OpenAnimation { Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); let elem = ShaderRenderElement::new( - ProgramType::Open, + ProgramType::LayerOpen, area.size, None, scale.x as f32, diff --git a/src/niri.rs b/src/niri.rs index 29a08b83e1..2995dea251 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -133,6 +133,7 @@ use crate::input::{ mods_with_wheel_binds, TabletData, }; use crate::ipc::server::IpcServer; +use crate::layer::closing_layer::ClosingLayer; use crate::layer::mapped::LayerSurfaceRenderElement; use crate::layer::MappedLayer; use crate::layout::tile::TileRenderElement; @@ -239,8 +240,8 @@ pub struct Niri { /// Extra data for mapped layer surfaces. pub mapped_layer_surfaces: HashMap, - /// Layers in the closing animation - pub closing_layers: Vec, + /// Layer surfaces in closing animations. + pub closing_layers: Vec, // Cached root surface for every surface, so that we can access it in destroyed() where the // normal get_parent() is cleared out. @@ -413,6 +414,14 @@ pub struct Niri { pub casting: Screencasting, } +#[derive(Debug)] +pub struct ClosingLayerState { + pub output: Output, + pub layer: Layer, + pub for_backdrop: bool, + pub animation: ClosingLayer, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PointerVisibility { /// The pointer is visible. @@ -3998,6 +4007,13 @@ impl Niri { self.exit_confirm_dialog.advance_animations(); self.screenshot_ui.advance_animations(); self.window_mru_ui.advance_animations(); + for mapped in self.mapped_layer_surfaces.values_mut() { + mapped.advance_animations(); + } + self.closing_layers.retain_mut(|closing| { + closing.animation.advance_animations(); + closing.animation.are_animations_ongoing() + }); for state in self.output_state.values_mut() { if let Some(transition) = &mut state.screen_transition { @@ -4199,7 +4215,9 @@ impl Niri { } macro_rules! push_normal_from_layer { ($layer:expr, $backdrop:expr, $push:expr) => {{ - self.render_layer_normal(renderer, target, &layer_map, $layer, $backdrop, $push); + self.render_layer_normal( + renderer, output, target, &layer_map, $layer, $backdrop, $push, + ); }}; ($layer:expr, true) => {{ push_normal_from_layer!($layer, true, &mut |elem| push(elem.into())); @@ -4308,6 +4326,7 @@ impl Niri { fn render_layer_normal( &self, renderer: &mut R, + output: &Output, target: RenderTarget, layer_map: &LayerMap, layer: Layer, @@ -4317,6 +4336,23 @@ impl Niri { for (mapped, geo) in self.layers_in_render_order(layer_map, layer, for_backdrop) { mapped.render_normal(renderer, geo.loc.to_f64(), target, push); } + + let scale = Scale::from(output.current_scale().fractional_scale()); + let view_rect = Rectangle::from_size(output_size(output)); + for closing in self.closing_layers.iter().rev() { + if &closing.output != output + || closing.layer != layer + || closing.for_backdrop != for_backdrop + { + continue; + } + + let elem = + closing + .animation + .render(renderer.as_gles_renderer(), view_rect, scale, target); + push(elem.into()); + } } fn render_layer_popups( @@ -4372,6 +4408,10 @@ impl Niri { .layers() .filter_map(|surface| self.mapped_layer_surfaces.get(surface)) .any(|mapped| mapped.are_animations_ongoing()); + state.unfinished_animations_remain |= self + .closing_layers + .iter() + .any(|closing| closing.output == *output); } // Render. From f7659acb187a3737fe9a8992cb3bc8420c1e2065 Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Mon, 23 Feb 2026 10:10:44 +0530 Subject: [PATCH 06/13] more boilerplate --- src/layout/closing_window.rs | 7 +++++-- src/layout/opening_window.rs | 7 +++++-- src/render_helpers/resize.rs | 4 ++-- src/render_helpers/shaders/mod.rs | 24 ++++++++++++------------ 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/layout/closing_window.rs b/src/layout/closing_window.rs index b61b6c8dfb..fab6246c66 100644 --- a/src/layout/closing_window.rs +++ b/src/layout/closing_window.rs @@ -200,7 +200,10 @@ impl ClosingWindow { let progress = anim.value(); let clamped_progress = anim.clamped_value().clamp(0., 1.); - if Shaders::get(renderer).program(ProgramType::Close).is_some() { + if Shaders::get(renderer) + .program(ProgramType::WindowClose) + .is_some() + { let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32); let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32); @@ -225,7 +228,7 @@ impl ClosingWindow { Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); return ShaderRenderElement::new( - ProgramType::Close, + ProgramType::WindowClose, view_rect.size, None, scale.x as f32, diff --git a/src/layout/opening_window.rs b/src/layout/opening_window.rs index 5ba97a3598..ecfb6c0f09 100644 --- a/src/layout/opening_window.rs +++ b/src/layout/opening_window.rs @@ -63,7 +63,10 @@ impl OpenAnimation { .render(renderer, scale, elements) .context("error rendering to offscreen buffer")?; - if Shaders::get(renderer).program(ProgramType::Open).is_some() { + if Shaders::get(renderer) + .program(ProgramType::WindowOpen) + .is_some() + { // OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we // can assume that below. let offset = elem.offset(); @@ -98,7 +101,7 @@ impl OpenAnimation { Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); let elem = ShaderRenderElement::new( - ProgramType::Open, + ProgramType::WindowOpen, area.size, None, scale.x as f32, diff --git a/src/render_helpers/resize.rs b/src/render_helpers/resize.rs index a413e7a2ca..95f30a1ccc 100644 --- a/src/render_helpers/resize.rs +++ b/src/render_helpers/resize.rs @@ -87,7 +87,7 @@ impl ResizeRenderElement { // Create the shader. Self( ShaderRenderElement::new( - ProgramType::Resize, + ProgramType::WindowResize, area.size, None, scale.x, @@ -116,7 +116,7 @@ impl ResizeRenderElement { pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool { Shaders::get(renderer) - .program(ProgramType::Resize) + .program(ProgramType::WindowResize) .is_some() } } diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index c244023f23..320cf7e56d 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -15,7 +15,7 @@ pub struct Shaders { pub clipped_surface: Option, pub resize: Option, pub gradient_fade: Option, - pub custom_resize: RefCell>, + pub custom_window_resize: RefCell>, pub custom_window_close: RefCell>, pub custom_window_open: RefCell>, pub custom_layer_close: RefCell>, @@ -26,9 +26,9 @@ pub struct Shaders { pub enum ProgramType { Border, Shadow, - Resize, - Close, - Open, + WindowResize, + WindowClose, + WindowOpen, LayerClose, LayerOpen, } @@ -117,7 +117,7 @@ impl Shaders { clipped_surface, resize, gradient_fade, - custom_resize: RefCell::new(None), + custom_window_resize: RefCell::new(None), custom_window_close: RefCell::new(None), custom_window_open: RefCell::new(None), custom_layer_close: RefCell::new(None), @@ -138,11 +138,11 @@ impl Shaders { .expect("shaders::init() must be called when creating the renderer") } - pub fn replace_custom_resize_program( + pub fn replace_custom_window_resize_program( &self, program: Option, ) -> Option { - self.custom_resize.replace(program) + self.custom_window_resize.replace(program) } pub fn replace_custom_window_close_program( @@ -177,13 +177,13 @@ impl Shaders { match program { ProgramType::Border => self.border.clone(), ProgramType::Shadow => self.shadow.clone(), - ProgramType::Resize => self - .custom_resize + ProgramType::WindowResize => self + .custom_window_resize .borrow() .clone() .or_else(|| self.resize.clone()), - ProgramType::Close => self.custom_window_close.borrow().clone(), - ProgramType::Open => self.custom_window_open.borrow().clone(), + ProgramType::WindowClose => self.custom_window_close.borrow().clone(), + ProgramType::WindowOpen => self.custom_window_open.borrow().clone(), ProgramType::LayerClose => self.custom_layer_close.borrow().clone(), ProgramType::LayerOpen => self.custom_layer_open.borrow().clone(), } @@ -238,7 +238,7 @@ pub fn set_custom_resize_program(renderer: &mut GlesRenderer, src: Option<&str>) None }; - if let Some(prev) = Shaders::get(renderer).replace_custom_resize_program(program) { + if let Some(prev) = Shaders::get(renderer).replace_custom_window_resize_program(program) { if let Err(err) = prev.destroy(renderer) { warn!("error destroying previous custom resize shader: {err:?}"); } From 31046b4866124616f3f1d45346c86fa7db5d591c Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Mon, 23 Feb 2026 12:52:13 +0530 Subject: [PATCH 07/13] finally it seems to work --- src/handlers/layer_shell.rs | 37 +++++++------------ src/layer/closing_layer.rs | 17 +++++---- src/layer/mapped.rs | 73 +++++++++++++++++++++++++++++-------- src/layer/opening_layer.rs | 4 ++ src/niri.rs | 3 ++ 5 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index c9b0063066..5a04e9a3c1 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -4,7 +4,6 @@ use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Rectangle, Scale}; -use smithay::wayland::compositor::SurfaceAttributes; use smithay::wayland::compositor::{get_parent, with_states}; use smithay::wayland::shell::wlr_layer::{ self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler, @@ -68,7 +67,7 @@ impl WlrLayerShellHandler for State { let geo = map.layer_geometry(&layer); if let Some(mapped) = self.niri.mapped_layer_surfaces.remove(&layer) { - if let Some(geo) = geo { + if let Some(geo) = geo.or_else(|| mapped.last_geometry()) { self.start_close_animation_for_layer(&output, &layer, geo, mapped); } } @@ -130,6 +129,15 @@ impl State { if is_mapped(surface) { let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface); + if let Some(idx) = self + .niri + .closing_layers + .iter() + .position(|closing| &closing.surface == layer) + { + self.niri.closing_layers.remove(idx); + } + // Resolve rules for newly mapped layer surfaces. if was_unmapped { let config = self.niri.config.borrow(); @@ -148,9 +156,7 @@ impl State { self.niri.clock.clone(), &config, ); - - // Mark for animation when buffer arrives - mapped.has_pending_open_animation = true; + mapped.start_open_animation(&config.animations); let prev = self .niri @@ -161,24 +167,6 @@ impl State { } } - // Layer surfaces may map before they have renderable content; defer open animation - // until we see the first buffer attachment. - if with_states(surface, |states| { - states - .cached_state - .get::() - .current() - .buffer - .is_some() - }) { - if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) { - if mapped.has_pending_open_animation { - mapped.start_open_animation(&self.niri.config.borrow().animations); - mapped.has_pending_open_animation = false; - } - } - } - // Keep a fresh snapshot while the surface is mapped, so close animation still has // contents on null-buffer unmap commits. if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) { @@ -211,7 +199,7 @@ impl State { // The surface is unmapped. let geo = map.layer_geometry(layer); if let Some(mapped) = self.niri.mapped_layer_surfaces.remove(layer) { - if let Some(geo) = geo { + if let Some(geo) = geo.or_else(|| mapped.last_geometry()) { self.start_close_animation_for_layer(&output, layer, geo, mapped); } @@ -291,6 +279,7 @@ impl State { match res { Ok(animation) => self.niri.closing_layers.push(ClosingLayerState { output: output.clone(), + surface: layer.clone(), layer: layer.layer(), for_backdrop: mapped.place_within_backdrop(), animation, diff --git a/src/layer/closing_layer.rs b/src/layer/closing_layer.rs index c71732e443..5483eb5267 100644 --- a/src/layer/closing_layer.rs +++ b/src/layer/closing_layer.rs @@ -152,18 +152,20 @@ impl ClosingLayer { let relative = self.pos - view_rect.loc; let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale); + let tex_scale = buffer.texture_scale(); + let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32); + let tex_loc = Vec2::new(offset.x as f32, offset.y as f32); + let tex_size = buffer.texture().size(); + let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale; + + // The close shader runs on a frozen snapshot. Use snapshot-space geometry so shader + // transforms remain valid after unmap even if the mapped geometry changed. let geo_loc = Vec2::new(pos.x as f32, pos.y as f32); - let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32); + let geo_size = tex_size; let input_to_geo = Mat3::from_scale(area_size / geo_size) * Mat3::from_translation((area_loc - geo_loc) / area_size); - let tex_scale = self.buffer.texture_scale(); - let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32); - let tex_loc = Vec2::new(offset.x as f32, offset.y as f32); - let tex_size = self.buffer.texture().size(); - let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale; - let geo_to_tex = Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); @@ -208,6 +210,7 @@ impl ClosingLayer { let mut location = self.pos + offset; location.x -= view_rect.loc.x; + location.y -= view_rect.loc.y; let elem = RelocateRenderElement::from_element( elem, location.to_physical_precise_round(scale), diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index 37e64b57f2..3d8f62ed6b 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -6,6 +6,7 @@ use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::{LayerSurface, PopupManager}; use smithay::utils::{Logical, Point, Scale, Size}; use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer}; +use std::time::Duration; use super::ResolvedLayerRules; use crate::animation::{Animation, Clock}; @@ -42,15 +43,21 @@ pub struct MappedLayer { /// Scale of the output the layer surface is on (and rounds its sizes to). scale: f64, - /// Whether there is an ongoing open animation that needs to be advanced and rendered. - pub has_pending_open_animation: bool, - /// The animation upon opening a layer. open_animation: Option, + /// Pending open animation waiting for renderable content. + pending_open_animation: Option<(Duration, niri_config::Animation)>, + + /// Whether the open animation has been started (prevents double triggers). + open_animation_started: bool, + /// The animation upon closing a layer. unmap_snapshot: Option, + /// Last known geometry while the layer was mapped. + last_geometry: Option>, + /// Clock for driving animations. clock: Clock, } @@ -91,9 +98,11 @@ impl MappedLayer { view_size, scale, shadow: Shadow::new(shadow_config), - has_pending_open_animation: false, open_animation: None, + pending_open_animation: None, + open_animation_started: false, unmap_snapshot: None, + last_geometry: None, clock, } } @@ -165,25 +174,48 @@ impl MappedLayer { } pub fn advance_animations(&mut self) { + if self.open_animation.is_none() { + if let Some((started_at, anim)) = &self.pending_open_animation { + if self.clock.now() >= *started_at { + self.open_animation = Some(OpenAnimation::new(Animation::new( + self.clock.clone(), + 0., + 1., + 0., + *anim, + ))); + self.pending_open_animation = None; + } + } + } + if let Some(open_anim) = &self.open_animation { if open_anim.is_done() { self.open_animation = None; + self.open_animation_started = false; } } } pub fn start_open_animation(&mut self, config: &niri_config::Animations) { - self.open_animation = Some(OpenAnimation::new(Animation::new( - self.clock.clone(), - 0., - 1., - 0., - config.layer_open.anim, - ))); + if self.open_animation_started { + return; + } + + self.pending_open_animation = Some((self.clock.now(), config.layer_open.anim)); + self.open_animation = None; + self.open_animation_started = true; + } + + pub fn reset_open_animation_state(&mut self) { + self.open_animation_started = false; + self.pending_open_animation = None; + self.open_animation = None; } pub fn are_animations_ongoing(&self) -> bool { self.rules.baba_is_float + || self.pending_open_animation.is_some() || self .open_animation .as_ref() @@ -194,6 +226,14 @@ impl MappedLayer { &self.surface } + pub fn set_last_geometry(&mut self, geo: smithay::utils::Rectangle) { + self.last_geometry = Some(geo); + } + + pub fn last_geometry(&self) -> Option> { + self.last_geometry + } + pub fn rules(&self) -> &ResolvedLayerRules { &self.rules } @@ -250,11 +290,14 @@ impl MappedLayer { let mut pushed = false; if let Some(open) = &self.open_animation { let renderer = renderer.as_gles_renderer(); - let mut elements = Vec::new(); - self.render_normal_inner( + let mut elements: Vec> = Vec::new(); + push_elements_from_surface_tree( renderer, - Point::from((0., 0.)), - target, + self.surface.wl_surface(), + Point::from((0, 0)), + scale, + alpha, + Kind::ScanoutCandidate, &mut |elem| elements.push(elem), ); diff --git a/src/layer/opening_layer.rs b/src/layer/opening_layer.rs index 558c3a57bd..e0f203c08e 100644 --- a/src/layer/opening_layer.rs +++ b/src/layer/opening_layer.rs @@ -58,6 +58,10 @@ impl OpenAnimation { let progress = self.anim.value(); let clamped_progress = self.anim.clamped_value().clamp(0., 1.); + if elements.is_empty() { + anyhow::bail!("no elements to animate"); + } + let (elem, _sync_point, mut data) = self .buffer .render(renderer, scale, elements) diff --git a/src/niri.rs b/src/niri.rs index 2995dea251..fad9c25bf2 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -417,6 +417,7 @@ pub struct Niri { #[derive(Debug)] pub struct ClosingLayerState { pub output: Output, + pub surface: LayerSurface, pub layer: Layer, pub for_backdrop: bool, pub animation: ClosingLayer, @@ -4045,6 +4046,8 @@ impl Niri { continue; }; + mapped.set_last_geometry(geo); + mapped.update_render_elements(geo.size.to_f64()); } } From 13f0142b66c647054fb69854a443c7a47bc81251 Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Tue, 24 Feb 2026 02:21:14 +0530 Subject: [PATCH 08/13] cleanup --- src/handlers/layer_shell.rs | 5 +- src/layer/mapped.rs | 94 ++++++++++++------------------------- src/layer/mod.rs | 1 + src/niri.rs | 2 - 4 files changed, 34 insertions(+), 68 deletions(-) diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index 5a04e9a3c1..cef43e1e91 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -67,7 +67,7 @@ impl WlrLayerShellHandler for State { let geo = map.layer_geometry(&layer); if let Some(mapped) = self.niri.mapped_layer_surfaces.remove(&layer) { - if let Some(geo) = geo.or_else(|| mapped.last_geometry()) { + if let Some(geo) = geo { self.start_close_animation_for_layer(&output, &layer, geo, mapped); } } @@ -156,6 +156,7 @@ impl State { self.niri.clock.clone(), &config, ); + // Start the open animation immediately on map. mapped.start_open_animation(&config.animations); let prev = self @@ -199,7 +200,7 @@ impl State { // The surface is unmapped. let geo = map.layer_geometry(layer); if let Some(mapped) = self.niri.mapped_layer_surfaces.remove(layer) { - if let Some(geo) = geo.or_else(|| mapped.last_geometry()) { + if let Some(geo) = geo { self.start_close_animation_for_layer(&output, layer, geo, mapped); } diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index 3d8f62ed6b..f0deb927ce 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -6,13 +6,11 @@ use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::{LayerSurface, PopupManager}; use smithay::utils::{Logical, Point, Scale, Size}; use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer}; -use std::time::Duration; use super::ResolvedLayerRules; use crate::animation::{Animation, Clock}; use crate::layer::closing_layer::ClosingLayerRenderElement; -use crate::layer::opening_layer::OpenAnimation; -use crate::layer::opening_layer::OpeningLayerRenderElement; +use crate::layer::opening_layer::{OpenAnimation, OpeningLayerRenderElement}; use crate::layout::shadow::Shadow; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; @@ -46,18 +44,9 @@ pub struct MappedLayer { /// The animation upon opening a layer. open_animation: Option, - /// Pending open animation waiting for renderable content. - pending_open_animation: Option<(Duration, niri_config::Animation)>, - - /// Whether the open animation has been started (prevents double triggers). - open_animation_started: bool, - /// The animation upon closing a layer. unmap_snapshot: Option, - /// Last known geometry while the layer was mapped. - last_geometry: Option>, - /// Clock for driving animations. clock: Clock, } @@ -99,10 +88,7 @@ impl MappedLayer { scale, shadow: Shadow::new(shadow_config), open_animation: None, - pending_open_animation: None, - open_animation_started: false, unmap_snapshot: None, - last_geometry: None, clock, } } @@ -148,7 +134,8 @@ impl MappedLayer { &mut |elem| contents.push(elem), ); - // A bit of a hack to render blocked out as for screencast, but I think it's fine here. + // A bit of a hack to render blocked out as for screencast, but I think it's fine here as + // well. let mut blocked_out_contents = Vec::new(); self.render_normal_inner( renderer, @@ -174,48 +161,35 @@ impl MappedLayer { } pub fn advance_animations(&mut self) { - if self.open_animation.is_none() { - if let Some((started_at, anim)) = &self.pending_open_animation { - if self.clock.now() >= *started_at { - self.open_animation = Some(OpenAnimation::new(Animation::new( - self.clock.clone(), - 0., - 1., - 0., - *anim, - ))); - self.pending_open_animation = None; - } - } - } - - if let Some(open_anim) = &self.open_animation { - if open_anim.is_done() { - self.open_animation = None; - self.open_animation_started = false; - } + if self + .open_animation + .as_ref() + .is_some_and(|open_anim| open_anim.is_done()) + { + self.open_animation = None; } } - pub fn start_open_animation(&mut self, config: &niri_config::Animations) { - if self.open_animation_started { + pub fn start_open_animation(&mut self, anim_config: &niri_config::Animations) { + if self.open_animation.is_some() { return; } - self.pending_open_animation = Some((self.clock.now(), config.layer_open.anim)); - self.open_animation = None; - self.open_animation_started = true; + self.open_animation = Some(OpenAnimation::new(Animation::new( + self.clock.clone(), + 0., + 1., + 0., + anim_config.layer_open.anim, + ))); } pub fn reset_open_animation_state(&mut self) { - self.open_animation_started = false; - self.pending_open_animation = None; self.open_animation = None; } pub fn are_animations_ongoing(&self) -> bool { self.rules.baba_is_float - || self.pending_open_animation.is_some() || self .open_animation .as_ref() @@ -226,14 +200,6 @@ impl MappedLayer { &self.surface } - pub fn set_last_geometry(&mut self, geo: smithay::utils::Rectangle) { - self.last_geometry = Some(geo); - } - - pub fn last_geometry(&self) -> Option> { - self.last_geometry - } - pub fn rules(&self) -> &ResolvedLayerRules { &self.rules } @@ -287,7 +253,6 @@ impl MappedLayer { let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.); let location = location + self.bob_offset(); - let mut pushed = false; if let Some(open) = &self.open_animation { let renderer = renderer.as_gles_renderer(); let mut elements: Vec> = Vec::new(); @@ -301,21 +266,22 @@ impl MappedLayer { &mut |elem| elements.push(elem), ); - let geo_size = self.surface.cached_state().size.to_f64(); - match open.render(renderer, &elements, geo_size, location, scale, alpha) { - Ok((elem, _)) => { - push(elem.into()); - pushed = true; - } - Err(err) => { - warn!("error rendering layer opening animation: {err:?}"); + if !elements.is_empty() { + let geo_size = self.surface.cached_state().size.to_f64(); + let res = open.render(renderer, &elements, geo_size, location, scale, alpha); + match res { + Ok((elem, _)) => { + push(elem.into()); + return; + } + Err(err) => { + warn!("error rendering layer opening animation: {err:?}"); + } } } } - if !pushed { - self.render_normal_inner(renderer, location, target, push); - } + self.render_normal_inner(renderer, location, target, push); } fn render_normal_inner( diff --git a/src/layer/mod.rs b/src/layer/mod.rs index 4d60bc33f1..425474e66a 100644 --- a/src/layer/mod.rs +++ b/src/layer/mod.rs @@ -4,6 +4,7 @@ use niri_config::{BlockOutFrom, CornerRadius, ShadowRule}; use smithay::desktop::LayerSurface; pub mod mapped; + pub mod closing_layer; pub mod opening_layer; pub use mapped::MappedLayer; diff --git a/src/niri.rs b/src/niri.rs index fad9c25bf2..931b68a962 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -4046,8 +4046,6 @@ impl Niri { continue; }; - mapped.set_last_geometry(geo); - mapped.update_render_elements(geo.size.to_f64()); } } From 05139d259cf457215f9c899b89433d3d349f0a34 Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Tue, 24 Feb 2026 02:53:34 +0530 Subject: [PATCH 09/13] fix open animation by wiring the offscreen_data tracking properly --- src/handlers/layer_shell.rs | 11 +++++++---- src/layer/mapped.rs | 35 ++++++++++++++++++++++++++++++++++- src/niri.rs | 23 ++++++++++++++++++++--- 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index cef43e1e91..cb452c3dc6 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -127,7 +127,7 @@ impl State { .unwrap(); if is_mapped(surface) { - let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface); + let was_mapped = self.niri.mapped_layer_surfaces.contains_key(layer); if let Some(idx) = self .niri @@ -138,8 +138,11 @@ impl State { self.niri.closing_layers.remove(idx); } - // Resolve rules for newly mapped layer surfaces. - if was_unmapped { + // Handle map edge: create state and start the open animation once. + // And resolve rules for newly mapped layer surfaces. + if !was_mapped { + self.niri.unmapped_layer_surfaces.remove(surface); + let config = self.niri.config.borrow(); let rules = &config.layer_rules; @@ -190,7 +193,7 @@ impl State { // https://github.com/niri-wm/niri/issues/641 let on_demand = layer.cached_state().keyboard_interactivity == wlr_layer::KeyboardInteractivity::OnDemand; - if was_unmapped && on_demand { + if !was_mapped && on_demand { // I guess it'd make sense to check that no higher-layer on-demand surface // has focus, but Smithay's Layer doesn't implement Ord so this would be a // little annoying. diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index f0deb927ce..9e9ecab4f7 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -1,3 +1,5 @@ +use std::cell::{Ref, RefCell}; + use niri_config::utils::MergeWith as _; use niri_config::{Config, LayerRule}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; @@ -13,6 +15,7 @@ use crate::layer::closing_layer::ClosingLayerRenderElement; use crate::layer::opening_layer::{OpenAnimation, OpeningLayerRenderElement}; use crate::layout::shadow::Shadow; use crate::niri_render_elements; +use crate::render_helpers::offscreen::OffscreenData; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::shadow::ShadowRenderElement; use crate::render_helpers::snapshot::RenderSnapshot; @@ -44,6 +47,9 @@ pub struct MappedLayer { /// The animation upon opening a layer. open_animation: Option, + /// Offscreen state from the current frame's opening animation render. + offscreen_data: RefCell>, + /// The animation upon closing a layer. unmap_snapshot: Option, @@ -88,6 +94,7 @@ impl MappedLayer { scale, shadow: Shadow::new(shadow_config), open_animation: None, + offscreen_data: RefCell::new(None), unmap_snapshot: None, clock, } @@ -160,6 +167,10 @@ impl MappedLayer { self.unmap_snapshot.take() } + pub fn offscreen_data(&self) -> Ref<'_, Option> { + self.offscreen_data.borrow() + } + pub fn advance_animations(&mut self) { if self .open_animation @@ -186,6 +197,7 @@ impl MappedLayer { pub fn reset_open_animation_state(&mut self) { self.open_animation = None; + self.set_offscreen_data(None); } pub fn are_animations_ongoing(&self) -> bool { @@ -253,6 +265,8 @@ impl MappedLayer { let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.); let location = location + self.bob_offset(); + self.set_offscreen_data(None); + if let Some(open) = &self.open_animation { let renderer = renderer.as_gles_renderer(); let mut elements: Vec> = Vec::new(); @@ -270,7 +284,8 @@ impl MappedLayer { let geo_size = self.surface.cached_state().size.to_f64(); let res = open.render(renderer, &elements, geo_size, location, scale, alpha); match res { - Ok((elem, _)) => { + Ok((elem, data)) => { + self.set_offscreen_data(Some(data)); push(elem.into()); return; } @@ -361,4 +376,22 @@ impl MappedLayer { ); } } + + fn set_offscreen_data(&self, data: Option) { + let Some(data) = data else { + self.offscreen_data.replace(None); + return; + }; + + let mut offscreen_data = self.offscreen_data.borrow_mut(); + match &mut *offscreen_data { + None => { + *offscreen_data = Some(data); + } + Some(existing) => { + existing.id = data.id; + existing.states.states.extend(data.states.states); + } + } + } } diff --git a/src/niri.rs b/src/niri.rs index 931b68a962..9eee92050f 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -4636,11 +4636,28 @@ impl Niri { } for surface in layer_map_for_output(output).layers() { + let offscreen_data = self + .mapped_layer_surfaces + .get(surface) + .map(MappedLayer::offscreen_data); + surface.with_surfaces(|surface, states| { - update_surface_primary_scanout_output( - surface, + let primary_scanout_output = states + .data_map + .get_or_insert_threadsafe(Mutex::::default); + let mut primary_scanout_output = primary_scanout_output.lock().unwrap(); + + let mut id = Id::from_wayland_resource(surface); + + if let Some(data) = offscreen_data.as_ref().and_then(|data| data.as_ref()) { + if data.states.element_was_presented(id.clone()) { + id = data.id.clone(); + } + } + + primary_scanout_output.update_from_render_element_states( + id, output, - states, render_element_states, // Layer surfaces are shown only on one output at a time. |_, _, output, _| output, From fdbe81766126c9c954f262b549f80d35ad24d4df Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Tue, 24 Feb 2026 11:03:19 +0530 Subject: [PATCH 10/13] fix for swaync --- src/layer/mapped.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index 9e9ecab4f7..fbd3c94cfc 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -21,7 +21,7 @@ use crate::render_helpers::shadow::ShadowRenderElement; use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::surface::push_elements_from_surface_tree; -use crate::render_helpers::RenderTarget; +use crate::render_helpers::{encompassing_geo, RenderTarget}; use crate::utils::{baba_is_float_offset, round_logical_in_physical}; #[derive(Debug)] @@ -268,10 +268,9 @@ impl MappedLayer { self.set_offscreen_data(None); if let Some(open) = &self.open_animation { - let renderer = renderer.as_gles_renderer(); let mut elements: Vec> = Vec::new(); push_elements_from_surface_tree( - renderer, + renderer.as_gles_renderer(), self.surface.wl_surface(), Point::from((0, 0)), scale, @@ -281,8 +280,27 @@ impl MappedLayer { ); if !elements.is_empty() { - let geo_size = self.surface.cached_state().size.to_f64(); - let res = open.render(renderer, &elements, geo_size, location, scale, alpha); + let mut geo_size = self.surface.cached_state().size.to_f64(); + if geo_size.w <= 0. || geo_size.h <= 0. { + geo_size = encompassing_geo(scale, elements.iter()) + .size + .to_f64() + .to_logical(scale); + } + + if geo_size.w <= 0. || geo_size.h <= 0. { + self.render_normal_inner(renderer, location, target, push); + return; + } + + let res = open.render( + renderer.as_gles_renderer(), + &elements, + geo_size, + location, + scale, + alpha, + ); match res { Ok((elem, data)) => { self.set_offscreen_data(Some(data)); From cf7a5f5964e308896c37d88e582fb71ad563919a Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Thu, 26 Feb 2026 01:42:13 +0530 Subject: [PATCH 11/13] clean up unneccessary differences between layer and window anims --- src/layer/closing_layer.rs | 15 ++++++--------- src/layer/opening_layer.rs | 4 ---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/layer/closing_layer.rs b/src/layer/closing_layer.rs index 5483eb5267..f27d075287 100644 --- a/src/layer/closing_layer.rs +++ b/src/layer/closing_layer.rs @@ -152,20 +152,18 @@ impl ClosingLayer { let relative = self.pos - view_rect.loc; let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale); + let geo_loc = Vec2::new(pos.x as f32, pos.y as f32); + let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32); + + let input_to_geo = Mat3::from_scale(area_size / geo_size) + * Mat3::from_translation((area_loc - geo_loc) / area_size); + let tex_scale = buffer.texture_scale(); let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32); let tex_loc = Vec2::new(offset.x as f32, offset.y as f32); let tex_size = buffer.texture().size(); let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale; - // The close shader runs on a frozen snapshot. Use snapshot-space geometry so shader - // transforms remain valid after unmap even if the mapped geometry changed. - let geo_loc = Vec2::new(pos.x as f32, pos.y as f32); - let geo_size = tex_size; - - let input_to_geo = Mat3::from_scale(area_size / geo_size) - * Mat3::from_translation((area_loc - geo_loc) / area_size); - let geo_to_tex = Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); @@ -210,7 +208,6 @@ impl ClosingLayer { let mut location = self.pos + offset; location.x -= view_rect.loc.x; - location.y -= view_rect.loc.y; let elem = RelocateRenderElement::from_element( elem, location.to_physical_precise_round(scale), diff --git a/src/layer/opening_layer.rs b/src/layer/opening_layer.rs index e0f203c08e..558c3a57bd 100644 --- a/src/layer/opening_layer.rs +++ b/src/layer/opening_layer.rs @@ -58,10 +58,6 @@ impl OpenAnimation { let progress = self.anim.value(); let clamped_progress = self.anim.clamped_value().clamp(0., 1.); - if elements.is_empty() { - anyhow::bail!("no elements to animate"); - } - let (elem, _sync_point, mut data) = self .buffer .render(renderer, scale, elements) From 224df996fa2d843cf148a7973db56b85d750c881 Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Tue, 24 Feb 2026 14:01:48 +0530 Subject: [PATCH 12/13] so much boilerplate good god --- niri-config/src/animations.rs | 61 ++++++++- niri-config/src/lib.rs | 96 ++++++++++++++ src/backend/tty.rs | 38 ++++++ src/backend/winit.rs | 38 ++++++ src/handlers/layer_shell.rs | 66 ++++++++- src/layer/closing_layer.rs | 12 +- src/layer/mapped.rs | 15 +-- src/layer/mod.rs | 1 - src/layer/opening_layer.rs | 11 +- src/niri.rs | 150 +++++++++++++++++++-- src/render_helpers/shaders/mod.rs | 213 ++++++++++++++++++++++++++++++ 11 files changed, 667 insertions(+), 34 deletions(-) diff --git a/niri-config/src/animations.rs b/niri-config/src/animations.rs index 27b86cac54..a705bc5c13 100644 --- a/niri-config/src/animations.rs +++ b/niri-config/src/animations.rs @@ -13,6 +13,12 @@ pub struct Animations { pub window_close: WindowCloseAnim, pub layer_open: LayerOpenAnim, pub layer_close: LayerCloseAnim, + pub layer_bar_open: LayerOpenAnim, + pub layer_bar_close: LayerCloseAnim, + pub layer_wallpaper_open: LayerOpenAnim, + pub layer_wallpaper_close: LayerCloseAnim, + pub layer_launcher_open: LayerOpenAnim, + pub layer_launcher_close: LayerCloseAnim, pub horizontal_view_movement: HorizontalViewMovementAnim, pub window_movement: WindowMovementAnim, pub window_resize: WindowResizeAnim, @@ -35,6 +41,12 @@ impl Default for Animations { window_close: Default::default(), layer_open: Default::default(), layer_close: Default::default(), + layer_bar_open: Default::default(), + layer_bar_close: Default::default(), + layer_wallpaper_open: Default::default(), + layer_wallpaper_close: Default::default(), + layer_launcher_open: Default::default(), + layer_launcher_close: Default::default(), window_resize: Default::default(), config_notification_open_close: Default::default(), exit_confirmation_open_close: Default::default(), @@ -64,6 +76,18 @@ pub struct AnimationsPart { #[knuffel(child)] pub layer_close: Option, #[knuffel(child)] + pub layer_bar_open: Option, + #[knuffel(child)] + pub layer_bar_close: Option, + #[knuffel(child)] + pub layer_wallpaper_open: Option, + #[knuffel(child)] + pub layer_wallpaper_close: Option, + #[knuffel(child)] + pub layer_launcher_open: Option, + #[knuffel(child)] + pub layer_launcher_close: Option, + #[knuffel(child)] pub horizontal_view_movement: Option, #[knuffel(child)] pub window_movement: Option, @@ -90,6 +114,41 @@ impl MergeWith for Animations { merge!((self, part), slowdown); + let prev_layer_open = self.layer_open.clone(); + if let Some(layer_open) = &part.layer_open { + self.layer_open = layer_open.clone(); + if part.layer_bar_open.is_none() && self.layer_bar_open == prev_layer_open { + self.layer_bar_open = layer_open.clone(); + } + if part.layer_wallpaper_open.is_none() && self.layer_wallpaper_open == prev_layer_open { + self.layer_wallpaper_open = layer_open.clone(); + } + if part.layer_launcher_open.is_none() && self.layer_launcher_open == prev_layer_open { + self.layer_launcher_open = layer_open.clone(); + } + } + + let prev_layer_close = self.layer_close.clone(); + if let Some(layer_close) = &part.layer_close { + self.layer_close = layer_close.clone(); + if part.layer_bar_close.is_none() && self.layer_bar_close == prev_layer_close { + self.layer_bar_close = layer_close.clone(); + } + if part.layer_wallpaper_close.is_none() + && self.layer_wallpaper_close == prev_layer_close + { + self.layer_wallpaper_close = layer_close.clone(); + } + if part.layer_launcher_close.is_none() && self.layer_launcher_close == prev_layer_close + { + self.layer_launcher_close = layer_close.clone(); + } + } + + merge_clone!((self, part), layer_bar_open, layer_bar_close); + merge_clone!((self, part), layer_wallpaper_open, layer_wallpaper_close); + merge_clone!((self, part), layer_launcher_open, layer_launcher_close); + // Animation properties are fairly tied together, except maybe `off`. So let's just save // ourselves the work and not merge within individual animations. merge_clone!( @@ -97,8 +156,6 @@ impl MergeWith for Animations { workspace_switch, window_open, window_close, - layer_open, - layer_close, horizontal_view_movement, window_movement, window_resize, diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index b61fe1c1a0..ace464efe9 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1520,6 +1520,102 @@ mod tests { }, custom_shader: None, }, + layer_open: LayerOpenAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 150, + curve: EaseOutExpo, + }, + ), + }, + custom_shader: None, + }, + layer_close: LayerCloseAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 150, + curve: EaseOutQuad, + }, + ), + }, + custom_shader: None, + }, + layer_bar_open: LayerOpenAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 150, + curve: EaseOutExpo, + }, + ), + }, + custom_shader: None, + }, + layer_bar_close: LayerCloseAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 150, + curve: EaseOutQuad, + }, + ), + }, + custom_shader: None, + }, + layer_wallpaper_open: LayerOpenAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 150, + curve: EaseOutExpo, + }, + ), + }, + custom_shader: None, + }, + layer_wallpaper_close: LayerCloseAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 150, + curve: EaseOutQuad, + }, + ), + }, + custom_shader: None, + }, + layer_launcher_open: LayerOpenAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 150, + curve: EaseOutExpo, + }, + ), + }, + custom_shader: None, + }, + layer_launcher_close: LayerCloseAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 150, + curve: EaseOutQuad, + }, + ), + }, + custom_shader: None, + }, horizontal_view_movement: HorizontalViewMovementAnim( Animation { off: false, diff --git a/src/backend/tty.rs b/src/backend/tty.rs index ead059cd0e..4c16f6a292 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -832,6 +832,44 @@ impl Tty { if let Some(src) = config.animations.layer_open.custom_shader.as_deref() { shaders::set_custom_layer_open_program(gles_renderer, Some(src)); } + if let Some(src) = config.animations.layer_bar_close.custom_shader.as_deref() { + shaders::set_custom_layer_bar_close_program(gles_renderer, Some(src)); + } + if let Some(src) = config.animations.layer_bar_open.custom_shader.as_deref() { + shaders::set_custom_layer_bar_open_program(gles_renderer, Some(src)); + } + if let Some(src) = config + .animations + .layer_wallpaper_close + .custom_shader + .as_deref() + { + shaders::set_custom_layer_wallpaper_close_program(gles_renderer, Some(src)); + } + if let Some(src) = config + .animations + .layer_wallpaper_open + .custom_shader + .as_deref() + { + shaders::set_custom_layer_wallpaper_open_program(gles_renderer, Some(src)); + } + if let Some(src) = config + .animations + .layer_launcher_close + .custom_shader + .as_deref() + { + shaders::set_custom_layer_launcher_close_program(gles_renderer, Some(src)); + } + if let Some(src) = config + .animations + .layer_launcher_open + .custom_shader + .as_deref() + { + shaders::set_custom_layer_launcher_open_program(gles_renderer, Some(src)); + } drop(config); niri.update_shaders(); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 90632b8fcc..9fe579769f 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -166,6 +166,44 @@ impl Winit { if let Some(src) = config.animations.layer_open.custom_shader.as_deref() { shaders::set_custom_layer_open_program(renderer, Some(src)); } + if let Some(src) = config.animations.layer_bar_close.custom_shader.as_deref() { + shaders::set_custom_layer_bar_close_program(renderer, Some(src)); + } + if let Some(src) = config.animations.layer_bar_open.custom_shader.as_deref() { + shaders::set_custom_layer_bar_open_program(renderer, Some(src)); + } + if let Some(src) = config + .animations + .layer_wallpaper_close + .custom_shader + .as_deref() + { + shaders::set_custom_layer_wallpaper_close_program(renderer, Some(src)); + } + if let Some(src) = config + .animations + .layer_wallpaper_open + .custom_shader + .as_deref() + { + shaders::set_custom_layer_wallpaper_open_program(renderer, Some(src)); + } + if let Some(src) = config + .animations + .layer_launcher_close + .custom_shader + .as_deref() + { + shaders::set_custom_layer_launcher_close_program(renderer, Some(src)); + } + if let Some(src) = config + .animations + .layer_launcher_open + .custom_shader + .as_deref() + { + shaders::set_custom_layer_launcher_open_program(renderer, Some(src)); + } drop(config); niri.update_shaders(); diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index cb452c3dc6..058c5db26d 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -6,8 +6,8 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Rectangle, Scale}; use smithay::wayland::compositor::{get_parent, with_states}; use smithay::wayland::shell::wlr_layer::{ - self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler, - WlrLayerShellState, + self, Anchor, ExclusiveZone, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, + WlrLayerShellHandler, WlrLayerShellState, }; use smithay::wayland::shell::xdg::PopupSurface; @@ -15,8 +15,16 @@ use crate::animation::Animation; use crate::layer::closing_layer::ClosingLayer; use crate::layer::{MappedLayer, ResolvedLayerRules}; use crate::niri::{ClosingLayerState, State}; +use crate::render_helpers::shaders::ProgramType; use crate::utils::{is_mapped, output_size, send_scale_transform}; +#[derive(Clone, Copy)] +enum LayerAnimationKind { + Bar, + Wallpaper, + Launcher, +} + impl WlrLayerShellHandler for State { fn shell_state(&mut self) -> &mut WlrLayerShellState { &mut self.niri.layer_shell_state @@ -148,6 +156,21 @@ impl State { let rules = &config.layer_rules; let rules = ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup); + let kind = resolve_layer_animation_kind(layer); + let (anim_config, program) = match kind { + LayerAnimationKind::Bar => { + (&config.animations.layer_bar_open, ProgramType::LayerBarOpen) + } + LayerAnimationKind::Wallpaper => ( + &config.animations.layer_wallpaper_open, + ProgramType::LayerWallpaperOpen, + ), + LayerAnimationKind::Launcher => ( + &config.animations.layer_launcher_open, + ProgramType::LayerLauncherOpen, + ), + }; + let output_size = output_size(&output); let scale = output.current_scale().fractional_scale(); @@ -159,8 +182,9 @@ impl State { self.niri.clock.clone(), &config, ); + // Start the open animation immediately on map. - mapped.start_open_animation(&config.animations); + mapped.start_open_animation(anim_config, program); let prev = self .niri @@ -251,8 +275,24 @@ impl State { geo: Rectangle, mut mapped: MappedLayer, ) { - let anim_config = self.niri.config.borrow().animations.layer_close.anim; let scale = Scale::from(output.current_scale().fractional_scale()); + let kind = resolve_layer_animation_kind(layer); + let config = self.niri.config.borrow(); + + let (anim_config, program) = match kind { + LayerAnimationKind::Bar => ( + config.animations.layer_bar_close.anim, + ProgramType::LayerBarClose, + ), + LayerAnimationKind::Wallpaper => ( + config.animations.layer_wallpaper_close.anim, + ProgramType::LayerWallpaperClose, + ), + LayerAnimationKind::Launcher => ( + config.animations.layer_launcher_close.anim, + ProgramType::LayerLauncherClose, + ), + }; self.backend.with_primary_renderer(|renderer| { let snapshot = mapped.take_unmap_snapshot().or_else(|| { @@ -278,6 +318,7 @@ impl State { geo.size.to_f64(), geo.loc.to_f64(), anim, + program, ); match res { @@ -293,3 +334,20 @@ impl State { }); } } + +fn resolve_layer_animation_kind(layer: &LayerSurface) -> LayerAnimationKind { + let state = layer.cached_state(); + let anchor = state.anchor; + let has_exclusive_zone = matches!(state.exclusive_zone, ExclusiveZone::Exclusive(_)); + let has_all_edges = anchor + .contains(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT) + && anchor.bits().count_ones() == 4; + + if has_exclusive_zone { + LayerAnimationKind::Bar + } else if has_all_edges { + LayerAnimationKind::Wallpaper + } else { + LayerAnimationKind::Launcher + } +} diff --git a/src/layer/closing_layer.rs b/src/layer/closing_layer.rs index f27d075287..32ddb67ce1 100644 --- a/src/layer/closing_layer.rs +++ b/src/layer/closing_layer.rs @@ -48,6 +48,9 @@ pub struct ClosingLayer { /// The closing animation. anim: Animation, + /// Program type for shader selection. + program: ProgramType, + /// Random seed for the shader. random_seed: f32, } @@ -67,6 +70,7 @@ impl ClosingLayer { geo_size: Size, pos: Point, anim: Animation, + program: ProgramType, ) -> anyhow::Result { let _span = tracy_client::span!("ClosingWindow::new"); @@ -108,6 +112,7 @@ impl ClosingLayer { buffer_offset, blocked_out_buffer_offset, anim, + program, random_seed: fastrand::f32(), }) } @@ -140,10 +145,7 @@ impl ClosingLayer { let progress = anim.value(); let clamped_progress = anim.clamped_value().clamp(0., 1.); - if Shaders::get(renderer) - .program(ProgramType::LayerClose) - .is_some() - { + if Shaders::get(renderer).program(self.program).is_some() { let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32); let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32); @@ -168,7 +170,7 @@ impl ClosingLayer { Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); return ShaderRenderElement::new( - ProgramType::LayerClose, + self.program, view_rect.size, None, scale.x as f32, diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index fbd3c94cfc..a6fd50432f 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -1,5 +1,6 @@ use std::cell::{Ref, RefCell}; +use niri_config::animations::LayerOpenAnim; use niri_config::utils::MergeWith as _; use niri_config::{Config, LayerRule}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; @@ -17,6 +18,7 @@ use crate::layout::shadow::Shadow; use crate::niri_render_elements; use crate::render_helpers::offscreen::OffscreenData; use crate::render_helpers::renderer::NiriRenderer; +use crate::render_helpers::shaders::ProgramType; use crate::render_helpers::shadow::ShadowRenderElement; use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; @@ -181,18 +183,15 @@ impl MappedLayer { } } - pub fn start_open_animation(&mut self, anim_config: &niri_config::Animations) { + pub fn start_open_animation(&mut self, anim_config: &LayerOpenAnim, program: ProgramType) { if self.open_animation.is_some() { return; } - self.open_animation = Some(OpenAnimation::new(Animation::new( - self.clock.clone(), - 0., - 1., - 0., - anim_config.layer_open.anim, - ))); + self.open_animation = Some(OpenAnimation::new( + Animation::new(self.clock.clone(), 0., 1., 0., anim_config.anim), + program, + )); } pub fn reset_open_animation_state(&mut self) { diff --git a/src/layer/mod.rs b/src/layer/mod.rs index 425474e66a..efdf33b787 100644 --- a/src/layer/mod.rs +++ b/src/layer/mod.rs @@ -71,7 +71,6 @@ impl ResolvedLayerRules { if let Some(x) = rule.baba_is_float { resolved.baba_is_float = x; } - resolved.shadow.merge_with(&rule.shadow); } diff --git a/src/layer/opening_layer.rs b/src/layer/opening_layer.rs index 558c3a57bd..dd386c1bf7 100644 --- a/src/layer/opening_layer.rs +++ b/src/layer/opening_layer.rs @@ -22,6 +22,7 @@ pub struct OpenAnimation { anim: Animation, random_seed: f32, buffer: OffscreenBuffer, + program: ProgramType, } niri_render_elements! { @@ -32,11 +33,12 @@ niri_render_elements! { } impl OpenAnimation { - pub fn new(anim: Animation) -> Self { + pub fn new(anim: Animation, program: ProgramType) -> Self { Self { anim, random_seed: fastrand::f32(), buffer: OffscreenBuffer::default(), + program, } } @@ -63,10 +65,7 @@ impl OpenAnimation { .render(renderer, scale, elements) .context("error rendering to offscreen buffer")?; - if Shaders::get(renderer) - .program(ProgramType::LayerOpen) - .is_some() - { + if Shaders::get(renderer).program(self.program).is_some() { // OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we // can assume that below. let offset = elem.offset(); @@ -101,7 +100,7 @@ impl OpenAnimation { Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); let elem = ShaderRenderElement::new( - ProgramType::LayerOpen, + self.program, area.size, None, scale.x as f32, diff --git a/src/niri.rs b/src/niri.rs index 9eee92050f..2fbf112cdc 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -1541,22 +1541,156 @@ impl State { layer_rules_changed = true; } - if config.animations.layer_close.custom_shader - != old_config.animations.layer_close.custom_shader + let new_layer_close_shader = config + .animations + .layer_bar_close + .custom_shader + .as_deref() + .or(config + .animations + .layer_wallpaper_close + .custom_shader + .as_deref()) + .or(config + .animations + .layer_launcher_close + .custom_shader + .as_deref()) + .or(config.animations.layer_close.custom_shader.as_deref()); + let old_layer_close_shader = old_config + .animations + .layer_bar_close + .custom_shader + .as_deref() + .or(old_config + .animations + .layer_wallpaper_close + .custom_shader + .as_deref()) + .or(old_config + .animations + .layer_launcher_close + .custom_shader + .as_deref()) + .or(old_config.animations.layer_close.custom_shader.as_deref()); + if new_layer_close_shader != old_layer_close_shader { + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_layer_close_program(renderer, new_layer_close_shader); + }); + shaders_changed = true; + } + + let new_layer_open_shader = config + .animations + .layer_bar_open + .custom_shader + .as_deref() + .or(config + .animations + .layer_wallpaper_open + .custom_shader + .as_deref()) + .or(config + .animations + .layer_launcher_open + .custom_shader + .as_deref()) + .or(config.animations.layer_open.custom_shader.as_deref()); + let old_layer_open_shader = old_config + .animations + .layer_bar_open + .custom_shader + .as_deref() + .or(old_config + .animations + .layer_wallpaper_open + .custom_shader + .as_deref()) + .or(old_config + .animations + .layer_launcher_open + .custom_shader + .as_deref()) + .or(old_config.animations.layer_open.custom_shader.as_deref()); + if new_layer_open_shader != old_layer_open_shader { + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_layer_open_program(renderer, new_layer_open_shader); + }); + shaders_changed = true; + } + + if config.animations.layer_bar_open.custom_shader + != old_config.animations.layer_bar_open.custom_shader + { + let src = config.animations.layer_bar_open.custom_shader.as_deref(); + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_layer_bar_open_program(renderer, src); + }); + shaders_changed = true; + } + + if config.animations.layer_bar_close.custom_shader + != old_config.animations.layer_bar_close.custom_shader + { + let src = config.animations.layer_bar_close.custom_shader.as_deref(); + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_layer_bar_close_program(renderer, src); + }); + shaders_changed = true; + } + + if config.animations.layer_wallpaper_open.custom_shader + != old_config.animations.layer_wallpaper_open.custom_shader + { + let src = config + .animations + .layer_wallpaper_open + .custom_shader + .as_deref(); + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_layer_wallpaper_open_program(renderer, src); + }); + shaders_changed = true; + } + + if config.animations.layer_wallpaper_close.custom_shader + != old_config.animations.layer_wallpaper_close.custom_shader + { + let src = config + .animations + .layer_wallpaper_close + .custom_shader + .as_deref(); + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_layer_wallpaper_close_program(renderer, src); + }); + shaders_changed = true; + } + + if config.animations.layer_launcher_open.custom_shader + != old_config.animations.layer_launcher_open.custom_shader { - let src = config.animations.layer_close.custom_shader.as_deref(); + let src = config + .animations + .layer_launcher_open + .custom_shader + .as_deref(); self.backend.with_primary_renderer(|renderer| { - shaders::set_custom_layer_close_program(renderer, src); + shaders::set_custom_layer_launcher_open_program(renderer, src); }); shaders_changed = true; } - if config.animations.layer_open.custom_shader - != old_config.animations.layer_open.custom_shader + if config.animations.layer_launcher_close.custom_shader + != old_config.animations.layer_launcher_close.custom_shader { - let src = config.animations.layer_open.custom_shader.as_deref(); + let src = config + .animations + .layer_launcher_close + .custom_shader + .as_deref(); self.backend.with_primary_renderer(|renderer| { - shaders::set_custom_layer_open_program(renderer, src); + shaders::set_custom_layer_launcher_close_program(renderer, src); }); shaders_changed = true; } diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index 320cf7e56d..4e49f958e5 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -20,6 +20,12 @@ pub struct Shaders { pub custom_window_open: RefCell>, pub custom_layer_close: RefCell>, pub custom_layer_open: RefCell>, + pub custom_layer_bar_close: RefCell>, + pub custom_layer_bar_open: RefCell>, + pub custom_layer_wallpaper_close: RefCell>, + pub custom_layer_wallpaper_open: RefCell>, + pub custom_layer_launcher_close: RefCell>, + pub custom_layer_launcher_open: RefCell>, } #[derive(Debug, Clone, Copy)] @@ -31,6 +37,12 @@ pub enum ProgramType { WindowOpen, LayerClose, LayerOpen, + LayerBarClose, + LayerBarOpen, + LayerWallpaperClose, + LayerWallpaperOpen, + LayerLauncherClose, + LayerLauncherOpen, } impl Shaders { @@ -122,6 +134,12 @@ impl Shaders { custom_window_open: RefCell::new(None), custom_layer_close: RefCell::new(None), custom_layer_open: RefCell::new(None), + custom_layer_bar_close: RefCell::new(None), + custom_layer_bar_open: RefCell::new(None), + custom_layer_wallpaper_close: RefCell::new(None), + custom_layer_wallpaper_open: RefCell::new(None), + custom_layer_launcher_close: RefCell::new(None), + custom_layer_launcher_open: RefCell::new(None), } } @@ -173,6 +191,48 @@ impl Shaders { self.custom_layer_open.replace(program) } + pub fn replace_custom_layer_bar_close_program( + &self, + program: Option, + ) -> Option { + self.custom_layer_bar_close.replace(program) + } + + pub fn replace_custom_layer_bar_open_program( + &self, + program: Option, + ) -> Option { + self.custom_layer_bar_open.replace(program) + } + + pub fn replace_custom_layer_wallpaper_close_program( + &self, + program: Option, + ) -> Option { + self.custom_layer_wallpaper_close.replace(program) + } + + pub fn replace_custom_layer_wallpaper_open_program( + &self, + program: Option, + ) -> Option { + self.custom_layer_wallpaper_open.replace(program) + } + + pub fn replace_custom_layer_launcher_close_program( + &self, + program: Option, + ) -> Option { + self.custom_layer_launcher_close.replace(program) + } + + pub fn replace_custom_layer_launcher_open_program( + &self, + program: Option, + ) -> Option { + self.custom_layer_launcher_open.replace(program) + } + pub fn program(&self, program: ProgramType) -> Option { match program { ProgramType::Border => self.border.clone(), @@ -186,6 +246,36 @@ impl Shaders { ProgramType::WindowOpen => self.custom_window_open.borrow().clone(), ProgramType::LayerClose => self.custom_layer_close.borrow().clone(), ProgramType::LayerOpen => self.custom_layer_open.borrow().clone(), + ProgramType::LayerBarClose => self + .custom_layer_bar_close + .borrow() + .clone() + .or_else(|| self.custom_layer_close.borrow().clone()), + ProgramType::LayerBarOpen => self + .custom_layer_bar_open + .borrow() + .clone() + .or_else(|| self.custom_layer_open.borrow().clone()), + ProgramType::LayerWallpaperClose => self + .custom_layer_wallpaper_close + .borrow() + .clone() + .or_else(|| self.custom_layer_close.borrow().clone()), + ProgramType::LayerWallpaperOpen => self + .custom_layer_wallpaper_open + .borrow() + .clone() + .or_else(|| self.custom_layer_open.borrow().clone()), + ProgramType::LayerLauncherClose => self + .custom_layer_launcher_close + .borrow() + .clone() + .or_else(|| self.custom_layer_close.borrow().clone()), + ProgramType::LayerLauncherOpen => self + .custom_layer_launcher_open + .borrow() + .clone() + .or_else(|| self.custom_layer_open.borrow().clone()), } } } @@ -371,6 +461,129 @@ pub fn set_custom_layer_close_program(renderer: &mut GlesRenderer, src: Option<& } } +pub fn set_custom_layer_bar_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_open_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom layer bar open shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_layer_bar_open_program(program) { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer bar open shader: {err:?}"); + } + } +} + +pub fn set_custom_layer_bar_close_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_close_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom layer bar close shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_layer_bar_close_program(program) { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer bar close shader: {err:?}"); + } + } +} + +pub fn set_custom_layer_wallpaper_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_open_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom layer wallpaper open shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_layer_wallpaper_open_program(program) + { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer wallpaper open shader: {err:?}"); + } + } +} + +pub fn set_custom_layer_wallpaper_close_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_close_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom layer wallpaper close shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_layer_wallpaper_close_program(program) + { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer wallpaper close shader: {err:?}"); + } + } +} + +pub fn set_custom_layer_launcher_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_open_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom layer launcher open shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_layer_launcher_open_program(program) { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer launcher open shader: {err:?}"); + } + } +} + +pub fn set_custom_layer_launcher_close_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_close_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom layer launcher close shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_layer_launcher_close_program(program) + { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer launcher close shader: {err:?}"); + } + } +} + pub fn mat3_uniform(name: &str, mat: Mat3) -> Uniform<'_> { Uniform::new( name, From 07a64d08d2747a64f41ba6f1cb214a09bc25c7ff Mon Sep 17 00:00:00 2001 From: Chaitanya Dubakula Date: Thu, 5 Mar 2026 01:02:36 +0530 Subject: [PATCH 13/13] simplyify the boilerplate and some other corrections --- niri-config/src/animations.rs | 73 ++++++--------- niri-config/src/lib.rs | 78 ++-------------- src/backend/tty.rs | 30 ++++-- src/backend/winit.rs | 30 ++++-- src/handlers/layer_shell.rs | 50 ++++++++-- src/layer/closing_layer.rs | 2 +- src/layer/mapped.rs | 5 - src/niri.rs | 169 +++++++++++++++++----------------- 8 files changed, 198 insertions(+), 239 deletions(-) diff --git a/niri-config/src/animations.rs b/niri-config/src/animations.rs index a705bc5c13..caa734aba1 100644 --- a/niri-config/src/animations.rs +++ b/niri-config/src/animations.rs @@ -13,12 +13,12 @@ pub struct Animations { pub window_close: WindowCloseAnim, pub layer_open: LayerOpenAnim, pub layer_close: LayerCloseAnim, - pub layer_bar_open: LayerOpenAnim, - pub layer_bar_close: LayerCloseAnim, - pub layer_wallpaper_open: LayerOpenAnim, - pub layer_wallpaper_close: LayerCloseAnim, - pub layer_launcher_open: LayerOpenAnim, - pub layer_launcher_close: LayerCloseAnim, + pub layer_bar_open: Option, + pub layer_bar_close: Option, + pub layer_wallpaper_open: Option, + pub layer_wallpaper_close: Option, + pub layer_launcher_open: Option, + pub layer_launcher_close: Option, pub horizontal_view_movement: HorizontalViewMovementAnim, pub window_movement: WindowMovementAnim, pub window_resize: WindowResizeAnim, @@ -41,12 +41,12 @@ impl Default for Animations { window_close: Default::default(), layer_open: Default::default(), layer_close: Default::default(), - layer_bar_open: Default::default(), - layer_bar_close: Default::default(), - layer_wallpaper_open: Default::default(), - layer_wallpaper_close: Default::default(), - layer_launcher_open: Default::default(), - layer_launcher_close: Default::default(), + layer_bar_open: None, + layer_bar_close: None, + layer_wallpaper_open: None, + layer_wallpaper_close: None, + layer_launcher_open: None, + layer_launcher_close: None, window_resize: Default::default(), config_notification_open_close: Default::default(), exit_confirmation_open_close: Default::default(), @@ -114,41 +114,6 @@ impl MergeWith for Animations { merge!((self, part), slowdown); - let prev_layer_open = self.layer_open.clone(); - if let Some(layer_open) = &part.layer_open { - self.layer_open = layer_open.clone(); - if part.layer_bar_open.is_none() && self.layer_bar_open == prev_layer_open { - self.layer_bar_open = layer_open.clone(); - } - if part.layer_wallpaper_open.is_none() && self.layer_wallpaper_open == prev_layer_open { - self.layer_wallpaper_open = layer_open.clone(); - } - if part.layer_launcher_open.is_none() && self.layer_launcher_open == prev_layer_open { - self.layer_launcher_open = layer_open.clone(); - } - } - - let prev_layer_close = self.layer_close.clone(); - if let Some(layer_close) = &part.layer_close { - self.layer_close = layer_close.clone(); - if part.layer_bar_close.is_none() && self.layer_bar_close == prev_layer_close { - self.layer_bar_close = layer_close.clone(); - } - if part.layer_wallpaper_close.is_none() - && self.layer_wallpaper_close == prev_layer_close - { - self.layer_wallpaper_close = layer_close.clone(); - } - if part.layer_launcher_close.is_none() && self.layer_launcher_close == prev_layer_close - { - self.layer_launcher_close = layer_close.clone(); - } - } - - merge_clone!((self, part), layer_bar_open, layer_bar_close); - merge_clone!((self, part), layer_wallpaper_open, layer_wallpaper_close); - merge_clone!((self, part), layer_launcher_open, layer_launcher_close); - // Animation properties are fairly tied together, except maybe `off`. So let's just save // ourselves the work and not merge within individual animations. merge_clone!( @@ -156,6 +121,8 @@ impl MergeWith for Animations { workspace_switch, window_open, window_close, + layer_open, + layer_close, horizontal_view_movement, window_movement, window_resize, @@ -165,6 +132,18 @@ impl MergeWith for Animations { overview_open_close, recent_windows_close, ); + + // Specific layer animation overrides: None means "fall back to the generic layer_open / + // layer_close at runtime". merge_clone_opt! preserves None when not set in the part. + merge_clone_opt!( + (self, part), + layer_bar_open, + layer_bar_close, + layer_wallpaper_open, + layer_wallpaper_close, + layer_launcher_open, + layer_launcher_close, + ); } } diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index ace464efe9..024f3e47ac 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1544,78 +1544,12 @@ mod tests { }, custom_shader: None, }, - layer_bar_open: LayerOpenAnim { - anim: Animation { - off: false, - kind: Easing( - EasingParams { - duration_ms: 150, - curve: EaseOutExpo, - }, - ), - }, - custom_shader: None, - }, - layer_bar_close: LayerCloseAnim { - anim: Animation { - off: false, - kind: Easing( - EasingParams { - duration_ms: 150, - curve: EaseOutQuad, - }, - ), - }, - custom_shader: None, - }, - layer_wallpaper_open: LayerOpenAnim { - anim: Animation { - off: false, - kind: Easing( - EasingParams { - duration_ms: 150, - curve: EaseOutExpo, - }, - ), - }, - custom_shader: None, - }, - layer_wallpaper_close: LayerCloseAnim { - anim: Animation { - off: false, - kind: Easing( - EasingParams { - duration_ms: 150, - curve: EaseOutQuad, - }, - ), - }, - custom_shader: None, - }, - layer_launcher_open: LayerOpenAnim { - anim: Animation { - off: false, - kind: Easing( - EasingParams { - duration_ms: 150, - curve: EaseOutExpo, - }, - ), - }, - custom_shader: None, - }, - layer_launcher_close: LayerCloseAnim { - anim: Animation { - off: false, - kind: Easing( - EasingParams { - duration_ms: 150, - curve: EaseOutQuad, - }, - ), - }, - custom_shader: None, - }, + layer_bar_open: None, + layer_bar_close: None, + layer_wallpaper_open: None, + layer_wallpaper_close: None, + layer_launcher_open: None, + layer_launcher_close: None, horizontal_view_movement: HorizontalViewMovementAnim( Animation { off: false, diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 4c16f6a292..503a81f80b 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -832,41 +832,51 @@ impl Tty { if let Some(src) = config.animations.layer_open.custom_shader.as_deref() { shaders::set_custom_layer_open_program(gles_renderer, Some(src)); } - if let Some(src) = config.animations.layer_bar_close.custom_shader.as_deref() { + if let Some(src) = config + .animations + .layer_bar_close + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) + { shaders::set_custom_layer_bar_close_program(gles_renderer, Some(src)); } - if let Some(src) = config.animations.layer_bar_open.custom_shader.as_deref() { + if let Some(src) = config + .animations + .layer_bar_open + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) + { shaders::set_custom_layer_bar_open_program(gles_renderer, Some(src)); } if let Some(src) = config .animations .layer_wallpaper_close - .custom_shader - .as_deref() + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) { shaders::set_custom_layer_wallpaper_close_program(gles_renderer, Some(src)); } if let Some(src) = config .animations .layer_wallpaper_open - .custom_shader - .as_deref() + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) { shaders::set_custom_layer_wallpaper_open_program(gles_renderer, Some(src)); } if let Some(src) = config .animations .layer_launcher_close - .custom_shader - .as_deref() + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) { shaders::set_custom_layer_launcher_close_program(gles_renderer, Some(src)); } if let Some(src) = config .animations .layer_launcher_open - .custom_shader - .as_deref() + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) { shaders::set_custom_layer_launcher_open_program(gles_renderer, Some(src)); } diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 9fe579769f..7449a99b10 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -166,41 +166,51 @@ impl Winit { if let Some(src) = config.animations.layer_open.custom_shader.as_deref() { shaders::set_custom_layer_open_program(renderer, Some(src)); } - if let Some(src) = config.animations.layer_bar_close.custom_shader.as_deref() { + if let Some(src) = config + .animations + .layer_bar_close + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) + { shaders::set_custom_layer_bar_close_program(renderer, Some(src)); } - if let Some(src) = config.animations.layer_bar_open.custom_shader.as_deref() { + if let Some(src) = config + .animations + .layer_bar_open + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) + { shaders::set_custom_layer_bar_open_program(renderer, Some(src)); } if let Some(src) = config .animations .layer_wallpaper_close - .custom_shader - .as_deref() + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) { shaders::set_custom_layer_wallpaper_close_program(renderer, Some(src)); } if let Some(src) = config .animations .layer_wallpaper_open - .custom_shader - .as_deref() + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) { shaders::set_custom_layer_wallpaper_open_program(renderer, Some(src)); } if let Some(src) = config .animations .layer_launcher_close - .custom_shader - .as_deref() + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) { shaders::set_custom_layer_launcher_close_program(renderer, Some(src)); } if let Some(src) = config .animations .layer_launcher_open - .custom_shader - .as_deref() + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) { shaders::set_custom_layer_launcher_open_program(renderer, Some(src)); } diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index 058c5db26d..41e4440ed3 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -158,15 +158,28 @@ impl State { let kind = resolve_layer_animation_kind(layer); let (anim_config, program) = match kind { - LayerAnimationKind::Bar => { - (&config.animations.layer_bar_open, ProgramType::LayerBarOpen) - } + LayerAnimationKind::Bar => ( + config + .animations + .layer_bar_open + .as_ref() + .unwrap_or(&config.animations.layer_open), + ProgramType::LayerBarOpen, + ), LayerAnimationKind::Wallpaper => ( - &config.animations.layer_wallpaper_open, + config + .animations + .layer_wallpaper_open + .as_ref() + .unwrap_or(&config.animations.layer_open), ProgramType::LayerWallpaperOpen, ), LayerAnimationKind::Launcher => ( - &config.animations.layer_launcher_open, + config + .animations + .layer_launcher_open + .as_ref() + .unwrap_or(&config.animations.layer_open), ProgramType::LayerLauncherOpen, ), }; @@ -281,15 +294,30 @@ impl State { let (anim_config, program) = match kind { LayerAnimationKind::Bar => ( - config.animations.layer_bar_close.anim, + config + .animations + .layer_bar_close + .as_ref() + .unwrap_or(&config.animations.layer_close) + .anim, ProgramType::LayerBarClose, ), LayerAnimationKind::Wallpaper => ( - config.animations.layer_wallpaper_close.anim, + config + .animations + .layer_wallpaper_close + .as_ref() + .unwrap_or(&config.animations.layer_close) + .anim, ProgramType::LayerWallpaperClose, ), LayerAnimationKind::Launcher => ( - config.animations.layer_launcher_close.anim, + config + .animations + .layer_launcher_close + .as_ref() + .unwrap_or(&config.animations.layer_close) + .anim, ProgramType::LayerLauncherClose, ), }; @@ -335,6 +363,12 @@ impl State { } } +/// Classifies a layer surface into one of three animation kinds based on its geometry hints. +/// +/// The heuristic, in priority order: +/// - **Bar**: has an exclusive zone (reserves screen space) — e.g. panels, docks. +/// - **Wallpaper**: anchored to all four edges with no exclusive zone — e.g. desktop backgrounds. +/// - **Launcher**: everything else — e.g. app launchers, notification overlays. fn resolve_layer_animation_kind(layer: &LayerSurface) -> LayerAnimationKind { let state = layer.cached_state(); let anchor = state.anchor; diff --git a/src/layer/closing_layer.rs b/src/layer/closing_layer.rs index 32ddb67ce1..b7611113f8 100644 --- a/src/layer/closing_layer.rs +++ b/src/layer/closing_layer.rs @@ -72,7 +72,7 @@ impl ClosingLayer { anim: Animation, program: ProgramType, ) -> anyhow::Result { - let _span = tracy_client::span!("ClosingWindow::new"); + let _span = tracy_client::span!("ClosingLayer::new"); let mut render_to_texture = |elements: Vec| -> anyhow::Result<_> { let (texture, _sync_point, geo) = render_to_encompassing_texture( diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index a6fd50432f..ba63cd3ada 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -194,11 +194,6 @@ impl MappedLayer { )); } - pub fn reset_open_animation_state(&mut self) { - self.open_animation = None; - self.set_offscreen_data(None); - } - pub fn are_animations_ongoing(&self) -> bool { self.rules.baba_is_float || self diff --git a/src/niri.rs b/src/niri.rs index 2fbf112cdc..48324e185d 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -1541,38 +1541,8 @@ impl State { layer_rules_changed = true; } - let new_layer_close_shader = config - .animations - .layer_bar_close - .custom_shader - .as_deref() - .or(config - .animations - .layer_wallpaper_close - .custom_shader - .as_deref()) - .or(config - .animations - .layer_launcher_close - .custom_shader - .as_deref()) - .or(config.animations.layer_close.custom_shader.as_deref()); - let old_layer_close_shader = old_config - .animations - .layer_bar_close - .custom_shader - .as_deref() - .or(old_config - .animations - .layer_wallpaper_close - .custom_shader - .as_deref()) - .or(old_config - .animations - .layer_launcher_close - .custom_shader - .as_deref()) - .or(old_config.animations.layer_close.custom_shader.as_deref()); + let new_layer_close_shader = config.animations.layer_close.custom_shader.as_deref(); + let old_layer_close_shader = old_config.animations.layer_close.custom_shader.as_deref(); if new_layer_close_shader != old_layer_close_shader { self.backend.with_primary_renderer(|renderer| { shaders::set_custom_layer_close_program(renderer, new_layer_close_shader); @@ -1580,38 +1550,8 @@ impl State { shaders_changed = true; } - let new_layer_open_shader = config - .animations - .layer_bar_open - .custom_shader - .as_deref() - .or(config - .animations - .layer_wallpaper_open - .custom_shader - .as_deref()) - .or(config - .animations - .layer_launcher_open - .custom_shader - .as_deref()) - .or(config.animations.layer_open.custom_shader.as_deref()); - let old_layer_open_shader = old_config - .animations - .layer_bar_open - .custom_shader - .as_deref() - .or(old_config - .animations - .layer_wallpaper_open - .custom_shader - .as_deref()) - .or(old_config - .animations - .layer_launcher_open - .custom_shader - .as_deref()) - .or(old_config.animations.layer_open.custom_shader.as_deref()); + let new_layer_open_shader = config.animations.layer_open.custom_shader.as_deref(); + let old_layer_open_shader = old_config.animations.layer_open.custom_shader.as_deref(); if new_layer_open_shader != old_layer_open_shader { self.backend.with_primary_renderer(|renderer| { shaders::set_custom_layer_open_program(renderer, new_layer_open_shader); @@ -1619,76 +1559,132 @@ impl State { shaders_changed = true; } - if config.animations.layer_bar_open.custom_shader - != old_config.animations.layer_bar_open.custom_shader + if config + .animations + .layer_bar_open + .as_ref() + .map(|a| a.custom_shader.as_deref()) + != old_config + .animations + .layer_bar_open + .as_ref() + .map(|a| a.custom_shader.as_deref()) { - let src = config.animations.layer_bar_open.custom_shader.as_deref(); + let src = config + .animations + .layer_bar_open + .as_ref() + .and_then(|a| a.custom_shader.as_deref()); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_layer_bar_open_program(renderer, src); }); shaders_changed = true; } - if config.animations.layer_bar_close.custom_shader - != old_config.animations.layer_bar_close.custom_shader + if config + .animations + .layer_bar_close + .as_ref() + .map(|a| a.custom_shader.as_deref()) + != old_config + .animations + .layer_bar_close + .as_ref() + .map(|a| a.custom_shader.as_deref()) { - let src = config.animations.layer_bar_close.custom_shader.as_deref(); + let src = config + .animations + .layer_bar_close + .as_ref() + .and_then(|a| a.custom_shader.as_deref()); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_layer_bar_close_program(renderer, src); }); shaders_changed = true; } - if config.animations.layer_wallpaper_open.custom_shader - != old_config.animations.layer_wallpaper_open.custom_shader + if config + .animations + .layer_wallpaper_open + .as_ref() + .map(|a| a.custom_shader.as_deref()) + != old_config + .animations + .layer_wallpaper_open + .as_ref() + .map(|a| a.custom_shader.as_deref()) { let src = config .animations .layer_wallpaper_open - .custom_shader - .as_deref(); + .as_ref() + .and_then(|a| a.custom_shader.as_deref()); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_layer_wallpaper_open_program(renderer, src); }); shaders_changed = true; } - if config.animations.layer_wallpaper_close.custom_shader - != old_config.animations.layer_wallpaper_close.custom_shader + if config + .animations + .layer_wallpaper_close + .as_ref() + .map(|a| a.custom_shader.as_deref()) + != old_config + .animations + .layer_wallpaper_close + .as_ref() + .map(|a| a.custom_shader.as_deref()) { let src = config .animations .layer_wallpaper_close - .custom_shader - .as_deref(); + .as_ref() + .and_then(|a| a.custom_shader.as_deref()); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_layer_wallpaper_close_program(renderer, src); }); shaders_changed = true; } - if config.animations.layer_launcher_open.custom_shader - != old_config.animations.layer_launcher_open.custom_shader + if config + .animations + .layer_launcher_open + .as_ref() + .map(|a| a.custom_shader.as_deref()) + != old_config + .animations + .layer_launcher_open + .as_ref() + .map(|a| a.custom_shader.as_deref()) { let src = config .animations .layer_launcher_open - .custom_shader - .as_deref(); + .as_ref() + .and_then(|a| a.custom_shader.as_deref()); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_layer_launcher_open_program(renderer, src); }); shaders_changed = true; } - if config.animations.layer_launcher_close.custom_shader - != old_config.animations.layer_launcher_close.custom_shader + if config + .animations + .layer_launcher_close + .as_ref() + .map(|a| a.custom_shader.as_deref()) + != old_config + .animations + .layer_launcher_close + .as_ref() + .map(|a| a.custom_shader.as_deref()) { let src = config .animations .layer_launcher_close - .custom_shader - .as_deref(); + .as_ref() + .and_then(|a| a.custom_shader.as_deref()); self.backend.with_primary_renderer(|renderer| { shaders::set_custom_layer_launcher_close_program(renderer, src); }); @@ -4458,6 +4454,7 @@ impl Niri { }) } + #[allow(clippy::too_many_arguments)] fn render_layer_normal( &self, renderer: &mut R,