From f227bd0fefedc9797205dcce50c84fa757e5d2d9 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Fri, 6 Dec 2024 17:44:17 -0800 Subject: [PATCH 1/6] fix(cli): handle spaces in bar config paths More PowerShell script username-with-spaces path handling shenanigans. re #1161 --- komorebic/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 7e090f2d3..84ee71da4 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -2094,7 +2094,7 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue)) let mut config = StaticConfig::read(config)?; if let Some(display_bar_configurations) = &mut config.bar_configurations { for config_file_path in &mut *display_bar_configurations { - let script = r#"Start-Process 'komorebi-bar' '--config "CONFIGFILE"' -WindowStyle hidden"# + let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"# .replace("CONFIGFILE", &config_file_path.to_string_lossy()); match powershell_script::run(&script) { From 280ca0ffcd4f3e64770eec27532e1b12646820cb Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 7 Dec 2024 10:57:48 -0800 Subject: [PATCH 2/6] perf(cli): validate komorebi proc earlier on start This is a small change to the start command which moves the check for the komorebi processes to come a little bit earlier. This small change will make running commands like "komorebic start --bar" around 3s faster when komorebi is already running. --- komorebic/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 84ee71da4..c00e9274c 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -1990,8 +1990,14 @@ fn main() -> Result<()> { ) }; + let mut system = sysinfo::System::new_all(); + system.refresh_processes(ProcessesToUpdate::All); + let mut attempts = 0; - let mut running = false; + let mut running = system + .processes_by_name("komorebi.exe".as_ref()) + .next() + .is_some(); while !running && attempts <= 2 { match powershell_script::run(&script) { @@ -2006,7 +2012,6 @@ fn main() -> Result<()> { print!("Waiting for komorebi.exe to start..."); std::thread::sleep(Duration::from_secs(3)); - let mut system = sysinfo::System::new_all(); system.refresh_processes(ProcessesToUpdate::All); if system From 440d78e8f429e17ecf9b619119182b55e9f1bbca Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 7 Dec 2024 11:08:39 -0800 Subject: [PATCH 3/6] feat(cli): add support for starting/stopping masir This commit adds support to the start and stop commands for starting and stopping masir. --- komorebi/src/static_config.rs | 2 +- komorebic/src/main.rs | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 40a2dcaec..6b8d34688 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -268,7 +268,7 @@ pub struct StaticConfig { /// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op) #[serde(skip_serializing_if = "Option::is_none")] pub unmanaged_window_operation_behaviour: Option, - /// END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None) + /// END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead #[serde(skip_serializing_if = "Option::is_none")] pub focus_follows_mouse: Option, /// Enable or disable mouse follows focus (default: true) diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index c00e9274c..7521b3b08 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -779,6 +779,9 @@ struct Start { /// Start komorebi-bar in a background process #[clap(long)] bar: bool, + /// Start masir in a background process for focus-follows-mouse + #[clap(long)] + masir: bool, } #[derive(Parser)] @@ -792,6 +795,9 @@ struct Stop { /// Stop komorebi-bar if it is running as a background process #[clap(long)] bar: bool, + /// Stop masir if it is running as a background process + #[clap(long)] + masir: bool, } #[derive(Parser)] @@ -888,6 +894,9 @@ struct EnableAutostart { /// Enable autostart of komorebi-bar #[clap(long)] bar: bool, + /// Enable autostart of masir + #[clap(long)] + masir: bool, } #[derive(Parser)] @@ -1507,6 +1516,10 @@ fn main() -> Result<()> { arguments.push_str(" --ahk"); } + if args.bar { + arguments.push_str(" --masir"); + } + Command::new("powershell") .arg("-c") .arg("$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut($env:SHORTCUT_PATH); $Shortcut.TargetPath = $env:TARGET_PATH; $Shortcut.Arguments = $env:TARGET_ARGS; $Shortcut.Save()") @@ -1927,6 +1940,10 @@ fn main() -> Result<()> { bail!("could not find whkd, please make sure it is installed before using the --whkd flag"); } + if arg.masir && which("masir").is_err() { + bail!("could not find masir, please make sure it is installed before using the --masir flag"); + } + if arg.ahk && which(&ahk).is_err() { bail!("could not find autohotkey, please make sure it is installed before using the --ahk flag"); } @@ -2130,6 +2147,23 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue)) } } + if arg.masir { + let script = r" +if (!(Get-Process masir -ErrorAction SilentlyContinue)) +{ + Start-Process masir -WindowStyle hidden +} + "; + match powershell_script::run(script) { + Ok(_) => { + println!("{script}"); + } + Err(error) => { + println!("Error: {error}"); + } + } + } + println!("\nThank you for using komorebi!\n"); println!("# Sponsorship"); println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - $5/month makes a big difference"); @@ -2198,6 +2232,20 @@ Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue } } + if arg.masir { + let script = r" +Stop-Process -Name:masir -ErrorAction SilentlyContinue + "; + match powershell_script::run(script) { + Ok(_) => { + println!("{script}"); + } + Err(error) => { + println!("Error: {error}"); + } + } + } + if arg.ahk { let script = r#" if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { From e6b5b78857cff017135db2008bb62648632feee6 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 7 Dec 2024 11:14:23 -0800 Subject: [PATCH 4/6] feat(cli): add kill cmd This commit adds a new komorebic command, "kill", to kill background processes that may be started by "komorebic start", without terminating the main komorebi process. This is useful when iterating on changes to external components like the bar which may require restarts. --- komorebic/src/main.rs | 90 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 7521b3b08..24c1db045 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -800,6 +800,22 @@ struct Stop { masir: bool, } +#[derive(Parser)] +struct Kill { + /// Kill whkd if it is running as a background process + #[clap(long)] + whkd: bool, + /// Kill ahk if it is running as a background process + #[clap(long)] + ahk: bool, + /// Kill komorebi-bar if it is running as a background process + #[clap(long)] + bar: bool, + /// Kill masir if it is running as a background process + #[clap(long)] + masir: bool, +} + #[derive(Parser)] struct SaveResize { /// File to which the resize layout dimensions should be saved @@ -922,6 +938,8 @@ enum SubCommand { Start(Start), /// Stop the komorebi.exe process and restore all hidden windows Stop(Stop), + /// Kill background processes started by komorebic + Kill(Kill), /// Check komorebi configuration and related files for common errors Check, /// Show the path to komorebi.json @@ -2305,6 +2323,78 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue } } } + SubCommand::Kill(arg) => { + if arg.whkd { + let script = r" +Stop-Process -Name:whkd -ErrorAction SilentlyContinue + "; + match powershell_script::run(script) { + Ok(_) => { + println!("{script}"); + } + Err(error) => { + println!("Error: {error}"); + } + } + } + + if arg.bar { + let script = r" +Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue + "; + match powershell_script::run(script) { + Ok(_) => { + println!("{script}"); + } + Err(error) => { + println!("Error: {error}"); + } + } + } + + if arg.masir { + let script = r" +Stop-Process -Name:masir -ErrorAction SilentlyContinue + "; + match powershell_script::run(script) { + Ok(_) => { + println!("{script}"); + } + Err(error) => { + println!("Error: {error}"); + } + } + } + + if arg.ahk { + let script = r#" +if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { + (Get-CimInstance Win32_Process | Where-Object { + ($_.CommandLine -like '*komorebi.ahk"') -and + ($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe')) + } | Select-Object -First 1) | ForEach-Object { + Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue + } +} else { + (Get-WmiObject Win32_Process | Where-Object { + ($_.CommandLine -like '*komorebi.ahk"') -and + ($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe')) + } | Select-Object -First 1) | ForEach-Object { + Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue + } +} +"#; + + match powershell_script::run(script) { + Ok(_) => { + println!("{script}"); + } + Err(error) => { + println!("Error: {error}"); + } + } + } + } SubCommand::IgnoreRule(arg) => { send_message(&SocketMessage::IgnoreRule(arg.identifier, arg.id))?; } From ede0b23bb48acbbedd80a677340edfdae0a34865 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Tue, 3 Dec 2024 20:04:02 -0800 Subject: [PATCH 5/6] feat(borders): track window movements + animations This commit introduces a number of changes to the border manager module to enable borders to track the movements of windows as they are being animated. As part of these changes, the code paths for borders to track user movement of windows have also been overhauled. The biggest conceptual change introduced here is borrowed from @lukeyou05's work on tacky-borders, where the primary event listener of the komorebi process now forwards EVENT_OBJECT_LOCATIONCHANGE and EVENT_OBJECT_DESTROY messages from application windows directly on to their borders. These events are handled directly in the border window callbacks, outside of the main border manager module event processing loop. In order to handle these events more performantly in the border window callbacks, a number of state trackers have been added to the Border struct. When handling EVENT_OBJECT_NAMECHANGE, these values are read directly from the struct, whereas when handling WM_PAINT, which is sent by the system whenever we invalidate a border window, we update the state values on the Border structs from the various atomic configuration variables in the mod.rs file. Another trick I borrowed from tacky-borders is to store a pointer to the Border object alongside a border window whenever it is created with CreateWindowExW, which can be accessed within the callback as GWLP_USERDATA. There is some unfortunate introduction of unsafe code to make this happen, but the callback uses null checks to exit the callback early to ensure (to the best of my ability) that there are no pointer dereferencing issues once we start making border changes in the context of the callback. There are a few other Direct2D related optimizations throughout this commit, mainly avoiding the recreation of objects like brush properties and brushes. Finally, the border_z_order option is now deprecated as the border window is now tracking the z-ordering of the application window it is associated with by default - this should resolve a whole host of subtle border z-ordering issues, especially when dragging windows around using the mouse. This work would not have been possible without the guidance of @lukeyou05, so if you like this feature, please make sure you thank him too! --- komorebi/src/border_manager/border.rs | 318 +++++++++++++++++++------- komorebi/src/border_manager/mod.rs | 210 ++++++----------- komorebi/src/core/mod.rs | 2 + komorebi/src/core/rect.rs | 6 + komorebi/src/process_command.rs | 12 +- komorebi/src/static_config.rs | 7 +- komorebi/src/window.rs | 6 - komorebi/src/windows_api.rs | 29 ++- komorebi/src/windows_callbacks.rs | 48 +++- komorebi/src/winevent_listener.rs | 4 +- 10 files changed, 394 insertions(+), 248 deletions(-) diff --git a/komorebi/src/border_manager/border.rs b/komorebi/src/border_manager/border.rs index 305c188aa..a367ee9f6 100644 --- a/komorebi/src/border_manager/border.rs +++ b/komorebi/src/border_manager/border.rs @@ -5,16 +5,18 @@ use crate::border_manager::BORDER_WIDTH; use crate::border_manager::FOCUS_STATE; use crate::border_manager::RENDER_TARGETS; use crate::border_manager::STYLE; -use crate::border_manager::Z_ORDER; use crate::core::BorderStyle; use crate::core::Rect; use crate::windows_api; use crate::WindowsApi; use crate::WINDOWS_11; +use color_eyre::eyre::anyhow; +use std::collections::HashMap; use std::ops::Deref; use std::sync::atomic::Ordering; use std::sync::mpsc; use std::sync::LazyLock; +use std::sync::OnceLock; use windows::Foundation::Numerics::Matrix3x2; use windows::Win32::Foundation::BOOL; use windows::Win32::Foundation::FALSE; @@ -30,6 +32,8 @@ use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F; use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U; use windows::Win32::Graphics::Direct2D::D2D1CreateFactory; use windows::Win32::Graphics::Direct2D::ID2D1Factory; +use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget; +use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush; use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE; use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES; use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED; @@ -43,22 +47,26 @@ use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION; use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE; use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND; use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN; -use windows::Win32::Graphics::Gdi::BeginPaint; use windows::Win32::Graphics::Gdi::CreateRectRgn; -use windows::Win32::Graphics::Gdi::EndPaint; use windows::Win32::Graphics::Gdi::InvalidateRect; -use windows::Win32::Graphics::Gdi::PAINTSTRUCT; +use windows::Win32::Graphics::Gdi::ValidateRect; use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW; use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW; use windows::Win32::UI::WindowsAndMessaging::GetMessageW; use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics; +use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW; use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage; +use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW; use windows::Win32::UI::WindowsAndMessaging::TranslateMessage; +use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW; +use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY; +use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE; +use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA; use windows::Win32::UI::WindowsAndMessaging::MSG; use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN; +use windows::Win32::UI::WindowsAndMessaging::WM_CREATE; use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY; use windows::Win32::UI::WindowsAndMessaging::WM_PAINT; -use windows::Win32::UI::WindowsAndMessaging::WM_SIZE; use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW; use windows_core::PCWSTR; @@ -89,14 +97,36 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL { true.into() } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Border { pub hwnd: isize, + pub render_target: OnceLock, + pub tracking_hwnd: isize, + pub window_rect: Rect, + pub window_kind: WindowKind, + pub style: BorderStyle, + pub width: i32, + pub offset: i32, + pub brush_properties: D2D1_BRUSH_PROPERTIES, + pub rounded_rect: D2D1_ROUNDED_RECT, + pub brushes: HashMap, } impl From for Border { fn from(value: isize) -> Self { - Self { hwnd: value } + Self { + hwnd: value, + render_target: OnceLock::new(), + tracking_hwnd: 0, + window_rect: Rect::default(), + window_kind: WindowKind::Unfocused, + style: STYLE.load(), + width: BORDER_WIDTH.load(Ordering::Relaxed), + offset: BORDER_OFFSET.load(Ordering::Relaxed), + brush_properties: D2D1_BRUSH_PROPERTIES::default(), + rounded_rect: D2D1_ROUNDED_RECT::default(), + brushes: HashMap::new(), + } } } @@ -105,7 +135,7 @@ impl Border { HWND(windows_api::as_ptr!(self.hwnd)) } - pub fn create(id: &str) -> color_eyre::Result { + pub fn create(id: &str, tracking_hwnd: isize) -> color_eyre::Result { let name: Vec = format!("komoborder-{id}\0").encode_utf16().collect(); let class_name = PCWSTR(name.as_ptr()); @@ -121,12 +151,30 @@ impl Border { let _ = WindowsApi::register_class_w(&window_class); - let (hwnd_sender, hwnd_receiver) = mpsc::channel(); + let (border_sender, border_receiver) = mpsc::channel(); let instance = h_module.0 as isize; std::thread::spawn(move || -> color_eyre::Result<()> { - let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?; - hwnd_sender.send(hwnd)?; + let mut border = Self { + hwnd: 0, + render_target: OnceLock::new(), + tracking_hwnd, + window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(), + window_kind: WindowKind::Unfocused, + style: STYLE.load(), + width: BORDER_WIDTH.load(Ordering::Relaxed), + offset: BORDER_OFFSET.load(Ordering::Relaxed), + brush_properties: Default::default(), + rounded_rect: Default::default(), + brushes: HashMap::new(), + }; + + let border_pointer = std::ptr::addr_of!(border); + let hwnd = + WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?; + + border.hwnd = hwnd; + border_sender.send(border_pointer as isize)?; let mut msg: MSG = MSG::default(); @@ -145,8 +193,8 @@ impl Border { Ok(()) }); - let hwnd = hwnd_receiver.recv()?; - let border = Self { hwnd }; + let border_ref = border_receiver.recv()?; + let border = unsafe { &mut *(border_ref as *mut Border) }; // I have literally no idea, apparently this is to get rid of the black pixels // around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys @@ -167,7 +215,7 @@ impl Border { } let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES { - hwnd: HWND(windows_api::as_ptr!(hwnd)), + hwnd: HWND(windows_api::as_ptr!(border.hwnd)), pixelSize: Default::default(), presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY, }; @@ -188,10 +236,47 @@ impl Border { .CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties) } { Ok(render_target) => unsafe { + border.brush_properties = *BRUSH_PROPERTIES.deref(); + for window_kind in [ + WindowKind::Single, + WindowKind::Stack, + WindowKind::Monocle, + WindowKind::Unfocused, + WindowKind::Floating, + ] { + let color = window_kind_colour(window_kind); + let color = D2D1_COLOR_F { + r: ((color & 0xFF) as f32) / 255.0, + g: (((color >> 8) & 0xFF) as f32) / 255.0, + b: (((color >> 16) & 0xFF) as f32) / 255.0, + a: 1.0, + }; + + if let Ok(brush) = + render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties)) + { + border.brushes.insert(window_kind, brush); + } + } + render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + + if border.render_target.set(render_target.clone()).is_err() { + return Err(anyhow!("could not store border render target")); + } + + border.rounded_rect = { + let radius = 8.0 + border.width as f32 / 2.0; + D2D1_ROUNDED_RECT { + rect: Default::default(), + radiusX: radius, + radiusY: radius, + } + }; + let mut render_targets = RENDER_TARGETS.lock(); - render_targets.insert(hwnd, render_target); - Ok(border) + render_targets.insert(border.hwnd, render_target); + Ok(border.clone()) }, Err(error) => Err(error.into()), } @@ -203,27 +288,17 @@ impl Border { WindowsApi::close_window(self.hwnd) } - pub fn update(&self, rect: &Rect, should_invalidate: bool) -> color_eyre::Result<()> { - // Make adjustments to the border + pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> { let mut rect = *rect; - rect.add_margin(BORDER_WIDTH.load(Ordering::Relaxed)); - rect.add_padding(-BORDER_OFFSET.load(Ordering::Relaxed)); - - // Update the position of the border if required - // This effectively handles WM_MOVE - // Also if I remove this no borders render at all lol - if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) { - WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?; - } + rect.add_margin(self.width); + rect.add_padding(-self.offset); - // Invalidate the rect to trigger the callback to update colours etc. - if should_invalidate { - self.invalidate(); - } + WindowsApi::set_border_pos(self.hwnd, &rect, reference_hwnd)?; Ok(()) } + // this triggers WM_PAINT in the callback below pub fn invalidate(&self) { let _ = unsafe { InvalidateRect(self.hwnd(), None, false) }; } @@ -236,50 +311,156 @@ impl Border { ) -> LRESULT { unsafe { match message { - WM_SIZE | WM_PAINT => { - if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) { - let render_targets = RENDER_TARGETS.lock(); - if let Some(render_target) = render_targets.get(&(window.0 as isize)) { - let pixel_size = D2D_SIZE_U { + WM_CREATE => { + let mut border_pointer: *mut Border = + GetWindowLongPtrW(window, GWLP_USERDATA) as _; + + if border_pointer.is_null() { + let create_struct: *mut CREATESTRUCTW = lparam.0 as *mut _; + border_pointer = (*create_struct).lpCreateParams as *mut _; + SetWindowLongPtrW(window, GWLP_USERDATA, border_pointer as _); + } + + LRESULT(0) + } + EVENT_OBJECT_DESTROY => { + let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _; + + if border_pointer.is_null() { + return LRESULT(0); + } + + // we don't actually want to destroy the window here, just hide it for quicker + // visual feedback to the user; the actual destruction will be handled by the + // core border manager loop + WindowsApi::hide_window(window.0 as isize); + LRESULT(0) + } + EVENT_OBJECT_LOCATIONCHANGE => { + let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _; + + if border_pointer.is_null() { + return LRESULT(0); + } + + let reference_hwnd = lparam.0; + + let old_rect = (*border_pointer).window_rect; + let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default(); + + (*border_pointer).window_rect = rect; + + if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) { + tracing::error!("failed to update border position {error}"); + } + + if !rect.is_same_size_as(&old_rect) { + if let Some(render_target) = (*border_pointer).render_target.get() { + let border_width = (*border_pointer).width; + let border_offset = (*border_pointer).offset; + + (*border_pointer).rounded_rect.rect = D2D_RECT_F { + left: (border_width / 2 - border_offset) as f32, + top: (border_width / 2 - border_offset) as f32, + right: (rect.right - border_width / 2 + border_offset) as f32, + bottom: (rect.bottom - border_width / 2 + border_offset) as f32, + }; + + let _ = render_target.Resize(&D2D_SIZE_U { width: rect.right as u32, height: rect.bottom as u32, - }; + }); + + let window_kind = (*border_pointer).window_kind; + if let Some(brush) = (*border_pointer).brushes.get(&window_kind) { + render_target.BeginDraw(); + render_target.Clear(None); + + // Calculate border radius based on style + let style = match (*border_pointer).style { + BorderStyle::System => { + if *WINDOWS_11 { + BorderStyle::Rounded + } else { + BorderStyle::Square + } + } + BorderStyle::Rounded => BorderStyle::Rounded, + BorderStyle::Square => BorderStyle::Square, + }; + + match style { + BorderStyle::Rounded => { + render_target.DrawRoundedRectangle( + &(*border_pointer).rounded_rect, + brush, + border_width as f32, + None, + ); + } + BorderStyle::Square => { + render_target.DrawRectangle( + &(*border_pointer).rounded_rect.rect, + brush, + border_width as f32, + None, + ); + } + _ => {} + } + + let _ = render_target.EndDraw(None, None); + } + } + } + + LRESULT(0) + } + WM_PAINT => { + if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) { + let border_pointer: *mut Border = + GetWindowLongPtrW(window, GWLP_USERDATA) as _; - let border_width = BORDER_WIDTH.load(Ordering::SeqCst); - let border_offset = BORDER_OFFSET.load(Ordering::SeqCst); + if border_pointer.is_null() { + return LRESULT(0); + } - let rect = D2D_RECT_F { + if let Some(render_target) = (*border_pointer).render_target.get() { + (*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed); + (*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed); + + let border_width = (*border_pointer).width; + let border_offset = (*border_pointer).offset; + + (*border_pointer).rounded_rect.rect = D2D_RECT_F { left: (border_width / 2 - border_offset) as f32, top: (border_width / 2 - border_offset) as f32, right: (rect.right - border_width / 2 + border_offset) as f32, bottom: (rect.bottom - border_width / 2 + border_offset) as f32, }; - let _ = render_target.Resize(&pixel_size); + let _ = render_target.Resize(&D2D_SIZE_U { + width: rect.right as u32, + height: rect.bottom as u32, + }); // Get window kind and color - let window_kind = FOCUS_STATE + + (*border_pointer).window_kind = FOCUS_STATE .lock() .get(&(window.0 as isize)) .copied() .unwrap_or(WindowKind::Unfocused); - let color = window_kind_colour(window_kind); - let color = D2D1_COLOR_F { - r: ((color & 0xFF) as f32) / 255.0, - g: (((color >> 8) & 0xFF) as f32) / 255.0, - b: (((color >> 16) & 0xFF) as f32) / 255.0, - a: 1.0, - }; - - if let Ok(brush) = render_target - .CreateSolidColorBrush(&color, Some(BRUSH_PROPERTIES.deref())) - { + let window_kind = (*border_pointer).window_kind; + if let Some(brush) = (*border_pointer).brushes.get(&window_kind) { render_target.BeginDraw(); render_target.Clear(None); + (*border_pointer).style = STYLE.load(); + // Calculate border radius based on style - let style = match STYLE.load() { + let style = match (*border_pointer).style { BorderStyle::System => { if *WINDOWS_11 { BorderStyle::Rounded @@ -293,31 +474,17 @@ impl Border { match style { BorderStyle::Rounded => { - let radius = 8.0 + border_width as f32 / 2.0; - let rounded_rect = D2D1_ROUNDED_RECT { - rect, - radiusX: radius, - radiusY: radius, - }; - render_target.DrawRoundedRectangle( - &rounded_rect, - &brush, + &(*border_pointer).rounded_rect, + brush, border_width as f32, None, ); } BorderStyle::Square => { - let rect = D2D_RECT_F { - left: rect.left, - top: rect.top, - right: rect.right, - bottom: rect.bottom, - }; - render_target.DrawRectangle( - &rect, - &brush, + &(*border_pointer).rounded_rect.rect, + brush, border_width as f32, None, ); @@ -326,17 +493,14 @@ impl Border { } let _ = render_target.EndDraw(None, None); - - // If we don't do this we'll get spammed with WM_PAINT according to Raymond Chen - // https://stackoverflow.com/questions/41783234/why-does-my-call-to-d2d1rendertargetdrawtext-result-in-a-wm-paint-being-se#comment70756781_41783234 - let _ = BeginPaint(window, &mut PAINTSTRUCT::default()); - let _ = EndPaint(window, &PAINTSTRUCT::default()); } } } + let _ = ValidateRect(window, None); LRESULT(0) } WM_DESTROY => { + SetWindowLongPtrW(window, GWLP_USERDATA, 0); PostQuitMessage(0); LRESULT(0) } diff --git a/komorebi/src/border_manager/mod.rs b/komorebi/src/border_manager/mod.rs index 2af2dd9a6..6d2f087bf 100644 --- a/komorebi/src/border_manager/mod.rs +++ b/komorebi/src/border_manager/mod.rs @@ -11,7 +11,7 @@ use crate::Rgb; use crate::WindowManager; use crate::WindowsApi; use border::border_hwnds; -use border::Border; +pub use border::Border; use crossbeam_channel::Receiver; use crossbeam_channel::Sender; use crossbeam_utils::atomic::AtomicCell; @@ -30,18 +30,13 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::OnceLock; use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget; -use windows::Win32::System::Threading::GetCurrentThread; -use windows::Win32::System::Threading::SetThreadPriority; -use windows::Win32::System::Threading::THREAD_PRIORITY_TIME_CRITICAL; pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8); pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1); pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true); -pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false); lazy_static! { - pub static ref Z_ORDER: AtomicCell = AtomicCell::new(ZOrder::Bottom); pub static ref STYLE: AtomicCell = AtomicCell::new(BorderStyle::System); pub static ref IMPLEMENTATION: AtomicCell = AtomicCell::new(BorderImplementation::Komorebi); @@ -59,6 +54,7 @@ lazy_static! { lazy_static! { static ref BORDERS_MONITORS: Mutex> = Mutex::new(HashMap::new()); static ref BORDER_STATE: Mutex> = Mutex::new(HashMap::new()); + static ref WINDOWS_BORDERS: Mutex> = Mutex::new(HashMap::new()); static ref FOCUS_STATE: Mutex> = Mutex::new(HashMap::new()); static ref RENDER_TARGETS: Mutex> = Mutex::new(HashMap::new()); @@ -80,6 +76,10 @@ fn event_rx() -> Receiver { channel().1.clone() } +pub fn window_border(hwnd: isize) -> Option { + WINDOWS_BORDERS.lock().get(&hwnd).cloned() +} + pub fn send_notification(hwnd: Option) { if event_tx().try_send(Notification(hwnd)).is_err() { tracing::warn!("channel is full; dropping notification") @@ -122,31 +122,22 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> { fn window_kind_colour(focus_kind: WindowKind) -> u32 { match focus_kind { - WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst), - WindowKind::Single => FOCUSED.load(Ordering::SeqCst), - WindowKind::Stack => STACK.load(Ordering::SeqCst), - WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst), - WindowKind::Floating => FLOATING.load(Ordering::SeqCst), + WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed), + WindowKind::Single => FOCUSED.load(Ordering::Relaxed), + WindowKind::Stack => STACK.load(Ordering::Relaxed), + WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed), + WindowKind::Floating => FLOATING.load(Ordering::Relaxed), } } pub fn listen_for_notifications(wm: Arc>) { - std::thread::spawn(move || { - unsafe { - if let Err(error) = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL) - { - tracing::error!("{error}"); + std::thread::spawn(move || loop { + match handle_notifications(wm.clone()) { + Ok(()) => { + tracing::warn!("restarting finished thread"); } - } - - loop { - match handle_notifications(wm.clone()) { - Ok(()) => { - tracing::warn!("restarting finished thread"); - } - Err(error) => { - tracing::warn!("restarting failed thread: {}", error); - } + Err(error) => { + tracing::warn!("restarting failed thread: {}", error); } } }); @@ -155,7 +146,6 @@ pub fn listen_for_notifications(wm: Arc>) { pub fn handle_notifications(wm: Arc>) -> color_eyre::Result<()> { tracing::info!("listening"); - BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst); let receiver = event_rx(); event_tx().send(Notification(None))?; @@ -172,7 +162,6 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result let focused_workspace_idx = state.monitors.elements()[focused_monitor_idx].focused_workspace_idx(); let monitors = state.monitors.clone(); - let weak_pending_move_op = Arc::downgrade(&state.pending_move_op); let pending_move_op = *state.pending_move_op; let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces() [focused_workspace_idx] @@ -271,11 +260,10 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result let mut borders = BORDER_STATE.lock(); let mut borders_monitors = BORDERS_MONITORS.lock(); + let mut windows_borders = WINDOWS_BORDERS.lock(); // If borders are disabled if !BORDER_ENABLED.load_consume() - // Or if they are temporarily disabled - || BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst) // Or if the wm is paused || is_paused // Or if we are handling an alt-tab across workspaces @@ -287,6 +275,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result } borders.clear(); + windows_borders.clear(); previous_is_paused = is_paused; continue 'receiver; @@ -316,10 +305,15 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result // Handle the monocle container separately if let Some(monocle) = ws.monocle_container() { + let mut new_border = false; let border = match borders.entry(monocle.id().clone()) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { - if let Ok(border) = Border::create(monocle.id()) { + if let Ok(border) = Border::create( + monocle.id(), + monocle.focused_window().copied().unwrap_or_default().hwnd, + ) { + new_border = true; entry.insert(border) } else { continue 'monitors; @@ -328,6 +322,10 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result }; borders_monitors.insert(monocle.id().clone(), monitor_idx); + windows_borders.insert( + monocle.focused_window().cloned().unwrap_or_default().hwnd, + border.clone(), + ); { let mut focus_state = FOCUS_STATE.lock(); @@ -341,11 +339,16 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result ); } - let rect = WindowsApi::window_rect( - monocle.focused_window().copied().unwrap_or_default().hwnd, - )?; + let reference_hwnd = + monocle.focused_window().copied().unwrap_or_default().hwnd; + + let rect = WindowsApi::window_rect(reference_hwnd)?; - border.update(&rect, true)?; + if new_border { + border.set_position(&rect, reference_hwnd)?; + } + + border.invalidate(); let border_hwnd = border.hwnd; let mut to_remove = vec![]; @@ -414,65 +417,16 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result } for (idx, c) in ws.containers().iter().enumerate() { - let hwnd = c.focused_window().copied().unwrap_or_default().hwnd; - let notification_hwnd = notification.0.unwrap_or_default(); - - // Update border when moving or resizing with mouse - if pending_move_op.is_some() - && idx == ws.focused_container_idx() - && hwnd == notification_hwnd - { - let restore_z_order = Z_ORDER.load(); - Z_ORDER.store(ZOrder::TopMost); - - let mut rect = WindowsApi::window_rect( - c.focused_window().copied().unwrap_or_default().hwnd, - )?; - - // We create a new variable to track the actual pending move op so - // that the other variable `pending_move_op` still holds the - // pending move info so that when the move ends we know on the next - // notification that the previous pending move and pending move are - // different (because a move just finished) and still handle the - // notification. If otherwise we updated the pending_move_op here - // directly then the next pending move after finish would be the - // same because we had already updated it here. - let mut sync_pending_move_op = - weak_pending_move_op.upgrade().and_then(|p| *p); - while sync_pending_move_op.is_some() { - sync_pending_move_op = - weak_pending_move_op.upgrade().and_then(|p| *p); - let border = match borders.entry(c.id().clone()) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - if let Ok(border) = Border::create(c.id()) { - entry.insert(border) - } else { - continue 'monitors; - } - } - }; - - let new_rect = WindowsApi::window_rect( - c.focused_window().copied().unwrap_or_default().hwnd, - )?; - - if rect != new_rect { - rect = new_rect; - border.update(&rect, false)?; - } - } - - Z_ORDER.store(restore_z_order); - - continue 'monitors; - } - // Get the border entry for this container from the map or create one + let mut new_border = false; let border = match borders.entry(c.id().clone()) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { - if let Ok(border) = Border::create(c.id()) { + if let Ok(border) = Border::create( + c.id(), + c.focused_window().copied().unwrap_or_default().hwnd, + ) { + new_border = true; entry.insert(border) } else { continue 'monitors; @@ -481,6 +435,10 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result }; borders_monitors.insert(c.id().clone(), monitor_idx); + windows_borders.insert( + c.focused_window().cloned().unwrap_or_default().hwnd, + border.clone(), + ); #[allow(unused_assignments)] let mut last_focus_state = None; @@ -501,66 +459,35 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result last_focus_state = focus_state.insert(border.hwnd, new_focus_state); } - let rect = WindowsApi::window_rect( - c.focused_window().copied().unwrap_or_default().hwnd, - )?; + let reference_hwnd = + c.focused_window().copied().unwrap_or_default().hwnd; + + let rect = WindowsApi::window_rect(reference_hwnd)?; let should_invalidate = match last_focus_state { None => true, Some(last_focus_state) => last_focus_state != new_focus_state, }; - border.update(&rect, should_invalidate)?; + if new_border { + border.set_position(&rect, reference_hwnd)?; + } + + if should_invalidate { + border.invalidate(); + } } { - let restore_z_order = Z_ORDER.load(); - Z_ORDER.store(ZOrder::TopMost); - 'windows: for window in ws.floating_windows() { - let hwnd = window.hwnd; - let notification_hwnd = notification.0.unwrap_or_default(); - - if pending_move_op.is_some() && hwnd == notification_hwnd { - let mut rect = WindowsApi::window_rect(hwnd)?; - - // Check comment above for containers move - let mut sync_pending_move_op = - weak_pending_move_op.upgrade().and_then(|p| *p); - while sync_pending_move_op.is_some() { - sync_pending_move_op = - weak_pending_move_op.upgrade().and_then(|p| *p); - let border = match borders.entry(hwnd.to_string()) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - if let Ok(border) = - Border::create(&hwnd.to_string()) - { - entry.insert(border) - } else { - continue 'monitors; - } - } - }; - - let new_rect = WindowsApi::window_rect(hwnd)?; - - if rect != new_rect { - rect = new_rect; - border.update(&rect, true)?; - } - } - - Z_ORDER.store(restore_z_order); - - continue 'monitors; - } - + let mut new_border = false; let border = match borders.entry(window.hwnd.to_string()) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { - if let Ok(border) = Border::create(&window.hwnd.to_string()) + if let Ok(border) = + Border::create(&window.hwnd.to_string(), window.hwnd) { + new_border = true; entry.insert(border) } else { continue 'monitors; @@ -569,6 +496,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result }; borders_monitors.insert(window.hwnd.to_string(), monitor_idx); + windows_borders.insert(window.hwnd, border.clone()); let mut should_destroy = false; @@ -607,10 +535,14 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result Some(last_focus_state) => last_focus_state != new_focus_state, }; - border.update(&rect, should_invalidate)?; - } + if new_border { + border.set_position(&rect, window.hwnd)?; + } - Z_ORDER.store(restore_z_order); + if should_invalidate { + border.invalidate(); + } + } } } } diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index a2803cec9..d4e149663 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -306,6 +306,8 @@ pub enum BorderImplementation { ValueEnum, JsonSchema, PartialEq, + Eq, + Hash, )] pub enum WindowKind { Single, diff --git a/komorebi/src/core/rect.rs b/komorebi/src/core/rect.rs index 1378dc1d6..cc7b0492e 100644 --- a/komorebi/src/core/rect.rs +++ b/komorebi/src/core/rect.rs @@ -37,6 +37,12 @@ impl From for RECT { } } +impl Rect { + pub fn is_same_size_as(&self, rhs: &Self) -> bool { + self.right == rhs.right && self.bottom == rhs.bottom + } +} + impl Rect { /// decrease the size of self by the padding amount. pub fn add_padding(&mut self, padding: T) diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 13a70feb8..456154138 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -670,12 +670,10 @@ impl WindowManager { self.update_focused_workspace(self.mouse_follows_focus, true)?; } SocketMessage::Retile => { - border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst); border_manager::destroy_all_borders()?; self.retile_all(false)? } SocketMessage::RetileWithResizeDimensions => { - border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst); border_manager::destroy_all_borders()?; self.retile_all(true)? } @@ -1518,6 +1516,16 @@ impl WindowManager { } SocketMessage::Border(enable) => { border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst); + if !enable { + match IMPLEMENTATION.load() { + BorderImplementation::Komorebi => { + border_manager::destroy_all_borders()?; + } + BorderImplementation::Windows => { + self.remove_all_accents()?; + } + } + } } SocketMessage::BorderImplementation(implementation) => { if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) { diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 6b8d34688..533037a79 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -11,7 +11,6 @@ use crate::border_manager; use crate::border_manager::ZOrder; use crate::border_manager::IMPLEMENTATION; use crate::border_manager::STYLE; -use crate::border_manager::Z_ORDER; use crate::colour::Colour; use crate::core::BorderImplementation; use crate::core::StackbarLabel; @@ -297,7 +296,7 @@ pub struct StaticConfig { #[serde(skip_serializing_if = "Option::is_none")] #[serde(alias = "active_window_border_style")] pub border_style: Option, - /// Active window border z-order (default: System) + /// DEPRECATED from v0.1.31: no longer required #[serde(skip_serializing_if = "Option::is_none")] pub border_z_order: Option, /// Active window border implementation (default: Komorebi) @@ -497,7 +496,7 @@ impl StaticConfig { } pub fn deprecated(raw: &str) { - let deprecated_options = ["invisible_borders"]; + let deprecated_options = ["invisible_borders", "border_z_order"]; let deprecated_variants = vec![ ("Hide", "window_hiding_behaviour", "Cloak"), ("Minimize", "window_hiding_behaviour", "Cloak"), @@ -600,7 +599,7 @@ impl From<&WindowManager> for StaticConfig { ), transparency_ignore_rules: None, border_style: Option::from(STYLE.load()), - border_z_order: Option::from(Z_ORDER.load()), + border_z_order: None, border_implementation: Option::from(IMPLEMENTATION.load()), default_workspace_padding: Option::from( DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst), diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index 9740b5600..84a8f2756 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -10,7 +10,6 @@ use crate::animation::ANIMATION_ENABLED_PER_ANIMATION; use crate::animation::ANIMATION_MANAGER; use crate::animation::ANIMATION_STYLE_GLOBAL; use crate::animation::ANIMATION_STYLE_PER_ANIMATION; -use crate::border_manager; use crate::com::SetCloak; use crate::focus_manager; use crate::stackbar_manager; @@ -193,9 +192,6 @@ impl RenderDispatcher for MovementRenderDispatcher { } fn pre_render(&self) -> Result<()> { - border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst); - border_manager::send_notification(Some(self.hwnd)); - stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst); stackbar_manager::send_notification(); @@ -224,10 +220,8 @@ impl RenderDispatcher for MovementRenderDispatcher { focus_manager::send_notification(self.hwnd) } - border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst); stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst); - border_manager::send_notification(Some(self.hwnd)); stackbar_manager::send_notification(); transparency_manager::send_notification(); } diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index be9e8ef37..9469492ee 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -106,7 +106,6 @@ use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE; use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT; use windows::Win32::UI::WindowsAndMessaging::HWND_TOP; use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA; -use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY; use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS; use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD; use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE; @@ -129,7 +128,6 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE; use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW; use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC; use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED; -use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED; use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE; use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW; use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST; @@ -154,6 +152,7 @@ macro_rules! as_ptr { }; } +use crate::border_manager::Border; pub(crate) use as_ptr; pub enum WindowsResult { @@ -453,7 +452,13 @@ impl WindowsApi { } pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> { - let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE }; + let flags = { + SetWindowPosition::NO_SEND_CHANGING + | SetWindowPosition::NO_ACTIVATE + | SetWindowPosition::NO_REDRAW + | SetWindowPosition::SHOW_WINDOW + }; + Self::set_window_pos( HWND(as_ptr!(hwnd)), layout, @@ -1090,10 +1095,14 @@ impl WindowsApi { .process() } - pub fn create_border_window(name: PCWSTR, instance: isize) -> Result { + pub fn create_border_window( + name: PCWSTR, + instance: isize, + border: *const Border, + ) -> Result { unsafe { - let hwnd = CreateWindowExW( - WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE, + CreateWindowExW( + WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE, name, name, WS_POPUP | WS_SYSMENU, @@ -1104,12 +1113,8 @@ impl WindowsApi { None, None, HINSTANCE(as_ptr!(instance)), - None, - )?; - - SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?; - - hwnd + Some(border as _), + )? } .process() } diff --git a/komorebi/src/windows_callbacks.rs b/komorebi/src/windows_callbacks.rs index c7551fa50..5c3f2a96d 100644 --- a/komorebi/src/windows_callbacks.rs +++ b/komorebi/src/windows_callbacks.rs @@ -1,10 +1,6 @@ use std::collections::VecDeque; -use windows::Win32::Foundation::BOOL; -use windows::Win32::Foundation::HWND; -use windows::Win32::Foundation::LPARAM; -use windows::Win32::UI::Accessibility::HWINEVENTHOOK; - +use crate::border_manager; use crate::container::Container; use crate::window::RuleDebug; use crate::window::Window; @@ -12,6 +8,19 @@ use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::winevent::WinEvent; use crate::winevent_listener; +use windows::Win32::Foundation::BOOL; +use windows::Win32::Foundation::HWND; +use windows::Win32::Foundation::LPARAM; +use windows::Win32::Foundation::WPARAM; +use windows::Win32::UI::Accessibility::HWINEVENTHOOK; +use windows::Win32::UI::WindowsAndMessaging::GetWindowLongW; +use windows::Win32::UI::WindowsAndMessaging::SendNotifyMessageW; +use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE; +use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE; +use windows::Win32::UI::WindowsAndMessaging::OBJID_WINDOW; +use windows::Win32::UI::WindowsAndMessaging::WS_CHILD; +use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE; +use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW; pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL { let containers = unsafe { &mut *(lparam.0 as *mut VecDeque) }; @@ -60,6 +69,15 @@ pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL { true.into() } +fn has_filtered_style(hwnd: HWND) -> bool { + let style = unsafe { GetWindowLongW(hwnd, GWL_STYLE) as u32 }; + let ex_style = unsafe { GetWindowLongW(hwnd, GWL_EXSTYLE) as u32 }; + + style & WS_CHILD.0 != 0 + || ex_style & WS_EX_TOOLWINDOW.0 != 0 + || ex_style & WS_EX_NOACTIVATE.0 != 0 +} + pub extern "system" fn win_event_hook( _h_win_event_hook: HWINEVENTHOOK, event: u32, @@ -69,8 +87,7 @@ pub extern "system" fn win_event_hook( _id_event_thread: u32, _dwms_event_time: u32, ) { - // OBJID_WINDOW - if id_object != 0 { + if id_object != OBJID_WINDOW.0 { return; } @@ -81,6 +98,23 @@ pub extern "system" fn win_event_hook( Err(_) => return, }; + // this forwards the message to the window's border when it moves or is destroyed + // see border_manager/border.rs + if matches!( + winevent, + WinEvent::ObjectLocationChange | WinEvent::ObjectDestroy + ) && !has_filtered_style(hwnd) + { + let border_window = border_manager::window_border(hwnd.0 as isize); + + if let Some(border) = border_window { + unsafe { + let _ = + SendNotifyMessageW(border.hwnd(), event, WPARAM(0), LPARAM(hwnd.0 as isize)); + } + } + } + let event_type = match WindowManagerEvent::from_win_event(winevent, window) { None => { tracing::trace!( diff --git a/komorebi/src/winevent_listener.rs b/komorebi/src/winevent_listener.rs index 2bbfc4562..1187fff3c 100644 --- a/komorebi/src/winevent_listener.rs +++ b/komorebi/src/winevent_listener.rs @@ -11,6 +11,8 @@ use windows::Win32::UI::WindowsAndMessaging::TranslateMessage; use windows::Win32::UI::WindowsAndMessaging::EVENT_MAX; use windows::Win32::UI::WindowsAndMessaging::EVENT_MIN; use windows::Win32::UI::WindowsAndMessaging::MSG; +use windows::Win32::UI::WindowsAndMessaging::WINEVENT_OUTOFCONTEXT; +use windows::Win32::UI::WindowsAndMessaging::WINEVENT_SKIPOWNPROCESS; use crate::window_manager_event::WindowManagerEvent; use crate::windows_callbacks; @@ -31,7 +33,7 @@ pub fn start() { Some(windows_callbacks::win_event_hook), 0, 0, - 0, + WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS, ) }; From 4f306e5bfd72696d994fbf04f74d06a632d3084e Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 7 Dec 2024 16:43:28 -0800 Subject: [PATCH 6/6] docs(schema): update all json schemas --- schema.bar.json | 962 +++++++++++++++++++++++++++++++++++++++++++++++- schema.json | 140 +++++-- 2 files changed, 1058 insertions(+), 44 deletions(-) diff --git a/schema.bar.json b/schema.bar.json index 8cc6c01f7..65af91291 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -9,6 +9,736 @@ "right_widgets" ], "properties": { + "center_widgets": { + "description": "Center widgets (ordered left-to-right)", + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "required": [ + "Battery" + ], + "properties": { + "Battery": { + "type": "object", + "required": [ + "enable" + ], + "properties": { + "data_refresh_interval": { + "description": "Data refresh interval (default: 10 seconds)", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "enable": { + "description": "Enable the Battery widget", + "type": "boolean" + }, + "label_prefix": { + "description": "Display label prefix", + "oneOf": [ + { + "description": "Show no prefix", + "type": "string", + "enum": [ + "None" + ] + }, + { + "description": "Show an icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show an icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Cpu" + ], + "properties": { + "Cpu": { + "type": "object", + "required": [ + "enable" + ], + "properties": { + "data_refresh_interval": { + "description": "Data refresh interval (default: 10 seconds)", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "enable": { + "description": "Enable the Cpu widget", + "type": "boolean" + }, + "label_prefix": { + "description": "Display label prefix", + "oneOf": [ + { + "description": "Show no prefix", + "type": "string", + "enum": [ + "None" + ] + }, + { + "description": "Show an icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show an icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Date" + ], + "properties": { + "Date": { + "type": "object", + "required": [ + "enable", + "format" + ], + "properties": { + "enable": { + "description": "Enable the Date widget", + "type": "boolean" + }, + "format": { + "description": "Set the Date format", + "oneOf": [ + { + "description": "Month/Date/Year format (09/08/24)", + "type": "string", + "enum": [ + "MonthDateYear" + ] + }, + { + "description": "Year-Month-Date format (2024-09-08)", + "type": "string", + "enum": [ + "YearMonthDate" + ] + }, + { + "description": "Date-Month-Year format (8-Sep-2024)", + "type": "string", + "enum": [ + "DateMonthYear" + ] + }, + { + "description": "Day Date Month Year format (8 September 2024)", + "type": "string", + "enum": [ + "DayDateMonthYear" + ] + }, + { + "description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)", + "type": "object", + "required": [ + "Custom" + ], + "properties": { + "Custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "label_prefix": { + "description": "Display label prefix", + "oneOf": [ + { + "description": "Show no prefix", + "type": "string", + "enum": [ + "None" + ] + }, + { + "description": "Show an icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show an icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Komorebi" + ], + "properties": { + "Komorebi": { + "type": "object", + "required": [ + "workspaces" + ], + "properties": { + "configuration_switcher": { + "description": "Configure the Configuration Switcher widget", + "type": "object", + "required": [ + "configurations", + "enable" + ], + "properties": { + "configurations": { + "description": "A map of display friendly name => path to configuration.json", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enable": { + "description": "Enable the Komorebi Configurations widget", + "type": "boolean" + } + } + }, + "focused_window": { + "description": "Configure the Focused Window widget", + "type": "object", + "required": [ + "enable" + ], + "properties": { + "display": { + "description": "Display format of the currently focused window", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, + "enable": { + "description": "Enable the Komorebi Focused Window widget", + "type": "boolean" + }, + "show_icon": { + "description": "DEPRECATED: use 'display' instead (Show the icon of the currently focused window)", + "type": "boolean" + } + } + }, + "layout": { + "description": "Configure the Layout widget", + "type": "object", + "required": [ + "enable" + ], + "properties": { + "display": { + "description": "Display format of the current layout", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, + "enable": { + "description": "Enable the Komorebi Layout widget", + "type": "boolean" + }, + "options": { + "description": "List of layout options", + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "enum": [ + "BSP", + "Columns", + "Rows", + "VerticalStack", + "HorizontalStack", + "UltrawideVerticalStack", + "Grid", + "RightMainVerticalStack" + ] + }, + { + "type": "null" + }, + { + "type": "null" + }, + { + "type": "null" + }, + { + "type": "null" + } + ] + } + } + } + }, + "workspaces": { + "description": "Configure the Workspaces widget", + "type": "object", + "required": [ + "enable", + "hide_empty_workspaces" + ], + "properties": { + "display": { + "description": "Display format of the workspace", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, + "enable": { + "description": "Enable the Komorebi Workspaces widget", + "type": "boolean" + }, + "hide_empty_workspaces": { + "description": "Hide workspaces without any windows", + "type": "boolean" + } + } + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Media" + ], + "properties": { + "Media": { + "type": "object", + "required": [ + "enable" + ], + "properties": { + "enable": { + "description": "Enable the Media widget", + "type": "boolean" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Memory" + ], + "properties": { + "Memory": { + "type": "object", + "required": [ + "enable" + ], + "properties": { + "data_refresh_interval": { + "description": "Data refresh interval (default: 10 seconds)", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "enable": { + "description": "Enable the Memory widget", + "type": "boolean" + }, + "label_prefix": { + "description": "Display label prefix", + "oneOf": [ + { + "description": "Show no prefix", + "type": "string", + "enum": [ + "None" + ] + }, + { + "description": "Show an icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show an icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Network" + ], + "properties": { + "Network": { + "type": "object", + "required": [ + "enable", + "show_network_activity", + "show_total_data_transmitted" + ], + "properties": { + "data_refresh_interval": { + "description": "Data refresh interval (default: 10 seconds)", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "enable": { + "description": "Enable the Network widget", + "type": "boolean" + }, + "label_prefix": { + "description": "Display label prefix", + "oneOf": [ + { + "description": "Show no prefix", + "type": "string", + "enum": [ + "None" + ] + }, + { + "description": "Show an icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show an icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, + "network_activity_fill_characters": { + "description": "Characters to reserve for network activity data", + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "show_network_activity": { + "description": "Show network activity", + "type": "boolean" + }, + "show_total_data_transmitted": { + "description": "Show total data transmitted", + "type": "boolean" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Storage" + ], + "properties": { + "Storage": { + "type": "object", + "required": [ + "enable" + ], + "properties": { + "data_refresh_interval": { + "description": "Data refresh interval (default: 10 seconds)", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "enable": { + "description": "Enable the Storage widget", + "type": "boolean" + }, + "label_prefix": { + "description": "Display label prefix", + "oneOf": [ + { + "description": "Show no prefix", + "type": "string", + "enum": [ + "None" + ] + }, + { + "description": "Show an icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show an icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Time" + ], + "properties": { + "Time": { + "type": "object", + "required": [ + "enable", + "format" + ], + "properties": { + "enable": { + "description": "Enable the Time widget", + "type": "boolean" + }, + "format": { + "description": "Set the Time format", + "oneOf": [ + { + "description": "Twelve-hour format (with seconds)", + "type": "string", + "enum": [ + "TwelveHour" + ] + }, + { + "description": "Twenty-four-hour format (with seconds)", + "type": "string", + "enum": [ + "TwentyFourHour" + ] + }, + { + "description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)", + "type": "object", + "required": [ + "Custom" + ], + "properties": { + "Custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "label_prefix": { + "description": "Display label prefix", + "oneOf": [ + { + "description": "Show no prefix", + "type": "string", + "enum": [ + "None" + ] + }, + { + "description": "Show an icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show an icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + } + } + } + }, + "additionalProperties": false + } + ] + } + }, "font_family": { "description": "Font family", "type": "string" @@ -507,16 +1237,41 @@ "description": "Configure the Focused Window widget", "type": "object", "required": [ - "enable", - "show_icon" + "enable" ], "properties": { + "display": { + "description": "Display format of the currently focused window", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, "enable": { "description": "Enable the Komorebi Focused Window widget", "type": "boolean" }, "show_icon": { - "description": "Show the icon of the currently focused window", + "description": "DEPRECATED: use 'display' instead (Show the icon of the currently focused window)", "type": "boolean" } } @@ -528,9 +1283,68 @@ "enable" ], "properties": { + "display": { + "description": "Display format of the current layout", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, "enable": { "description": "Enable the Komorebi Layout widget", "type": "boolean" + }, + "options": { + "description": "List of layout options", + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "enum": [ + "BSP", + "Columns", + "Rows", + "VerticalStack", + "HorizontalStack", + "UltrawideVerticalStack", + "Grid", + "RightMainVerticalStack" + ] + }, + { + "type": "null" + }, + { + "type": "null" + }, + { + "type": "null" + }, + { + "type": "null" + } + ] + } } } }, @@ -542,6 +1356,32 @@ "hide_empty_workspaces" ], "properties": { + "display": { + "description": "Display format of the workspace", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, "enable": { "description": "Enable the Komorebi Workspaces widget", "type": "boolean" @@ -1225,16 +2065,41 @@ "description": "Configure the Focused Window widget", "type": "object", "required": [ - "enable", - "show_icon" + "enable" ], "properties": { + "display": { + "description": "Display format of the currently focused window", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, "enable": { "description": "Enable the Komorebi Focused Window widget", "type": "boolean" }, "show_icon": { - "description": "Show the icon of the currently focused window", + "description": "DEPRECATED: use 'display' instead (Show the icon of the currently focused window)", "type": "boolean" } } @@ -1246,9 +2111,68 @@ "enable" ], "properties": { + "display": { + "description": "Display format of the current layout", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, "enable": { "description": "Enable the Komorebi Layout widget", "type": "boolean" + }, + "options": { + "description": "List of layout options", + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "enum": [ + "BSP", + "Columns", + "Rows", + "VerticalStack", + "HorizontalStack", + "UltrawideVerticalStack", + "Grid", + "RightMainVerticalStack" + ] + }, + { + "type": "null" + }, + { + "type": "null" + }, + { + "type": "null" + }, + { + "type": "null" + } + ] + } } } }, @@ -1260,6 +2184,32 @@ "hide_empty_workspaces" ], "properties": { + "display": { + "description": "Display format of the workspace", + "oneOf": [ + { + "description": "Show only icon", + "type": "string", + "enum": [ + "Icon" + ] + }, + { + "description": "Show only text", + "type": "string", + "enum": [ + "Text" + ] + }, + { + "description": "Show both icon and text", + "type": "string", + "enum": [ + "IconAndText" + ] + } + ] + }, "enable": { "description": "Enable the Komorebi Workspaces widget", "type": "boolean" diff --git a/schema.json b/schema.json index d20abb4b3..f092d9e57 100644 --- a/schema.json +++ b/schema.json @@ -13,13 +13,35 @@ "properties": { "duration": { "description": "Set the animation duration in ms (default: 250)", - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + ] }, "enabled": { "description": "Enable or disable animations (default: false)", - "type": "boolean" + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + { + "type": "boolean" + } + ] }, "fps": { "description": "Set the animation FPS (default: 60)", @@ -29,38 +51,80 @@ }, "style": { "description": "Set the animation style (default: Linear)", - "type": "string", - "enum": [ - "Linear", - "EaseInSine", - "EaseOutSine", - "EaseInOutSine", - "EaseInQuad", - "EaseOutQuad", - "EaseInOutQuad", - "EaseInCubic", - "EaseInOutCubic", - "EaseInQuart", - "EaseOutQuart", - "EaseInOutQuart", - "EaseInQuint", - "EaseOutQuint", - "EaseInOutQuint", - "EaseInExpo", - "EaseOutExpo", - "EaseInOutExpo", - "EaseInCirc", - "EaseOutCirc", - "EaseInOutCirc", - "EaseInBack", - "EaseOutBack", - "EaseInOutBack", - "EaseInElastic", - "EaseOutElastic", - "EaseInOutElastic", - "EaseInBounce", - "EaseOutBounce", - "EaseInOutBounce" + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "Linear", + "EaseInSine", + "EaseOutSine", + "EaseInOutSine", + "EaseInQuad", + "EaseOutQuad", + "EaseInOutQuad", + "EaseInCubic", + "EaseInOutCubic", + "EaseInQuart", + "EaseOutQuart", + "EaseInOutQuart", + "EaseInQuint", + "EaseOutQuint", + "EaseInOutQuint", + "EaseInExpo", + "EaseOutExpo", + "EaseInOutExpo", + "EaseInCirc", + "EaseOutCirc", + "EaseInOutCirc", + "EaseInBack", + "EaseOutBack", + "EaseInOutBack", + "EaseInElastic", + "EaseOutElastic", + "EaseInOutElastic", + "EaseInBounce", + "EaseOutBounce", + "EaseInOutBounce" + ] + } + }, + { + "type": "string", + "enum": [ + "Linear", + "EaseInSine", + "EaseOutSine", + "EaseInOutSine", + "EaseInQuad", + "EaseOutQuad", + "EaseInOutQuad", + "EaseInCubic", + "EaseInOutCubic", + "EaseInQuart", + "EaseOutQuart", + "EaseInOutQuart", + "EaseInQuint", + "EaseOutQuint", + "EaseInOutQuint", + "EaseInExpo", + "EaseOutExpo", + "EaseInOutExpo", + "EaseInCirc", + "EaseOutCirc", + "EaseInOutCirc", + "EaseInBack", + "EaseOutBack", + "EaseInOutBack", + "EaseInElastic", + "EaseOutElastic", + "EaseInOutElastic", + "EaseInBounce", + "EaseOutBounce", + "EaseInOutBounce" + ] + } ] } } @@ -420,7 +484,7 @@ "format": "int32" }, "border_z_order": { - "description": "Active window border z-order (default: System)", + "description": "DEPRECATED from v0.1.31: no longer required", "type": "string", "enum": [ "Top", @@ -579,7 +643,7 @@ } }, "focus_follows_mouse": { - "description": "END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)", + "description": "END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead", "oneOf": [ { "description": "A custom FFM implementation (slightly more CPU-intensive)",