diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-13 17:46:37 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-14 08:32:14 +0400 |
| commit | befdebfa03399eeed7869fb0788d553f7aa4dcdb (patch) | |
| tree | 4b59f51b62611570f7c92227ce8f6836ab06dab9 /src | |
| parent | 7960a73e9dda9f82c67e2f7b37766c88f5889c14 (diff) | |
| download | niri-befdebfa03399eeed7869fb0788d553f7aa4dcdb.tar.gz niri-befdebfa03399eeed7869fb0788d553f7aa4dcdb.tar.bz2 niri-befdebfa03399eeed7869fb0788d553f7aa4dcdb.zip | |
Add the beginnings of window rules
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers/compositor.rs | 32 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 116 | ||||
| -rw-r--r-- | src/layout/mod.rs | 71 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 4 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 21 |
5 files changed, 222 insertions, 22 deletions
diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index de92d17c..2d6a6406 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -16,6 +16,7 @@ use smithay::wayland::dmabuf::get_dmabuf; use smithay::wayland::shm::{ShmHandler, ShmState}; use smithay::{delegate_compositor, delegate_shm}; +use super::xdg_shell::{initial_configure_sent, resolve_window_rules}; use crate::niri::{ClientState, State}; use crate::utils::clone2; @@ -113,13 +114,28 @@ impl CompositorHandler for State { .and_then(|parent| self.niri.layout.find_window_and_output(&parent)) .map(|(win, _)| win.clone()); + let (width, output) = { + let config = self.niri.config.borrow(); + let rules = resolve_window_rules(&config.window_rules, window.toplevel()); + let output = rules + .open_on_output + .and_then(|name| self.niri.output_by_name.get(name)) + .cloned(); + (rules.default_width, output) + }; + let win = window.clone(); // Open dialogs immediately to the right of their parent window. let output = if let Some(p) = parent { - self.niri.layout.add_window_right_of(&p, win, None, false) + self.niri.layout.add_window_right_of(&p, win, width, false) + } else if let Some(output) = &output { + self.niri + .layout + .add_window_on_output(output, win, width, false); + Some(output) } else { - self.niri.layout.add_window(win, None, false) + self.niri.layout.add_window(win, width, false) }; if let Some(output) = output.cloned() { @@ -131,7 +147,17 @@ impl CompositorHandler for State { // The toplevel remains unmapped. let window = entry.get().clone(); - self.send_initial_configure_if_needed(&window); + + // Send the initial configure in an idle, in case the client sent some more info + // after the initial commit. + if !initial_configure_sent(window.toplevel()) { + self.niri.event_loop.insert_idle(move |state| { + if !window.toplevel().alive() { + return; + } + state.send_initial_configure_if_needed(&window); + }); + } return; } diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 397bdbdc..89cc6869 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -1,3 +1,4 @@ +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, @@ -19,13 +20,91 @@ 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, + XdgShellState, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes, }; use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell}; +use crate::layout::workspace::ColumnWidth; use crate::niri::{PopupGrabState, State}; use crate::utils::clone2; +#[derive(Debug, Default)] +pub struct ResolvedWindowRule<'a> { + /// Default width for this window. + /// + /// - `None`: unset. + /// - `Some(None)`: set to empty. + /// - `Some(Some(width))`: set to a particular width. + pub default_width: Option<Option<ColumnWidth>>, + + /// Output to open this window on. + pub open_on_output: Option<&'a str>, +} + +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<'a>( + rules: &'a [WindowRule], + toplevel: &ToplevelSurface, +) -> ResolvedWindowRule<'a> { + let _span = tracy_client::span!("resolve_window_rules"); + + let mut resolved = ResolvedWindowRule::default(); + + with_states(toplevel.wl_surface(), |states| { + let role = states + .data_map + .get::<XdgToplevelSurfaceData>() + .unwrap() + .lock() + .unwrap(); + + 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.first().copied().map(ColumnWidth::from)) + { + resolved.default_width = Some(x); + } + + if let Some(x) = rule.open_on_output.as_deref() { + resolved.open_on_output = Some(x); + } + } + }); + + resolved +} + impl XdgShellHandler for State { fn xdg_shell_state(&mut self) -> &mut XdgShellState { &mut self.niri.xdg_shell_state @@ -237,9 +316,20 @@ impl XdgShellHandler for State { let window = window.clone(); self.niri.layout.set_fullscreen(&window, false); } else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) { - if let Some(ws) = self.niri.layout.active_workspace() { + let config = self.niri.config.borrow(); + let rules = resolve_window_rules(&config.window_rules, window.toplevel()); + + let output = rules + .open_on_output + .and_then(|name| self.niri.output_by_name.get(name)); + let mon = output.map(|o| self.niri.layout.monitor_for_output(o).unwrap()); + let ws = mon + .map(|mon| mon.active_workspace_ref()) + .or_else(|| self.niri.layout.active_workspace()); + + if let Some(ws) = ws { window.toplevel().with_pending_state(|state| { - state.size = Some(ws.new_window_size()); + state.size = Some(ws.new_window_size(rules.default_width)); state.states.unset(xdg_toplevel::State::Fullscreen); }); } @@ -333,7 +423,7 @@ impl KdeDecorationHandler for State { delegate_kde_decoration!(State); -fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool { +pub fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool { with_states(toplevel.wl_surface(), |states| { states .data_map @@ -352,14 +442,26 @@ impl State { return; } + let _span = tracy_client::span!("State::send_initial_configure_if_needed"); + + let config = self.niri.config.borrow(); + let rules = resolve_window_rules(&config.window_rules, toplevel); + + let output = rules + .open_on_output + .and_then(|name| self.niri.output_by_name.get(name)); + let mon = output.map(|o| self.niri.layout.monitor_for_output(o).unwrap()); + let ws = mon + .map(|mon| mon.active_workspace_ref()) + .or_else(|| self.niri.layout.active_workspace()); + // Tell the surface the preferred size and bounds for its likely output. - if let Some(ws) = self.niri.layout.active_workspace() { - ws.configure_new_window(window); + if let Some(ws) = ws { + ws.configure_new_window(window, rules.default_width); } // If the user prefers no CSD, it's a reasonable assumption that they would prefer to get // rid of the various client-side rounded corners also by using the tiled state. - let config = self.niri.config.borrow(); if config.prefer_no_csd { toplevel.with_pending_state(|state| { state.states.set(xdg_toplevel::State::TiledLeft); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index dfc7ee0a..947b8cea 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -531,12 +531,15 @@ impl<W: LayoutElement> Layout<W> { pub fn add_window( &mut self, window: W, - width: Option<ColumnWidth>, + width: Option<Option<ColumnWidth>>, is_full_width: bool, ) -> Option<&Output> { - let width = width - .or(self.options.default_width) - .unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); + let width = match width { + Some(Some(width)) => Some(width), + Some(None) => None, + None => self.options.default_width, + } + .unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); match &mut self.monitor_set { MonitorSet::Normal { @@ -584,12 +587,15 @@ impl<W: LayoutElement> Layout<W> { &mut self, right_of: &W, window: W, - width: Option<ColumnWidth>, + width: Option<Option<ColumnWidth>>, is_full_width: bool, ) -> Option<&Output> { - let width = width - .or(self.options.default_width) - .unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); + let width = match width { + Some(Some(width)) => Some(width), + Some(None) => None, + None => self.options.default_width, + } + .unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { @@ -612,6 +618,55 @@ impl<W: LayoutElement> Layout<W> { } } + /// Adds a new window to the layout on a specific output. + pub fn add_window_on_output( + &mut self, + output: &Output, + window: W, + width: Option<Option<ColumnWidth>>, + is_full_width: bool, + ) { + let width = match width { + Some(Some(width)) => Some(width), + Some(None) => None, + None => self.options.default_width, + } + .unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); + + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = &mut self.monitor_set + else { + panic!() + }; + + let (mon_idx, mon) = monitors + .iter_mut() + .enumerate() + .find(|(_, mon)| mon.output == *output) + .unwrap(); + + // Don't steal focus from an active fullscreen window. + let mut activate = true; + let ws = &mon.workspaces[mon.active_workspace_idx]; + if mon_idx == *active_monitor_idx + && !ws.columns.is_empty() + && ws.columns[ws.active_column_idx].is_fullscreen + { + activate = false; + } + + mon.add_window( + mon.active_workspace_idx, + window, + activate, + width, + is_full_width, + ); + } + pub fn remove_window(&mut self, window: &W) { match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index cb99279a..ff63ed4f 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -76,6 +76,10 @@ impl<W: LayoutElement> Monitor<W> { } } + pub fn active_workspace_ref(&self) -> &Workspace<W> { + &self.workspaces[self.active_workspace_idx] + } + pub fn active_workspace(&mut self) -> &mut Workspace<W> { &mut self.workspaces[self.active_workspace_idx] } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 64bc4bc7..7c9b68fc 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -321,8 +321,17 @@ impl<W: LayoutElement> Workspace<W> { )) } - pub fn new_window_size(&self) -> Size<i32, Logical> { - let width = if let Some(width) = self.options.default_width { + pub fn new_window_size( + &self, + default_width: Option<Option<ColumnWidth>>, + ) -> Size<i32, Logical> { + let default_width = match default_width { + Some(Some(width)) => Some(width), + Some(None) => None, + None => self.options.default_width, + }; + + let width = if let Some(width) = default_width { let mut width = width.resolve(&self.options, self.working_area.size.w); if !self.options.border.off { width -= self.options.border.width as i32 * 2; @@ -340,8 +349,12 @@ impl<W: LayoutElement> Workspace<W> { Size::from((width, max(height, 1))) } - pub fn configure_new_window(&self, window: &Window) { - let size = self.new_window_size(); + pub fn configure_new_window( + &self, + window: &Window, + default_width: Option<Option<ColumnWidth>>, + ) { + let size = self.new_window_size(default_width); let bounds = self.toplevel_bounds(); if let Some(output) = self.output.as_ref() { |
