diff --git a/niri-config/src/animations.rs b/niri-config/src/animations.rs index 346b62517a..caa734aba1 100644 --- a/niri-config/src/animations.rs +++ b/niri-config/src/animations.rs @@ -11,6 +11,14 @@ pub struct Animations { pub workspace_switch: WorkspaceSwitchAnim, pub window_open: WindowOpenAnim, pub window_close: WindowCloseAnim, + pub layer_open: LayerOpenAnim, + pub layer_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, @@ -31,6 +39,14 @@ impl Default for Animations { window_movement: Default::default(), window_open: Default::default(), window_close: Default::default(), + layer_open: Default::default(), + layer_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(), @@ -56,6 +72,22 @@ pub struct AnimationsPart { #[knuffel(child)] pub window_close: Option, #[knuffel(child)] + pub layer_open: Option, + #[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, @@ -89,6 +121,8 @@ impl MergeWith for Animations { workspace_switch, window_open, window_close, + layer_open, + layer_close, horizontal_view_movement, window_movement, window_resize, @@ -98,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, + ); } } @@ -193,6 +239,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 +537,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/niri-config/src/lib.rs b/niri-config/src/lib.rs index b61fe1c1a0..024f3e47ac 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1520,6 +1520,36 @@ 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: 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 7b52b272dd..503a81f80b 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -821,10 +821,64 @@ 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)); + } + 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)); + } + 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 + .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 + .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 + .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 + .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 + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) + { + shaders::set_custom_layer_launcher_open_program(gles_renderer, Some(src)); } drop(config); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index beace1dce5..7449a99b10 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -155,10 +155,64 @@ 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)); + } + 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)); + } + 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 + .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 + .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 + .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 + .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 + .as_ref() + .and_then(|a| a.custom_shader.as_deref()) + { + shaders::set_custom_layer_launcher_open_program(renderer, Some(src)); } drop(config); diff --git a/src/handlers/layer_shell.rs b/src/handlers/layer_shell.rs index e0f1084f5e..41e4440ed3 100644 --- a/src/handlers/layer_shell.rs +++ b/src/handlers/layer_shell.rs @@ -3,17 +3,28 @@ 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::{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; +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::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 @@ -50,17 +61,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 @@ -114,19 +135,59 @@ 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 + .closing_layers + .iter() + .position(|closing| &closing.surface == layer) + { + self.niri.closing_layers.remove(idx); + } + + // 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); - // Resolve rules for newly mapped layer surfaces. - if was_unmapped { let config = self.niri.config.borrow(); 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 + .as_ref() + .unwrap_or(&config.animations.layer_open), + ProgramType::LayerBarOpen, + ), + LayerAnimationKind::Wallpaper => ( + config + .animations + .layer_wallpaper_open + .as_ref() + .unwrap_or(&config.animations.layer_open), + ProgramType::LayerWallpaperOpen, + ), + LayerAnimationKind::Launcher => ( + config + .animations + .layer_launcher_open + .as_ref() + .unwrap_or(&config.animations.layer_open), + ProgramType::LayerLauncherOpen, + ), + }; + 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 +196,9 @@ impl State { &config, ); + // Start the open animation immediately on map. + mapped.start_open_animation(anim_config, program); + let prev = self .niri .mapped_layer_surfaces @@ -144,6 +208,14 @@ impl State { } } + // 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 @@ -158,7 +230,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. @@ -166,7 +238,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()); @@ -203,4 +280,108 @@ impl State { true } + + fn start_close_animation_for_layer( + &mut self, + output: &Output, + layer: &LayerSurface, + geo: Rectangle, + mut mapped: MappedLayer, + ) { + 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 + .as_ref() + .unwrap_or(&config.animations.layer_close) + .anim, + ProgramType::LayerBarClose, + ), + LayerAnimationKind::Wallpaper => ( + config + .animations + .layer_wallpaper_close + .as_ref() + .unwrap_or(&config.animations.layer_close) + .anim, + ProgramType::LayerWallpaperClose, + ), + LayerAnimationKind::Launcher => ( + config + .animations + .layer_launcher_close + .as_ref() + .unwrap_or(&config.animations.layer_close) + .anim, + ProgramType::LayerLauncherClose, + ), + }; + + 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, + program, + ); + + 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, + }), + Err(err) => warn!("error starting layer close animation: {err:?}"), + } + }); + } +} + +/// 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; + 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 new file mode 100644 index 0000000000..b7611113f8 --- /dev/null +++ b/src/layer/closing_layer.rs @@ -0,0 +1,221 @@ +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, + + /// Program type for shader selection. + program: ProgramType, + + /// 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, + program: ProgramType, + ) -> anyhow::Result { + 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( + 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, + program, + 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(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); + + // 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 = 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; + + let geo_to_tex = + Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); + + return ShaderRenderElement::new( + self.program, + 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/mapped.rs b/src/layer/mapped.rs index c59baf13f1..ba63cd3ada 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -1,20 +1,29 @@ +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; 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::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::shaders::ProgramType; 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)] @@ -37,6 +46,15 @@ pub struct MappedLayer { /// Scale of the output the layer surface is on (and rounds its sizes to). scale: f64, + /// 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, + /// Clock for driving animations. clock: Clock, } @@ -46,9 +64,16 @@ niri_render_elements! { Wayland = WaylandSurfaceRenderElement, SolidColor = SolidColorRenderElement, Shadow = ShadowRenderElement, + Opening = OpeningLayerRenderElement, + Closing = ClosingLayerRenderElement, } } +pub type LayerSurfaceRenderSnapshot = RenderSnapshot< + LayerSurfaceRenderElement, + LayerSurfaceRenderElement, +>; + impl MappedLayer { pub fn new( surface: LayerSurface, @@ -70,6 +95,9 @@ impl MappedLayer { view_size, scale, shadow: Shadow::new(shadow_config), + open_animation: None, + offscreen_data: RefCell::new(None), + unmap_snapshot: None, clock, } } @@ -105,8 +133,73 @@ 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_inner( + 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 as + // well. + let mut blocked_out_contents = Vec::new(); + self.render_normal_inner( + 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 offscreen_data(&self) -> Ref<'_, Option> { + self.offscreen_data.borrow() + } + + pub fn advance_animations(&mut self) { + 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, 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.anim), + program, + )); + } + 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 { @@ -166,6 +259,68 @@ 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 mut elements: Vec> = Vec::new(); + push_elements_from_surface_tree( + renderer.as_gles_renderer(), + self.surface.wl_surface(), + Point::from((0, 0)), + scale, + alpha, + Kind::ScanoutCandidate, + &mut |elem| elements.push(elem), + ); + + if !elements.is_empty() { + 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)); + push(elem.into()); + return; + } + Err(err) => { + warn!("error rendering layer opening animation: {err:?}"); + } + } + } + } + + 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); @@ -233,4 +388,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/layer/mod.rs b/src/layer/mod.rs index b74b56088e..efdf33b787 100644 --- a/src/layer/mod.rs +++ b/src/layer/mod.rs @@ -4,6 +4,9 @@ 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. @@ -68,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 new file mode 100644 index 0000000000..dd386c1bf7 --- /dev/null +++ b/src/layer/opening_layer.rs @@ -0,0 +1,144 @@ +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, + program: ProgramType, +} + +niri_render_elements! { + OpeningLayerRenderElement => { + Offscreen = RelocateRenderElement>, + Shader = ShaderRenderElement, + } +} + +impl OpenAnimation { + pub fn new(anim: Animation, program: ProgramType) -> Self { + Self { + anim, + random_seed: fastrand::f32(), + buffer: OffscreenBuffer::default(), + program, + } + } + + 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(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(); + 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( + self.program, + 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)) + } +} 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/niri.rs b/src/niri.rs index d84c390abf..48324e185d 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,6 +240,9 @@ pub struct Niri { /// Extra data for mapped layer surfaces. pub mapped_layer_surfaces: HashMap, + /// 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. pub root_surface: HashMap, @@ -410,6 +414,15 @@ pub struct Niri { pub casting: Screencasting, } +#[derive(Debug)] +pub struct ClosingLayerState { + pub output: Output, + pub surface: LayerSurface, + pub layer: Layer, + pub for_backdrop: bool, + pub animation: ClosingLayer, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PointerVisibility { /// The pointer is visible. @@ -1528,6 +1541,156 @@ impl State { layer_rules_changed = true; } + 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); + }); + shaders_changed = true; + } + + 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); + }); + shaders_changed = true; + } + + 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 + .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 + .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 + .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 + .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 + .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 + .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 + .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 + .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 + .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 + .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 + .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); + }); + shaders_changed = true; + } + if config.animations.window_resize.custom_shader != old_config.animations.window_resize.custom_shader { @@ -1543,7 +1706,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 +1716,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; } @@ -2441,6 +2604,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, @@ -3974,6 +4138,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 { @@ -4175,7 +4346,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())); @@ -4281,9 +4454,11 @@ impl Niri { }) } + #[allow(clippy::too_many_arguments)] fn render_layer_normal( &self, renderer: &mut R, + output: &Output, target: RenderTarget, layer_map: &LayerMap, layer: Layer, @@ -4293,6 +4468,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( @@ -4348,6 +4540,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. @@ -4571,11 +4767,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, 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 6fccce7f59..4e49f958e5 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -15,18 +15,34 @@ pub struct Shaders { pub clipped_surface: Option, pub resize: Option, pub gradient_fade: Option, - pub custom_resize: RefCell>, - pub custom_close: RefCell>, - pub custom_open: RefCell>, + pub custom_window_resize: RefCell>, + pub custom_window_close: RefCell>, + 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)] pub enum ProgramType { Border, Shadow, - Resize, - Close, - Open, + WindowResize, + WindowClose, + WindowOpen, + LayerClose, + LayerOpen, + LayerBarClose, + LayerBarOpen, + LayerWallpaperClose, + LayerWallpaperOpen, + LayerLauncherClose, + LayerLauncherOpen, } impl Shaders { @@ -113,9 +129,17 @@ impl Shaders { clipped_surface, resize, gradient_fade, - custom_resize: RefCell::new(None), - custom_close: RefCell::new(None), - custom_open: 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), + 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), } } @@ -132,38 +156,126 @@ 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_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 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(), 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_close.borrow().clone(), - ProgramType::Open => self.custom_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(), + 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()), } } } @@ -216,7 +328,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:?}"); } @@ -246,12 +358,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; } } @@ -259,9 +371,9 @@ pub fn set_custom_close_program(renderer: &mut GlesRenderer, src: Option<&str>) 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:?}"); } } } @@ -289,12 +401,174 @@ 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 window open shader: {err:?}"); + return; + } + } + } else { + None + }; + + 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 window open shader: {err:?}"); + } + } +} + +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_layer_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_layer_close_program(program) { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom layer close shader: {err:?}"); + } + } +} + +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 open shader: {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; } } @@ -302,9 +576,10 @@ pub fn set_custom_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { None }; - if let Some(prev) = Shaders::get(renderer).replace_custom_open_program(program) { + 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 open shader: {err:?}"); + warn!("error destroying previous custom layer launcher close shader: {err:?}"); } } }