aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/input/mod.rs3
-rw-r--r--src/layout/mod.rs84
-rw-r--r--src/layout/monitor.rs4
-rw-r--r--src/layout/workspace.rs152
4 files changed, 204 insertions, 39 deletions
diff --git a/src/input/mod.rs b/src/input/mod.rs
index e5820495..6211535d 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -1035,6 +1035,9 @@ impl State {
Action::SwitchPresetColumnWidth => {
self.niri.layout.toggle_width();
}
+ Action::SwitchPresetWindowHeight => {
+ self.niri.layout.toggle_window_height();
+ }
Action::CenterColumn => {
self.niri.layout.center_column();
// FIXME: granular
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 35f4a2c7..ea29311e 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -34,7 +34,9 @@ use std::mem;
use std::rc::Rc;
use std::time::Duration;
-use niri_config::{CenterFocusedColumn, Config, FloatOrInt, Struts, Workspace as WorkspaceConfig};
+use niri_config::{
+ CenterFocusedColumn, Config, FloatOrInt, PresetSize, Struts, Workspace as WorkspaceConfig,
+};
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::Id;
@@ -238,11 +240,12 @@ pub struct Options {
pub center_focused_column: CenterFocusedColumn,
pub always_center_single_column: bool,
/// Column widths that `toggle_width()` switches between.
- pub preset_widths: Vec<ColumnWidth>,
+ pub preset_column_widths: Vec<ColumnWidth>,
/// Initial width for new columns.
- pub default_width: Option<ColumnWidth>,
+ pub default_column_width: Option<ColumnWidth>,
+ /// Window height that `toggle_window_height()` switches between.
+ pub preset_window_heights: Vec<PresetSize>,
pub animations: niri_config::Animations,
-
// Debug flags.
pub disable_resize_throttling: bool,
pub disable_transactions: bool,
@@ -257,15 +260,20 @@ impl Default for Options {
border: Default::default(),
center_focused_column: Default::default(),
always_center_single_column: false,
- preset_widths: vec![
+ preset_column_widths: vec![
ColumnWidth::Proportion(1. / 3.),
ColumnWidth::Proportion(0.5),
ColumnWidth::Proportion(2. / 3.),
],
- default_width: None,
+ default_column_width: None,
animations: Default::default(),
disable_resize_throttling: false,
disable_transactions: false,
+ preset_window_heights: vec![
+ PresetSize::Proportion(1. / 3.),
+ PresetSize::Proportion(0.5),
+ PresetSize::Proportion(2. / 3.),
+ ],
}
}
}
@@ -273,21 +281,26 @@ impl Default for Options {
impl Options {
fn from_config(config: &Config) -> Self {
let layout = &config.layout;
- let preset_column_widths = &layout.preset_column_widths;
- let preset_widths = if preset_column_widths.is_empty() {
- Options::default().preset_widths
+ let preset_column_widths = if layout.preset_column_widths.is_empty() {
+ Options::default().preset_column_widths
} else {
- preset_column_widths
+ layout
+ .preset_column_widths
.iter()
.copied()
.map(ColumnWidth::from)
.collect()
};
+ let preset_window_heights = if layout.preset_window_heights.is_empty() {
+ Options::default().preset_window_heights
+ } else {
+ layout.preset_window_heights.clone()
+ };
// Missing default_column_width maps to Some(ColumnWidth::Proportion(0.5)),
// while present, but empty, maps to None.
- let default_width = layout
+ let default_column_width = layout
.default_column_width
.as_ref()
.map(|w| w.0.map(ColumnWidth::from))
@@ -300,11 +313,12 @@ impl Options {
border: layout.border,
center_focused_column: layout.center_focused_column,
always_center_single_column: layout.always_center_single_column,
- preset_widths,
- default_width,
+ preset_column_widths,
+ default_column_width,
animations: config.animations.clone(),
disable_resize_throttling: config.debug.disable_resize_throttling,
disable_transactions: config.debug.disable_transactions,
+ preset_window_heights,
}
}
@@ -1937,6 +1951,13 @@ impl<W: LayoutElement> Layout<W> {
monitor.toggle_width();
}
+ pub fn toggle_window_height(&mut self) {
+ let Some(monitor) = self.active_monitor() else {
+ return;
+ };
+ monitor.toggle_window_height();
+ }
+
pub fn toggle_full_width(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
@@ -3027,6 +3048,7 @@ mod tests {
},
MoveColumnToOutput(#[proptest(strategy = "1..=5u8")] u8),
SwitchPresetColumnWidth,
+ SwitchPresetWindowHeight,
MaximizeColumn,
SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
SetWindowHeight {
@@ -3453,6 +3475,7 @@ mod tests {
Op::MoveWorkspaceDown => layout.move_workspace_down(),
Op::MoveWorkspaceUp => layout.move_workspace_up(),
Op::SwitchPresetColumnWidth => layout.toggle_width(),
+ Op::SwitchPresetWindowHeight => layout.toggle_window_height(),
Op::MaximizeColumn => layout.toggle_full_width(),
Op::SetColumnWidth(change) => layout.set_column_width(change),
Op::SetWindowHeight { id, change } => {
@@ -4416,6 +4439,41 @@ mod tests {
}
#[test]
+ fn preset_height_change_removes_preset() {
+ let mut config = Config::default();
+ config.layout.preset_window_heights = vec![PresetSize::Fixed(1), PresetSize::Fixed(2)];
+
+ let mut layout = Layout::new(&config);
+
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ id: 1,
+ bbox: Rectangle::from_loc_and_size((0, 0), (1280, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::AddWindow {
+ id: 2,
+ bbox: Rectangle::from_loc_and_size((0, 0), (1280, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::ConsumeOrExpelWindowLeft,
+ Op::SwitchPresetWindowHeight,
+ Op::SwitchPresetWindowHeight,
+ ];
+ for op in ops {
+ op.apply(&mut layout);
+ }
+
+ // Leave only one.
+ config.layout.preset_window_heights = vec![PresetSize::Fixed(1)];
+
+ layout.update_config(&config);
+
+ layout.verify_invariants();
+ }
+
+ #[test]
fn working_area_starts_at_physical_pixel() {
let struts = Struts {
left: FloatOrInt(0.5),
diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs
index 3227c9c4..b712d8e9 100644
--- a/src/layout/monitor.rs
+++ b/src/layout/monitor.rs
@@ -740,6 +740,10 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().toggle_width();
}
+ pub fn toggle_window_height(&mut self) {
+ self.active_workspace().toggle_window_height();
+ }
+
pub fn toggle_full_width(&mut self) {
self.active_workspace().toggle_full_width();
}
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 1ec54b1a..5a0ce052 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -4,7 +4,7 @@ use std::rc::Rc;
use std::time::Duration;
use niri_config::{
- CenterFocusedColumn, OutputName, PresetWidth, Struts, Workspace as WorkspaceConfig,
+ CenterFocusedColumn, OutputName, PresetSize, Struts, Workspace as WorkspaceConfig,
};
use niri_ipc::SizeChange;
use ordered_float::NotNan;
@@ -202,16 +202,17 @@ pub enum ColumnWidth {
/// Height of a window in a column.
///
-/// Proportional height is intentionally omitted. With column widths you frequently want e.g. two
-/// columns side-by-side with 50% width each, and you want them to remain this way when moving to a
-/// differently sized monitor. Windows in a column, however, already auto-size to fill the available
-/// height, giving you this behavior. The only reason to set a different window height, then, is
-/// when you want something in the window to fit exactly, e.g. to fit 30 lines in a terminal, which
-/// corresponds to the `Fixed` variant.
+/// Every window but one in a column must be `Auto`-sized so that the total height can add up to
+/// the workspace height. Resizing a window converts all other windows to `Auto`, weighted to
+/// preserve their visual heights at the moment of the conversion.
///
-/// This does not preclude the usual set of binds to set or resize a window proportionally. Just,
-/// they are converted to, and stored as fixed height right away, so that once you resize a window
-/// to fit the desired content, it can never become smaller than that when moving between monitors.
+/// In contrast to column widths, proportional height changes are converted to, and stored as,
+/// fixed height right away. With column widths you frequently want e.g. two columns side-by-side
+/// with 50% width each, and you want them to remain this way when moving to a differently sized
+/// monitor. Windows in a column, however, already auto-size to fill the available height, giving
+/// you this behavior. The main reason to set a different window height, then, is when you want
+/// something in the window to fit exactly, e.g. to fit 30 lines in a terminal, which corresponds
+/// to the `Fixed` variant.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowHeight {
/// Automatically computed *tile* height, distributed across the column according to weights.
@@ -221,6 +222,16 @@ pub enum WindowHeight {
Auto { weight: f64 },
/// Fixed *window* height in logical pixels.
Fixed(f64),
+ /// One of the *tile* height proportion presets.
+ Preset(usize),
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum ResolvedSize {
+ /// Size of the tile including borders.
+ Tile(f64),
+ /// Size of the window excluding borders.
+ Window(f64),
}
#[derive(Debug)]
@@ -319,18 +330,29 @@ impl ColumnWidth {
ColumnWidth::Proportion(proportion) => {
(view_width - options.gaps) * proportion - options.gaps
}
- ColumnWidth::Preset(idx) => options.preset_widths[idx].resolve(options, view_width),
+ ColumnWidth::Preset(idx) => {
+ options.preset_column_widths[idx].resolve(options, view_width)
+ }
ColumnWidth::Fixed(width) => width,
}
}
}
-impl From<PresetWidth> for ColumnWidth {
- fn from(value: PresetWidth) -> Self {
+impl From<PresetSize> for ColumnWidth {
+ fn from(value: PresetSize) -> Self {
match value {
- PresetWidth::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)),
- PresetWidth::Fixed(f) => Self::Fixed(f64::from(f.clamp(1, 100000))),
+ PresetSize::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)),
+ PresetSize::Fixed(f) => Self::Fixed(f64::from(f.clamp(1, 100000))),
+ }
+ }
+}
+
+fn resolve_preset_size(preset: PresetSize, options: &Options, view_size: f64) -> ResolvedSize {
+ match preset {
+ PresetSize::Proportion(proportion) => {
+ ResolvedSize::Tile((view_size - options.gaps) * proportion - options.gaps)
}
+ PresetSize::Fixed(width) => ResolvedSize::Window(f64::from(width)),
}
}
@@ -650,7 +672,7 @@ impl<W: LayoutElement> Workspace<W> {
match default_width {
Some(Some(width)) => Some(width),
Some(None) => None,
- None => self.options.default_width,
+ None => self.options.default_column_width,
}
}
@@ -2351,6 +2373,17 @@ impl<W: LayoutElement> Workspace<W> {
cancel_resize_for_column(&mut self.interactive_resize, col);
}
+ pub fn toggle_window_height(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ let col = &mut self.columns[self.active_column_idx];
+ col.toggle_window_height(None, true);
+
+ cancel_resize_for_column(&mut self.interactive_resize, col);
+ }
+
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
let (mut col_idx, tile_idx) = self
.columns
@@ -3016,12 +3049,18 @@ impl<W: LayoutElement> Column<W> {
let mut update_sizes = false;
// If preset widths changed, make our width non-preset.
- if self.options.preset_widths != options.preset_widths {
+ if self.options.preset_column_widths != options.preset_column_widths {
if let ColumnWidth::Preset(idx) = self.width {
- self.width = self.options.preset_widths[idx];
+ self.width = self.options.preset_column_widths[idx];
}
}
+ // If preset heights changed, make our heights non-preset.
+ if self.options.preset_window_heights != options.preset_window_heights {
+ self.convert_heights_to_auto();
+ update_sizes = true;
+ }
+
if self.options.gaps != options.gaps {
update_sizes = true;
}
@@ -3220,6 +3259,7 @@ impl<W: LayoutElement> Column<W> {
let width = width.resolve(&self.options, self.working_area.size.w);
let width = f64::max(f64::min(width, max_width), min_width);
+ let height = self.working_area.size.h;
// Compute the tile heights. Start by converting window heights to tile heights.
let mut heights = zip(&self.tiles, &self.data)
@@ -3228,8 +3268,19 @@ impl<W: LayoutElement> Column<W> {
WindowHeight::Fixed(height) => {
WindowHeight::Fixed(tile.tile_height_for_window_height(height.round().max(1.)))
}
+ WindowHeight::Preset(idx) => {
+ let preset = self.options.preset_window_heights[idx];
+ let window_height = match resolve_preset_size(preset, &self.options, height) {
+ ResolvedSize::Tile(h) => tile.window_height_for_tile_height(h),
+ ResolvedSize::Window(h) => h,
+ };
+ let tile_height = tile
+ .tile_height_for_window_height(window_height.round().clamp(1., 100000.));
+ WindowHeight::Fixed(tile_height)
+ }
})
.collect::<Vec<_>>();
+
let gaps_left = self.options.gaps * (self.tiles.len() + 1) as f64;
let mut height_left = self.working_area.size.h - gaps_left;
let mut auto_tiles_left = self.tiles.len();
@@ -3283,6 +3334,7 @@ impl<W: LayoutElement> Column<W> {
let weight = match *h {
WindowHeight::Auto { weight } => weight,
WindowHeight::Fixed(_) => continue,
+ WindowHeight::Preset(_) => unreachable!(),
};
let factor = weight / total_weight_2;
@@ -3321,6 +3373,7 @@ impl<W: LayoutElement> Column<W> {
let weight = match *h {
WindowHeight::Auto { weight } => weight,
WindowHeight::Fixed(_) => continue,
+ WindowHeight::Preset(_) => unreachable!(),
};
let factor = weight / total_weight;
@@ -3456,6 +3509,10 @@ impl<W: LayoutElement> Column<W> {
);
found_fixed = true;
}
+
+ if let WindowHeight::Preset(idx) = data.height {
+ assert!(self.options.preset_window_heights.len() > idx);
+ }
}
}
@@ -3467,11 +3524,11 @@ impl<W: LayoutElement> Column<W> {
};
let idx = match width {
- ColumnWidth::Preset(idx) => (idx + 1) % self.options.preset_widths.len(),
+ ColumnWidth::Preset(idx) => (idx + 1) % self.options.preset_column_widths.len(),
_ => {
let current = self.width();
self.options
- .preset_widths
+ .preset_column_widths
.iter()
.position(|prop| {
let resolved = prop.resolve(&self.options, self.working_area.size.w);
@@ -3500,7 +3557,7 @@ impl<W: LayoutElement> Column<W> {
let current_px = width.resolve(&self.options, self.working_area.size.w);
let current = match width {
- ColumnWidth::Preset(idx) => self.options.preset_widths[idx],
+ ColumnWidth::Preset(idx) => self.options.preset_column_widths[idx],
current => current,
};
@@ -3551,17 +3608,18 @@ impl<W: LayoutElement> Column<W> {
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
// Start by converting all heights to automatic, since only one window in the column can be
- // fixed-height. If the current tile is already fixed, however, we can skip that step.
- // Which is not only for optimization, but also preserves automatic weights in case one
- // window is resized in such a way that other windows hit their min size, and then back.
- if !matches!(self.data[tile_idx].height, WindowHeight::Fixed(_)) {
+ // non-auto-height. If the current tile is already non-auto, however, we can skip that
+ // step. Which is not only for optimization, but also preserves automatic weights in case
+ // one window is resized in such a way that other windows hit their min size, and then
+ // back.
+ if matches!(self.data[tile_idx].height, WindowHeight::Auto { .. }) {
self.convert_heights_to_auto();
}
let current = self.data[tile_idx].height;
let tile = &self.tiles[tile_idx];
let current_window_px = match current {
- WindowHeight::Auto { .. } => tile.window_size().h,
+ WindowHeight::Auto { .. } | WindowHeight::Preset(_) => tile.window_size().h,
WindowHeight::Fixed(height) => height,
};
let current_tile_px = tile.tile_height_for_window_height(current_window_px);
@@ -3631,6 +3689,48 @@ impl<W: LayoutElement> Column<W> {
self.update_tile_sizes(animate);
}
+ fn toggle_window_height(&mut self, tile_idx: Option<usize>, animate: bool) {
+ let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
+
+ // Start by converting all heights to automatic, since only one window in the column can be
+ // non-auto-height. If the current tile is already non-auto, however, we can skip that
+ // step. Which is not only for optimization, but also preserves automatic weights in case
+ // one window is resized in such a way that other windows hit their min size, and then
+ // back.
+ if matches!(self.data[tile_idx].height, WindowHeight::Auto { .. }) {
+ self.convert_heights_to_auto();
+ }
+
+ let preset_idx = match self.data[tile_idx].height {
+ WindowHeight::Preset(idx) => (idx + 1) % self.options.preset_window_heights.len(),
+ _ => {
+ let current = self.data[tile_idx].size.h;
+ let tile = &self.tiles[tile_idx];
+ self.options
+ .preset_window_heights
+ .iter()
+ .copied()
+ .position(|preset| {
+ let resolved =
+ resolve_preset_size(preset, &self.options, self.working_area.size.h);
+ let window_height = match resolved {
+ ResolvedSize::Tile(h) => tile.window_height_for_tile_height(h),
+ ResolvedSize::Window(h) => h,
+ };
+ let resolved = tile.tile_height_for_window_height(
+ window_height.round().clamp(1., 100000.),
+ );
+
+ // Some allowance for fractional scaling purposes.
+ current + 1. < resolved
+ })
+ .unwrap_or(0)
+ }
+ };
+ self.data[tile_idx].height = WindowHeight::Preset(preset_idx);
+ self.update_tile_sizes(animate);
+ }
+
/// Converts all heights in the column to automatic, preserving the apparent heights.
///
/// All weights are recomputed to preserve the current tile heights while "centering" the