aboutsummaryrefslogtreecommitdiff
path: root/src/layout
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 /src/layout
parentb4add625b2ffdad3e003b3e437891daacf53a12f (diff)
downloadniri-bd559a26602874f4104e342e2ce02317ae1ae605.tar.gz
niri-bd559a26602874f4104e342e2ce02317ae1ae605.tar.bz2
niri-bd559a26602874f4104e342e2ce02317ae1ae605.zip
Implement window shadows
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/mod.rs19
-rw-r--r--src/layout/shadow.rs182
-rw-r--r--src/layout/tile.rs40
3 files changed, 233 insertions, 8 deletions
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>(