mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-24 02:01:18 +07:00
Implement border window rule
This commit is contained in:
@@ -701,6 +701,8 @@ pub struct WindowRule {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub max_height: Option<u16>,
|
||||
|
||||
#[knuffel(child, default)]
|
||||
pub border: BorderRule,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub draw_border_with_background: Option<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
@@ -737,6 +739,24 @@ pub enum BlockOutFrom {
|
||||
ScreenCapture,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct BorderRule {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child)]
|
||||
pub on: bool,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub width: Option<u16>,
|
||||
#[knuffel(child)]
|
||||
pub active_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub inactive_color: Option<Color>,
|
||||
#[knuffel(child)]
|
||||
pub active_gradient: Option<Gradient>,
|
||||
#[knuffel(child)]
|
||||
pub inactive_gradient: Option<Gradient>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Binds(pub Vec<Bind>);
|
||||
|
||||
@@ -992,6 +1012,56 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
impl BorderRule {
|
||||
pub fn merge_with(&mut self, other: &Self) {
|
||||
self.off |= other.off;
|
||||
self.on |= other.on;
|
||||
|
||||
if let Some(x) = other.width {
|
||||
self.width = Some(x);
|
||||
}
|
||||
if let Some(x) = other.active_color {
|
||||
self.active_color = Some(x);
|
||||
}
|
||||
if let Some(x) = other.inactive_color {
|
||||
self.inactive_color = Some(x);
|
||||
}
|
||||
if let Some(x) = other.active_gradient {
|
||||
self.active_gradient = Some(x);
|
||||
}
|
||||
if let Some(x) = other.inactive_gradient {
|
||||
self.inactive_gradient = Some(x);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_against(&self, mut config: Border) -> Border {
|
||||
config.off |= self.off;
|
||||
if self.on {
|
||||
config.off = false;
|
||||
}
|
||||
|
||||
if let Some(x) = self.width {
|
||||
config.width = x;
|
||||
}
|
||||
if let Some(x) = self.active_color {
|
||||
config.active_color = x;
|
||||
config.active_gradient = None;
|
||||
}
|
||||
if let Some(x) = self.inactive_color {
|
||||
config.inactive_color = x;
|
||||
config.inactive_gradient = None;
|
||||
}
|
||||
if let Some(x) = self.active_gradient {
|
||||
config.active_gradient = Some(x);
|
||||
}
|
||||
if let Some(x) = self.inactive_gradient {
|
||||
config.inactive_gradient = Some(x);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Color {
|
||||
type Err = miette::Error;
|
||||
|
||||
@@ -1977,6 +2047,11 @@ mod tests {
|
||||
open-on-output "eDP-1"
|
||||
open-maximized true
|
||||
open-fullscreen false
|
||||
|
||||
border {
|
||||
on
|
||||
width 8
|
||||
}
|
||||
}
|
||||
|
||||
binds {
|
||||
@@ -2179,6 +2254,11 @@ mod tests {
|
||||
open_on_output: Some("eDP-1".to_owned()),
|
||||
open_maximized: Some(true),
|
||||
open_fullscreen: Some(false),
|
||||
border: BorderRule {
|
||||
on: true,
|
||||
width: Some(8),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
binds: Binds(vec![
|
||||
|
||||
@@ -147,7 +147,7 @@ impl Layout {
|
||||
|
||||
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
window.request_size(ws.new_window_size(width), false);
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false);
|
||||
window.communicate();
|
||||
|
||||
self.layout.add_window(window.clone(), width, false);
|
||||
@@ -161,7 +161,7 @@ impl Layout {
|
||||
width: Option<ColumnWidth>,
|
||||
) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
window.request_size(ws.new_window_size(width), false);
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false);
|
||||
window.communicate();
|
||||
|
||||
self.layout
|
||||
|
||||
@@ -230,7 +230,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
// The required configure will be the initial configure.
|
||||
}
|
||||
InitialConfigureState::Configured { output, .. } => {
|
||||
InitialConfigureState::Configured { rules, output, .. } => {
|
||||
// Figure out the monitor following a similar logic to initial configure.
|
||||
// FIXME: deduplicate.
|
||||
let mon = requested_output
|
||||
@@ -269,7 +269,7 @@ impl XdgShellHandler for State {
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||
});
|
||||
ws.configure_new_window(&unmapped.window, None);
|
||||
ws.configure_new_window(&unmapped.window, None, rules);
|
||||
}
|
||||
|
||||
// We already sent the initial configure, so we need to reconfigure.
|
||||
@@ -302,10 +302,10 @@ impl XdgShellHandler for State {
|
||||
// The required configure will be the initial configure.
|
||||
}
|
||||
InitialConfigureState::Configured {
|
||||
rules,
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
..
|
||||
} => {
|
||||
// Figure out the monitor following a similar logic to initial configure.
|
||||
// FIXME: deduplicate.
|
||||
@@ -349,7 +349,7 @@ impl XdgShellHandler for State {
|
||||
} else {
|
||||
*width
|
||||
};
|
||||
ws.configure_new_window(&unmapped.window, configure_width);
|
||||
ws.configure_new_window(&unmapped.window, configure_width, rules);
|
||||
}
|
||||
|
||||
// We already sent the initial configure, so we need to reconfigure.
|
||||
@@ -580,7 +580,7 @@ impl State {
|
||||
} else {
|
||||
width
|
||||
};
|
||||
ws.configure_new_window(window, configure_width);
|
||||
ws.configure_new_window(window, configure_width, &rules);
|
||||
}
|
||||
|
||||
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
|
||||
|
||||
+12
-6
@@ -463,8 +463,10 @@ impl<W: LayoutElement> Layout<W> {
|
||||
) -> Option<&Output> {
|
||||
let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
|
||||
if let ColumnWidth::Fixed(w) = &mut width {
|
||||
if !self.options.border.off {
|
||||
*w += self.options.border.width as i32 * 2;
|
||||
let rules = window.rules();
|
||||
let border_config = rules.border.resolve_against(self.options.border);
|
||||
if !border_config.off {
|
||||
*w += border_config.width as i32 * 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,8 +521,10 @@ impl<W: LayoutElement> Layout<W> {
|
||||
) -> Option<&Output> {
|
||||
let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
|
||||
if let ColumnWidth::Fixed(w) = &mut width {
|
||||
if !self.options.border.off {
|
||||
*w += self.options.border.width as i32 * 2;
|
||||
let rules = window.rules();
|
||||
let border_config = rules.border.resolve_against(self.options.border);
|
||||
if !border_config.off {
|
||||
*w += border_config.width as i32 * 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,8 +559,10 @@ impl<W: LayoutElement> Layout<W> {
|
||||
) {
|
||||
let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
|
||||
if let ColumnWidth::Fixed(w) = &mut width {
|
||||
if !self.options.border.off {
|
||||
*w += self.options.border.width as i32 * 2;
|
||||
let rules = window.rules();
|
||||
let border_config = rules.border.resolve_against(self.options.border);
|
||||
if !border_config.off {
|
||||
*w += border_config.width as i32 * 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -107,9 +107,12 @@ struct MoveAnimation {
|
||||
|
||||
impl<W: LayoutElement> Tile<W> {
|
||||
pub fn new(window: W, options: Rc<Options>) -> Self {
|
||||
let rules = window.rules();
|
||||
let border_config = rules.border.resolve_against(options.border);
|
||||
|
||||
Self {
|
||||
window,
|
||||
border: FocusRing::new(options.border.into()),
|
||||
border: FocusRing::new(border_config.into()),
|
||||
focus_ring: FocusRing::new(options.focus_ring),
|
||||
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
|
||||
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
|
||||
@@ -123,7 +126,11 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, options: Rc<Options>) {
|
||||
self.border.update_config(options.border.into());
|
||||
let rules = self.window.rules();
|
||||
|
||||
let border_config = rules.border.resolve_against(self.options.border);
|
||||
self.border.update_config(border_config.into());
|
||||
|
||||
self.focus_ring.update_config(options.focus_ring);
|
||||
self.options = options;
|
||||
}
|
||||
@@ -164,6 +171,10 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.resize_animation = None;
|
||||
}
|
||||
}
|
||||
|
||||
let rules = self.window.rules();
|
||||
let border_config = rules.border.resolve_against(self.options.border);
|
||||
self.border.update_config(border_config.into());
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
|
||||
|
||||
+46
-20
@@ -23,6 +23,7 @@ use crate::render_helpers::RenderTarget;
|
||||
use crate::swipe_tracker::SwipeTracker;
|
||||
use crate::utils::id::IdCounter;
|
||||
use crate::utils::output_size;
|
||||
use crate::window::ResolvedWindowRules;
|
||||
|
||||
/// Amount of touchpad movement to scroll the view for the width of one working area.
|
||||
const VIEW_GESTURE_WORKING_AREA_MOVEMENT: f64 = 1200.;
|
||||
@@ -406,16 +407,9 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
fn toplevel_bounds(&self) -> Size<i32, Logical> {
|
||||
let mut border = 0;
|
||||
if !self.options.border.off {
|
||||
border = self.options.border.width as i32 * 2;
|
||||
}
|
||||
|
||||
Size::from((
|
||||
max(self.working_area.size.w - self.options.gaps * 2 - border, 1),
|
||||
max(self.working_area.size.h - self.options.gaps * 2 - border, 1),
|
||||
))
|
||||
fn toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> {
|
||||
let border_config = rules.border.resolve_against(self.options.border);
|
||||
compute_toplevel_bounds(border_config, self.working_area.size, self.options.gaps)
|
||||
}
|
||||
|
||||
pub fn resolve_default_width(
|
||||
@@ -429,14 +423,20 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_window_size(&self, width: Option<ColumnWidth>) -> Size<i32, Logical> {
|
||||
pub fn new_window_size(
|
||||
&self,
|
||||
width: Option<ColumnWidth>,
|
||||
rules: &ResolvedWindowRules,
|
||||
) -> Size<i32, Logical> {
|
||||
let border = rules.border.resolve_against(self.options.border);
|
||||
|
||||
let width = if let Some(width) = width {
|
||||
let is_fixed = matches!(width, ColumnWidth::Fixed(_));
|
||||
|
||||
let mut width = width.resolve(&self.options, self.working_area.size.w);
|
||||
|
||||
if !is_fixed && !self.options.border.off {
|
||||
width -= self.options.border.width as i32 * 2;
|
||||
if !is_fixed && !border.off {
|
||||
width -= border.width as i32 * 2;
|
||||
}
|
||||
|
||||
max(1, width)
|
||||
@@ -445,14 +445,19 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
};
|
||||
|
||||
let mut height = self.working_area.size.h - self.options.gaps * 2;
|
||||
if !self.options.border.off {
|
||||
height -= self.options.border.width as i32 * 2;
|
||||
if !border.off {
|
||||
height -= border.width as i32 * 2;
|
||||
}
|
||||
|
||||
Size::from((width, max(height, 1)))
|
||||
}
|
||||
|
||||
pub fn configure_new_window(&self, window: &Window, width: Option<ColumnWidth>) {
|
||||
pub fn configure_new_window(
|
||||
&self,
|
||||
window: &Window,
|
||||
width: Option<ColumnWidth>,
|
||||
rules: &ResolvedWindowRules,
|
||||
) {
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let transform = output.current_transform();
|
||||
@@ -468,10 +473,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
if state.states.contains(xdg_toplevel::State::Fullscreen) {
|
||||
state.size = Some(self.view_size);
|
||||
} else {
|
||||
state.size = Some(self.new_window_size(width));
|
||||
state.size = Some(self.new_window_size(width, rules));
|
||||
}
|
||||
|
||||
state.bounds = Some(self.toplevel_bounds());
|
||||
state.bounds = Some(self.toplevel_bounds(rules));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2134,8 +2139,6 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, is_active: bool) {
|
||||
let bounds = self.toplevel_bounds();
|
||||
|
||||
for (col_idx, col) in self.columns.iter_mut().enumerate() {
|
||||
for (tile_idx, tile) in col.tiles.iter_mut().enumerate() {
|
||||
let win = tile.window_mut();
|
||||
@@ -2144,7 +2147,14 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
&& col.active_tile_idx == tile_idx;
|
||||
win.set_activated(active);
|
||||
|
||||
let border_config = win.rules().border.resolve_against(self.options.border);
|
||||
let bounds = compute_toplevel_bounds(
|
||||
border_config,
|
||||
self.working_area.size,
|
||||
self.options.gaps,
|
||||
);
|
||||
win.set_bounds(bounds);
|
||||
|
||||
win.send_pending_configure();
|
||||
win.refresh();
|
||||
}
|
||||
@@ -2824,3 +2834,19 @@ pub fn compute_working_area(output: &Output, struts: Struts) -> Rectangle<i32, L
|
||||
|
||||
working_area
|
||||
}
|
||||
|
||||
fn compute_toplevel_bounds(
|
||||
border_config: niri_config::Border,
|
||||
working_area_size: Size<i32, Logical>,
|
||||
gaps: i32,
|
||||
) -> Size<i32, Logical> {
|
||||
let mut border = 0;
|
||||
if !border_config.off {
|
||||
border = border_config.width as i32 * 2;
|
||||
}
|
||||
|
||||
Size::from((
|
||||
max(working_area_size.w - gaps * 2 - border, 1),
|
||||
max(working_area_size.h - gaps * 2 - border, 1),
|
||||
))
|
||||
}
|
||||
|
||||
+15
-1
@@ -1,4 +1,4 @@
|
||||
use niri_config::{BlockOutFrom, Match, WindowRule};
|
||||
use niri_config::{BlockOutFrom, BorderRule, Match, WindowRule};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
@@ -48,6 +48,9 @@ pub struct ResolvedWindowRules {
|
||||
/// Extra bound on the maximum window height.
|
||||
pub max_height: Option<u16>,
|
||||
|
||||
/// Window border overrides.
|
||||
pub border: BorderRule,
|
||||
|
||||
/// Whether or not to draw the border with a solid background.
|
||||
///
|
||||
/// `None` means using the SSD heuristic.
|
||||
@@ -87,6 +90,15 @@ impl ResolvedWindowRules {
|
||||
min_height: None,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
border: BorderRule {
|
||||
off: false,
|
||||
on: false,
|
||||
width: None,
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
},
|
||||
draw_border_with_background: None,
|
||||
opacity: None,
|
||||
block_out_from: None,
|
||||
@@ -158,6 +170,8 @@ impl ResolvedWindowRules {
|
||||
resolved.max_height = Some(x);
|
||||
}
|
||||
|
||||
resolved.border.merge_with(&rule.border);
|
||||
|
||||
if let Some(x) = rule.draw_border_with_background {
|
||||
resolved.draw_border_with_background = Some(x);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,16 @@ window-rule {
|
||||
block-out-from "screencast"
|
||||
// block-out-from "screen-capture"
|
||||
|
||||
border {
|
||||
// off
|
||||
on
|
||||
width 4
|
||||
active-color "#ffc87f"
|
||||
inactive-color "#505050"
|
||||
active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||
inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
}
|
||||
|
||||
min-width 100
|
||||
max-width 200
|
||||
min-height 300
|
||||
@@ -337,6 +347,26 @@ window-rule {
|
||||
}
|
||||
```
|
||||
|
||||
#### `border`
|
||||
|
||||
<sup>Since: 0.1.6</sup>
|
||||
|
||||
Override the border options for the window.
|
||||
|
||||
This rule has the same options as the normal border config in the [layout](./Configuration:-Layout.md) section, so check the documentation there.
|
||||
|
||||
However, in addition to `off` to disable the border, this window rule has an `on` flag that enables the border for the window even if the border is otherwise disabled.
|
||||
The `on` flag has precedence over the `off` flag, in case both are set.
|
||||
|
||||
```
|
||||
window-rule {
|
||||
border {
|
||||
on
|
||||
width 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Size Overrides
|
||||
|
||||
You can amend the window's minimum and maximum size in logical pixels.
|
||||
|
||||
Reference in New Issue
Block a user