Skip to content

Commit

Permalink
Implement layer rules: opacity and block-out-from
Browse files Browse the repository at this point in the history
  • Loading branch information
YaLTeR committed Nov 14, 2024
1 parent fbbd3ba commit 1a0612c
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 17 deletions.
22 changes: 22 additions & 0 deletions niri-config/src/layer_rule.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use crate::{BlockOutFrom, RegexEq};

#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
#[knuffel(children(name = "match"))]
pub matches: Vec<Match>,
#[knuffel(children(name = "exclude"))]
pub excludes: Vec<Match>,

#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
}

#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Match {
#[knuffel(property, str)]
pub namespace: Option<RegexEq>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
21 changes: 21 additions & 0 deletions niri-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::time::Duration;
use bitflags::bitflags;
use knuffel::errors::DecodeError;
use knuffel::Decode as _;
use layer_rule::LayerRule;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform, WorkspaceReferenceArg};
use smithay::backend::renderer::Color32F;
Expand All @@ -20,6 +21,8 @@ use smithay::reexports::input;

pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.2, 0.2, 0.2, 1.]);

pub mod layer_rule;

mod utils;
pub use utils::RegexEq;

Expand Down Expand Up @@ -53,6 +56,8 @@ pub struct Config {
pub environment: Environment,
#[knuffel(children(name = "window-rule"))]
pub window_rules: Vec<WindowRule>,
#[knuffel(children(name = "layer-rule"))]
pub layer_rules: Vec<LayerRule>,
#[knuffel(child, default)]
pub binds: Binds,
#[knuffel(child, default)]
Expand Down Expand Up @@ -3119,6 +3124,11 @@ mod tests {
}
}
layer-rule {
match namespace="^notifications$"
block-out-from "screencast"
}
binds {
Mod+T allow-when-locked=true { spawn "alacritty"; }
Mod+Q { close-window; }
Expand Down Expand Up @@ -3391,6 +3401,17 @@ mod tests {
},
..Default::default()
}],
layer_rules: vec![
LayerRule {
matches: vec![layer_rule::Match {
namespace: Some(RegexEq::from_str("^notifications$").unwrap()),
at_startup: None,
}],
excludes: vec![],
opacity: None,
block_out_from: Some(BlockOutFrom::Screencast),
}
],
workspaces: vec![
Workspace {
name: WorkspaceName("workspace-1".to_string()),
Expand Down
18 changes: 18 additions & 0 deletions src/handlers/layer_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use smithay::wayland::shell::wlr_layer::{
};
use smithay::wayland::shell::xdg::PopupSurface;

use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::utils::send_scale_transform;

Expand Down Expand Up @@ -60,6 +61,7 @@ impl WlrLayerShellHandler for State {
layer.map(|layer| (o.clone(), map, layer))
}) {
map.unmap_layer(&layer);
self.niri.mapped_layer_surfaces.remove(&layer);
Some(output)
} else {
None
Expand Down Expand Up @@ -128,6 +130,21 @@ impl State {
if is_mapped {
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);

// Resolve rules for newly mapped layer surfaces.
if was_unmapped {
let rules = &self.niri.config.borrow().layer_rules;
let rules =
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
let mapped = MappedLayer::new(layer.clone(), rules);
let prev = self
.niri
.mapped_layer_surfaces
.insert(layer.clone(), mapped);
if prev.is_some() {
error!("MappedLayer was present for an unmapped surface");
}
}

// Give focus to newly mapped on-demand surfaces. Some launchers like
// lxqt-runner rely on this behavior. While this behavior doesn't make much
// sense for other clients like panels, the consensus seems to be that it's not
Expand All @@ -151,6 +168,7 @@ impl State {
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
}
} else {
self.niri.mapped_layer_surfaces.remove(layer);
self.niri.unmapped_layer_surfaces.insert(surface.clone());
}
} else {
Expand Down
122 changes: 122 additions & 0 deletions src/layer/mapped.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use std::cell::RefCell;

use niri_config::layer_rule::LayerRule;
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Rectangle, Scale};

use super::ResolvedLayerRules;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{RenderTarget, SplitElements};

#[derive(Debug)]
pub struct MappedLayer {
/// The surface itself.
surface: LayerSurface,

/// Up-to-date rules.
rules: ResolvedLayerRules,

/// Buffer to draw instead of the surface when it should be blocked out.
block_out_buffer: RefCell<SolidColorBuffer>,
}

niri_render_elements! {
LayerSurfaceRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
}
}

impl MappedLayer {
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules) -> Self {
Self {
surface,
rules,
block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])),
}
}

pub fn surface(&self) -> &LayerSurface {
&self.surface
}

pub fn rules(&self) -> &ResolvedLayerRules {
&self.rules
}

/// Recomputes the resolved layer rules and returns whether they changed.
pub fn recompute_layer_rules(&mut self, rules: &[LayerRule], is_at_startup: bool) -> bool {
let new_rules = ResolvedLayerRules::compute(rules, &self.surface, is_at_startup);
if new_rules == self.rules {
return false;
}

self.rules = new_rules;
true
}

pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
geometry: Rectangle<i32, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
let mut rv = SplitElements::default();

let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);

if target.should_block_out(self.rules.block_out_from) {
// Round to physical pixels.
let geometry = geometry
.to_f64()
.to_physical_precise_round(scale)
.to_logical(scale);

let mut buffer = self.block_out_buffer.borrow_mut();
buffer.resize(geometry.size.to_f64());
let elem = SolidColorRenderElement::from_buffer(
&buffer,
geometry.loc,
alpha,
Kind::Unspecified,
);
rv.normal.push(elem.into());
} else {
// Layer surfaces don't have extra geometry like windows.
let buf_pos = geometry.loc;

let surface = self.surface.wl_surface();
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
// Layer surfaces don't have extra geometry like windows.
let offset = popup_offset - popup.geometry().loc;

rv.popups.extend(render_elements_from_surface_tree(
renderer,
popup.wl_surface(),
(buf_pos + offset).to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
));
}

rv.normal = render_elements_from_surface_tree(
renderer,
surface,
buf_pos.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
);
}

rv
}
}
69 changes: 69 additions & 0 deletions src/layer/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::BlockOutFrom;
use smithay::desktop::LayerSurface;

pub mod mapped;
pub use mapped::MappedLayer;

/// Rules fully resolved for a layer-shell surface.
#[derive(Debug, PartialEq)]
pub struct ResolvedLayerRules {
/// Extra opacity to draw this window with.
pub opacity: Option<f32>,
/// Whether to block out this window from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
}

impl ResolvedLayerRules {
pub const fn empty() -> Self {
Self {
opacity: None,
block_out_from: None,
}
}

pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
let _span = tracy_client::span!("ResolvedLayerRules::compute");

let mut resolved = ResolvedLayerRules::empty();

for rule in rules {
let matches = |m: &Match| {
if let Some(at_startup) = m.at_startup {
if at_startup != is_at_startup {
return false;
}
}

surface_matches(surface, m)
};

if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
continue;
}

if rule.excludes.iter().any(matches) {
continue;
}

if let Some(x) = rule.opacity {
resolved.opacity = Some(x);
}
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
}

resolved
}
}

fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
if let Some(namespace_re) = &m.namespace {
if !namespace_re.0.is_match(surface.namespace()) {
return false;
}
}

true
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod frame_clock;
pub mod handlers;
pub mod input;
pub mod ipc;
pub mod layer;
pub mod layout;
pub mod niri;
pub mod protocols;
Expand Down
Loading

0 comments on commit 1a0612c

Please sign in to comment.