diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-01-15 14:16:05 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-01-17 23:10:01 +0300 |
| commit | bd559a26602874f4104e342e2ce02317ae1ae605 (patch) | |
| tree | 5ba6d9d511f3ca1342a5874afdc33ecb3f93953f | |
| parent | b4add625b2ffdad3e003b3e437891daacf53a12f (diff) | |
| download | niri-bd559a26602874f4104e342e2ce02317ae1ae605.tar.gz niri-bd559a26602874f4104e342e2ce02317ae1ae605.tar.bz2 niri-bd559a26602874f4104e342e2ce02317ae1ae605.zip | |
Implement window shadows
| -rw-r--r-- | niri-config/src/lib.rs | 150 | ||||
| -rw-r--r-- | resources/default-config.kdl | 37 | ||||
| -rw-r--r-- | src/layout/mod.rs | 19 | ||||
| -rw-r--r-- | src/layout/shadow.rs | 182 | ||||
| -rw-r--r-- | src/layout/tile.rs | 40 | ||||
| -rw-r--r-- | src/render_helpers/mod.rs | 1 | ||||
| -rw-r--r-- | src/render_helpers/shaders/mod.rs | 24 | ||||
| -rw-r--r-- | src/render_helpers/shaders/shadow.frag | 142 | ||||
| -rw-r--r-- | src/render_helpers/shadow.rs | 257 | ||||
| -rw-r--r-- | src/window/mod.rs | 16 | ||||
| -rw-r--r-- | wiki/Configuration:-Layout.md | 56 | ||||
| -rw-r--r-- | wiki/Configuration:-Window-Rules.md | 32 |
12 files changed, 947 insertions, 9 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 7ad013f4..167ef6ac 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -3,6 +3,7 @@ extern crate tracing; use std::collections::HashSet; use std::ffi::OsStr; +use std::ops::Mul; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; @@ -436,6 +437,8 @@ pub struct Layout { #[knuffel(child, default)] pub border: Border, #[knuffel(child, default)] + pub shadow: Shadow, + #[knuffel(child, default)] pub insert_hint: InsertHint, #[knuffel(child, unwrap(children), default)] pub preset_column_widths: Vec<PresetSize>, @@ -460,6 +463,7 @@ impl Default for Layout { Self { focus_ring: Default::default(), border: Default::default(), + shadow: Default::default(), insert_hint: Default::default(), preset_column_widths: Default::default(), default_column_width: Default::default(), @@ -609,6 +613,49 @@ impl From<FocusRing> for Border { } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub struct Shadow { + #[knuffel(child)] + pub on: bool, + #[knuffel(child, default = Self::default().offset)] + pub offset: ShadowOffset, + #[knuffel(child, unwrap(argument), default = Self::default().softness)] + pub softness: FloatOrInt<0, 1024>, + #[knuffel(child, unwrap(argument), default = Self::default().spread)] + pub spread: FloatOrInt<0, 1024>, + #[knuffel(child, unwrap(argument), default = Self::default().draw_behind_window)] + pub draw_behind_window: bool, + #[knuffel(child, default = Self::default().color)] + pub color: Color, + #[knuffel(child)] + pub inactive_color: Option<Color>, +} + +impl Default for Shadow { + fn default() -> Self { + Self { + on: false, + offset: ShadowOffset { + x: FloatOrInt(0.), + y: FloatOrInt(5.), + }, + softness: FloatOrInt(30.), + spread: FloatOrInt(5.), + draw_behind_window: false, + color: Color::from_rgba8_unpremul(0, 0, 0, 0x70), + inactive_color: None, + } + } +} + +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub struct ShadowOffset { + #[knuffel(property, default)] + pub x: FloatOrInt<-65535, 65535>, + #[knuffel(property, default)] + pub y: FloatOrInt<-65535, 65535>, +} + +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct InsertHint { #[knuffel(child)] pub off: bool, @@ -679,6 +726,15 @@ impl Color { } } +impl Mul<f32> for Color { + type Output = Self; + + fn mul(mut self, rhs: f32) -> Self::Output { + self.a *= rhs; + self + } +} + #[derive(knuffel::Decode, Debug, PartialEq)] pub struct Cursor { #[knuffel(child, unwrap(argument), default = String::from("default"))] @@ -1007,6 +1063,8 @@ pub struct WindowRule { pub focus_ring: BorderRule, #[knuffel(child, default)] pub border: BorderRule, + #[knuffel(child, default)] + pub shadow: ShadowRule, #[knuffel(child, unwrap(argument))] pub draw_border_with_background: Option<bool>, #[knuffel(child, unwrap(argument))] @@ -1084,6 +1142,26 @@ pub struct BorderRule { pub inactive_gradient: Option<Gradient>, } +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct ShadowRule { + #[knuffel(child)] + pub off: bool, + #[knuffel(child)] + pub on: bool, + #[knuffel(child)] + pub offset: Option<ShadowOffset>, + #[knuffel(child, unwrap(argument))] + pub softness: Option<FloatOrInt<0, 1024>>, + #[knuffel(child, unwrap(argument))] + pub spread: Option<FloatOrInt<0, 1024>>, + #[knuffel(child, unwrap(argument))] + pub draw_behind_window: Option<bool>, + #[knuffel(child)] + pub color: Option<Color>, + #[knuffel(child)] + pub inactive_color: Option<Color>, +} + #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct FloatingPosition { #[knuffel(property)] @@ -1803,6 +1881,67 @@ impl BorderRule { } } +impl ShadowRule { + pub fn merge_with(&mut self, other: &Self) { + if other.off { + self.off = true; + self.on = false; + } + + if other.on { + self.off = false; + self.on = true; + } + + if let Some(x) = other.offset { + self.offset = Some(x); + } + if let Some(x) = other.softness { + self.softness = Some(x); + } + if let Some(x) = other.spread { + self.spread = Some(x); + } + if let Some(x) = other.draw_behind_window { + self.draw_behind_window = Some(x); + } + if let Some(x) = other.color { + self.color = Some(x); + } + if let Some(x) = other.inactive_color { + self.inactive_color = Some(x); + } + } + + pub fn resolve_against(&self, mut config: Shadow) -> Shadow { + config.on |= self.on; + if self.off { + config.on = false; + } + + if let Some(x) = self.offset { + config.offset = x; + } + if let Some(x) = self.softness { + config.softness = x; + } + if let Some(x) = self.spread { + config.spread = x; + } + if let Some(x) = self.draw_behind_window { + config.draw_behind_window = x; + } + if let Some(x) = self.color { + config.color = x; + } + if let Some(x) = self.inactive_color { + config.inactive_color = Some(x); + } + + config + } +} + impl CornerRadius { pub fn fit_to(self, width: f32, height: f32) -> Self { // Like in CSS: https://drafts.csswg.org/css-backgrounds/#corner-overlap @@ -3221,6 +3360,10 @@ mod tests { inactive-color "rgba(255, 200, 100, 0.0)" } + shadow { + offset x=10 y=-20 + } + preset-column-widths { proportion 0.25 proportion 0.5 @@ -3460,6 +3603,13 @@ mod tests { active_gradient: None, inactive_gradient: None, }, + shadow: Shadow { + offset: ShadowOffset { + x: FloatOrInt(10.), + y: FloatOrInt(-20.), + }, + ..Default::default() + }, insert_hint: InsertHint { off: false, color: Color::from_rgba8_unpremul(255, 200, 127, 255), diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 5696a88c..5936a80e 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -191,6 +191,43 @@ layout { // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" } + // You can enable drop shadows for windows. + shadow { + // Uncomment the next line to enable shadows. + // on + + // By default, the shadow draws only around its window, and not behind it. + // Uncomment this setting to make the shadow draw behind its window. + // + // Note that niri has no way of knowing about the CSD window corner + // radius. It has to assume that windows have square corners, leading to + // shadow artifacts inside the CSD rounded corners. This setting fixes + // those artifacts. + // + // However, instead you may want to set prefer-no-csd and/or + // geometry-corner-radius. Then, niri will know the corner radius and + // draw the shadow correctly, without having to draw it behind the + // window. These will also remove client-side shadows if the window + // draws any. + // + // draw-behind-window true + + // You can change how shadows look. The values below are in logical + // pixels and match the CSS box-shadow properties. + + // Softness controls the shadow blur radius. + softness 30 + + // Spread expands the shadow. + spread 5 + + // Offset moves the shadow relative to the window. + offset x=0 y=5 + + // You can also change the shadow color and opacity. + color "#0007" + } + // Struts shrink the area occupied by windows, similarly to layer-shell panels. // You can think of them as a kind of outer gaps. They are set in logical pixels. // Left and right struts will cause the next window to the side to always be visible. diff --git a/src/layout/mod.rs b/src/layout/mod.rs index f2baa11d..34f64993 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -77,6 +77,7 @@ pub mod insert_hint_element; pub mod monitor; pub mod opening_window; pub mod scrolling; +pub mod shadow; pub mod tile; pub mod workspace; @@ -304,6 +305,7 @@ pub struct Options { pub struts: Struts, pub focus_ring: niri_config::FocusRing, pub border: niri_config::Border, + pub shadow: niri_config::Shadow, pub insert_hint: niri_config::InsertHint, pub center_focused_column: CenterFocusedColumn, pub always_center_single_column: bool, @@ -327,6 +329,7 @@ impl Default for Options { struts: Default::default(), focus_ring: Default::default(), border: Default::default(), + shadow: Default::default(), insert_hint: Default::default(), center_focused_column: Default::default(), always_center_single_column: false, @@ -509,6 +512,7 @@ impl Options { struts: layout.struts, focus_ring: layout.focus_ring, border: layout.border, + shadow: layout.shadow, insert_hint: layout.insert_hint, center_focused_column: layout.center_focused_column, always_center_single_column: layout.always_center_single_column, @@ -7073,11 +7077,25 @@ mod tests { } prop_compose! { + fn arbitrary_shadow()( + on in any::<bool>(), + width in arbitrary_spacing(), + ) -> niri_config::Shadow { + niri_config::Shadow { + on, + softness: FloatOrInt(width), + ..Default::default() + } + } + } + + prop_compose! { fn arbitrary_options()( gaps in arbitrary_spacing(), struts in arbitrary_struts(), focus_ring in arbitrary_focus_ring(), border in arbitrary_border(), + shadow in arbitrary_shadow(), center_focused_column in arbitrary_center_focused_column(), always_center_single_column in any::<bool>(), empty_workspace_above_first in any::<bool>(), @@ -7090,6 +7108,7 @@ mod tests { empty_workspace_above_first, focus_ring, border, + shadow, ..Default::default() } } diff --git a/src/layout/shadow.rs b/src/layout/shadow.rs new file mode 100644 index 00000000..1600e333 --- /dev/null +++ b/src/layout/shadow.rs @@ -0,0 +1,182 @@ +use std::iter::zip; + +use niri_config::CornerRadius; +use smithay::utils::{Logical, Point, Rectangle, Size}; + +use crate::render_helpers::renderer::NiriRenderer; +use crate::render_helpers::shadow::ShadowRenderElement; + +#[derive(Debug)] +pub struct Shadow { + shader_rects: Vec<Rectangle<f64, Logical>>, + shaders: Vec<ShadowRenderElement>, + config: niri_config::Shadow, +} + +impl Shadow { + pub fn new(config: niri_config::Shadow) -> Self { + Self { + shader_rects: Vec::new(), + shaders: Vec::new(), + config, + } + } + + pub fn update_config(&mut self, config: niri_config::Shadow) { + self.config = config; + } + + pub fn update_shaders(&mut self) { + for elem in &mut self.shaders { + elem.damage_all(); + } + } + + pub fn update_render_elements( + &mut self, + win_size: Size<f64, Logical>, + is_active: bool, + radius: CornerRadius, + scale: f64, + ) { + let ceil = |logical: f64| (logical * scale).ceil() / scale; + + // All of this stuff should end up aligned to physical pixels because: + // * Window size is rounded to physical pixels before being passed to this function. + // * We will ceil the corner radii below. + // * We do not divide anything, only add, subtract and multiply by integers. + // * At rendering time, tile positions are rounded to physical pixels. + + let width = self.config.softness.0; + // Like in CSS box-shadow. + let sigma = width / 2.; + // Adjust width to draw all necessary pixels. + let width = ceil(sigma * 3.); + + let offset = self.config.offset; + let offset = Point::from((ceil(offset.x.0), ceil(offset.y.0))); + + let spread = ceil(self.config.spread.0); + let offset = offset - Point::from((spread, spread)); + + let win_radius = radius.fit_to(win_size.w as f32, win_size.h as f32); + + let box_size = win_size + Size::from((spread, spread)).upscale(2.); + let radius = win_radius.expanded_by(spread as f32); + + let shader_size = box_size + Size::from((width, width)).upscale(2.); + + let color = if is_active { + self.config.color + } else { + // Default to slightly more transparent. + self.config + .inactive_color + .unwrap_or(self.config.color * 0.75) + }; + + let shader_geo = Rectangle::new(Point::from((-width, -width)), shader_size); + + // This is actually offset relative to shader_geo, this is handled below. + let window_geo = Rectangle::new(Point::from((0., 0.)), win_size); + + if !self.config.draw_behind_window { + let top_left = ceil(f64::from(win_radius.top_left)); + let top_right = f64::min(win_size.w - top_left, ceil(f64::from(win_radius.top_right))); + let bottom_left = f64::min( + win_size.h - top_left, + ceil(f64::from(win_radius.bottom_left)), + ); + let bottom_right = f64::min( + win_size.h - top_right, + f64::min( + win_size.w - bottom_left, + ceil(f64::from(win_radius.bottom_right)), + ), + ); + + let top_left = Rectangle::new(Point::from((0., 0.)), Size::from((top_left, top_left))); + let top_right = Rectangle::new( + Point::from((win_size.w - top_right, 0.)), + Size::from((top_right, top_right)), + ); + let bottom_right = Rectangle::new( + Point::from((win_size.w - bottom_right, win_size.h - bottom_right)), + Size::from((bottom_right, bottom_right)), + ); + let bottom_left = Rectangle::new( + Point::from((0., win_size.h - bottom_left)), + Size::from((bottom_left, bottom_left)), + ); + + let mut background = + window_geo.subtract_rects([top_left, top_right, bottom_right, bottom_left]); + for rect in &mut background { + rect.loc -= offset; + } + + self.shader_rects = shader_geo.subtract_rects(background); + self.shaders + .resize_with(self.shader_rects.len(), Default::default); + + for (shader, rect) in zip(&mut self.shaders, &mut self.shader_rects) { + shader.update( + rect.size, + Rectangle::new(rect.loc.upscale(-1.), box_size), + color, + sigma as f32, + radius, + scale as f32, + Rectangle::new(window_geo.loc - offset - rect.loc, window_geo.size), + win_radius, + ); + + rect.loc += offset; + } + } else { + self.shader_rects.resize_with(1, Default::default); + self.shader_rects[0] = shader_geo; + + self.shaders.resize_with(1, Default::default); + self.shaders[0].update( + shader_geo.size, + Rectangle::new(shader_geo.loc.upscale(-1.), box_size), + color, + sigma as f32, + radius, + scale as f32, + Rectangle::zero(), + Default::default(), + ); + + self.shader_rects[0].loc += offset; + } + } + + pub fn render( + &self, + renderer: &mut impl NiriRenderer, + location: Point<f64, Logical>, + ) -> impl Iterator<Item = ShadowRenderElement> { + let mut rv = Vec::new(); + + if !self.config.on { + return rv.into_iter(); + } + + let has_shadow_shader = ShadowRenderElement::has_shader(renderer); + if !has_shadow_shader { + return rv.into_iter(); + } + + let mut push = |shader: &ShadowRenderElement, location: Point<f64, Logical>| { + rv.push(shader.clone().with_location(location)); + }; + + for (shader, rect) in zip(&self.shaders, &self.shader_rects) { + push(shader, location + rect.loc); + } + + rv.into_iter() + } +} diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 9cc5237e..a9fd8e13 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -8,6 +8,7 @@ use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform}; use super::focus_ring::{FocusRing, FocusRingRenderElement}; use super::opening_window::{OpenAnimation, OpeningWindowRenderElement}; +use super::shadow::Shadow; use super::{ LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options, SizeFrac, RESIZE_ANIMATION_THRESHOLD, @@ -19,6 +20,7 @@ use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, Rounde use crate::render_helpers::damage::ExtraDamage; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::resize::ResizeRenderElement; +use crate::render_helpers::shadow::ShadowRenderElement; use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::{render_to_encompassing_texture, RenderTarget}; @@ -36,6 +38,9 @@ pub struct Tile<W: LayoutElement> { /// The focus ring around the window. focus_ring: FocusRing, + /// The shadow around the window. + shadow: Shadow, + /// Whether this tile is fullscreen. /// /// This will update only when the `window` actually goes fullscreen, rather than right away, @@ -111,6 +116,7 @@ niri_render_elements! { Opening = OpeningWindowRenderElement, Resize = ResizeRenderElement, Border = BorderRenderElement, + Shadow = ShadowRenderElement, ClippedSurface = ClippedSurfaceRenderElement<R>, ExtraDamage = ExtraDamage, } @@ -143,12 +149,14 @@ impl<W: LayoutElement> Tile<W> { let rules = window.rules(); let border_config = rules.border.resolve_against(options.border); let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into()); + let shadow_config = rules.shadow.resolve_against(options.shadow); let is_fullscreen = window.is_fullscreen(); Self { window, border: FocusRing::new(border_config.into()), focus_ring: FocusRing::new(focus_ring_config.into()), + shadow: Shadow::new(shadow_config), is_fullscreen, fullscreen_backdrop: SolidColorBuffer::new(view_size, [0., 0., 0., 1.]), unfullscreen_to_floating: false, @@ -198,12 +206,16 @@ impl<W: LayoutElement> Tile<W> { .resolve_against(self.options.focus_ring.into()); self.focus_ring.update_config(focus_ring_config.into()); + let shadow_config = rules.shadow.resolve_against(self.options.shadow); + self.shadow.update_config(shadow_config); + self.fullscreen_backdrop.resize(view_size); } pub fn update_shaders(&mut self) { self.border.update_shaders(); self.focus_ring.update_shaders(); + self.shadow.update_shaders(); } pub fn update_window(&mut self) { @@ -254,6 +266,9 @@ impl<W: LayoutElement> Tile<W> { .resolve_against(self.options.focus_ring.into()); self.focus_ring.update_config(focus_ring_config.into()); + let shadow_config = rules.shadow.resolve_against(self.options.shadow); + self.shadow.update_config(shadow_config); + let window_size = self.window_size(); let radius = rules .geometry_corner_radius @@ -323,19 +338,26 @@ impl<W: LayoutElement> Tile<W> { self.scale, ); - let draw_focus_ring_with_background = if self.effective_border_width().is_some() { - false - } else { - draw_border_with_background - }; let radius = if self.is_fullscreen { CornerRadius::default() } else if self.effective_border_width().is_some() { radius } else { rules.geometry_corner_radius.unwrap_or_default() - } - .expanded_by(self.focus_ring.width() as f32); + }; + self.shadow.update_render_elements( + self.animated_tile_size(), + is_active, + radius, + self.scale, + ); + + let draw_focus_ring_with_background = if self.effective_border_width().is_some() { + false + } else { + draw_border_with_background + }; + let radius = radius.expanded_by(self.focus_ring.width() as f32); self.focus_ring.update_render_elements( self.animated_tile_size(), is_active, @@ -899,7 +921,9 @@ impl<W: LayoutElement> Tile<W> { let rv = rv.chain(elem.into_iter().flatten()); let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into)); - rv.chain(elem.into_iter().flatten()) + let rv = rv.chain(elem.into_iter().flatten()); + + rv.chain(self.shadow.render(renderer, location).map(Into::into)) } pub fn render<R: NiriRenderer>( diff --git a/src/render_helpers/mod.rs b/src/render_helpers/mod.rs index 98ccf19e..45e6e527 100644 --- a/src/render_helpers/mod.rs +++ b/src/render_helpers/mod.rs @@ -31,6 +31,7 @@ pub mod resize; pub mod resources; pub mod shader_element; pub mod shaders; +pub mod shadow; pub mod snapshot; pub mod solid_color; pub mod surface; diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index 91ba32d1..ddc58cdb 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -11,6 +11,7 @@ use super::shader_element::ShaderProgram; pub struct Shaders { pub border: Option<ShaderProgram>, + pub shadow: Option<ShaderProgram>, pub clipped_surface: Option<GlesTexProgram>, pub resize: Option<ShaderProgram>, pub custom_resize: RefCell<Option<ShaderProgram>>, @@ -21,6 +22,7 @@ pub struct Shaders { #[derive(Debug, Clone, Copy)] pub enum ProgramType { Border, + Shadow, Resize, Close, Open, @@ -53,6 +55,26 @@ impl Shaders { }) .ok(); + let shadow = ShaderProgram::compile( + renderer, + include_str!("shadow.frag"), + &[ + UniformName::new("shadow_color", UniformType::_4f), + UniformName::new("sigma", UniformType::_1f), + UniformName::new("input_to_geo", UniformType::Matrix3x3), + UniformName::new("geo_size", UniformType::_2f), + UniformName::new("corner_radius", UniformType::_4f), + UniformName::new("window_input_to_geo", UniformType::Matrix3x3), + UniformName::new("window_geo_size", UniformType::_2f), + UniformName::new("window_corner_radius", UniformType::_4f), + ], + &[], + ) + .map_err(|err| { + warn!("error compiling shadow shader: {err:?}"); + }) + .ok(); + let clipped_surface = renderer .compile_custom_texture_shader( include_str!("clipped_surface.frag"), @@ -76,6 +98,7 @@ impl Shaders { Self { border, + shadow, clipped_surface, resize, custom_resize: RefCell::new(None), @@ -121,6 +144,7 @@ impl Shaders { pub fn program(&self, program: ProgramType) -> Option<ShaderProgram> { match program { ProgramType::Border => self.border.clone(), + ProgramType::Shadow => self.shadow.clone(), ProgramType::Resize => self .custom_resize .borrow() diff --git a/src/render_helpers/shaders/shadow.frag b/src/render_helpers/shaders/shadow.frag new file mode 100644 index 00000000..3912d71f --- /dev/null +++ b/src/render_helpers/shaders/shadow.frag @@ -0,0 +1,142 @@ +precision highp float; + +#if defined(DEBUG_FLAGS) +uniform float niri_tint; +#endif + +uniform float niri_alpha; +uniform float niri_scale; + +uniform vec2 niri_size; +varying vec2 niri_v_coords; + +uniform vec4 shadow_color; +uniform float sigma; + +uniform mat3 input_to_geo; +uniform vec2 geo_size; +uniform vec4 corner_radius; + +uniform mat3 window_input_to_geo; +uniform vec2 window_geo_size; +uniform vec4 window_corner_radius; + +// Based on: https://madebyevan.com/shaders/fast-rounded-rectangle-shadows/ +// +// License: CC0 (http://creativecommons.org/publicdomain/zero/1.0/) + +// A standard gaussian function, used for weighting samples +float gaussian(float x, float sigma) { + const float pi = 3.141592653589793; + return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +vec2 erf(vec2 x) { + vec2 s = sign(x), a = abs(x); + x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; + x *= x; + return s - s / (x * x); +} + +// Return the blurred mask along the x dimension +float roundedBoxShadowX(float x, float y, float sigma, float corner, vec2 halfSize) { + float delta = min(halfSize.y - corner - abs(y), 0.0); + float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); + vec2 integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +// Return the mask for the shadow of a box from lower to upper +float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float corner) { + // Center everything to make the math easier + vec2 center = (lower + upper) * 0.5; + vec2 halfSize = (upper - lower) * 0.5; + point -= center; + + // The signal is only non-zero in a limited range, so don't waste samples + float low = point.y - halfSize.y; + float high = point.y + halfSize.y; + float start = clamp(-3.0 * sigma, low, high); + float end = clamp(3.0 * sigma, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + float step = (end - start) / 4.0; + float y = start + step * 0.5; + float value = 0.0; + for (int i = 0; i < 4; i++) { + value += roundedBoxShadowX(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step; + y += step; + } + + return value; +} + +float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) { + vec2 center; + float radius; + + if (coords.x < corner_radius.x && coords.y < corner_radius.x) { + radius = corner_radius.x; + center = vec2(radius, radius); + } else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) { + radius = corner_radius.y; + center = vec2(size.x - radius, radius); + } else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) { + radius = corner_radius.z; + center = vec2(size.x - radius, size.y - radius); + } else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) { + radius = corner_radius.w; + center = vec2(radius, size.y - radius); + } else { + return 1.0; + } + + float dist = distance(coords, center); + float half_px = 0.5 / niri_scale; + return 1.0 - smoothstep(radius - half_px, radius + half_px, dist); +} + +void main() { + vec3 coords_geo = input_to_geo * vec3(niri_v_coords, 1.0); + vec3 coords_window_geo = window_input_to_geo * vec3(niri_v_coords, 1.0); + + vec4 color = shadow_color; + + float shadow_value; + if (sigma < 0.1) { + // With low enough sigma just draw a rounded rectangle. + shadow_value = rounding_alpha(coords_geo.xy, geo_size, corner_radius); + } else { + shadow_value = roundedBoxShadow( + vec2(0.0, 0.0), + geo_size, + coords_geo.xy, + sigma, + // FIXME: figure out how to blur with different corner radii. + // + // GTK seems to call blurring separately for the rect and for the 4 corners: + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gsk/gpu/shaders/gskgpuboxshadow.glsl + corner_radius.x + ); + } + color = color * shadow_value; + + // Cut out the inside of the window geometry if requested. + if (window_geo_size != vec2(0.0, 0.0)) { + if (0.0 <= coords_window_geo.x && coords_window_geo.x <= window_geo_size.x + && 0.0 <= coords_window_geo.y && coords_window_geo.y <= window_geo_size.y) { + float alpha = rounding_alpha(coords_window_geo.xy, window_geo_size, window_corner_radius); + color = color * (1.0 - alpha); + } + } + + color = color * niri_alpha; + +#if defined(DEBUG_FLAGS) + if (niri_tint == 1.0) + color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; +#endif + + gl_FragColor = color; +} diff --git a/src/render_helpers/shadow.rs b/src/render_helpers/shadow.rs new file mode 100644 index 00000000..b98202e4 --- /dev/null +++ b/src/render_helpers/shadow.rs @@ -0,0 +1,257 @@ +use std::collections::HashMap; + +use glam::{Mat3, Vec2}; +use niri_config::{Color, CornerRadius}; +use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage}; +use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform}; +use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions}; +use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform}; + +use super::renderer::NiriRenderer; +use super::shader_element::ShaderRenderElement; +use super::shaders::{mat3_uniform, ProgramType, Shaders}; +use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError}; + +/// Renders a rounded rectangle shadow. +#[derive(Debug, Clone)] +pub struct ShadowRenderElement { + inner: ShaderRenderElement, + params: Parameters, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct Parameters { + size: Size<f64, Logical>, + geometry: Rectangle<f64, Logical>, + color: Color, + sigma: f32, + corner_radius: CornerRadius, + // Should only be used for visual improvements, i.e. corner radius anti-aliasing. + scale: f32, + + window_geometry: Rectangle<f64, Logical>, + window_corner_radius: CornerRadius, +} + +impl ShadowRenderElement { + #[allow(clippy::too_many_arguments)] + pub fn new( + size: Size<f64, Logical>, + geometry: Rectangle<f64, Logical>, + color: Color, + sigma: f32, + corner_radius: CornerRadius, + scale: f32, + window_geometry: Rectangle<f64, Logical>, + window_corner_radius: CornerRadius, + ) -> Self { + let inner = ShaderRenderElement::empty(ProgramType::Shadow, Kind::Unspecified); + let mut rv = Self { + inner, |
