Implement window shadows

This commit is contained in:
Ivan Molodetskikh
2025-01-15 14:16:05 +03:00
parent b4add625b2
commit bd559a2660
12 changed files with 947 additions and 9 deletions

View File

@@ -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(),
@@ -608,6 +612,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)]
@@ -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),

View File

@@ -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.

View File

@@ -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,
@@ -7072,12 +7076,26 @@ 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()
}
}

182
src/layout/shadow.rs Normal file
View File

@@ -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()
}
}

View File

@@ -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>(

View File

@@ -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;

View File

@@ -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()

View File

@@ -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;
}

View File

@@ -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,
params: Parameters {
size,
geometry,
color,
sigma,
corner_radius,
scale,
window_geometry,
window_corner_radius,
},
};
rv.update_inner();
rv
}
pub fn empty() -> Self {
let inner = ShaderRenderElement::empty(ProgramType::Shadow, Kind::Unspecified);
Self {
inner,
params: Parameters {
size: Default::default(),
geometry: Default::default(),
color: Default::default(),
sigma: 0.,
corner_radius: Default::default(),
scale: 1.,
window_geometry: Default::default(),
window_corner_radius: Default::default(),
},
}
}
pub fn damage_all(&mut self) {
self.inner.damage_all();
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
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,
) {
let params = Parameters {
size,
geometry,
color,
sigma,
corner_radius,
scale,
window_geometry,
window_corner_radius,
};
if self.params == params {
return;
}
self.params = params;
self.update_inner();
}
fn update_inner(&mut self) {
let Parameters {
size,
geometry,
color,
sigma,
corner_radius,
scale,
window_geometry,
window_corner_radius,
} = self.params;
let area_size = Vec2::new(size.w as f32, size.h as f32);
let geo_loc = Vec2::new(geometry.loc.x as f32, geometry.loc.y as f32);
let geo_size = Vec2::new(geometry.size.w as f32, geometry.size.h as f32);
let input_to_geo =
Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size);
let window_geo_loc = Vec2::new(window_geometry.loc.x as f32, window_geometry.loc.y as f32);
let window_geo_size =
Vec2::new(window_geometry.size.w as f32, window_geometry.size.h as f32);
let window_input_to_geo =
Mat3::from_scale(area_size) * Mat3::from_translation(-window_geo_loc / area_size);
self.inner.update(
size,
None,
scale,
vec![
Uniform::new("shadow_color", color.to_array_premul()),
Uniform::new("sigma", sigma),
mat3_uniform("input_to_geo", input_to_geo),
Uniform::new("geo_size", geo_size.to_array()),
Uniform::new("corner_radius", <[f32; 4]>::from(corner_radius)),
mat3_uniform("window_input_to_geo", window_input_to_geo),
Uniform::new("window_geo_size", window_geo_size.to_array()),
Uniform::new(
"window_corner_radius",
<[f32; 4]>::from(window_corner_radius),
),
],
HashMap::new(),
);
}
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.inner = self.inner.with_location(location);
self
}
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
Shaders::get(renderer)
.program(ProgramType::Shadow)
.is_some()
}
}
impl Default for ShadowRenderElement {
fn default() -> Self {
Self::empty()
}
}
impl Element for ShadowRenderElement {
fn id(&self) -> &Id {
self.inner.id()
}
fn current_commit(&self) -> CommitCounter {
self.inner.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.inner.geometry(scale)
}
fn transform(&self) -> Transform {
self.inner.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.inner.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
self.inner.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.inner.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.inner.alpha()
}
fn kind(&self) -> Kind {
self.inner.kind()
}
}
impl RenderElement<GlesRenderer> for ShadowRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.inner.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for ShadowRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.inner.underlying_storage(renderer)
}
}

View File

@@ -1,7 +1,8 @@
use std::cmp::{max, min};
use niri_config::{
BlockOutFrom, BorderRule, CornerRadius, FloatingPosition, Match, PresetSize, WindowRule,
BlockOutFrom, BorderRule, CornerRadius, FloatingPosition, Match, PresetSize, ShadowRule,
WindowRule,
};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::utils::{Logical, Size};
@@ -77,6 +78,8 @@ pub struct ResolvedWindowRules {
pub focus_ring: BorderRule,
/// Window border overrides.
pub border: BorderRule,
/// Shadow overrides.
pub shadow: ShadowRule,
/// Whether or not to draw the border with a solid background.
///
@@ -171,6 +174,16 @@ impl ResolvedWindowRules {
active_gradient: None,
inactive_gradient: None,
},
shadow: ShadowRule {
off: false,
on: false,
offset: None,
softness: None,
spread: None,
draw_behind_window: None,
color: None,
inactive_color: None,
},
draw_border_with_background: None,
opacity: None,
geometry_corner_radius: None,
@@ -268,6 +281,7 @@ impl ResolvedWindowRules {
resolved.focus_ring.merge_with(&rule.focus_ring);
resolved.border.merge_with(&rule.border);
resolved.shadow.merge_with(&rule.shadow);
if let Some(x) = rule.draw_border_with_background {
resolved.draw_border_with_background = Some(x);

View File

@@ -43,6 +43,16 @@ layout {
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear"
}
shadow {
// on
softness 30
spread 5
offset x=0 y=5
draw-behind-window true
color "#00000070"
// inactive-color "#00000054"
}
insert-hint {
// off
color "#ffc87f80"
@@ -322,6 +332,52 @@ layout {
}
```
### `shadow`
<sup>Since: next release</sup>
Shadow rendered behind a window.
Set `on` to enable the shadow.
`softness` controls the shadow softness/size in logical pixels, same as CSS box-shadow *blur radius*.
Setting `softness 0` will give you hard shadows.
`spread` is the distance to expand the window rectangle in logical pixels, same as CSS box-shadow spread.
`offset` moves the shadow relative to the window in logical pixels, same as CSS box-shadow offset.
Set `draw-behind-window` to `true` to make shadows draw behind the window rather than just around it.
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.
`color` is the shadow color and opacity.
`inactive-color` lets you override the shadow color for inactive windows; by default, a more transparent `color` is used.
Shadow drawing will follow the window corner radius set with the `geometry-corner-radius` [window rule](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules).
> [!NOTE]
> Currently, shadow drawing only supports matching radius for all corners. If you set `geometry-corner-radius` to four values instead of one, the first (top-left) corner radius will be used for shadows.
```kdl
// Enable shadows.
layout {
shadow {
on
}
}
// Also ask windows to omit client-side decorations, so that
// they don't draw their own window shadows.
prefer-no-csd
```
### `insert-hint`
<sup>Since: 0.1.10</sup>

View File

@@ -68,6 +68,16 @@ window-rule {
// Same as focus-ring.
}
shadow {
// on
softness 40
spread 5
offset x=0 y=5
draw-behind-window true
color "#00000064"
// inactive-color "#00000064"
}
geometry-corner-radius 12
clip-to-geometry true
@@ -590,6 +600,28 @@ window-rule {
}
```
#### `shadow`
<sup>Since: next release</sup>
Override the shadow options for the window.
These rules have the same options as the normal shadow config in the [layout](./Configuration:-Layout.md) section, so check the documentation there.
However, in addition to `on` to enable the shadow, this window rule has an `off` flag that disables the shadow for the window even if it was otherwise enabled.
The `on` flag has precedence over the `off` flag, in case both are set.
```kdl
// Turn on shadows for floating windows.
window-rule {
match is-floating=true
shadow {
on
}
}
```
#### `geometry-corner-radius`
<sup>Since: 0.1.6</sup>