mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Implement window rule reloading and min/max size rules
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
+21
-82
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+43
-19
@@ -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, .. } => {
|
||||
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 &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);
|
||||
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 {
|
||||
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 &mon.workspaces {
|
||||
if let Some(window) = ws.find_wl_surface(wl_surface) {
|
||||
return Some((window, &mon.output));
|
||||
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,
|
||||
|
||||
+30
-1
@@ -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
|
||||
|
||||
+32
-4
@@ -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 {
|
||||
|
||||
+106
-1
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user