From f7c97127066f4d1e831807e0cc3f6736b6f5b858 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 22 Feb 2025 11:54:24 -0800 Subject: [PATCH] feat(wm): add tiling and floating ws layers This commit introduces an implementation of workspace layers to komorebi. Workspace layers change the kinds of windows that certain commands operate on. This implementation features two variants, WorkspaceLayer::Tiling and WorkspaceLayer::Floating. The default behaviour until now has been WorkspaceLayer::Tiling. When the user sets WorkspaceLayer::Floating, either through the 'toggle-workspace-layer' command or the new bar widget, the 'move', 'focus', 'cycle-focus' and 'resize-axis' commands will operate on floating windows, if the currently focused window is a floating window. As I don't have 'cycle-focus' bound to anything, 'focus up' and 'focus down' double as incrementing and decrementing cycle focus commands, iterating focus through the floating windows assigned to a workspace. Floating windows in komorebi belong to specific workspaces, therefore commands such as 'move' and 'resize-axis' will restrict movement and resizing to the bounds of their workspace's work area (or more accurately, the work area of the monitor that the workspace belongs to, as floating windows are never constrained by workspace-specific work area restrictions). --- docs/cli/toggle-workspace-layer.md | 12 + komorebi-bar/src/bar.rs | 14 +- komorebi-bar/src/komorebi.rs | 56 ++++- komorebi-client/src/lib.rs | 1 + komorebi/src/core/mod.rs | 1 + komorebi/src/process_command.rs | 54 +++- komorebi/src/process_event.rs | 7 + komorebi/src/window_manager.rs | 385 ++++++++++++++++++++++++----- komorebi/src/workspace.rs | 21 ++ komorebic/src/main.rs | 5 + mkdocs.yml | 1 + schema.bar.json | 39 +++ 12 files changed, 516 insertions(+), 80 deletions(-) create mode 100644 docs/cli/toggle-workspace-layer.md diff --git a/docs/cli/toggle-workspace-layer.md b/docs/cli/toggle-workspace-layer.md new file mode 100644 index 00000000..1758b2bd --- /dev/null +++ b/docs/cli/toggle-workspace-layer.md @@ -0,0 +1,12 @@ +# toggle-workspace-layer + +``` +Toggle between the Tiling and Floating layers on the focused workspace + +Usage: komorebic.exe toggle-workspace-layer + +Options: + -h, --help + Print help + +``` diff --git a/komorebi-bar/src/bar.rs b/komorebi-bar/src/bar.rs index cca94605..092b0a42 100644 --- a/komorebi-bar/src/bar.rs +++ b/komorebi-bar/src/bar.rs @@ -365,16 +365,10 @@ impl Komobar { &SocketMessage::MonitorWorkAreaOffset(monitor_index, new_rect), ) { tracing::error!( - "error applying work area offset to monitor '{}': {}", - monitor_index, - error, + "error applying work area offset to monitor '{monitor_index}': {error}" ); } else { - tracing::info!( - "work area offset applied to monitor: {}\n, {:#?}", - monitor_index, - new_rect - ); + tracing::info!("work area offset applied to monitor: {monitor_index}",); } } } @@ -631,10 +625,10 @@ impl Komobar { let window = komorebi_client::Window::from(hwnd); match window.set_position(&self.size_rect, false) { Ok(_) => { - tracing::info!("updated bar position: {:#?}", &self.size_rect); + tracing::info!("updated bar position"); } Err(error) => { - tracing::error!("{}", error.to_string()) + tracing::error!("{error}") } } } diff --git a/komorebi-bar/src/komorebi.rs b/komorebi-bar/src/komorebi.rs index 43b08240..4b4830f7 100644 --- a/komorebi-bar/src/komorebi.rs +++ b/komorebi-bar/src/komorebi.rs @@ -33,6 +33,7 @@ use komorebi_client::Rect; use komorebi_client::SocketMessage; use komorebi_client::Window; use komorebi_client::Workspace; +use komorebi_client::WorkspaceLayer; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -49,6 +50,8 @@ pub struct KomorebiConfig { pub workspaces: Option, /// Configure the Layout widget pub layout: Option, + /// Configure the Workspace Layer widget + pub workspace_layer: Option, /// Configure the Focused Window widget pub focused_window: Option, /// Configure the Configuration Switcher widget @@ -75,6 +78,12 @@ pub struct KomorebiLayoutConfig { pub display: Option, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct KomorebiWorkspaceLayerConfig { + /// Enable the Komorebi Workspace Layer widget + pub enable: bool, +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KomorebiFocusedWindowConfig { /// Enable the Komorebi Focused Window widget @@ -127,6 +136,7 @@ impl From<&KomorebiConfig> for Komorebi { workspaces: value.workspaces, layout: value.layout.clone(), focused_window: value.focused_window, + workspace_layer: value.workspace_layer, configuration_switcher, } } @@ -138,6 +148,7 @@ pub struct Komorebi { pub workspaces: Option, pub layout: Option, pub focused_window: Option, + pub workspace_layer: Option, pub configuration_switcher: Option, } @@ -154,7 +165,7 @@ impl BarWidget for Komorebi { let format = workspaces.display.unwrap_or(DisplayFormat::Text); config.apply_on_widget(false, ui, |ui| { - for (i, (ws, container_information)) in + for (i, (ws, container_information, _)) in komorebi_notification_state.workspaces.iter().enumerate() { if SelectableFrame::new( @@ -281,6 +292,42 @@ impl BarWidget for Komorebi { } } + if let Some(layer_config) = &self.workspace_layer { + if layer_config.enable { + let layer = komorebi_notification_state + .workspaces + .iter() + .find(|o| komorebi_notification_state.selected_workspace.eq(&o.0)) + .map(|(_, _, layer)| layer); + + if let Some(layer) = layer { + let name = layer.to_string(); + config.apply_on_widget(false, ui, |ui| { + if SelectableFrame::new(false) + .show(ui, |ui| ui.add(Label::new(name).selectable(false))) + .clicked() + && komorebi_client::send_batch([ + SocketMessage::MouseFollowsFocus(false), + SocketMessage::ToggleWorkspaceLayer, + SocketMessage::MouseFollowsFocus( + komorebi_notification_state.mouse_follows_focus, + ), + ]) + .is_err() + { + tracing::error!( + "could not send the following batch of messages to komorebi:\n\ + MouseFollowsFocus(false), + ToggleWorkspaceLayer, + MouseFollowsFocus({})", + komorebi_notification_state.mouse_follows_focus, + ); + } + }); + } + } + } + if let Some(layout_config) = &self.layout { if layout_config.enable { let workspace_idx: Option = komorebi_notification_state @@ -476,7 +523,11 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle { #[derive(Clone, Debug)] pub struct KomorebiNotificationState { - pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>, + pub workspaces: Vec<( + String, + KomorebiNotificationStateContainerInformation, + WorkspaceLayer, + )>, pub selected_workspace: String, pub focused_container_information: KomorebiNotificationStateContainerInformation, pub layout: KomorebiLayout, @@ -592,6 +643,7 @@ impl KomorebiNotificationState { workspaces.push(( ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)), ws.into(), + ws.layer().to_owned(), )); } } diff --git a/komorebi-client/src/lib.rs b/komorebi-client/src/lib.rs index cb26551e..adbc33ec 100644 --- a/komorebi-client/src/lib.rs +++ b/komorebi-client/src/lib.rs @@ -48,6 +48,7 @@ pub use komorebi::ring::Ring; pub use komorebi::window::Window; pub use komorebi::window_manager_event::WindowManagerEvent; pub use komorebi::workspace::Workspace; +pub use komorebi::workspace::WorkspaceLayer; pub use komorebi::AnimationsConfig; pub use komorebi::AspectRatio; pub use komorebi::BorderColours; diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index b0479d90..c9fd4c11 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -149,6 +149,7 @@ pub enum SocketMessage { NamedWorkspaceLayoutCustomRule(String, usize, PathBuf), ClearWorkspaceLayoutRules(usize, usize), ClearNamedWorkspaceLayoutRules(String), + ToggleWorkspaceLayer, // Configuration ReloadConfiguration, ReplaceConfiguration(PathBuf), diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index b14c7b51..500cd839 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -66,6 +66,7 @@ use crate::window_manager; use crate::window_manager::WindowManager; use crate::windows_api::WindowsApi; use crate::winevent_listener; +use crate::workspace::WorkspaceLayer; use crate::workspace::WorkspaceWindowLocation; use crate::GlobalState; use crate::Notification; @@ -291,13 +292,37 @@ impl WindowManager { } } SocketMessage::FocusWindow(direction) => { - self.focus_container_in_direction(direction)?; + let focused_workspace = self.focused_workspace()?; + match focused_workspace.layer() { + WorkspaceLayer::Tiling => { + self.focus_container_in_direction(direction)?; + } + WorkspaceLayer::Floating => { + self.focus_floating_window_in_direction(direction)?; + } + } } SocketMessage::MoveWindow(direction) => { - self.move_container_in_direction(direction)?; + let focused_workspace = self.focused_workspace()?; + match focused_workspace.layer() { + WorkspaceLayer::Tiling => { + self.move_container_in_direction(direction)?; + } + WorkspaceLayer::Floating => { + self.move_floating_window_in_direction(direction)?; + } + } } SocketMessage::CycleFocusWindow(direction) => { - self.focus_container_in_cycle_direction(direction)?; + let focused_workspace = self.focused_workspace()?; + match focused_workspace.layer() { + WorkspaceLayer::Tiling => { + self.focus_container_in_cycle_direction(direction)?; + } + WorkspaceLayer::Floating => { + self.focus_floating_window_in_cycle_direction(direction)?; + } + } } SocketMessage::CycleMoveWindow(direction) => { self.move_container_in_cycle_direction(direction)?; @@ -1020,6 +1045,29 @@ impl WindowManager { self.focus_workspace(workspace_idx)?; } } + SocketMessage::ToggleWorkspaceLayer => { + let mouse_follows_focus = self.mouse_follows_focus; + let workspace = self.focused_workspace_mut()?; + + match workspace.layer() { + WorkspaceLayer::Tiling => { + workspace.set_layer(WorkspaceLayer::Floating); + + if let Some(first) = workspace.floating_windows().first() { + first.focus(mouse_follows_focus)?; + } + } + WorkspaceLayer::Floating => { + workspace.set_layer(WorkspaceLayer::Tiling); + + if let Some(container) = workspace.focused_container() { + if let Some(window) = container.focused_window() { + window.focus(mouse_follows_focus)?; + } + } + } + }; + } SocketMessage::Stop => { self.stop(false)?; } diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index b71f3bb4..44d56566 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -27,6 +27,7 @@ use crate::window_manager::WindowManager; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::winevent::WinEvent; +use crate::workspace::WorkspaceLayer; use crate::workspace_reconciliator; use crate::workspace_reconciliator::ALT_TAB_HWND; use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT; @@ -282,10 +283,13 @@ impl WindowManager { } else { workspace.focus_container_by_window(window.hwnd)?; } + + workspace.set_layer(WorkspaceLayer::Tiling); } Some(idx) => { if let Some(window) = workspace.floating_windows().get(idx) { window.focus(false)?; + workspace.set_layer(WorkspaceLayer::Floating); } } } @@ -394,11 +398,13 @@ impl WindowManager { if behaviour.float_override { workspace.floating_windows_mut().push(window); + workspace.set_layer(WorkspaceLayer::Floating); self.update_focused_workspace(false, false)?; } else { match behaviour.current_behaviour { WindowContainerBehaviour::Create => { workspace.new_container_for_window(window); + workspace.set_layer(WorkspaceLayer::Tiling); self.update_focused_workspace(false, false)?; } WindowContainerBehaviour::Append => { @@ -408,6 +414,7 @@ impl WindowManager { anyhow!("there is no focused container") })? .add_window(window); + workspace.set_layer(WorkspaceLayer::Tiling); self.update_focused_workspace(true, false)?; stackbar_manager::send_notification(); } diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 89f7d4ec..ab0add8b 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -48,6 +48,8 @@ use crate::core::WindowContainerBehaviour; use crate::core::WindowManagementBehaviour; use crate::border_manager; +use crate::border_manager::BORDER_OFFSET; +use crate::border_manager::BORDER_WIDTH; use crate::border_manager::STYLE; use crate::config_generation::WorkspaceMatchingRule; use crate::container::Container; @@ -74,6 +76,7 @@ use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::winevent_listener; use crate::workspace::Workspace; +use crate::workspace::WorkspaceLayer; use crate::BorderColours; use crate::Colour; use crate::CrossBoundaryBehaviour; @@ -331,6 +334,7 @@ impl From<&WindowManager> for State { .window_container_behaviour_rules .clone(), float_override: workspace.float_override, + layer: workspace.layer, workspace_config: None, }) .collect::>(); @@ -1390,86 +1394,168 @@ impl WindowManager { delta: i32, update: bool, ) -> Result<()> { - let work_area = self.focused_monitor_work_area()?; + let mouse_follows_focus = self.mouse_follows_focus; + let mut focused_monitor_work_area = self.focused_monitor_work_area()?; let workspace = self.focused_workspace_mut()?; - match workspace.layout() { - Layout::Default(layout) => { - tracing::info!("resizing window"); - let len = NonZeroUsize::new(workspace.containers().len()) - .ok_or_else(|| anyhow!("there must be at least one container"))?; - let focused_idx = workspace.focused_container_idx(); - let focused_idx_resize = workspace - .resize_dimensions() - .get(focused_idx) - .ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?; + match workspace.layer() { + WorkspaceLayer::Floating => { + let workspace = self.focused_workspace()?; + let focused_hwnd = WindowsApi::foreground_window()?; - if direction - .destination( - workspace.layout().as_boxed_direction().as_ref(), - workspace.layout_flip(), - focused_idx, - len, - ) - .is_some() - { - let unaltered = layout.calculate( - &work_area, - len, - workspace.container_padding(), - workspace.layout_flip(), - &[], - ); + let border_offset = BORDER_OFFSET.load(Ordering::SeqCst); + let border_width = BORDER_WIDTH.load(Ordering::SeqCst); + focused_monitor_work_area.left += border_offset; + focused_monitor_work_area.left += border_width; + focused_monitor_work_area.top += border_offset; + focused_monitor_work_area.top += border_width; + focused_monitor_work_area.right -= border_offset; + focused_monitor_work_area.right -= border_width; + focused_monitor_work_area.bottom -= border_offset; + focused_monitor_work_area.bottom -= border_width; - let mut direction = direction; - - // We only ever want to operate on the unflipped Rect positions when resizing, then we - // can flip them however they need to be flipped once the resizing has been done - if let Some(flip) = workspace.layout_flip() { - match flip { - Axis::Horizontal => { - if matches!(direction, OperationDirection::Left) - || matches!(direction, OperationDirection::Right) - { - direction = direction.opposite(); + for window in workspace.floating_windows().iter() { + if window.hwnd == focused_hwnd { + let mut rect = WindowsApi::window_rect(window.hwnd)?; + match (direction, sizing) { + (OperationDirection::Left, Sizing::Increase) => { + if rect.left - delta < focused_monitor_work_area.left { + rect.left = focused_monitor_work_area.left; + } else { + rect.left -= delta; } } - Axis::Vertical => { - if matches!(direction, OperationDirection::Up) - || matches!(direction, OperationDirection::Down) + (OperationDirection::Left, Sizing::Decrease) => { + rect.left += delta; + } + (OperationDirection::Right, Sizing::Increase) => { + if rect.left + rect.right + delta * 2 + > focused_monitor_work_area.right { - direction = direction.opposite(); + rect.right = focused_monitor_work_area.right - rect.left; + } else { + rect.right += delta * 2; } } - Axis::HorizontalAndVertical => direction = direction.opposite(), + (OperationDirection::Right, Sizing::Decrease) => { + rect.right -= delta * 2; + } + (OperationDirection::Up, Sizing::Increase) => { + if rect.top - delta < focused_monitor_work_area.top { + rect.top = focused_monitor_work_area.top; + } else { + rect.top -= delta; + } + } + (OperationDirection::Up, Sizing::Decrease) => { + rect.top += delta; + } + (OperationDirection::Down, Sizing::Increase) => { + if rect.top + rect.bottom + delta * 2 + > focused_monitor_work_area.bottom + { + rect.bottom = focused_monitor_work_area.bottom - rect.top; + } else { + rect.bottom += delta * 2; + } + } + (OperationDirection::Down, Sizing::Decrease) => { + rect.bottom -= delta * 2; + } } + + WindowsApi::position_window(window.hwnd, &rect, false)?; + if mouse_follows_focus { + WindowsApi::center_cursor_in_rect(&rect)?; + } + + break; } - - let resize = layout.resize( - unaltered - .get(focused_idx) - .ok_or_else(|| anyhow!("there is no last layout"))?, - focused_idx_resize, - direction, - sizing, - delta, - ); - - workspace.resize_dimensions_mut()[focused_idx] = resize; - - return if update { - self.update_focused_workspace(false, false) - } else { - Ok(()) - }; } - - tracing::warn!("cannot resize container in this direction"); } - Layout::Custom(_) => { - tracing::warn!("containers cannot be resized when using custom layouts"); + WorkspaceLayer::Tiling => { + match workspace.layout() { + Layout::Default(layout) => { + tracing::info!("resizing window"); + let len = NonZeroUsize::new(workspace.containers().len()) + .ok_or_else(|| anyhow!("there must be at least one container"))?; + let focused_idx = workspace.focused_container_idx(); + let focused_idx_resize = workspace + .resize_dimensions() + .get(focused_idx) + .ok_or_else(|| { + anyhow!("there is no resize adjustment for this container") + })?; + + if direction + .destination( + workspace.layout().as_boxed_direction().as_ref(), + workspace.layout_flip(), + focused_idx, + len, + ) + .is_some() + { + let unaltered = layout.calculate( + &focused_monitor_work_area, + len, + workspace.container_padding(), + workspace.layout_flip(), + &[], + ); + + let mut direction = direction; + + // We only ever want to operate on the unflipped Rect positions when resizing, then we + // can flip them however they need to be flipped once the resizing has been done + if let Some(flip) = workspace.layout_flip() { + match flip { + Axis::Horizontal => { + if matches!(direction, OperationDirection::Left) + || matches!(direction, OperationDirection::Right) + { + direction = direction.opposite(); + } + } + Axis::Vertical => { + if matches!(direction, OperationDirection::Up) + || matches!(direction, OperationDirection::Down) + { + direction = direction.opposite(); + } + } + Axis::HorizontalAndVertical => direction = direction.opposite(), + } + } + + let resize = layout.resize( + unaltered + .get(focused_idx) + .ok_or_else(|| anyhow!("there is no last layout"))?, + focused_idx_resize, + direction, + sizing, + delta, + ); + + workspace.resize_dimensions_mut()[focused_idx] = resize; + + return if update { + self.update_focused_workspace(false, false) + } else { + Ok(()) + }; + } + + tracing::warn!("cannot resize container in this direction"); + } + Layout::Custom(_) => { + tracing::warn!("containers cannot be resized when using custom layouts"); + } + } } } + Ok(()) } @@ -1858,6 +1944,56 @@ impl WindowManager { self.update_focused_workspace(mouse_follows_focus, true) } + #[tracing::instrument(skip(self))] + pub fn focus_floating_window_in_direction( + &mut self, + direction: OperationDirection, + ) -> Result<()> { + let mouse_follows_focus = self.mouse_follows_focus; + let focused_workspace = self.focused_workspace()?; + + let mut target_idx = None; + let len = focused_workspace.floating_windows().len(); + + if len > 1 { + let focused_hwnd = WindowsApi::foreground_window()?; + for (idx, window) in focused_workspace.floating_windows().iter().enumerate() { + if window.hwnd == focused_hwnd { + match direction { + OperationDirection::Left => {} + OperationDirection::Right => {} + OperationDirection::Up => { + if idx == len - 1 { + target_idx = Some(0) + } else { + target_idx = Some(idx + 1) + } + } + OperationDirection::Down => { + if idx == 0 { + target_idx = Some(len - 1) + } else { + target_idx = Some(idx - 1) + } + } + } + } + } + + if target_idx.is_none() { + target_idx = Some(0); + } + } + + if let Some(idx) = target_idx { + if let Some(window) = focused_workspace.floating_windows().get(idx) { + window.focus(mouse_follows_focus)?; + } + } + + Ok(()) + } + #[tracing::instrument(skip(self))] pub fn focus_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> { self.handle_unmanaged_window_behaviour()?; @@ -2003,6 +2139,75 @@ impl WindowManager { Ok(()) } + #[tracing::instrument(skip(self))] + pub fn move_floating_window_in_direction( + &mut self, + direction: OperationDirection, + ) -> Result<()> { + let mouse_follows_focus = self.mouse_follows_focus; + + let mut focused_monitor_work_area = self.focused_monitor_work_area()?; + let border_offset = BORDER_OFFSET.load(Ordering::SeqCst); + let border_width = BORDER_WIDTH.load(Ordering::SeqCst); + focused_monitor_work_area.left += border_offset; + focused_monitor_work_area.left += border_width; + focused_monitor_work_area.top += border_offset; + focused_monitor_work_area.top += border_width; + focused_monitor_work_area.right -= border_offset; + focused_monitor_work_area.right -= border_width; + focused_monitor_work_area.bottom -= border_offset; + focused_monitor_work_area.bottom -= border_width; + + let focused_workspace = self.focused_workspace()?; + let delta = self.resize_delta; + + let focused_hwnd = WindowsApi::foreground_window()?; + for window in focused_workspace.floating_windows().iter() { + if window.hwnd == focused_hwnd { + let mut rect = WindowsApi::window_rect(window.hwnd)?; + match direction { + OperationDirection::Left => { + if rect.left - delta < focused_monitor_work_area.left { + rect.left = focused_monitor_work_area.left; + } else { + rect.left -= delta; + } + } + OperationDirection::Right => { + if rect.left + delta + rect.right > focused_monitor_work_area.right { + rect.left = focused_monitor_work_area.right - rect.right; + } else { + rect.left += delta; + } + } + OperationDirection::Up => { + if rect.top - delta < focused_monitor_work_area.top { + rect.top = focused_monitor_work_area.top; + } else { + rect.top -= delta; + } + } + OperationDirection::Down => { + if rect.top + delta + rect.bottom > focused_monitor_work_area.bottom { + rect.top = focused_monitor_work_area.bottom - rect.bottom; + } else { + rect.top += delta; + } + } + } + + WindowsApi::position_window(window.hwnd, &rect, false)?; + if mouse_follows_focus { + WindowsApi::center_cursor_in_rect(&rect)?; + } + + break; + } + } + + Ok(()) + } + #[tracing::instrument(skip(self))] pub fn move_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> { self.handle_unmanaged_window_behaviour()?; @@ -2179,6 +2384,54 @@ impl WindowManager { Ok(()) } + #[tracing::instrument(skip(self))] + pub fn focus_floating_window_in_cycle_direction( + &mut self, + direction: CycleDirection, + ) -> Result<()> { + let mouse_follows_focus = self.mouse_follows_focus; + let focused_workspace = self.focused_workspace()?; + + let mut target_idx = None; + let len = focused_workspace.floating_windows().len(); + + if len > 1 { + let focused_hwnd = WindowsApi::foreground_window()?; + for (idx, window) in focused_workspace.floating_windows().iter().enumerate() { + if window.hwnd == focused_hwnd { + match direction { + CycleDirection::Previous => { + if idx == 0 { + target_idx = Some(len - 1) + } else { + target_idx = Some(idx - 1) + } + } + CycleDirection::Next => { + if idx == len - 1 { + target_idx = Some(0) + } else { + target_idx = Some(idx - 1) + } + } + } + } + } + + if target_idx.is_none() { + target_idx = Some(0); + } + } + + if let Some(idx) = target_idx { + if let Some(window) = focused_workspace.floating_windows().get(idx) { + window.focus(mouse_follows_focus)?; + } + } + + Ok(()) + } + #[tracing::instrument(skip(self))] pub fn focus_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> { self.handle_unmanaged_window_behaviour()?; @@ -2526,8 +2779,10 @@ impl WindowManager { } if is_floating_window { + workspace.set_layer(WorkspaceLayer::Tiling); self.unfloat_window()?; } else { + workspace.set_layer(WorkspaceLayer::Floating); self.float_window()?; } diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index e91dfdb4..25ec9fd9 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -1,4 +1,6 @@ use std::collections::VecDeque; +use std::fmt::Display; +use std::fmt::Formatter; use std::num::NonZeroUsize; use std::sync::atomic::Ordering; @@ -92,11 +94,29 @@ pub struct Workspace { pub window_container_behaviour_rules: Option>, #[getset(get = "pub", get_mut = "pub", set = "pub")] pub float_override: Option, + #[getset(get = "pub", get_mut = "pub", set = "pub")] + pub layer: WorkspaceLayer, #[serde(skip_serializing_if = "Option::is_none")] #[getset(get = "pub", set = "pub")] pub workspace_config: Option, } +#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub enum WorkspaceLayer { + #[default] + Tiling, + Floating, +} + +impl Display for WorkspaceLayer { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + WorkspaceLayer::Tiling => write!(f, "Tiling"), + WorkspaceLayer::Floating => write!(f, "Floating"), + } + } +} + impl_ring_elements!(Workspace, Container); impl Default for Workspace { @@ -122,6 +142,7 @@ impl Default for Workspace { window_container_behaviour_rules: None, float_override: None, workspace_config: None, + layer: Default::default(), } } } diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index e2f4124e..1ad1cef4 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -1269,6 +1269,8 @@ enum SubCommand { /// mode, for the currently focused workspace. If there was no override value set for the /// workspace previously it takes the opposite of the global value. ToggleWorkspaceFloatOverride, + /// Toggle between the Tiling and Floating layers on the focused workspace + ToggleWorkspaceLayer, /// Toggle window tiling on the focused workspace TogglePause, /// Toggle window tiling on the focused workspace @@ -2854,6 +2856,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { SubCommand::ToggleWorkspaceFloatOverride => { send_message(&SocketMessage::ToggleWorkspaceFloatOverride)?; } + SubCommand::ToggleWorkspaceLayer => { + send_message(&SocketMessage::ToggleWorkspaceLayer)?; + } SubCommand::WindowHidingBehaviour(arg) => { send_message(&SocketMessage::WindowHidingBehaviour(arg.hiding_behaviour))?; } diff --git a/mkdocs.yml b/mkdocs.yml index 2e584b56..972a1d15 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -177,6 +177,7 @@ nav: - cli/toggle-float-override.md - cli/toggle-workspace-window-container-behaviour.md - cli/toggle-workspace-float-override.md + - cli/toggle-workspace-layer.md - cli/toggle-pause.md - cli/toggle-tiling.md - cli/toggle-float.md diff --git a/schema.bar.json b/schema.bar.json index 41a66dd8..d0ecce6c 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -504,6 +504,19 @@ } } }, + "workspace_layer": { + "description": "Configure the Workspace Layer widget", + "type": "object", + "required": [ + "enable" + ], + "properties": { + "enable": { + "description": "Enable the Komorebi Workspace Layer widget", + "type": "boolean" + } + } + }, "workspaces": { "description": "Configure the Workspaces widget", "type": "object", @@ -1811,6 +1824,19 @@ } } }, + "workspace_layer": { + "description": "Configure the Workspace Layer widget", + "type": "object", + "required": [ + "enable" + ], + "properties": { + "enable": { + "description": "Enable the Komorebi Workspace Layer widget", + "type": "boolean" + } + } + }, "workspaces": { "description": "Configure the Workspaces widget", "type": "object", @@ -3051,6 +3077,19 @@ } } }, + "workspace_layer": { + "description": "Configure the Workspace Layer widget", + "type": "object", + "required": [ + "enable" + ], + "properties": { + "enable": { + "description": "Enable the Komorebi Workspace Layer widget", + "type": "boolean" + } + } + }, "workspaces": { "description": "Configure the Workspaces widget", "type": "object",