Implement preset window heights

This commit is contained in:
Christian Rieger
2024-09-05 23:37:10 +02:00
committed by Ivan Molodetskikh
parent 087a50a19c
commit d0e624e615
6 changed files with 236 additions and 51 deletions
+30 -12
View File
@@ -392,9 +392,11 @@ pub struct Layout {
#[knuffel(child, default)]
pub border: Border,
#[knuffel(child, unwrap(children), default)]
pub preset_column_widths: Vec<PresetWidth>,
pub preset_column_widths: Vec<PresetSize>,
#[knuffel(child)]
pub default_column_width: Option<DefaultColumnWidth>,
pub default_column_width: Option<DefaultPresetSize>,
#[knuffel(child, unwrap(children), default)]
pub preset_window_heights: Vec<PresetSize>,
#[knuffel(child, unwrap(argument), default)]
pub center_focused_column: CenterFocusedColumn,
#[knuffel(child)]
@@ -416,6 +418,7 @@ impl Default for Layout {
always_center_single_column: false,
gaps: FloatOrInt(16.),
struts: Default::default(),
preset_window_heights: Default::default(),
}
}
}
@@ -624,13 +627,13 @@ impl Default for Cursor {
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub enum PresetWidth {
pub enum PresetSize {
Proportion(#[knuffel(argument)] f64),
Fixed(#[knuffel(argument)] i32),
}
#[derive(Debug, Clone, PartialEq)]
pub struct DefaultColumnWidth(pub Option<PresetWidth>);
pub struct DefaultPresetSize(pub Option<PresetSize>);
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct Struts {
@@ -898,7 +901,7 @@ pub struct WindowRule {
// Rules applied at initial configure.
#[knuffel(child)]
pub default_column_width: Option<DefaultColumnWidth>,
pub default_column_width: Option<DefaultPresetSize>,
#[knuffel(child, unwrap(argument))]
pub open_on_output: Option<String>,
#[knuffel(child, unwrap(argument))]
@@ -1155,6 +1158,7 @@ pub enum Action {
#[knuffel(skip)]
ResetWindowHeightById(u64),
SwitchPresetColumnWidth,
SwitchPresetWindowHeight,
MaximizeColumn,
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
SwitchLayout(#[knuffel(argument, str)] LayoutSwitchTarget),
@@ -1266,6 +1270,7 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::ResetWindowHeight { id: None } => Self::ResetWindowHeight,
niri_ipc::Action::ResetWindowHeight { id: Some(id) } => Self::ResetWindowHeightById(id),
niri_ipc::Action::SwitchPresetColumnWidth {} => Self::SwitchPresetColumnWidth,
niri_ipc::Action::SwitchPresetWindowHeight {} => Self::SwitchPresetWindowHeight,
niri_ipc::Action::MaximizeColumn {} => Self::MaximizeColumn,
niri_ipc::Action::SetColumnWidth { change } => Self::SetColumnWidth(change),
niri_ipc::Action::SwitchLayout { layout } => Self::SwitchLayout(layout),
@@ -1882,7 +1887,7 @@ impl OutputName {
}
}
impl<S> knuffel::Decode<S> for DefaultColumnWidth
impl<S> knuffel::Decode<S> for DefaultPresetSize
where
S: knuffel::traits::ErrorSpan,
{
@@ -1902,7 +1907,7 @@ where
"expected no more than one child",
));
}
PresetWidth::decode_node(child, ctx).map(Some).map(Self)
PresetSize::decode_node(child, ctx).map(Some).map(Self)
} else {
Ok(Self(None))
}
@@ -2914,6 +2919,13 @@ mod tests {
fixed 1280
}
preset-window-heights {
proportion 0.25
proportion 0.5
fixed 960
fixed 1280
}
default-column-width { proportion 0.25; }
gaps 8
@@ -3104,14 +3116,20 @@ mod tests {
inactive_gradient: None,
},
preset_column_widths: vec![
PresetWidth::Proportion(0.25),
PresetWidth::Proportion(0.5),
PresetWidth::Fixed(960),
PresetWidth::Fixed(1280),
PresetSize::Proportion(0.25),
PresetSize::Proportion(0.5),
PresetSize::Fixed(960),
PresetSize::Fixed(1280),
],
default_column_width: Some(DefaultColumnWidth(Some(PresetWidth::Proportion(
default_column_width: Some(DefaultPresetSize(Some(PresetSize::Proportion(
0.25,
)))),
preset_window_heights: vec![
PresetSize::Proportion(0.25),
PresetSize::Proportion(0.5),
PresetSize::Fixed(960),
PresetSize::Fixed(1280),
],
gaps: FloatOrInt(8.),
struts: Struts {
left: FloatOrInt(1.),
+2
View File
@@ -350,6 +350,8 @@ pub enum Action {
},
/// Switch between preset column widths.
SwitchPresetColumnWidth {},
/// Switch between preset window heights.
SwitchPresetWindowHeight {},
/// Toggle the maximized state of the focused column.
MaximizeColumn {},
/// Change the width of the focused column.
+3
View File
@@ -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
+71 -13
View File
@@ -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 } => {
@@ -4415,6 +4438,41 @@ mod tests {
layout.verify_invariants();
}
#[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 {
+4
View File
@@ -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();
}
+126 -26
View File
@@ -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,21 +330,32 @@ 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)),
}
}
impl WindowHeight {
const fn auto_1() -> Self {
Self::Auto { weight: 1. }
@@ -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