From 7558ac14e6473c98bc4eea442b28a5ae330115ef Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 3 Oct 2023 11:38:42 +0400 Subject: Add set-column-width action --- resources/default-config.kdl | 11 +++++ src/config.rs | 98 ++++++++++++++++++++++++++++++++++++++++++-- src/input.rs | 3 ++ src/layout.rs | 81 +++++++++++++++++++++++++++++++++++- 4 files changed, 188 insertions(+), 5 deletions(-) diff --git a/resources/default-config.kdl b/resources/default-config.kdl index b68bd2a3..a6e3667e 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -173,6 +173,17 @@ binds { Mod+F { maximize-column; } Mod+Shift+F { fullscreen-window; } + // Finer width adjustments. + // This command can also: + // * set width in pixels: "1000" + // * adjust width in pixels: "-5" or "+5" + // * set width as a percentage of screen width: "25%" + // * adjust width as a percentage of screen width: "-10%" or "+10%" + // Pixel sizes use logical, or scaled, pixels. I.e. on an output with scale 2.0, + // set-column-width "100" will make the column occupy 200 physical screen pixels. + Mod+Minus { set-column-width "-10%"; } + Mod+Equal { set-column-width "+10%"; } + Print { screenshot; } Mod+Shift+E { quit; } diff --git a/src/config.rs b/src/config.rs index 7ff7e338..9f73518b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -181,10 +181,10 @@ impl Default for Cursor { } } -#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)] +#[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Binds(#[knuffel(children)] pub Vec); -#[derive(knuffel::Decode, Debug, PartialEq, Eq)] +#[derive(knuffel::Decode, Debug, PartialEq)] pub struct Bind { #[knuffel(node_name)] pub key: Key, @@ -209,7 +209,7 @@ bitflags! { } } -#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] +#[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub enum Action { #[knuffel(skip)] None, @@ -248,6 +248,15 @@ pub enum Action { MoveWindowToMonitorUp, SwitchPresetColumnWidth, MaximizeColumn, + SetColumnWidth(#[knuffel(argument, str)] SizeChange), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SizeChange { + SetFixed(i32), + SetProportion(f64), + AdjustFixed(i32), + AdjustProportion(f64), } #[derive(knuffel::Decode, Debug, PartialEq)] @@ -383,6 +392,58 @@ impl FromStr for Key { } } +impl FromStr for SizeChange { + type Err = miette::Error; + + fn from_str(s: &str) -> Result { + match s.split_once('%') { + Some((value, empty)) => { + if !empty.is_empty() { + return Err(miette!("trailing characters after '%' are not allowed")); + } + + match value.bytes().next() { + Some(b'-' | b'+') => { + let value = value + .parse() + .into_diagnostic() + .context("error parsing value")?; + Ok(Self::AdjustProportion(value)) + } + Some(_) => { + let value = value + .parse() + .into_diagnostic() + .context("error parsing value")?; + Ok(Self::SetProportion(value)) + } + None => Err(miette!("value is missing")), + } + } + None => { + let value = s; + match value.bytes().next() { + Some(b'-' | b'+') => { + let value = value + .parse() + .into_diagnostic() + .context("error parsing value")?; + Ok(Self::AdjustFixed(value)) + } + Some(_) => { + let value = value + .parse() + .into_diagnostic() + .context("error parsing value")?; + Ok(Self::SetFixed(value)) + } + None => Err(miette!("value is missing")), + } + } + } + } +} + #[cfg(test)] mod tests { use miette::NarratableReportHandler; @@ -586,4 +647,35 @@ mod tests { assert!("1920x1080@".parse::().is_err()); assert!("1920x1080@60Hz".parse::().is_err()); } + + #[test] + fn parse_size_change() { + assert_eq!( + "10".parse::().unwrap(), + SizeChange::SetFixed(10), + ); + assert_eq!( + "+10".parse::().unwrap(), + SizeChange::AdjustFixed(10), + ); + assert_eq!( + "-10".parse::().unwrap(), + SizeChange::AdjustFixed(-10), + ); + assert_eq!( + "10%".parse::().unwrap(), + SizeChange::SetProportion(10.), + ); + assert_eq!( + "+10%".parse::().unwrap(), + SizeChange::AdjustProportion(10.), + ); + assert_eq!( + "-10%".parse::().unwrap(), + SizeChange::AdjustProportion(-10.), + ); + + assert!("-".parse::().is_err()); + assert!("10% ".parse::().is_err()); + } } diff --git a/src/input.rs b/src/input.rs index b52d5c77..d40221b8 100644 --- a/src/input.rs +++ b/src/input.rs @@ -296,6 +296,9 @@ impl State { self.move_cursor_to_output(&output); } } + Action::SetColumnWidth(change) => { + self.niri.monitor_set.set_column_width(change); + } } } } diff --git a/src/layout.rs b/src/layout.rs index 89fc0a29..f0b58dd1 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -52,7 +52,7 @@ use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::SurfaceCachedState; use crate::animation::Animation; -use crate::config::{Color, Config}; +use crate::config::{Color, Config, SizeChange}; const PADDING: i32 = 16; const WIDTH_PROPORTIONS: [ColumnWidth; 3] = [ @@ -288,7 +288,8 @@ impl ColumnWidth { match self { ColumnWidth::Proportion(proportion) => (view_width as f64 * proportion).floor() as i32, ColumnWidth::PresetProportion(idx) => WIDTH_PROPORTIONS[idx].resolve(view_width), - ColumnWidth::Fixed(width) => width, + // FIXME: remove this PADDING from here after redesigning how padding works. + ColumnWidth::Fixed(width) => width + PADDING, } } } @@ -912,6 +913,13 @@ impl MonitorSet { monitor.toggle_full_width(); } + pub fn set_column_width(&mut self, change: SizeChange) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.set_column_width(change); + } + pub fn focus_output(&mut self, output: &Output) { if let MonitorSet::Normal { monitors, @@ -1278,6 +1286,10 @@ impl Monitor { fn toggle_full_width(&mut self) { self.active_workspace().toggle_full_width(); } + + fn set_column_width(&mut self, change: SizeChange) { + self.active_workspace().set_column_width(change); + } } impl Monitor { @@ -1871,6 +1883,18 @@ impl Workspace { self.columns[self.active_column_idx].toggle_full_width(self.view_size, self.working_area); } + fn set_column_width(&mut self, change: SizeChange) { + if self.columns.is_empty() { + return; + } + + self.columns[self.active_column_idx].set_column_width( + self.view_size, + self.working_area, + change, + ); + } + pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) { let (mut col_idx, win_idx) = self .columns @@ -2201,6 +2225,48 @@ impl Column { self.set_width(view_size, working_area, width); } + fn set_column_width( + &mut self, + view_size: Size, + working_area: Rectangle, + change: SizeChange, + ) { + let current_px = self.width.resolve(working_area.size.w - PADDING) - PADDING; + + let current = match self.width { + ColumnWidth::PresetProportion(idx) => WIDTH_PROPORTIONS[idx], + current => current, + }; + + // FIXME: fix overflows then remove limits. + const MAX_PX: i32 = 100000; + const MAX_F: f64 = 10000.; + + let width = match (current, change) { + (_, SizeChange::SetFixed(fixed)) => ColumnWidth::Fixed(fixed.clamp(1, MAX_PX)), + (_, SizeChange::SetProportion(proportion)) => { + ColumnWidth::Proportion((proportion / 100.).clamp(0., MAX_F)) + } + (_, SizeChange::AdjustFixed(delta)) => { + let width = current_px.saturating_add(delta).clamp(1, MAX_PX); + ColumnWidth::Fixed(width) + } + (ColumnWidth::Proportion(current), SizeChange::AdjustProportion(delta)) => { + let proportion = (current + delta / 100.).clamp(0., MAX_F); + ColumnWidth::Proportion(proportion) + } + (ColumnWidth::Fixed(_), SizeChange::AdjustProportion(delta)) => { + let current = + (current_px + PADDING) as f64 / (working_area.size.w - PADDING) as f64; + let proportion = (current + delta / 100.).clamp(0., MAX_F); + ColumnWidth::Proportion(proportion) + } + (ColumnWidth::PresetProportion(_), _) => unreachable!(), + }; + + self.set_width(view_size, working_area, width); + } + fn set_fullscreen( &mut self, view_size: Size, @@ -2384,6 +2450,15 @@ mod tests { }) } + fn arbitrary_size_change() -> impl Strategy { + prop_oneof![ + (0..).prop_map(SizeChange::SetFixed), + (0f64..).prop_map(SizeChange::SetProportion), + any::().prop_map(SizeChange::AdjustFixed), + any::().prop_map(SizeChange::AdjustProportion), + ] + } + #[derive(Debug, Clone, Copy, Arbitrary)] enum Op { AddOutput(#[proptest(strategy = "1..=5usize")] usize), @@ -2417,6 +2492,7 @@ mod tests { MoveWindowToOutput(#[proptest(strategy = "1..=5u8")] u8), SwitchPresetColumnWidth, MaximizeColumn, + SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange), Communicate(#[proptest(strategy = "1..=5usize")] usize), } @@ -2506,6 +2582,7 @@ mod tests { } Op::SwitchPresetColumnWidth => monitor_set.toggle_width(), Op::MaximizeColumn => monitor_set.toggle_full_width(), + Op::SetColumnWidth(change) => monitor_set.set_column_width(change), Op::Communicate(id) => { let mut window = None; match monitor_set { -- cgit