diff --git a/komorebi-core/src/rect.rs b/komorebi-core/src/rect.rs index 811aaef9..164b6e3a 100644 --- a/komorebi-core/src/rect.rs +++ b/komorebi-core/src/rect.rs @@ -26,13 +26,29 @@ impl From for Rect { } } +impl From for RECT { + fn from(rect: Rect) -> Self { + Self { + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + } + } +} + impl Rect { /// decrease the size of self by the padding amount. - pub fn add_padding(&mut self, padding: i32) { - self.left += padding; - self.top += padding; - self.right -= padding * 2; - self.bottom -= padding * 2; + pub fn add_padding(&mut self, padding: T) + where + T: Into>, + { + if let Some(padding) = padding.into() { + self.left += padding; + self.top += padding; + self.right -= padding * 2; + self.bottom -= padding * 2; + } } /// increase the size of self by the margin amount. @@ -43,6 +59,14 @@ impl Rect { self.bottom += margin * 2; } + pub fn left_padding(&mut self, padding: i32) { + self.left += padding; + } + + pub fn right_padding(&mut self, padding: i32) { + self.right -= padding; + } + #[must_use] pub const fn contains_point(&self, point: (i32, i32)) -> bool { point.0 >= self.left diff --git a/komorebi/src/container.rs b/komorebi/src/container.rs index de496451..19fce336 100644 --- a/komorebi/src/container.rs +++ b/komorebi/src/container.rs @@ -7,13 +7,18 @@ use serde::Deserialize; use serde::Serialize; use crate::ring::Ring; +use crate::stackbar::Stackbar; use crate::window::Window; +use crate::StackbarMode; +use crate::STACKBAR_MODE; #[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)] pub struct Container { #[getset(get = "pub")] id: String, windows: Ring, + #[getset(get = "pub", get_mut = "pub")] + stackbar: Option, } impl_ring_elements!(Container, Window); @@ -23,6 +28,10 @@ impl Default for Container { Self { id: nanoid!(), windows: Ring::default(), + stackbar: match *STACKBAR_MODE.lock() { + StackbarMode::Always => Stackbar::create().ok(), + StackbarMode::Never | StackbarMode::OnStack => None, + }, } } } @@ -34,6 +43,38 @@ impl PartialEq for Container { } impl Container { + pub fn hide(&self, omit: Option) { + if let Some(stackbar) = self.stackbar() { + stackbar.hide(); + } + + for window in self.windows().iter().rev() { + let mut should_hide = omit.is_none(); + + if !should_hide { + if let Some(omit) = omit { + if omit != window.hwnd { + should_hide = true + } + } + } + + if should_hide { + window.hide(); + } + } + } + + pub fn restore(&self) { + if let Some(stackbar) = self.stackbar() { + stackbar.restore(); + } + + if let Some(window) = self.focused_window() { + window.restore(); + } + } + pub fn load_focused_window(&mut self) { let focused_idx = self.focused_window_idx(); for (i, window) in self.windows_mut().iter_mut().enumerate() { @@ -81,6 +122,10 @@ impl Container { pub fn remove_window_by_idx(&mut self, idx: usize) -> Option { let window = self.windows_mut().remove(idx); + if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack) && self.windows().len() <= 1 { + self.stackbar = None; + } + if idx != 0 { self.focus_window(idx - 1); }; @@ -95,6 +140,14 @@ impl Container { pub fn add_window(&mut self, window: Window) { self.windows_mut().push_back(window); + + if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack) + && self.windows().len() > 1 + && self.stackbar.is_none() + { + self.stackbar = Stackbar::create().ok(); + } + self.focus_window(self.windows().len() - 1); } diff --git a/komorebi/src/lib.rs b/komorebi/src/lib.rs index ab366fe5..690d44fd 100644 --- a/komorebi/src/lib.rs +++ b/komorebi/src/lib.rs @@ -10,6 +10,7 @@ pub mod process_command; pub mod process_event; pub mod process_movement; pub mod set_window_position; +pub mod stackbar; pub mod static_config; pub mod styles; pub mod window; @@ -23,6 +24,7 @@ pub mod workspace; use lazy_static::lazy_static; use std::collections::HashMap; +use std::collections::VecDeque; use std::fs::File; use std::io::Write; use std::net::TcpStream; @@ -38,6 +40,7 @@ use std::sync::Arc; pub use hidden::*; pub use process_command::*; pub use process_event::*; +pub use stackbar::*; pub use static_config::*; pub use window_manager::*; pub use window_manager_event::*; @@ -198,6 +201,11 @@ lazy_static! { // Use app-specific titlebar removal options where possible // eg. Windows Terminal, IntelliJ IDEA, Firefox static ref NO_TITLEBAR: Arc>> = Arc::new(Mutex::new(vec![])); + + static ref STACKBAR_MODE: Arc> = Arc::new(Mutex::new(StackbarMode::Never)); + static ref WINDOWS_BY_BAR_HWNDS: Arc>>> = + Arc::new(Mutex::new(HashMap::new())); + } pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10); @@ -222,6 +230,12 @@ pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false); pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0); +pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white +pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text +pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray +pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40); +pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200); + #[must_use] pub fn current_virtual_desktop() -> Option> { let hkcu = RegKey::predef(HKEY_CURRENT_USER); diff --git a/komorebi/src/monitor.rs b/komorebi/src/monitor.rs index 2bb5a955..fb73f691 100644 --- a/komorebi/src/monitor.rs +++ b/komorebi/src/monitor.rs @@ -71,7 +71,7 @@ impl Monitor { if i == focused_idx { workspace.restore(mouse_follows_focus)?; } else { - workspace.hide(); + workspace.hide(None); } } diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 2794708c..ce371bc8 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -217,6 +217,7 @@ impl WindowManager { SocketMessage::UnstackWindow => self.remove_window_from_container()?, SocketMessage::CycleStack(direction) => { self.cycle_container_window_in_direction(direction)?; + self.focused_window()?.focus(self.mouse_follows_focus)?; } SocketMessage::ForceFocus => { let focused_window = self.focused_window()?; diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 3c623385..000dc268 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -518,6 +518,9 @@ impl WindowManager { } } } + WindowManagerEvent::ForceUpdate(_) => { + self.update_focused_workspace(false)?; + } WindowManagerEvent::DisplayChange(..) | WindowManagerEvent::MouseCapture(..) | WindowManagerEvent::Cloak(..) diff --git a/komorebi/src/stackbar.rs b/komorebi/src/stackbar.rs new file mode 100644 index 00000000..eaff1338 --- /dev/null +++ b/komorebi/src/stackbar.rs @@ -0,0 +1,294 @@ +use std::collections::VecDeque; +use std::sync::atomic::Ordering; +use std::time::Duration; + +use color_eyre::eyre::Result; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use windows::core::PCWSTR; +use windows::Win32::Foundation::COLORREF; +use windows::Win32::Foundation::HWND; +use windows::Win32::Foundation::LPARAM; +use windows::Win32::Foundation::LRESULT; +use windows::Win32::Foundation::WPARAM; +use windows::Win32::Graphics::Gdi::CreateFontIndirectW; +use windows::Win32::Graphics::Gdi::CreatePen; +use windows::Win32::Graphics::Gdi::CreateSolidBrush; +use windows::Win32::Graphics::Gdi::DrawTextW; +use windows::Win32::Graphics::Gdi::GetDC; +use windows::Win32::Graphics::Gdi::ReleaseDC; +use windows::Win32::Graphics::Gdi::SelectObject; +use windows::Win32::Graphics::Gdi::SetBkColor; +use windows::Win32::Graphics::Gdi::SetTextColor; +use windows::Win32::Graphics::Gdi::DT_CENTER; +use windows::Win32::Graphics::Gdi::DT_END_ELLIPSIS; +use windows::Win32::Graphics::Gdi::DT_SINGLELINE; +use windows::Win32::Graphics::Gdi::DT_VCENTER; +use windows::Win32::Graphics::Gdi::FONT_QUALITY; +use windows::Win32::Graphics::Gdi::FW_BOLD; +use windows::Win32::Graphics::Gdi::LOGFONTW; +use windows::Win32::Graphics::Gdi::PROOF_QUALITY; +use windows::Win32::Graphics::Gdi::PS_SOLID; +use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW; +use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW; +use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW; +use windows::Win32::UI::WindowsAndMessaging::GetMessageW; +use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage; +use windows::Win32::UI::WindowsAndMessaging::RegisterClassW; +use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes; +use windows::Win32::UI::WindowsAndMessaging::TranslateMessage; +use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW; +use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW; +use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY; +use windows::Win32::UI::WindowsAndMessaging::MSG; +use windows::Win32::UI::WindowsAndMessaging::SW_SHOW; +use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY; +use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN; +use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW; +use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED; +use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW; +use windows::Win32::UI::WindowsAndMessaging::WS_POPUP; +use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE; + +use komorebi_core::Rect; + +use crate::window::Window; +use crate::windows_api::WindowsApi; +use crate::winevent::WinEvent; +use crate::winevent_listener; +use crate::WindowManagerEvent; +use crate::DEFAULT_CONTAINER_PADDING; +use crate::STACKBAR_FOCUSED_TEXT_COLOUR; +use crate::STACKBAR_TAB_BACKGROUND_COLOUR; +use crate::STACKBAR_TAB_HEIGHT; +use crate::STACKBAR_TAB_WIDTH; +use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR; +use crate::TRANSPARENCY_COLOUR; +use crate::WINDOWS_BY_BAR_HWNDS; + +#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct Stackbar { + pub(crate) hwnd: isize, + #[serde(skip)] + pub is_cloned: bool, +} + +impl Drop for Stackbar { + fn drop(&mut self) { + if !self.is_cloned { + let _ = WindowsApi::close_window(self.hwnd()); + } + } +} + +impl Clone for Stackbar { + fn clone(&self) -> Self { + Self { + hwnd: self.hwnd, + is_cloned: true, + } + } +} +impl Stackbar { + unsafe extern "system" fn window_proc( + hwnd: HWND, + msg: u32, + w_param: WPARAM, + l_param: LPARAM, + ) -> LRESULT { + match msg { + WM_LBUTTONDOWN => { + let win_hwnds_by_topbar = WINDOWS_BY_BAR_HWNDS.lock(); + if let Some(win_hwnds) = win_hwnds_by_topbar.get(&hwnd.0) { + let x = l_param.0 as i32 & 0xFFFF; + let y = (l_param.0 as i32 >> 16) & 0xFFFF; + + let width = STACKBAR_TAB_WIDTH.load(Ordering::SeqCst); + let height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst); + let gap = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst); + + for (index, win_hwnd) in win_hwnds.iter().enumerate() { + let left = gap + (index as i32 * (width + gap)); + let right = left + width; + let top = 0; + let bottom = height; + + if x >= left && x <= right && y >= top && y <= bottom { + let window = Window { hwnd: *win_hwnd }; + let event_sender = winevent_listener::event_tx(); + let _ = event_sender.send(WindowManagerEvent::FocusChange( + WinEvent::ObjectFocus, + window, + )); + let _ = event_sender.send(WindowManagerEvent::ForceUpdate(window)); + } + } + } + + WINDOWS_BY_BAR_HWNDS.force_unlock(); + LRESULT(0) + } + WM_DESTROY => { + PostQuitMessage(0); + LRESULT(0) + } + _ => DefWindowProcW(hwnd, msg, w_param, l_param), + } + } + + pub const fn hwnd(&self) -> HWND { + HWND(self.hwnd) + } + + pub fn create() -> Result { + let name: Vec = "komorebi_stackbar\0".encode_utf16().collect(); + let class_name = PCWSTR(name.as_ptr()); + + let h_module = WindowsApi::module_handle_w()?; + + let wnd_class = WNDCLASSW { + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(Self::window_proc), + hInstance: h_module.into(), + lpszClassName: class_name, + hbrBackground: WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR), + ..Default::default() + }; + + unsafe { + RegisterClassW(&wnd_class); + } + + let (hwnd_sender, hwnd_receiver) = crossbeam_channel::bounded::(1); + + let name_cl = name.clone(); + std::thread::spawn(move || -> Result<()> { + unsafe { + let hwnd = CreateWindowExW( + WS_EX_TOOLWINDOW | WS_EX_LAYERED, + PCWSTR(name_cl.as_ptr()), + PCWSTR(name_cl.as_ptr()), + WS_POPUP | WS_VISIBLE, + 0, + 0, + 0, + 0, + None, + None, + h_module, + None, + ); + + SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?; + hwnd_sender.send(hwnd)?; + + let mut msg = MSG::default(); + while GetMessageW(&mut msg, hwnd, 0, 0).into() { + TranslateMessage(&msg); + DispatchMessageW(&msg); + std::thread::sleep(Duration::from_millis(10)); + } + } + + Ok(()) + }); + + Ok(Self { + hwnd: hwnd_receiver.recv()?.0, + ..Default::default() + }) + } + + pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> { + WindowsApi::position_window(self.hwnd(), layout, top) + } + + pub fn get_position_from_container_layout(&self, layout: &Rect) -> Rect { + Rect { + bottom: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst), + ..*layout + } + } + + pub fn update(&self, windows: &VecDeque, focused_hwnd: isize) -> Result<()> { + let width = STACKBAR_TAB_WIDTH.load(Ordering::SeqCst); + let height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst); + let gap = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst); + let background = STACKBAR_TAB_BACKGROUND_COLOUR.load(Ordering::SeqCst); + let focused_text_colour = STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst); + let unfocused_text_colour = STACKBAR_UNFOCUSED_TEXT_COLOUR.load(Ordering::SeqCst); + + unsafe { + let hdc = GetDC(self.hwnd()); + + let hpen = CreatePen(PS_SOLID, 0, COLORREF(background)); + let hbrush = CreateSolidBrush(COLORREF(background)); + + SelectObject(hdc, hpen); + SelectObject(hdc, hbrush); + SetBkColor(hdc, COLORREF(background)); + + let hfont = CreateFontIndirectW(&LOGFONTW { + lfWeight: FW_BOLD.0 as i32, + lfQuality: FONT_QUALITY(PROOF_QUALITY.0), + ..Default::default() + }); + + SelectObject(hdc, hfont); + + for (i, window) in windows.iter().enumerate() { + if window.hwnd == focused_hwnd { + SetTextColor(hdc, COLORREF(focused_text_colour)); + + window.focus(false)?; + } else { + SetTextColor(hdc, COLORREF(unfocused_text_colour)); + } + + let left = gap + (i as i32 * (width + gap)); + let mut tab_box = Rect { + top: 0, + left, + right: left + width, + bottom: height, + }; + + WindowsApi::round_rect(hdc, &tab_box, 8); + + let exe = window.exe()?; + let exe_trimmed = exe.trim_end_matches(".exe"); + let mut tab_title: Vec = exe_trimmed.encode_utf16().collect(); + + tab_box.left_padding(10); + tab_box.right_padding(10); + + DrawTextW( + hdc, + &mut tab_title, + &mut tab_box.into(), + DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS, + ); + } + + ReleaseDC(self.hwnd(), hdc); + } + + let mut windows_hwdns: VecDeque = VecDeque::new(); + for window in windows { + windows_hwdns.push_back(window.hwnd); + } + + WINDOWS_BY_BAR_HWNDS.lock().insert(self.hwnd, windows_hwdns); + + Ok(()) + } + + pub fn hide(&self) { + WindowsApi::hide_window(self.hwnd()) + } + + pub fn restore(&self) { + WindowsApi::show_window(self.hwnd(), SW_SHOW) + } +} diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 3a8ee999..b7149372 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -27,6 +27,12 @@ use crate::MANAGE_IDENTIFIERS; use crate::MONITOR_INDEX_PREFERENCES; use crate::OBJECT_NAME_CHANGE_ON_LAUNCH; use crate::REGEX_IDENTIFIERS; +use crate::STACKBAR_FOCUSED_TEXT_COLOUR; +use crate::STACKBAR_MODE; +use crate::STACKBAR_TAB_BACKGROUND_COLOUR; +use crate::STACKBAR_TAB_HEIGHT; +use crate::STACKBAR_TAB_WIDTH; +use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR; use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS; use crate::WORKSPACE_RULES; @@ -293,6 +299,30 @@ pub struct StaticConfig { /// Set display index preferences #[serde(skip_serializing_if = "Option::is_none")] pub display_index_preferences: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub stackbar: Option, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)] +pub enum StackbarMode { + Always, + Never, + OnStack, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct TabsConfig { + width: Option, + focused_text: Option, + unfocused_text: Option, + background: Option, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct StackbarConfig { + height: Option, + mode: Option, + tabs: Option, } impl From<&WindowManager> for StaticConfig { @@ -398,6 +428,7 @@ impl From<&WindowManager> for StaticConfig { object_name_change_applications: None, monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()), display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()), + stackbar: None, } } } @@ -490,6 +521,33 @@ impl StaticConfig { )?; } + if let Some(stackbar) = &self.stackbar { + if let Some(height) = &stackbar.height { + STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst); + } + if let Some(mode) = &stackbar.mode { + let mut stackbar_mode = STACKBAR_MODE.lock(); + *stackbar_mode = *mode; + } + if let Some(tabs) = &stackbar.tabs { + if let Some(background) = &tabs.background { + STACKBAR_TAB_BACKGROUND_COLOUR.store((*background).into(), Ordering::SeqCst); + } + + if let Some(colour) = &tabs.focused_text { + STACKBAR_FOCUSED_TEXT_COLOUR.store((*colour).into(), Ordering::SeqCst); + } + + if let Some(colour) = &tabs.unfocused_text { + STACKBAR_UNFOCUSED_TEXT_COLOUR.store((*colour).into(), Ordering::SeqCst); + } + + if let Some(width) = &tabs.width { + STACKBAR_TAB_WIDTH.store(*width, Ordering::SeqCst); + } + } + } + if let Some(path) = &self.app_specific_configuration_path { let path = resolve_home_path(path)?; let content = std::fs::read_to_string(path)?; diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 511045f3..d2067466 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -841,16 +841,16 @@ impl WindowManager { if !follow_focus && self.focused_container_mut().is_ok() { // and we have a stack with >1 windows if self.focused_container_mut()?.windows().len() > 1 - // and we don't have a maxed window - && self.focused_workspace()?.maximized_window().is_none() - // and we don't have a monocle container - && self.focused_workspace()?.monocle_container().is_none() + // and we don't have a maxed window + && self.focused_workspace()?.maximized_window().is_none() + // and we don't have a monocle container + && self.focused_workspace()?.monocle_container().is_none() { if let Ok(window) = self.focused_window_mut() { window.focus(self.mouse_follows_focus)?; } } - }; + } // This is to correctly restore and focus when switching to a workspace which // contains a managed maximized window @@ -1197,7 +1197,21 @@ impl WindowManager { } } - self.focused_window_mut()?.focus(self.mouse_follows_focus)?; + // When switching workspaces and landing focus on a window that is not stack, but a stack + // exists, and there is a stackbar visible, when changing focus to that container stack, + // the focused text colour will not be applied until the stack has been cycled at least once + // + // With this piece of code, we check if we have changed focus to a container stack with + // a stackbar, and if we have, we run a quick update to make sure the focused text colour + // has been applied + let focused_window = self.focused_window_mut()?; + let focused_window_hwnd = focused_window.hwnd; + focused_window.focus(self.mouse_follows_focus)?; + + let focused_container = self.focused_container()?; + if let Some(stackbar) = focused_container.stackbar() { + stackbar.update(focused_container.windows(), focused_window_hwnd)?; + } Ok(()) } diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index 13da588d..afa30873 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -28,6 +28,7 @@ pub enum WindowManagerEvent { Unmanage(Window), Raise(Window), DisplayChange(Window), + ForceUpdate(Window), } impl Display for WindowManagerEvent { @@ -78,6 +79,9 @@ impl Display for WindowManagerEvent { Self::DisplayChange(window) => { write!(f, "DisplayChange (Window: {window})") } + Self::ForceUpdate(window) => { + write!(f, "ForceUpdate (Window: {window})") + } } } } @@ -98,7 +102,8 @@ impl WindowManagerEvent { | Self::Raise(window) | Self::Manage(window) | Self::DisplayChange(window) - | Self::Unmanage(window) => window, + | Self::Unmanage(window) + | Self::ForceUpdate(window) => window, } } diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index ba46d92e..b4c8339e 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -36,6 +36,8 @@ use windows::Win32::Graphics::Gdi::GetMonitorInfoW; use windows::Win32::Graphics::Gdi::InvalidateRect; use windows::Win32::Graphics::Gdi::MonitorFromPoint; use windows::Win32::Graphics::Gdi::MonitorFromWindow; +use windows::Win32::Graphics::Gdi::Rectangle; +use windows::Win32::Graphics::Gdi::RoundRect; use windows::Win32::Graphics::Gdi::DISPLAY_DEVICEW; use windows::Win32::Graphics::Gdi::HBRUSH; use windows::Win32::Graphics::Gdi::HDC; @@ -66,6 +68,7 @@ use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_MOUSE; use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTDOWN; use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTUP; use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEINPUT; +use windows::Win32::UI::Input::KeyboardAndMouse::VK_LBUTTON; use windows::Win32::UI::Input::KeyboardAndMouse::VK_MENU; use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; use windows::Win32::UI::WindowsAndMessaging::BringWindowToTop; @@ -418,7 +421,7 @@ impl WindowsApi { .process() } - fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) { + pub fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) { // BOOL is returned but does not signify whether or not the operation was succesful // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow unsafe { ShowWindow(hwnd, command) }; @@ -527,6 +530,24 @@ impl WindowsApi { }) } + pub fn round_rect(hdc: HDC, rect: &Rect, border_radius: i32) { + unsafe { + RoundRect( + hdc, + rect.left, + rect.top, + rect.right, + rect.bottom, + border_radius, + border_radius, + ); + } + } + pub fn rectangle(hdc: HDC, rect: &Rect) { + unsafe { + Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom); + } + } fn set_cursor_pos(x: i32, y: i32) -> Result<()> { unsafe { SetCursorPos(x, y) }.process() } @@ -964,6 +985,13 @@ impl WindowsApi { actual != 0 } + pub fn lbutton_is_pressed() -> bool { + let state = unsafe { GetKeyState(i32::from(VK_LBUTTON.0)) }; + #[allow(clippy::cast_sign_loss)] + let actual = (state as u16) & 0x8000; + actual != 0 + } + pub fn left_click() -> u32 { let inputs = [ INPUT { diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 8c06223f..f1bd005e 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -33,6 +33,7 @@ use crate::DEFAULT_WORKSPACE_PADDING; use crate::INITIAL_CONFIGURATION_LOADED; use crate::NO_TITLEBAR; use crate::REMOVE_TITLEBARS; +use crate::STACKBAR_TAB_HEIGHT; #[allow(clippy::struct_field_names)] #[derive( @@ -143,55 +144,66 @@ impl Workspace { Ok(()) } - pub fn hide(&mut self) { - for container in self.containers_mut() { - for window in container.windows_mut() { + pub fn hide(&mut self, omit: Option) { + for window in self.floating_windows_mut().iter_mut().rev() { + let mut should_hide = omit.is_none(); + + if !should_hide { + if let Some(omit) = omit { + if omit != window.hwnd { + should_hide = true + } + } + } + + if should_hide { window.hide(); } } + for container in self.containers_mut() { + container.hide(omit) + } + if let Some(window) = self.maximized_window() { window.hide(); } if let Some(container) = self.monocle_container_mut() { - for window in container.windows_mut() { - window.hide(); - } - } - - for window in self.floating_windows() { - window.hide(); + container.hide(omit) } } pub fn restore(&mut self, mouse_follows_focus: bool) -> Result<()> { let idx = self.focused_container_idx(); let mut to_focus = None; + for (i, container) in self.containers_mut().iter_mut().enumerate() { if let Some(window) = container.focused_window_mut() { - window.restore(); - if idx == i { to_focus = Option::from(*window); } } + + container.restore(); } - if let Some(window) = self.maximized_window() { - window.maximize(); + for container in self.containers_mut() { + container.restore(); } if let Some(container) = self.monocle_container_mut() { - for window in container.windows_mut() { - window.restore(); - } + container.restore(); } for window in self.floating_windows() { window.restore(); } + if let Some(container) = self.focused_container_mut() { + container.focus_window(container.focused_window_idx()); + } + // Do this here to make sure that an error doesn't stop the restoration of other windows // Maximised windows should always be drawn at the top of the Z order if let Some(window) = to_focus { @@ -276,9 +288,23 @@ impl Workspace { let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst); let no_titlebar = NO_TITLEBAR.lock().clone(); - let windows = self.visible_windows_mut(); - for (i, window) in windows.into_iter().enumerate() { - if let (Some(window), Some(layout)) = (window, layouts.get(i)) { + let focused_hwnd = self + .focused_container() + .ok_or_else(|| anyhow!("couldn't find a focused container"))? + .focused_window() + .ok_or_else(|| anyhow!("couldn't find a focused window"))? + .hwnd; + + let container_padding = self.container_padding().unwrap_or(0); + let containers = self.containers_mut(); + + for (i, container) in containers.iter_mut().enumerate() { + let container_windows = container.windows().clone(); + let container_topbar = container.stackbar().clone(); + + if let (Some(window), Some(layout)) = + (container.focused_window_mut(), layouts.get(i)) + { if should_remove_titlebars && no_titlebar.contains(&window.exe()?) { window.remove_title_bar()?; } else if no_titlebar.contains(&window.exe()?) { @@ -300,6 +326,20 @@ impl Workspace { rect.add_padding(width); } + if let Some(stackbar) = container_topbar { + stackbar.set_position( + &stackbar.get_position_from_container_layout(layout), + false, + )?; + + stackbar.update(&container_windows, focused_hwnd)?; + let tab_height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst); + let total_height = tab_height + container_padding; + + rect.top += total_height; + rect.bottom -= total_height; + } + window.set_position(&rect, false)?; } } @@ -375,7 +415,18 @@ impl Workspace { .idx_for_window(hwnd) .ok_or_else(|| anyhow!("there is no window"))?; + let mut should_load = false; + + if container.focused_window_idx() != window_idx { + should_load = true + } + container.focus_window(window_idx); + + if should_load { + container.load_focused_window(); + } + self.focus_container(container_idx); Ok(())