mirror of
https://github.com/YaLTeR/niri.git
synced 2025-10-06 00:23:14 +02:00
Implement window shadows
This commit is contained in:
@@ -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),
|
||||
|
@@ -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.
|
||||
|
@@ -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
182
src/layout/shadow.rs
Normal 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()
|
||||
}
|
||||
}
|
@@ -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>(
|
||||
|
@@ -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;
|
||||
|
@@ -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()
|
||||
|
142
src/render_helpers/shaders/shadow.frag
Normal file
142
src/render_helpers/shaders/shadow.frag
Normal 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;
|
||||
}
|
257
src/render_helpers/shadow.rs
Normal file
257
src/render_helpers/shadow.rs
Normal 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)
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user