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); + } +}