From b4e61b079cb60f04b8fcf0d7203419ad083528f4 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Mon, 12 May 2025 17:13:33 -0700 Subject: [PATCH] feat(wm): add scrolling layout This commit adds a new DefaultLayout::Scrolling variant, along with a new LayoutOptions configuration which will initially be used to allow the user to declaratively specify the number of visible columns for the Scrolling layout, and a new komorebic "scrolling-layout-columns" command to allow the user to modify this value for the focused workspace at runtime. The Scrolling layout is inspired by the Niri scrolling window manager, presenting a workspace as an infinite scrollable horizontal strip with a viewport which includes the focused window + N other windows in columns. There is no support for splitting columns into multiple rows. This layout can currently only be applied to single-monitor setups as the scrolling would result in layout calculations which push the windows in the columns moving out of the viewport onto adjacent monitors. This implementation in the current state is enough to be useable for me personally, but if others want to iterate on this, make it handle hiding/restoring windows correctly when scrolling the viewport so that adjacent monitors don't get impacted etc., patches are always welcome. resolve #1434 --- komorebi-bar/src/widgets/komorebi_layout.rs | 6 + komorebi/src/border_manager/border.rs | 2 +- komorebi/src/border_manager/mod.rs | 2 +- komorebi/src/core/arrangement.rs | 142 ++++++++- komorebi/src/core/default_layout.rs | 22 +- komorebi/src/core/direction.rs | 8 + komorebi/src/core/mod.rs | 2 + komorebi/src/core/rect.rs | 4 + komorebi/src/process_command.rs | 25 +- komorebi/src/process_event.rs | 16 +- komorebi/src/static_config.rs | 22 +- komorebi/src/window_manager.rs | 12 + komorebi/src/workspace.rs | 33 ++ komorebic/src/main.rs | 13 + schema.bar.json | 326 +++++++++++++++++--- schema.json | 29 +- 16 files changed, 602 insertions(+), 62 deletions(-) diff --git a/komorebi-bar/src/widgets/komorebi_layout.rs b/komorebi-bar/src/widgets/komorebi_layout.rs index b8361097..906767ba 100644 --- a/komorebi-bar/src/widgets/komorebi_layout.rs +++ b/komorebi-bar/src/widgets/komorebi_layout.rs @@ -188,6 +188,12 @@ impl KomorebiLayout { painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke); painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); } + // TODO: @CtByte can you think of a nice icon to draw here? + komorebi_client::DefaultLayout::Scrolling => { + painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke); + painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); + painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke); + } }, KomorebiLayout::Monocle => {} KomorebiLayout::Floating => { diff --git a/komorebi/src/border_manager/border.rs b/komorebi/src/border_manager/border.rs index ef1d4bce..f45fb417 100644 --- a/komorebi/src/border_manager/border.rs +++ b/komorebi/src/border_manager/border.rs @@ -392,7 +392,7 @@ impl Border { tracing::error!("failed to update border position {error}"); } - if !rect.is_same_size_as(&old_rect) { + if !rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect) { if let Some(render_target) = (*border_pointer).render_target.as_ref() { let border_width = (*border_pointer).width; let border_offset = (*border_pointer).offset; diff --git a/komorebi/src/border_manager/mod.rs b/komorebi/src/border_manager/mod.rs index 39f8e4dc..20db7dd1 100644 --- a/komorebi/src/border_manager/mod.rs +++ b/komorebi/src/border_manager/mod.rs @@ -356,7 +356,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result }; if !should_process_notification { - tracing::trace!("monitor state matches latest snapshot, skipping notification"); + tracing::debug!("monitor state matches latest snapshot, skipping notification"); continue 'receiver; } diff --git a/komorebi/src/core/arrangement.rs b/komorebi/src/core/arrangement.rs index 7e750e1f..360336ea 100644 --- a/komorebi/src/core/arrangement.rs +++ b/komorebi/src/core/arrangement.rs @@ -12,8 +12,10 @@ use super::custom_layout::ColumnSplitWithCapacity; use super::CustomLayout; use super::DefaultLayout; use super::Rect; +use crate::default_layout::LayoutOptions; pub trait Arrangement { + #[allow(clippy::too_many_arguments)] fn calculate( &self, area: &Rect, @@ -21,6 +23,9 @@ pub trait Arrangement { container_padding: Option, layout_flip: Option, resize_dimensions: &[Option], + focused_idx: usize, + layout_options: Option, + latest_layout: &[Rect], ) -> Vec; } @@ -33,9 +38,110 @@ impl Arrangement for DefaultLayout { container_padding: Option, layout_flip: Option, resize_dimensions: &[Option], + focused_idx: usize, + layout_options: Option, + latest_layout: &[Rect], ) -> Vec { let len = usize::from(len); let mut dimensions = match self { + Self::Scrolling => { + let column_count = layout_options + .and_then(|o| o.scrolling.map(|s| s.columns)) + .unwrap_or(3); + + let column_width = area.right / column_count as i32; + let mut layouts = Vec::with_capacity(len); + + match len { + // treat < 3 windows the same as the columns layout + len if len < 3 => { + layouts = columns(area, len); + + let adjustment = calculate_columns_adjustment(resize_dimensions); + layouts.iter_mut().zip(adjustment.iter()).for_each( + |(layout, adjustment)| { + layout.top += adjustment.top; + layout.bottom += adjustment.bottom; + layout.left += adjustment.left; + layout.right += adjustment.right; + }, + ); + + if matches!( + layout_flip, + Some(Axis::Horizontal | Axis::HorizontalAndVertical) + ) { + if let 2.. = len { + columns_reverse(&mut layouts); + } + } + } + // treat >= column_count as scrolling + len => { + let visible_columns = area.right / column_width; + let first_visible: isize = if focused_idx == 0 { + // if focused idx is 0, we are at the beginning of the scrolling strip + 0 + } else { + let previous_first_visible = if latest_layout.is_empty() { + 0 + } else { + // previous first_visible based on the left position of the first visible window + let left_edge = area.left; + latest_layout + .iter() + .position(|rect| rect.left >= left_edge) + .unwrap_or(0) as isize + }; + + let focused_idx = focused_idx as isize; + + if focused_idx < previous_first_visible { + // focused window is off the left edge, we need to scroll left + focused_idx + } else if focused_idx + >= previous_first_visible + visible_columns as isize + { + // focused window is off the right edge, we need to scroll right + // and make sure it's the last visible window + (focused_idx + 1 - visible_columns as isize).max(0) + } else { + // focused window is already visible, we don't need to scroll + previous_first_visible + } + .min( + (len as isize) + .saturating_sub(visible_columns as isize) + .max(0), + ) + }; + + for i in 0..len { + let position = (i as isize) - first_visible; + let left = area.left + (position as i32 * column_width); + + layouts.push(Rect { + left, + top: area.top, + right: column_width, + bottom: area.bottom, + }); + } + + let adjustment = calculate_scrolling_adjustment(resize_dimensions); + layouts.iter_mut().zip(adjustment.iter()).for_each( + |(layout, adjustment)| { + layout.top += adjustment.top; + layout.bottom += adjustment.bottom; + layout.left += adjustment.left; + layout.right += adjustment.right; + }, + ); + } + } + + layouts + } Self::BSP => recursive_fibonacci( 0, len, @@ -487,6 +593,9 @@ impl Arrangement for CustomLayout { container_padding: Option, _layout_flip: Option, _resize_dimensions: &[Option], + _focused_idx: usize, + _layout_options: Option, + _latest_layout: &[Rect], ) -> Vec { let mut dimensions = vec![]; let container_count = len.get(); @@ -541,7 +650,7 @@ impl Arrangement for CustomLayout { }; match column { - Column::Primary(Option::Some(_)) => { + Column::Primary(Some(_)) => { let main_column_area = if idx == 0 { Self::main_column_area(area, primary_right, None) } else { @@ -1115,6 +1224,37 @@ fn calculate_ultrawide_adjustment(resize_dimensions: &[Option]) -> Vec]) -> Vec { + let len = resize_dimensions.len(); + let mut result = vec![Rect::default(); len]; + + if len <= 1 { + return result; + } + + for (i, rect) in resize_dimensions.iter().enumerate() { + if let Some(rect) = rect { + let is_leftmost = i == 0; + let is_rightmost = i == len - 1; + + resize_left(&mut result[i], rect.left); + resize_right(&mut result[i], rect.right); + resize_top(&mut result[i], rect.top); + resize_bottom(&mut result[i], rect.bottom); + + if !is_leftmost && rect.left != 0 { + resize_right(&mut result[i - 1], rect.left); + } + + if !is_rightmost && rect.right != 0 { + resize_left(&mut result[i + 1], rect.right); + } + } + } + + result +} + fn resize_left(rect: &mut Rect, resize: i32) { rect.left += resize / 2; rect.right += -resize / 2; diff --git a/komorebi/src/core/default_layout.rs b/komorebi/src/core/default_layout.rs index edbf9374..6eea2fbf 100644 --- a/komorebi/src/core/default_layout.rs +++ b/komorebi/src/core/default_layout.rs @@ -21,9 +21,24 @@ pub enum DefaultLayout { UltrawideVerticalStack, Grid, RightMainVerticalStack, + Scrolling, // NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle` } +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct LayoutOptions { + /// Options related to the Scrolling layout + pub scrolling: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ScrollingLayoutOptions { + /// Desired number of visible columns (default: 3) + pub columns: usize, +} + impl DefaultLayout { pub fn leftmost_index(&self, len: usize) -> usize { match self { @@ -31,6 +46,7 @@ impl DefaultLayout { n if n > 1 => 1, _ => 0, }, + Self::Scrolling => 0, DefaultLayout::BSP | DefaultLayout::Columns | DefaultLayout::Rows @@ -53,6 +69,7 @@ impl DefaultLayout { _ => len.saturating_sub(1), }, DefaultLayout::RightMainVerticalStack => 0, + DefaultLayout::Scrolling => len.saturating_sub(1), } } @@ -75,6 +92,7 @@ impl DefaultLayout { | Self::RightMainVerticalStack | Self::HorizontalStack | Self::UltrawideVerticalStack + | Self::Scrolling ) { return None; }; @@ -169,13 +187,15 @@ impl DefaultLayout { Self::HorizontalStack => Self::UltrawideVerticalStack, Self::UltrawideVerticalStack => Self::Grid, Self::Grid => Self::RightMainVerticalStack, - Self::RightMainVerticalStack => Self::BSP, + Self::RightMainVerticalStack => Self::Scrolling, + Self::Scrolling => Self::BSP, } } #[must_use] pub const fn cycle_previous(self) -> Self { match self { + Self::Scrolling => Self::RightMainVerticalStack, Self::RightMainVerticalStack => Self::Grid, Self::Grid => Self::UltrawideVerticalStack, Self::UltrawideVerticalStack => Self::HorizontalStack, diff --git a/komorebi/src/core/direction.rs b/komorebi/src/core/direction.rs index 37f95bcc..fc723c55 100644 --- a/komorebi/src/core/direction.rs +++ b/komorebi/src/core/direction.rs @@ -102,6 +102,7 @@ impl Direction for DefaultLayout { Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1, Self::UltrawideVerticalStack => idx > 2, Self::Grid => !is_grid_edge(op_direction, idx, count), + Self::Scrolling => false, }, OperationDirection::Down => match self { Self::BSP => idx != count - 1 && idx % 2 != 0, @@ -111,6 +112,7 @@ impl Direction for DefaultLayout { Self::HorizontalStack => idx == 0, Self::UltrawideVerticalStack => idx > 1 && idx != count - 1, Self::Grid => !is_grid_edge(op_direction, idx, count), + Self::Scrolling => false, }, OperationDirection::Left => match self { Self::BSP => idx != 0, @@ -120,6 +122,7 @@ impl Direction for DefaultLayout { Self::HorizontalStack => idx != 0 && idx != 1, Self::UltrawideVerticalStack => idx != 1, Self::Grid => !is_grid_edge(op_direction, idx, count), + Self::Scrolling => idx != 0, }, OperationDirection::Right => match self { Self::BSP => idx % 2 == 0 && idx != count - 1, @@ -133,6 +136,7 @@ impl Direction for DefaultLayout { _ => idx < 2, }, Self::Grid => !is_grid_edge(op_direction, idx, count), + Self::Scrolling => idx != count - 1, }, } } @@ -158,6 +162,7 @@ impl Direction for DefaultLayout { | Self::RightMainVerticalStack => idx - 1, Self::HorizontalStack => 0, Self::Grid => grid_neighbor(op_direction, idx, count), + Self::Scrolling => unreachable!(), } } @@ -176,6 +181,7 @@ impl Direction for DefaultLayout { Self::Columns => unreachable!(), Self::HorizontalStack => 1, Self::Grid => grid_neighbor(op_direction, idx, count), + Self::Scrolling => unreachable!(), } } @@ -203,6 +209,7 @@ impl Direction for DefaultLayout { _ => 0, }, Self::Grid => grid_neighbor(op_direction, idx, count), + Self::Scrolling => idx - 1, } } @@ -223,6 +230,7 @@ impl Direction for DefaultLayout { _ => unreachable!(), }, Self::Grid => grid_neighbor(op_direction, idx, count), + Self::Scrolling => idx + 1, } } } diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index 30ce6673..394864b5 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -1,6 +1,7 @@ #![warn(clippy::all)] #![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)] +use std::num::NonZeroUsize; use std::path::PathBuf; use std::str::FromStr; @@ -108,6 +109,7 @@ pub enum SocketMessage { AdjustWorkspacePadding(Sizing, i32), ChangeLayout(DefaultLayout), CycleLayout(CycleDirection), + ScrollingLayoutColumns(NonZeroUsize), ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf), FlipLayout(Axis), ToggleWorkspaceWindowContainerBehaviour, diff --git a/komorebi/src/core/rect.rs b/komorebi/src/core/rect.rs index 285b7d00..04ad653d 100644 --- a/komorebi/src/core/rect.rs +++ b/komorebi/src/core/rect.rs @@ -41,6 +41,10 @@ impl Rect { pub fn is_same_size_as(&self, rhs: &Self) -> bool { self.right == rhs.right && self.bottom == rhs.bottom } + + pub fn has_same_position_as(&self, rhs: &Self) -> bool { + self.left == rhs.left && self.top == rhs.top + } } impl Rect { diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index d92cc97e..13d41ca7 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -49,6 +49,8 @@ use crate::core::StateQuery; use crate::core::WindowContainerBehaviour; use crate::core::WindowKind; use crate::current_virtual_desktop; +use crate::default_layout::LayoutOptions; +use crate::default_layout::ScrollingLayoutOptions; use crate::monitor::MonitorInformation; use crate::notify_subscribers; use crate::stackbar_manager; @@ -933,6 +935,27 @@ impl WindowManager { self.retile_all(true)? } SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?, + SocketMessage::ScrollingLayoutColumns(count) => { + let focused_workspace = self.focused_workspace_mut()?; + + let options = match focused_workspace.layout_options() { + Some(mut opts) => { + if let Some(scrolling) = &mut opts.scrolling { + scrolling.columns = count.into(); + } + + opts + } + None => LayoutOptions { + scrolling: Some(ScrollingLayoutOptions { + columns: count.into(), + }), + }, + }; + + focused_workspace.set_layout_options(Some(options)); + self.update_focused_workspace(false, false)?; + } SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?, SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?, SocketMessage::ChangeLayoutCustom(ref path) => { @@ -1751,7 +1774,7 @@ Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue { for config_file_path in &mut *display_bar_configurations { let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"# - .replace("CONFIGFILE", &config_file_path.to_string_lossy()); + .replace("CONFIGFILE", &config_file_path.to_string_lossy()); match powershell_script::run(&script) { Ok(_) => { diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 91699632..fda05f20 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -25,6 +25,8 @@ use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::winevent::WinEvent; use crate::workspace::WorkspaceLayer; +use crate::DefaultLayout; +use crate::Layout; use crate::Notification; use crate::NotificationEvent; use crate::State; @@ -301,7 +303,11 @@ impl WindowManager { // don't want to trigger the full workspace updates when there are no managed // containers - this makes floating windows on empty workspaces go into very // annoying focus change loops which prevents users from interacting with them - if !self.focused_workspace()?.containers().is_empty() { + if !matches!( + self.focused_workspace()?.layout(), + Layout::Default(DefaultLayout::Scrolling) + ) && !self.focused_workspace()?.containers().is_empty() + { self.update_focused_workspace(self.mouse_follows_focus, false)?; } @@ -328,6 +334,14 @@ impl WindowManager { } workspace.set_layer(WorkspaceLayer::Tiling); + + if matches!( + self.focused_workspace()?.layout(), + Layout::Default(DefaultLayout::Scrolling) + ) && !self.focused_workspace()?.containers().is_empty() + { + self.update_focused_workspace(self.mouse_follows_focus, false)?; + } } Some(idx) => { if let Some(_window) = workspace.floating_windows().get(idx) { diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 58313387..737103c1 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -35,6 +35,7 @@ use crate::core::StackbarMode; use crate::core::WindowContainerBehaviour; use crate::core::WindowManagementBehaviour; use crate::current_virtual_desktop; +use crate::default_layout::LayoutOptions; use crate::monitor; use crate::monitor::Monitor; use crate::monitor_reconciliator; @@ -191,6 +192,9 @@ pub struct WorkspaceConfig { /// Layout (default: BSP) #[serde(skip_serializing_if = "Option::is_none")] pub layout: Option, + /// Layout-specific options (default: None) + #[serde(skip_serializing_if = "Option::is_none")] + pub layout_options: Option, /// END OF LIFE FEATURE: Custom Layout (default: None) #[serde(skip_serializing_if = "Option::is_none")] #[serde_as(as = "Option")] @@ -286,6 +290,7 @@ impl From<&Workspace> for WorkspaceConfig { Layout::Custom(_) => None, }) .flatten(), + layout_options: value.layout_options(), custom_layout: value .workspace_config() .as_ref() @@ -1331,7 +1336,7 @@ impl StaticConfig { } pub fn postload(path: &PathBuf, wm: &Arc>) -> Result<()> { - let value = Self::read(path)?; + let mut value = Self::read(path)?; let mut wm = wm.lock(); let configs_with_preference: Vec<_> = @@ -1342,6 +1347,8 @@ impl StaticConfig { workspace_matching_rules.clear(); drop(workspace_matching_rules); + let monitor_count = wm.monitors().len(); + let offset = wm.work_area_offset; for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() { let preferred_config_idx = { @@ -1371,8 +1378,8 @@ impl StaticConfig { }); if let Some(monitor_config) = value .monitors - .as_ref() - .and_then(|ms| idx.and_then(|i| ms.get(i))) + .as_mut() + .and_then(|ms| idx.and_then(|i| ms.get_mut(i))) { if let Some(used_config_idx) = idx { configs_used.push(used_config_idx); @@ -1395,7 +1402,14 @@ impl StaticConfig { monitor.update_workspaces_globals(offset); for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() { - if let Some(workspace_config) = monitor_config.workspaces.get(j) { + if let Some(workspace_config) = monitor_config.workspaces.get_mut(j) { + if monitor_count > 1 + && matches!(workspace_config.layout, Some(DefaultLayout::Scrolling)) + { + tracing::warn!("scrolling layout is only supported for a single monitor; falling back to columns layout"); + workspace_config.layout = Some(DefaultLayout::Columns); + } + ws.load_static_config(workspace_config)?; } } diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 6df2c179..4b7d71dc 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -329,6 +329,7 @@ impl From<&WindowManager> for State { maximized_window_restore_idx: workspace.maximized_window_restore_idx, floating_windows: workspace.floating_windows.clone(), layout: workspace.layout.clone(), + layout_options: workspace.layout_options, layout_rules: workspace.layout_rules.clone(), layout_flip: workspace.layout_flip, workspace_padding: workspace.workspace_padding, @@ -1579,6 +1580,9 @@ impl WindowManager { workspace.container_padding(), workspace.layout_flip(), &[], + workspace.focused_container_idx(), + workspace.layout_options(), + workspace.latest_layout(), ); let mut direction = direction; @@ -3352,8 +3356,16 @@ impl WindowManager { pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> { tracing::info!("changing layout"); + let monitor_count = self.monitors().len(); let workspace = self.focused_workspace_mut()?; + if monitor_count > 1 && matches!(layout, DefaultLayout::Scrolling) { + tracing::warn!( + "scrolling layout is only supported for a single monitor; not changing layout" + ); + return Ok(()); + } + match workspace.layout() { Layout::Default(_) => {} Layout::Custom(layout) => { diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 10f980df..944000d8 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -16,6 +16,7 @@ use crate::core::DefaultLayout; use crate::core::Layout; use crate::core::OperationDirection; use crate::core::Rect; +use crate::default_layout::LayoutOptions; use crate::locked_deque::LockedDeque; use crate::ring::Ring; use crate::should_act; @@ -70,6 +71,8 @@ pub struct Workspace { pub floating_windows: Ring, #[getset(get = "pub", get_mut = "pub", set = "pub")] pub layout: Layout, + #[getset(get_copy = "pub", set = "pub")] + pub layout_options: Option, #[getset(get = "pub", get_mut = "pub", set = "pub")] pub layout_rules: Vec<(usize, Layout)>, #[getset(get_copy = "pub", set = "pub")] @@ -139,6 +142,7 @@ impl Default for Workspace { monocle_container_restore_idx: None, floating_windows: Ring::default(), layout: Layout::Default(DefaultLayout::BSP), + layout_options: None, layout_rules: vec![], layout_flip: None, workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)), @@ -267,6 +271,7 @@ impl Workspace { self.set_layout_flip(config.layout_flip); self.set_floating_layer_behaviour(config.floating_layer_behaviour); self.set_wallpaper(config.wallpaper.clone()); + self.set_layout_options(config.layout_options); self.set_workspace_config(Some(config.clone())); @@ -583,6 +588,9 @@ impl Workspace { Some(container_padding), self.layout_flip(), self.resize_dimensions(), + self.focused_container_idx(), + self.layout_options(), + self.latest_layout(), ); let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst); @@ -1194,6 +1202,9 @@ impl Workspace { Layout::Default(DefaultLayout::UltrawideVerticalStack) => { self.enforce_resize_for_ultrawide(); } + Layout::Default(DefaultLayout::Scrolling) => { + self.enforce_resize_for_scrolling(); + } _ => self.enforce_no_resize(), } } @@ -1421,6 +1432,28 @@ impl Workspace { } } + fn enforce_resize_for_scrolling(&mut self) { + let resize_dimensions = self.resize_dimensions_mut(); + match resize_dimensions.len() { + 0 | 1 => self.enforce_no_resize(), + _ => { + let len = resize_dimensions.len(); + + for (i, rect) in resize_dimensions.iter_mut().enumerate() { + if let Some(rect) = rect { + rect.top = 0; + rect.bottom = 0; + + if i == 0 { + rect.left = 0; + } else if i == len - 1 { + rect.right = 0; + } + } + } + } + } + } fn enforce_no_resize(&mut self) { for rect in self.resize_dimensions_mut().iter_mut().flatten() { rect.left = 0; diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index aef6545e..268ef29d 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -9,6 +9,7 @@ use std::fs::OpenOptions; use std::io::BufRead; use std::io::BufReader; use std::io::Write; +use std::num::NonZeroUsize; use std::path::PathBuf; use std::process::Command; use std::sync::atomic::AtomicBool; @@ -963,6 +964,12 @@ struct EagerFocus { exe: String, } +#[derive(Parser)] +struct ScrollingLayoutColumns { + /// Desired number of visible columns + count: NonZeroUsize, +} + #[derive(Parser)] #[clap(author, about, version = build::CLAP_LONG_VERSION)] struct Opts { @@ -1202,6 +1209,9 @@ enum SubCommand { /// Cycle between available layouts #[clap(arg_required_else_help = true)] CycleLayout(CycleLayout), + /// Set the number of visible columns for the Scrolling layout on the focused workspace + #[clap(arg_required_else_help = true)] + ScrollingLayoutColumns(ScrollingLayoutColumns), /// Load a custom layout from file for the focused workspace #[clap(hide = true)] #[clap(arg_required_else_help = true)] @@ -2625,6 +2635,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { SubCommand::CycleLayout(arg) => { send_message(&SocketMessage::CycleLayout(arg.cycle_direction))?; } + SubCommand::ScrollingLayoutColumns(arg) => { + send_message(&SocketMessage::ScrollingLayoutColumns(arg.count))?; + } SubCommand::LoadCustomLayout(arg) => { send_message(&SocketMessage::ChangeLayoutCustom(arg.path))?; } diff --git a/schema.bar.json b/schema.bar.json index 92c32c88..8117fb36 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "KomobarConfig", - "description": "The `komorebi.bar.json` configuration file reference for `v0.1.37`", + "description": "The `komorebi.bar.json` configuration file reference for `v0.1.38`", "type": "object", "required": [ "left_widgets", @@ -616,7 +616,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, { @@ -2259,7 +2260,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, { @@ -4294,7 +4296,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -4327,6 +4330,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -5210,7 +5233,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -5248,7 +5272,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -5361,7 +5386,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -5404,7 +5430,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -10365,7 +10392,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -10398,6 +10426,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -11281,7 +11329,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -11319,7 +11368,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -11432,7 +11482,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -11475,7 +11526,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -16436,7 +16488,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -16469,6 +16522,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -17352,7 +17425,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -17390,7 +17464,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -17503,7 +17578,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -17546,7 +17622,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -22507,7 +22584,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -22540,6 +22618,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -23423,7 +23521,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -23461,7 +23560,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -23574,7 +23674,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -23617,7 +23718,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -28578,7 +28680,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -28611,6 +28714,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -29494,7 +29617,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -29532,7 +29656,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -29645,7 +29770,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -29688,7 +29814,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -34649,7 +34776,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -34682,6 +34810,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -35565,7 +35713,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -35603,7 +35752,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -35716,7 +35866,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -35759,7 +35910,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -40720,7 +40872,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -40753,6 +40906,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -41636,7 +41809,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -41674,7 +41848,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -41787,7 +41962,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -41830,7 +42006,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -46791,7 +46968,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -46824,6 +47002,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -47707,7 +47905,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -47745,7 +47944,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -47858,7 +48058,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -47901,7 +48102,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -52862,7 +53064,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "type": { @@ -52895,6 +53098,26 @@ } } }, + { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "integer", + "format": "uint", + "minimum": 1.0 + }, + "type": { + "type": "string", + "enum": [ + "ScrollingLayoutColumns" + ] + } + } + }, { "type": "object", "required": [ @@ -53778,7 +54001,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -53816,7 +54040,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -53929,7 +54154,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -53972,7 +54198,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } ], @@ -58490,7 +58717,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, { diff --git a/schema.json b/schema.json index a85a6e07..83c44896 100644 --- a/schema.json +++ b/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "StaticConfig", - "description": "The `komorebi.json` static configuration file reference for `v0.1.37`", + "description": "The `komorebi.json` static configuration file reference for `v0.1.38`", "type": "object", "properties": { "animation": { @@ -1790,7 +1790,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] }, "layout_flip": { @@ -1802,6 +1803,27 @@ "HorizontalAndVertical" ] }, + "layout_options": { + "description": "Layout-specific options (default: None)", + "type": "object", + "properties": { + "scrolling": { + "description": "Options related to the Scrolling layout", + "type": "object", + "required": [ + "columns" + ], + "properties": { + "columns": { + "description": "Desired number of visible columns (default: 3)", + "type": "integer", + "format": "uint", + "minimum": 0.0 + } + } + } + } + }, "layout_rules": { "description": "Layout rules in the format of threshold => layout (default: None)", "type": "object", @@ -1815,7 +1837,8 @@ "HorizontalStack", "UltrawideVerticalStack", "Grid", - "RightMainVerticalStack" + "RightMainVerticalStack", + "Scrolling" ] } },