aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-08-15 10:47:27 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2024-08-15 10:50:38 +0300
commit66f23c39809eef827ef7f47fab7bdbf68dd69a26 (patch)
treee8470d17649f6872be0173e248d16852af91282a /src
parent7a6ab31ad762f1f911dafb9fc0ade83d19c66d45 (diff)
downloadniri-66f23c39809eef827ef7f47fab7bdbf68dd69a26.tar.gz
niri-66f23c39809eef827ef7f47fab7bdbf68dd69a26.tar.bz2
niri-66f23c39809eef827ef7f47fab7bdbf68dd69a26.zip
layout: Implement weighted height distribution
The intention is to make columns add up to the working area height most of the time, while still preserving the ability to have one fixed-height window. Automatic heights are now distributed according to their weight, rather than evenly. This is similar to flex-grow in CSS or fraction in Typst. Resizing one window in a column still makes that window fixed, however it changes all other windows to automatic height, computing their weights in such a way as to preserve their apparent heights.
Diffstat (limited to 'src')
-rw-r--r--src/layout/mod.rs95
-rw-r--r--src/layout/workspace.rs125
2 files changed, 202 insertions, 18 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 97a839c1..39fa2837 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -4263,6 +4263,101 @@ mod tests {
compute_working_area(&output, struts);
}
+ #[test]
+ fn set_window_height_recomputes_to_auto() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ id: 0,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::AddWindow {
+ id: 1,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::ConsumeOrExpelWindowLeft,
+ Op::AddWindow {
+ id: 2,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::ConsumeOrExpelWindowLeft,
+ Op::SetWindowHeight(SizeChange::SetFixed(100)),
+ Op::FocusWindowUp,
+ Op::SetWindowHeight(SizeChange::SetFixed(200)),
+ ];
+
+ check_ops(&ops);
+ }
+
+ #[test]
+ fn one_window_in_column_becomes_weight_1() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ id: 0,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::AddWindow {
+ id: 1,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::ConsumeOrExpelWindowLeft,
+ Op::AddWindow {
+ id: 2,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::ConsumeOrExpelWindowLeft,
+ Op::SetWindowHeight(SizeChange::SetFixed(100)),
+ Op::Communicate(2),
+ Op::FocusWindowUp,
+ Op::SetWindowHeight(SizeChange::SetFixed(200)),
+ Op::Communicate(1),
+ Op::CloseWindow(0),
+ Op::CloseWindow(1),
+ ];
+
+ check_ops(&ops);
+ }
+
+ #[test]
+ fn one_window_in_column_becomes_weight_1_after_fullscreen() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ id: 0,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::AddWindow {
+ id: 1,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::ConsumeOrExpelWindowLeft,
+ Op::AddWindow {
+ id: 2,
+ bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
+ min_max_size: Default::default(),
+ },
+ Op::ConsumeOrExpelWindowLeft,
+ Op::SetWindowHeight(SizeChange::SetFixed(100)),
+ Op::Communicate(2),
+ Op::FocusWindowUp,
+ Op::SetWindowHeight(SizeChange::SetFixed(200)),
+ Op::Communicate(1),
+ Op::CloseWindow(0),
+ Op::FullscreenWindow(1),
+ ];
+
+ check_ops(&ops);
+ }
+
fn arbitrary_spacing() -> impl Strategy<Value = f64> {
// Give equal weight to:
// - 0: the element is disabled
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 7cd19670..f4f3917a 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -196,9 +196,12 @@ pub enum ColumnWidth {
/// to fit the desired content, it can never become smaller than that when moving between monitors.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowHeight {
- /// Automatically computed height, evenly distributed across the column.
- Auto,
- /// Fixed height in logical pixels.
+ /// Automatically computed *tile* height, distributed across the column according to weights.
+ ///
+ /// This controls the tile height rather than the window height because it's easier in the auto
+ /// height distribution algorithm.
+ Auto { weight: f64 },
+ /// Fixed *window* height in logical pixels.
Fixed(f64),
}
@@ -312,6 +315,12 @@ impl From<PresetWidth> for ColumnWidth {
}
}
+impl WindowHeight {
+ const fn auto_1() -> Self {
+ Self::Auto { weight: 1. }
+ }
+}
+
impl TileData {
pub fn new<W: LayoutElement>(tile: &Tile<W>, height: WindowHeight) -> Self {
let mut rv = Self {
@@ -1106,6 +1115,13 @@ impl<W: LayoutElement> Workspace<W> {
let tile = column.tiles.remove(window_idx);
column.data.remove(window_idx);
+ // If one window is left, reset its weight to 1.
+ if column.data.len() == 1 {
+ if let WindowHeight::Auto { weight } = &mut column.data[0].height {
+ *weight = 1.;
+ }
+ }
+
if let Some(output) = &self.output {
tile.window().output_leave(output);
}
@@ -2265,6 +2281,14 @@ impl<W: LayoutElement> Workspace<W> {
let window = col.tiles.remove(tile_idx).into_window();
col.data.remove(tile_idx);
col.active_tile_idx = min(col.active_tile_idx, col.tiles.len() - 1);
+
+ // If one window is left, reset its weight to 1.
+ if col.data.len() == 1 {
+ if let WindowHeight::Auto { weight } = &mut col.data[0].height {
+ *weight = 1.;
+ }
+ }
+
col.update_tile_sizes(false);
self.data[col_idx].update(col);
let width = col.width;
@@ -2937,7 +2961,7 @@ impl<W: LayoutElement> Column<W> {
fn add_tile(&mut self, tile: Tile<W>, animate: bool) {
self.is_fullscreen = false;
- self.data.push(TileData::new(&tile, WindowHeight::Auto));
+ self.data.push(TileData::new(&tile, WindowHeight::auto_1()));
self.tiles.push(tile);
self.update_tile_sizes(animate);
}
@@ -3020,7 +3044,7 @@ impl<W: LayoutElement> Column<W> {
// Compute the tile heights. Start by converting window heights to tile heights.
let mut heights = zip(&self.tiles, &self.data)
.map(|(tile, data)| match data.height {
- WindowHeight::Auto => WindowHeight::Auto,
+ auto @ WindowHeight::Auto { .. } => auto,
WindowHeight::Fixed(height) => {
WindowHeight::Fixed(tile.tile_height_for_window_height(height.round().max(1.)))
}
@@ -3062,18 +3086,31 @@ impl<W: LayoutElement> Column<W> {
// However, most max height uses are for fixed-size dialogs, where min height == max_height.
// This case is separately handled above.
while auto_tiles_left > 0 {
+ let mut total_weight: f64 = heights
+ .iter()
+ .filter_map(|h| {
+ if let WindowHeight::Auto { weight } = *h {
+ Some(weight)
+ } else {
+ None
+ }
+ })
+ .sum();
+
// Wayland requires us to round the requested size for a window to integer logical
// pixels, therefore we compute the remaining auto height dynamically.
let mut height_left_2 = height_left;
- let mut auto_tiles_left_2 = auto_tiles_left;
+ let mut total_weight_2 = total_weight;
let mut unsatisfied_min = false;
for ((h, tile), min_size) in zip(zip(&mut heights, &self.tiles), &min_size) {
- if matches!(h, WindowHeight::Fixed(_)) {
- continue;
- }
+ let weight = match *h {
+ WindowHeight::Auto { weight } => weight,
+ WindowHeight::Fixed(_) => continue,
+ };
+ let factor = weight / total_weight_2;
// Compute the current auto height.
- let auto = height_left_2 / auto_tiles_left_2 as f64;
+ let auto = height_left_2 * factor;
let mut auto = tile.tile_height_for_window_height(
tile.window_height_for_tile_height(auto).round().max(1.),
);
@@ -3087,8 +3124,8 @@ impl<W: LayoutElement> Column<W> {
unsatisfied_min = true;
}
- auto_tiles_left_2 -= 1;
height_left_2 -= auto;
+ total_weight_2 -= weight;
}
// If some min height was unsatisfied, then we allocated the tile more than the auto
@@ -3100,18 +3137,21 @@ impl<W: LayoutElement> Column<W> {
// All min heights were satisfied, fill them in.
for (h, tile) in zip(&mut heights, &self.tiles) {
- if matches!(h, WindowHeight::Fixed(_)) {
- continue;
- }
+ let weight = match *h {
+ WindowHeight::Auto { weight } => weight,
+ WindowHeight::Fixed(_) => continue,
+ };
+ let factor = weight / total_weight;
// Compute the current auto height.
- let auto = height_left / auto_tiles_left as f64;
+ let auto = height_left * factor;
let auto = tile.tile_height_for_window_height(
tile.window_height_for_tile_height(auto).round().max(1.),
);
*h = WindowHeight::Fixed(auto);
height_left -= auto;
+ total_weight -= weight;
auto_tiles_left -= 1;
}
@@ -3203,6 +3243,16 @@ impl<W: LayoutElement> Column<W> {
assert_eq!(self.tiles.len(), 1);
}
+ if self.tiles.len() == 1 {
+ if let WindowHeight::Auto { weight } = self.data[0].height {
+ assert_eq!(
+ weight, 1.,
+ "auto height weight must reset to 1 for a single window"
+ );
+ }
+ }
+
+ let mut found_fixed = false;
for (tile, data) in zip(&self.tiles, &self.data) {
assert!(Rc::ptr_eq(&self.options, &tile.options));
assert_eq!(self.scale, tile.scale());
@@ -3217,6 +3267,14 @@ impl<W: LayoutElement> Column<W> {
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
+
+ if matches!(data.height, WindowHeight::Fixed(_)) {
+ assert!(
+ !found_fixed,
+ "there can only be one fixed-height window in a column"
+ );
+ found_fixed = true;
+ }
}
}
@@ -3310,10 +3368,19 @@ impl<W: LayoutElement> Column<W> {
fn set_window_height(&mut self, change: SizeChange, 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
+ // 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(_)) {
+ 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 { .. } => tile.window_size().h,
WindowHeight::Fixed(height) => height,
};
let current_tile_px = tile.tile_height_for_window_height(current_window_px);
@@ -3362,10 +3429,32 @@ impl<W: LayoutElement> Column<W> {
fn reset_window_height(&mut self, tile_idx: Option<usize>, animate: bool) {
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
- self.data[tile_idx].height = WindowHeight::Auto;
+ self.data[tile_idx].height = WindowHeight::auto_1();
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
+ /// weights at the median window height (it gets weight = 1).
+ ///
+ /// One case where apparent heights will not be preserved is when the column is taller than the
+ /// working area.
+ fn convert_heights_to_auto(&mut self) {
+ let heights: Vec<_> = self.tiles.iter().map(|tile| tile.tile_size().h).collect();
+
+ // Weights are invariant to multiplication: a column with weights 2, 2, 1 is equivalent to
+ // a column with weights 4, 4, 2. So we find the median window height and use that as 1.
+ let mut sorted = heights.clone();
+ sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
+ let median = sorted[sorted.len() / 2];
+
+ for (data, height) in zip(&mut self.data, heights) {
+ let weight = height / median;
+ data.height = WindowHeight::Auto { weight };
+ }
+ }
+
fn set_fullscreen(&mut self, is_fullscreen: bool) {
if self.is_fullscreen == is_fullscreen {
return;
@@ -3399,7 +3488,7 @@ impl<W: LayoutElement> Column<W> {
// Chain with a dummy value to be able to get one past all tiles' Y.
let dummy = TileData {
- height: WindowHeight::Auto,
+ height: WindowHeight::auto_1(),
size: Size::default(),
interactively_resizing_by_left_edge: false,
};