aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-01-15 14:16:05 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-01-17 23:10:01 +0300
commitbd559a26602874f4104e342e2ce02317ae1ae605 (patch)
tree5ba6d9d511f3ca1342a5874afdc33ecb3f93953f
parentb4add625b2ffdad3e003b3e437891daacf53a12f (diff)
downloadniri-bd559a26602874f4104e342e2ce02317ae1ae605.tar.gz
niri-bd559a26602874f4104e342e2ce02317ae1ae605.tar.bz2
niri-bd559a26602874f4104e342e2ce02317ae1ae605.zip
Implement window shadows
-rw-r--r--niri-config/src/lib.rs150
-rw-r--r--resources/default-config.kdl37
-rw-r--r--src/layout/mod.rs19
-rw-r--r--src/layout/shadow.rs182
-rw-r--r--src/layout/tile.rs40
-rw-r--r--src/render_helpers/mod.rs1
-rw-r--r--src/render_helpers/shaders/mod.rs24
-rw-r--r--src/render_helpers/shaders/shadow.frag142
-rw-r--r--src/render_helpers/shadow.rs257
-rw-r--r--src/window/mod.rs16
-rw-r--r--wiki/Configuration:-Layout.md56
-rw-r--r--wiki/Configuration:-Window-Rules.md32
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,