aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/handlers/xdg_shell.rs103
-rw-r--r--src/layout/mod.rs58
-rw-r--r--src/niri.rs31
-rw-r--r--src/window/mapped.rs36
-rw-r--r--src/window/mod.rs107
5 files changed, 230 insertions, 105 deletions
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index 4d31645c..7bd47717 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -1,4 +1,3 @@
-use niri_config::{Match, WindowRule};
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@@ -20,7 +19,7 @@ use smithay::wayland::shell::wlr_layer::Layer;
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
- XdgShellState, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
+ XdgShellState, XdgToplevelSurfaceData,
};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
@@ -31,82 +30,6 @@ use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped};
-fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
- if let Some(app_id_re) = &m.app_id {
- let Some(app_id) = &role.app_id else {
- return false;
- };
- if !app_id_re.is_match(app_id) {
- return false;
- }
- }
-
- if let Some(title_re) = &m.title {
- let Some(title) = &role.title else {
- return false;
- };
- if !title_re.is_match(title) {
- return false;
- }
- }
-
- true
-}
-
-pub fn resolve_window_rules(
- rules: &[WindowRule],
- toplevel: &ToplevelSurface,
-) -> ResolvedWindowRules {
- let _span = tracy_client::span!("resolve_window_rules");
-
- let mut resolved = ResolvedWindowRules::default();
-
- with_states(toplevel.wl_surface(), |states| {
- let role = states
- .data_map
- .get::<XdgToplevelSurfaceData>()
- .unwrap()
- .lock()
- .unwrap();
-
- let mut open_on_output = None;
-
- for rule in rules {
- if !(rule.matches.is_empty() || rule.matches.iter().any(|m| window_matches(&role, m))) {
- continue;
- }
-
- if rule.excludes.iter().any(|m| window_matches(&role, m)) {
- continue;
- }
-
- if let Some(x) = rule
- .default_column_width
- .as_ref()
- .map(|d| d.0.map(ColumnWidth::from))
- {
- resolved.default_width = Some(x);
- }
-
- if let Some(x) = rule.open_on_output.as_deref() {
- open_on_output = Some(x);
- }
-
- if let Some(x) = rule.open_maximized {
- resolved.open_maximized = Some(x);
- }
-
- if let Some(x) = rule.open_fullscreen {
- resolved.open_fullscreen = Some(x);
- }
- }
-
- resolved.open_on_output = open_on_output.map(|x| x.to_owned());
- });
-
- resolved
-}
-
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
&mut self.niri.xdg_shell_state
@@ -574,7 +497,7 @@ impl State {
};
let config = self.niri.config.borrow();
- let rules = resolve_window_rules(&config.window_rules, toplevel);
+ let rules = ResolvedWindowRules::compute(&config.window_rules, toplevel);
// Pick the target monitor. First, check if we had an output set in the window rules.
let mon = rules
@@ -807,14 +730,30 @@ impl State {
}
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
- let resolve = || resolve_window_rules(&self.niri.config.borrow().window_rules, toplevel);
+ let resolve =
+ || ResolvedWindowRules::compute(&self.niri.config.borrow().window_rules, toplevel);
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
*rules = resolve();
}
- } else if let Some(mapped) = self.niri.layout.find_window_mut(toplevel.wl_surface()) {
- mapped.rules = resolve();
+ } else if let Some((mapped, output)) = self
+ .niri
+ .layout
+ .find_window_and_output_mut(toplevel.wl_surface())
+ {
+ let new_rules = resolve();
+ if mapped.rules != new_rules {
+ mapped.rules = new_rules;
+
+ let output = output.cloned();
+ let window = mapped.window.clone();
+ self.niri.layout.update_window(&window);
+
+ if let Some(output) = output {
+ self.niri.queue_redraw(output);
+ }
+ }
}
}
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 86ce5c85..c2c4bf4e 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -643,13 +643,30 @@ impl<W: LayoutElement> Layout<W> {
}
}
- pub fn find_window_mut(&mut self, wl_surface: &WlSurface) -> Option<&mut W> {
+ pub fn find_window_and_output(&self, wl_surface: &WlSurface) -> Option<(&W, &Output)> {
+ if let MonitorSet::Normal { monitors, .. } = &self.monitor_set {
+ for mon in monitors {
+ for ws in &mon.workspaces {
+ if let Some(window) = ws.find_wl_surface(wl_surface) {
+ return Some((window, &mon.output));
+ }
+ }
+ }
+ }
+
+ None
+ }
+
+ pub fn find_window_and_output_mut(
+ &mut self,
+ wl_surface: &WlSurface,
+ ) -> Option<(&mut W, Option<&Output>)> {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
- return Some(window);
+ return Some((window, Some(&mon.output)));
}
}
}
@@ -657,21 +674,7 @@ impl<W: LayoutElement> Layout<W> {
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
- return Some(window);
- }
- }
- }
- }
-
- None
- }
-
- pub fn find_window_and_output(&self, wl_surface: &WlSurface) -> Option<(&W, &Output)> {
- if let MonitorSet::Normal { monitors, .. } = &self.monitor_set {
- for mon in monitors {
- for ws in &mon.workspaces {
- if let Some(window) = ws.find_wl_surface(wl_surface) {
- return Some((window, &mon.output));
+ return Some((window, None));
}
}
}
@@ -851,6 +854,27 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ pub fn with_windows_mut(&mut self, mut f: impl FnMut(&mut W, Option<&Output>)) {
+ match &mut self.monitor_set {
+ MonitorSet::Normal { monitors, .. } => {
+ for mon in monitors {
+ for ws in &mut mon.workspaces {
+ for win in ws.windows_mut() {
+ f(win, Some(&mon.output));
+ }
+ }
+ }
+ }
+ MonitorSet::NoOutputs { workspaces } => {
+ for ws in workspaces {
+ for win in ws.windows_mut() {
+ f(win, None);
+ }
+ }
+ }
+ }
+ }
+
fn active_monitor(&mut self) -> Option<&mut Monitor<W>> {
let MonitorSet::Normal {
monitors,
diff --git a/src/niri.rs b/src/niri.rs
index 13aa6409..08f195fc 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -114,7 +114,7 @@ use crate::utils::spawning::CHILD_ENV;
use crate::utils::{
center, center_f64, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8,
};
-use crate::window::{Mapped, Unmapped};
+use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
use crate::{animation, niri_render_elements};
const CLEAR_COLOR: [f32; 4] = [0.2, 0.2, 0.2, 1.];
@@ -761,6 +761,7 @@ impl State {
let mut reload_xkb = None;
let mut libinput_config_changed = false;
let mut output_config_changed = false;
+ let mut window_rules_changed = false;
let mut old_config = self.niri.config.borrow_mut();
// Reload the cursor.
@@ -802,6 +803,10 @@ impl State {
self.niri.hotkey_overlay.on_hotkey_config_updated();
}
+ if config.window_rules != old_config.window_rules {
+ window_rules_changed = true;
+ }
+
*old_config = config;
// Release the borrow.
@@ -865,6 +870,30 @@ impl State {
}
}
+ if window_rules_changed {
+ let _span = tracy_client::span!("recompute window rules");
+
+ let window_rules = &self.niri.config.borrow().window_rules;
+
+ for unmapped in self.niri.unmapped_windows.values_mut() {
+ if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
+ *rules = ResolvedWindowRules::compute(
+ window_rules,
+ unmapped.window.toplevel().expect("no X11 support"),
+ );
+ }
+ }
+
+ let mut windows = vec![];
+ self.niri.layout.with_windows_mut(|mapped, _| {
+ mapped.rules = ResolvedWindowRules::compute(window_rules, mapped.toplevel());
+ windows.push(mapped.window.clone());
+ });
+ for win in windows {
+ self.niri.layout.update_window(&win);
+ }
+ }
+
// Can't really update xdg-decoration settings since we have to hide the globals for CSD
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
diff --git a/src/window/mapped.rs b/src/window/mapped.rs
index 4476dede..6afd995e 100644
--- a/src/window/mapped.rs
+++ b/src/window/mapped.rs
@@ -1,3 +1,5 @@
+use std::cmp::{max, min};
+
use smithay::backend::renderer::element::{AsRenderElements as _, Id};
use smithay::desktop::space::SpaceElement as _;
use smithay::desktop::Window;
@@ -82,17 +84,43 @@ impl LayoutElement for Mapped {
}
fn min_size(&self) -> Size<i32, Logical> {
- with_states(self.toplevel().wl_surface(), |state| {
+ let mut size = with_states(self.toplevel().wl_surface(), |state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.min_size
- })
+ });
+
+ if let Some(x) = self.rules.min_width {
+ size.w = max(size.w, i32::from(x));
+ }
+ if let Some(x) = self.rules.min_height {
+ size.h = max(size.h, i32::from(x));
+ }
+
+ size
}
fn max_size(&self) -> Size<i32, Logical> {
- with_states(self.toplevel().wl_surface(), |state| {
+ let mut size = with_states(self.toplevel().wl_surface(), |state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.max_size
- })
+ });
+
+ if let Some(x) = self.rules.max_width {
+ if size.w == 0 {
+ size.w = i32::from(x);
+ } else if x > 0 {
+ size.w = min(size.w, i32::from(x));
+ }
+ }
+ if let Some(x) = self.rules.max_height {
+ if size.h == 0 {
+ size.h = i32::from(x);
+ } else if x > 0 {
+ size.h = min(size.h, i32::from(x));
+ }
+ }
+
+ size
}
fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool {
diff --git a/src/window/mod.rs b/src/window/mod.rs
index 124a62f3..25b3531f 100644
--- a/src/window/mod.rs
+++ b/src/window/mod.rs
@@ -1,3 +1,9 @@
+use niri_config::{Match, WindowRule};
+use smithay::wayland::compositor::with_states;
+use smithay::wayland::shell::xdg::{
+ ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
+};
+
use crate::layout::workspace::ColumnWidth;
pub mod mapped;
@@ -7,7 +13,7 @@ pub mod unmapped;
pub use unmapped::{InitialConfigureState, Unmapped};
/// Rules fully resolved for a window.
-#[derive(Debug, Default)]
+#[derive(Debug, Default, PartialEq)]
pub struct ResolvedWindowRules {
/// Default width for this window.
///
@@ -24,4 +30,103 @@ pub struct ResolvedWindowRules {
/// Whether the window should open fullscreen.
pub open_fullscreen: Option<bool>,
+
+ /// Extra bound on the minimum window width.
+ pub min_width: Option<u16>,
+ /// Extra bound on the minimum window height.
+ pub min_height: Option<u16>,
+ /// Extra bound on the maximum window width.
+ pub max_width: Option<u16>,
+ /// Extra bound on the maximum window height.
+ pub max_height: Option<u16>,
+}
+
+impl ResolvedWindowRules {
+ pub fn compute(rules: &[WindowRule], toplevel: &ToplevelSurface) -> Self {
+ let _span = tracy_client::span!("ResolvedWindowRules::compute");
+
+ let mut resolved = ResolvedWindowRules::default();
+
+ with_states(toplevel.wl_surface(), |states| {
+ let role = states
+ .data_map
+ .get::<XdgToplevelSurfaceData>()
+ .unwrap()
+ .lock()
+ .unwrap();
+
+ let mut open_on_output = None;
+
+ for rule in rules {
+ if !(rule.matches.is_empty()
+ || rule.matches.iter().any(|m| window_matches(&role, m)))
+ {
+ continue;
+ }
+
+ if rule.excludes.iter().any(|m| window_matches(&role, m)) {
+ continue;
+ }
+
+ if let Some(x) = rule
+ .default_column_width
+ .as_ref()
+ .map(|d| d.0.map(ColumnWidth::from))
+ {
+ resolved.default_width = Some(x);
+ }
+
+ if let Some(x) = rule.open_on_output.as_deref() {
+ open_on_output = Some(x);
+ }
+
+ if let Some(x) = rule.open_maximized {
+ resolved.open_maximized = Some(x);
+ }
+
+ if let Some(x) = rule.open_fullscreen {
+ resolved.open_fullscreen = Some(x);
+ }
+
+ if let Some(x) = rule.min_width {
+ resolved.min_width = Some(x);
+ }
+ if let Some(x) = rule.min_height {
+ resolved.min_height = Some(x);
+ }
+ if let Some(x) = rule.max_width {
+ resolved.max_width = Some(x);
+ }
+ if let Some(x) = rule.max_height {
+ resolved.max_height = Some(x);
+ }
+ }
+
+ resolved.open_on_output = open_on_output.map(|x| x.to_owned());
+ });
+
+ resolved
+ }
+}
+
+fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
+ if let Some(app_id_re) = &m.app_id {
+ let Some(app_id) = &role.app_id else {
+ return false;
+ };
+ if !app_id_re.is_match(app_id) {
+ return false;
+ }
+ }
+
+ if let Some(title_re) = &m.title {
+ let Some(title) = &role.title else {
+ return false;
+ };
+ if !title_re.is_match(title) {
+ return false;
+ }
+ }
+
+ true
}