Implement window rule reloading and min/max size rules

This commit is contained in:
Ivan Molodetskikh 2024-03-19 15:20:03 +04:00
parent 3963f537a4
commit c61361de3c
7 changed files with 261 additions and 109 deletions

View File

@ -674,6 +674,7 @@ pub struct WindowRule {
#[knuffel(children(name = "exclude"))]
pub excludes: Vec<Match>,
// Rules applied at initial configure.
#[knuffel(child)]
pub default_column_width: Option<DefaultColumnWidth>,
#[knuffel(child, unwrap(argument))]
@ -682,6 +683,16 @@ pub struct WindowRule {
pub open_maximized: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_fullscreen: Option<bool>,
// Rules applied dynamically.
#[knuffel(child, unwrap(argument))]
pub min_width: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub min_height: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_width: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_height: Option<u16>,
}
#[derive(knuffel::Decode, Debug, Default, Clone)]

View File

@ -362,6 +362,8 @@ animations {
exclude app-id=r#"\.unwanted\."#
// Here are the properties that you can set on a window rule.
// These properties apply once, when a window first opens.
// You can override the default column width.
default-column-width { proportion 0.75; }
@ -377,6 +379,20 @@ animations {
open-fullscreen true
// You can also set this to false to prevent a window from opening fullscreen.
// open-fullscreen false
// The following properties apply dynamically while a window is open.
// You can amend the window's minimum and maximum size in logical pixels.
// Keep in mind that the window itself always has a final say in its size.
// These values instruct niri to never ask the window to be smaller than
// the minimum you set, or to be bigger than the maximum you set.
min-width 100
max-width 200
min-height 300
// Caveat: max-height will only apply to automatically-sized windows
// if it is equal to min-height. Either set this equal to min-height,
// or change the window height manually for this to apply.
max-height 300
}
// Here's a useful example. Work around WezTerm's initial configure bug

View File

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

View File

@ -643,21 +643,12 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn find_window_mut(&mut self, wl_surface: &WlSurface) -> Option<&mut W> {
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);
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
return Some(window);
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));
}
}
}
@ -666,12 +657,24 @@ impl<W: LayoutElement> Layout<W> {
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));
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, Some(&mon.output)));
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
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,

View File

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

View File

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

View File

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