From 3fa3d429fa3cf340372589e978e00f9a68e5185f Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Fri, 30 Oct 2026 15:50:30 -0700 Subject: [PATCH] feat(wm): track a window the user maximizes or restores A window maximized with its button or title bar fills the work area, but the window manager only had a window as maximized when it was done by the command, so the layout was not put back when it was restored and the rest of the workspace was laid out around a window that took all of it. The location change of a window of the window manager is looked at for a change of whether it is maximized, and the window manager is told of one as Maximize and Unmaximize, which do what the command does. What the command itself does is already known by the time these are read. --- komorebi/src/process_event.rs | 34 +++++++++++++++ komorebi/src/window_manager_event.rs | 14 +++++++ komorebi/src/windows_callbacks.rs | 62 ++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index bc97d0b3..86813656 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -884,6 +884,40 @@ impl WindowManager { } } } + WindowManagerEvent::Maximize(_, window) => { + // The user maximized a tiled window themselves, with its button or by + // double-clicking its title bar: it is the window that is maximized as if it had + // been by the command. It may have been restored again by the time this is read, + // and a maximize that the command made is already known. + let workspace = self.focused_workspace_mut()?; + + if WindowsApi::is_zoomed(window.hwnd) + && workspace.maximized_window.is_none() + && workspace.monocle_container.is_none() + && workspace.container_idx_for_window(window.hwnd).is_some() + { + tracing::info!("window maximized by the user"); + workspace.focus_container_by_window(window.hwnd)?; + workspace.layer = WorkspaceLayer::Tiling; + workspace.new_maximized_window()?; + self.update_focused_workspace(false, false)?; + } + } + WindowManagerEvent::Unmaximize(_, window) => { + // The user restored the window that is maximized, which puts it back in its + // tile as the command does. A restore that the command made is already known. + let workspace = self.focused_workspace_mut()?; + + if !WindowsApi::is_zoomed(window.hwnd) + && workspace + .maximized_window + .is_some_and(|maximized| maximized.hwnd == window.hwnd) + { + tracing::info!("window restored by the user"); + workspace.reintegrate_maximized_window()?; + self.update_focused_workspace(false, false)?; + } + } WindowManagerEvent::MouseCapture(..) | WindowManagerEvent::Cloak(..) | WindowManagerEvent::TitleUpdate(..) => {} diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index 9313751b..2e62be3e 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -25,6 +25,8 @@ pub enum WindowManagerEvent { MoveResizeStart(WinEvent, Window), MoveResizeEnd(WinEvent, Window), MouseCapture(WinEvent, Window), + Maximize(WinEvent, Window), + Unmaximize(WinEvent, Window), Manage(Window), Unmanage(Window), Raise(Window), @@ -73,6 +75,12 @@ impl Display for WindowManagerEvent { Self::MouseCapture(winevent, window) => { write!(f, "MouseCapture (WinEvent: {winevent}, Window: {window})",) } + Self::Maximize(winevent, window) => { + write!(f, "Maximize (WinEvent: {winevent}, Window: {window})") + } + Self::Unmaximize(winevent, window) => { + write!(f, "Unmaximize (WinEvent: {winevent}, Window: {window})") + } Self::Raise(window) => { write!(f, "Raise (Window: {window})") } @@ -96,6 +104,8 @@ impl WindowManagerEvent { | Self::MoveResizeStart(_, window) | Self::MoveResizeEnd(_, window) | Self::MouseCapture(_, window) + | Self::Maximize(_, window) + | Self::Unmaximize(_, window) | Self::Raise(window) | Self::Manage(window) | Self::Unmanage(window) @@ -119,6 +129,8 @@ impl WindowManagerEvent { WindowManagerEvent::MoveResizeStart(_, _) => "MoveResizeStart", WindowManagerEvent::MoveResizeEnd(_, _) => "MoveResizeEnd", WindowManagerEvent::MouseCapture(_, _) => "MouseCapture", + WindowManagerEvent::Maximize(_, _) => "Maximize", + WindowManagerEvent::Unmaximize(_, _) => "Unmaximize", WindowManagerEvent::Manage(_) => "Manage", WindowManagerEvent::Unmanage(_) => "Unmanage", WindowManagerEvent::Raise(_) => "Raise", @@ -138,6 +150,8 @@ impl WindowManagerEvent { | WindowManagerEvent::MoveResizeStart(event, _) | WindowManagerEvent::MoveResizeEnd(event, _) | WindowManagerEvent::MouseCapture(event, _) + | WindowManagerEvent::Maximize(event, _) + | WindowManagerEvent::Unmaximize(event, _) | WindowManagerEvent::TitleUpdate(event, _) => Some(event.to_string()), WindowManagerEvent::Manage(_) | WindowManagerEvent::Unmanage(_) diff --git a/komorebi/src/windows_callbacks.rs b/komorebi/src/windows_callbacks.rs index a0438baa..a9e26ffb 100644 --- a/komorebi/src/windows_callbacks.rs +++ b/komorebi/src/windows_callbacks.rs @@ -98,6 +98,49 @@ fn has_filtered_style(hwnd: HWND) -> bool { || ex_style & WS_EX_NOACTIVATE.0 != 0 } +lazy_static::lazy_static! { + /// The windows of the window manager that were maximized when last looked at + static ref ZOOMED: parking_lot::Mutex> = + parking_lot::Mutex::new(std::collections::HashSet::new()); +} + +#[derive(Debug, PartialEq, Eq)] +enum ZoomChange { + Maximized, + Restored, +} + +fn zoom_change(was_zoomed: bool, is_zoomed: bool) -> Option { + match (was_zoomed, is_zoomed) { + (false, true) => Some(ZoomChange::Maximized), + (true, false) => Some(ZoomChange::Restored), + _ => None, + } +} + +/// A window of the window manager that has been maximized or restored since the last time it was +/// looked at, which the user does with the button or the title bar of the window. This is read +/// for the location change of every window that moves, so it is a lookup of the handle and one +/// call, before anything is asked of the window. +fn zoom_event(winevent: WinEvent, window: Window) -> Option { + if !crate::reaper::HWNDS_CACHE.lock().contains_key(&window.hwnd) { + ZOOMED.lock().remove(&window.hwnd); + return None; + } + + let is_zoomed = WindowsApi::is_zoomed(window.hwnd); + let was_zoomed = if is_zoomed { + !ZOOMED.lock().insert(window.hwnd) + } else { + ZOOMED.lock().remove(&window.hwnd) + }; + + match zoom_change(was_zoomed, is_zoomed)? { + ZoomChange::Maximized => Some(WindowManagerEvent::Maximize(winevent, window)), + ZoomChange::Restored => Some(WindowManagerEvent::Unmaximize(winevent, window)), + } +} + pub extern "system" fn win_event_hook( _h_win_event_hook: HWINEVENTHOOK, event: u32, @@ -165,6 +208,12 @@ pub extern "system" fn win_event_hook( // is moved, and what a window is takes a call into it for each of the // exe, the title and the class: the hook is held up for as long as that takes if matches!(winevent, WinEvent::ObjectLocationChange) { + if let Some(event) = zoom_event(winevent, window) { + winevent_listener::event_tx() + .send(event) + .expect("could not send message on winevent_listener::event_tx"); + } + return; } @@ -185,3 +234,16 @@ pub extern "system" fn win_event_hook( .send(event_type) .expect("could not send message on winevent_listener::event_tx"); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_window_is_maximized_or_restored_only_when_it_changes() { + assert_eq!(zoom_change(false, true), Some(ZoomChange::Maximized)); + assert_eq!(zoom_change(true, false), Some(ZoomChange::Restored)); + assert_eq!(zoom_change(true, true), None); + assert_eq!(zoom_change(false, false), None); + } +}