From 598390d9cbe30d96822a2b12110e6545e65e3cd6 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Tue, 27 Oct 2026 22:53:44 -0700 Subject: [PATCH] feat(scrolling): add commands and config for the scrolling layout Focus, move, resize, retile and closing a window now go through the strip of columns in the Scrolling layout. New commands: consume, expel, toggle-column-stack, scrolling-layout-full-width and center-focus. A top level scrolling section configures centering, centring a lone column, and the minimum height of a window in a column of several. --- komorebi/src/core/mod.rs | 7 +++++ komorebi/src/process_command.rs | 50 ++++++++++++++++++--------------- komorebi/src/window_manager.rs | 23 +++++++++++++-- komorebi/src/workspace.rs | 42 +++++++++++---------------- komorebic/src/main.rs | 36 ++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 51 deletions(-) diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index abf128e6..ee53c1f0 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -41,6 +41,7 @@ pub use komorebi_layouts::OperationDirection; pub use komorebi_layouts::Rect; pub use komorebi_layouts::ScrollingLayoutOptions; pub use komorebi_layouts::Sizing; +pub use komorebi_layouts::scrolling::ScrollingCentering; pub use komorebi_layouts::validate_ratios; // Local modules and exports @@ -125,6 +126,12 @@ pub enum SocketMessage { CycleLayout(CycleDirection), LayoutRatios(Option>, Option>), ScrollingLayoutColumns(NonZeroUsize), + ScrollingLayoutFullWidth, + CenterFocus, + Expand, + Consume(OperationDirection), + Expel, + ToggleColumnStack, ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf), FlipLayout(Axis), ToggleWorkspaceWindowContainerBehaviour, diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index f87c3b1f..74486b17 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -64,7 +64,6 @@ use crate::core::MonocleFocusBehaviour; use crate::core::MoveBehaviour; use crate::core::OperationDirection; use crate::core::Rect; -use crate::core::ScrollingLayoutOptions; use crate::core::Sizing; use crate::core::SocketMessage; use crate::core::StateQuery; @@ -900,28 +899,33 @@ impl WindowManager { } 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(), - center_focused_column: Default::default(), - }), - grid: None, - column_ratios: None, - row_ratios: None, - }, - }; - - focused_workspace.layout_options = Some(options); + self.focused_workspace_mut()? + .set_scrolling_columns(count.into()); + self.update_focused_workspace(false, false)?; + } + SocketMessage::ScrollingLayoutFullWidth => { + self.focused_workspace_mut()?.toggle_scrolling_full_width(); + self.update_focused_workspace(false, false)?; + } + SocketMessage::CenterFocus => { + self.focused_workspace_mut()?.center_scrolling_column(); + self.update_focused_workspace(false, false)?; + } + SocketMessage::Expand => { + self.focused_workspace_mut()?.expand_scrolling_column(); + self.update_focused_workspace(false, false)?; + } + SocketMessage::Consume(direction) => { + self.focused_workspace_mut()?.consume_scrolling(direction); + self.update_focused_workspace(false, false)?; + } + SocketMessage::ToggleColumnStack => { + self.focused_workspace_mut()? + .toggle_scrolling_column_stack()?; + self.update_focused_workspace(false, false)?; + } + SocketMessage::Expel => { + self.focused_workspace_mut()?.expel_scrolling(); self.update_focused_workspace(false, false)?; } SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?, diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 5d37a094..7b89f3dc 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -771,6 +771,10 @@ impl WindowManager { // Reset any resize adjustments if we want to force a retile if !preserve_resize_dimensions { workspace.resize_dimensions.fill(None); + + // and the widths and heights of the Scrolling layout, which are resize + // adjustments too + workspace.reset_scrolling_sizes(); } if (workspace.wallpaper.is_some() || monitor_wp.is_some()) @@ -1271,6 +1275,16 @@ impl WindowManager { } WorkspaceLayer::Tiling => { match workspace.layout { + Layout::Default(DefaultLayout::Scrolling) => { + tracing::info!("resizing column"); + workspace.resize_scrolling_column(direction, sizing, delta); + + return if update { + self.update_focused_workspace(false, false) + } else { + Ok(()) + }; + } Layout::Default(layout) => { tracing::info!("resizing window"); let len = NonZeroUsize::new(workspace.containers().len()) @@ -2608,8 +2622,13 @@ impl WindowManager { } Some(new_idx) => { let workspace = self.focused_workspace_mut()?; - workspace.swap_containers(origin_container_idx, new_idx); - workspace.focus_container(new_idx); + + // a column of the Scrolling layout moves whole, and its tiles move within it, + // which is not a swap of two containers + if !(workspace.is_scrolling() && workspace.move_scrolling(direction)) { + workspace.swap_containers(origin_container_idx, new_idx); + workspace.focus_container(new_idx); + } } } diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 867d4188..a7efcd2d 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -1064,6 +1064,9 @@ impl Workspace { // this fn respects locked container indexes - we should use it for pretty much everything // except monocle and maximize toggles pub fn remove_container_by_idx(&mut self, idx: usize) -> Option { + // wherever it goes next, it goes as wide as it was + self.note_scrolling_departure(idx); + let container = self.containers_mut().remove_respecting_locks(idx); if idx < self.resize_dimensions.len() { @@ -1143,8 +1146,12 @@ impl Workspace { .ok_or_eyre("there is no window")?; if container.windows().is_empty() { + let container_id = container.id.clone(); self.remove_container_by_idx(container_idx); - self.focus_previous_container(); + + if !self.focus_after_scrolling_removal(&container_id) { + self.focus_previous_container(); + } } else { container.load_focused_window(); if let Some(window) = container.focused_window() { @@ -1183,6 +1190,12 @@ impl Workspace { } pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option { + // in a strip, left and right are the neighbouring columns and up and down are the tiles of + // the column, which the strip is the one to say + if self.is_scrolling() && self.scrolling.is_some() { + return self.scrolling_idx_for_direction(direction); + } + let len = NonZeroUsize::new(self.containers().len())?; direction.destination( @@ -1288,6 +1301,8 @@ impl Workspace { next } else if self.containers().is_empty() { 0 + } else if let Some(idx) = self.scrolling_insertion_idx() { + idx } else { self.resolve_placement_index(&window) }; @@ -1475,9 +1490,6 @@ impl Workspace { Layout::Default(DefaultLayout::UltrawideVerticalStack) => { self.enforce_resize_for_ultrawide(); } - Layout::Default(DefaultLayout::Scrolling) => { - self.enforce_resize_for_scrolling(); - } _ => self.enforce_no_resize(), } } @@ -1705,28 +1717,6 @@ impl Workspace { } } - fn enforce_resize_for_scrolling(&mut self) { - let resize_dimensions = &mut self.resize_dimensions; - 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.iter_mut().flatten() { rect.left = 0; diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 74b40a0a..1819f540 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -151,6 +151,7 @@ macro_rules! gen_enum_subcommand_args { gen_enum_subcommand_args! { Focus: OperationDirection, Move: OperationDirection, + Consume: OperationDirection, PreselectDirection: OperationDirection, CycleFocus: CycleDirection, CycleMove: CycleDirection, @@ -1284,6 +1285,23 @@ enum SubCommand { /// Set the number of visible columns for the Scrolling layout on the focused workspace #[clap(arg_required_else_help = true)] ScrollingLayoutColumns(ScrollingLayoutColumns), + /// Make the focused column of the Scrolling layout as wide as the screen, or put it back to + /// the width that it had + ScrollingLayoutFullWidth, + /// Scroll the Scrolling layout so that the focused column is in the middle of the screen, once + CenterFocus, + /// Make the focused column of the Scrolling layout fill the room that the columns in view leave + /// on the screen, which does nothing in any other layout + Expand, + /// Take the neighbouring window on one side into the focused column of the Scrolling layout + #[clap(arg_required_else_help = true)] + Consume(Consume), + /// Take the focused window out of its column in the Scrolling layout and put it in a column + /// of its own on the right + Expel, + /// Stack the windows of the focused column of the Scrolling layout, or spread the focused + /// stack out into a column with a window in each slot + ToggleColumnStack, /// Set the layout column and row ratios for the focused workspace LayoutRatios(LayoutRatios), /// Load a custom layout from file for the focused workspace @@ -2956,6 +2974,24 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { SubCommand::ScrollingLayoutColumns(args) => { send_message(&SocketMessage::ScrollingLayoutColumns(args.count))?; } + SubCommand::ScrollingLayoutFullWidth => { + send_message(&SocketMessage::ScrollingLayoutFullWidth)?; + } + SubCommand::CenterFocus => { + send_message(&SocketMessage::CenterFocus)?; + } + SubCommand::Expand => { + send_message(&SocketMessage::Expand)?; + } + SubCommand::Consume(args) => { + send_message(&SocketMessage::Consume(args.operation_direction))?; + } + SubCommand::Expel => { + send_message(&SocketMessage::Expel)?; + } + SubCommand::ToggleColumnStack => { + send_message(&SocketMessage::ToggleColumnStack)?; + } SubCommand::LayoutRatios(args) => { if args.columns.is_none() && args.rows.is_none() { println!(