diff --git a/Cargo.lock b/Cargo.lock index 832052d08..317b5136c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,6 +833,15 @@ version = "1.2.0" source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -882,6 +891,7 @@ dependencies = [ "uds_windows", "which", "widestring", + "win32-display-data", "windows 0.54.0", "windows-implement", "windows-interface", @@ -2465,6 +2475,16 @@ version = "1.1.0" source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +[[package]] +name = "win32-display-data" +version = "0.1.0" +source = "git+/~https://github.com/LGUG2Z/win32-display-data#2c47b9f1ca1f359ba2481d0b6ea8667ccd9d075c" +dependencies = [ + "itertools", + "thiserror", + "windows 0.54.0", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/komorebi/Cargo.toml b/komorebi/Cargo.toml index dac15009c..e4ca03f71 100644 --- a/komorebi/Cargo.toml +++ b/komorebi/Cargo.toml @@ -48,5 +48,7 @@ windows-interface = { workspace = true } winput = "0.2" winreg = "0.52" +win32-display-data = { git = "/~https://github.com/LGUG2Z/win32-display-data" } + [features] deadlock_detection = [] diff --git a/komorebi/src/hidden.rs b/komorebi/src/hidden.rs deleted file mode 100644 index a37991115..000000000 --- a/komorebi/src/hidden.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::sync::atomic::Ordering; - -use color_eyre::Result; -use windows::core::PCWSTR; -use windows::Win32::Foundation::HWND; -use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW; -use windows::Win32::UI::WindowsAndMessaging::FindWindowW; -use windows::Win32::UI::WindowsAndMessaging::GetMessageW; -use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW; -use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW; -use windows::Win32::UI::WindowsAndMessaging::MSG; -use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW; - -use crate::windows_callbacks; -use crate::WindowsApi; -use crate::HIDDEN_HWND; - -#[derive(Debug, Clone, Copy)] -pub struct Hidden { - pub(crate) hwnd: isize, -} - -impl From for Hidden { - fn from(hwnd: isize) -> Self { - Self { hwnd } - } -} - -impl Hidden { - pub const fn hwnd(self) -> HWND { - HWND(self.hwnd) - } - - pub fn create(name: &str) -> Result<()> { - let name: Vec = format!("{name}\0").encode_utf16().collect(); - let instance = WindowsApi::module_handle_w()?; - let class_name = PCWSTR(name.as_ptr()); - let brush = WindowsApi::create_solid_brush(0); - let window_class = WNDCLASSW { - hInstance: instance.into(), - lpszClassName: class_name, - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(windows_callbacks::hidden_window), - hbrBackground: brush, - ..Default::default() - }; - - let _atom = WindowsApi::register_class_w(&window_class)?; - - let name_cl = name.clone(); - std::thread::spawn(move || -> Result<()> { - let hwnd = WindowsApi::create_hidden_window(PCWSTR(name_cl.as_ptr()), instance)?; - let hidden = Self::from(hwnd); - - let mut message = MSG::default(); - - unsafe { - while GetMessageW(&mut message, hidden.hwnd(), 0, 0).into() { - DispatchMessageW(&message); - } - } - - Ok(()) - }); - - let mut hwnd = HWND(0); - while hwnd == HWND(0) { - hwnd = unsafe { FindWindowW(PCWSTR(name.as_ptr()), PCWSTR::null()) }; - } - - HIDDEN_HWND.store(hwnd.0, Ordering::SeqCst); - - Ok(()) - } -} diff --git a/komorebi/src/lib.rs b/komorebi/src/lib.rs index 675c8bfaa..c20088fe7 100644 --- a/komorebi/src/lib.rs +++ b/komorebi/src/lib.rs @@ -4,8 +4,8 @@ pub mod com; pub mod ring; pub mod colour; pub mod container; -pub mod hidden; pub mod monitor; +pub mod monitor_reconciliator; pub mod process_command; pub mod process_event; pub mod process_movement; @@ -33,13 +33,11 @@ use std::path::PathBuf; use std::process::Command; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicI32; -use std::sync::atomic::AtomicIsize; use std::sync::atomic::AtomicU32; use std::sync::atomic::Ordering; use std::sync::Arc; pub use colour::*; -pub use hidden::*; pub use process_command::*; pub use process_event::*; pub use stackbar::*; @@ -217,8 +215,6 @@ pub static SESSION_ID: AtomicU32 = AtomicU32::new(0); 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 diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index e991ee555..8b80af7f2 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -24,8 +24,8 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::EnvFilter; use komorebi::border_manager; -use komorebi::hidden::Hidden; use komorebi::load_configuration; +use komorebi::monitor_reconciliator; use komorebi::process_command::listen_for_commands; use komorebi::process_command::listen_for_commands_tcp; use komorebi::process_event::listen_for_events; @@ -188,8 +188,6 @@ fn main() -> Result<()> { #[cfg(feature = "deadlock_detection")] detect_deadlocks(); - Hidden::create("komorebi-hidden")?; - let static_config = opts.config.map_or_else( || { let komorebi_json = HOME_DIR.join("komorebi.json"); @@ -257,6 +255,7 @@ fn main() -> Result<()> { border_manager::listen_for_notifications(wm.clone()); workspace_reconciliator::listen_for_notifications(wm.clone()); + monitor_reconciliator::listen_for_notifications(wm.clone())?; let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1); ctrlc::set_handler(move || { diff --git a/komorebi/src/monitor.rs b/komorebi/src/monitor.rs index 74a38d640..162ac526f 100644 --- a/komorebi/src/monitor.rs +++ b/komorebi/src/monitor.rs @@ -27,9 +27,9 @@ pub struct Monitor { #[getset(get = "pub", set = "pub")] name: String, #[getset(get = "pub", set = "pub")] - device: Option, + device: String, #[getset(get = "pub", set = "pub")] - device_id: Option, + device_id: String, #[getset(get = "pub", set = "pub")] size: Rect, #[getset(get = "pub", set = "pub")] @@ -50,15 +50,22 @@ pub struct Monitor { impl_ring_elements!(Monitor, Workspace); -pub fn new(id: isize, size: Rect, work_area_size: Rect, name: String) -> Monitor { +pub fn new( + id: isize, + size: Rect, + work_area_size: Rect, + name: String, + device: String, + device_id: String, +) -> Monitor { let mut workspaces = Ring::default(); workspaces.elements_mut().push_back(Workspace::default()); Monitor { id, name, - device: None, - device_id: None, + device, + device_id, size, work_area_size, work_area_offset: None, diff --git a/komorebi/src/monitor_reconciliator/hidden.rs b/komorebi/src/monitor_reconciliator/hidden.rs new file mode 100644 index 000000000..87cc3f0b4 --- /dev/null +++ b/komorebi/src/monitor_reconciliator/hidden.rs @@ -0,0 +1,190 @@ +use std::sync::mpsc; + +use windows::core::PCWSTR; +use windows::Win32::Foundation::HWND; +use windows::Win32::Foundation::LPARAM; +use windows::Win32::Foundation::LRESULT; +use windows::Win32::Foundation::WPARAM; +use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW; +use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW; +use windows::Win32::UI::WindowsAndMessaging::GetMessageW; +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::DBT_DEVNODES_CHANGED; +use windows::Win32::UI::WindowsAndMessaging::MSG; +use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC; +use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND; +use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND; +use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA; +use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE; +use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE; +use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST; +use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE; +use windows::Win32::UI::WindowsAndMessaging::WM_WTSSESSION_CHANGE; +use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW; +use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK; +use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK; + +use crate::monitor_reconciliator; +use crate::WindowsApi; + +// This is a hidden window specifically spawned to listen to system-wide events related to monitors +#[derive(Debug, Clone, Copy)] +pub struct Hidden { + pub hwnd: isize, +} + +impl From for Hidden { + fn from(hwnd: isize) -> Self { + Self { hwnd } + } +} + +impl Hidden { + pub const fn hwnd(self) -> HWND { + HWND(self.hwnd) + } + + pub fn create(name: &str) -> color_eyre::Result { + let name: Vec = format!("{name}\0").encode_utf16().collect(); + let class_name = PCWSTR(name.as_ptr()); + + let h_module = WindowsApi::module_handle_w()?; + let window_class = WNDCLASSW { + hInstance: h_module.into(), + lpszClassName: class_name, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(Self::callback), + hbrBackground: WindowsApi::create_solid_brush(0), + ..Default::default() + }; + + let _ = WindowsApi::register_class_w(&window_class)?; + + let (hwnd_sender, hwnd_receiver) = mpsc::channel(); + + std::thread::spawn(move || -> color_eyre::Result<()> { + let hwnd = WindowsApi::create_hidden_window(PCWSTR(name.as_ptr()), h_module)?; + hwnd_sender.send(hwnd)?; + + let mut message = MSG::default(); + + unsafe { + while GetMessageW(&mut message, HWND(hwnd), 0, 0).into() { + TranslateMessage(&message); + DispatchMessageW(&message); + } + } + + Ok(()) + }); + + let hwnd = hwnd_receiver.recv()?; + + WindowsApi::wts_register_session_notification(hwnd)?; + + Ok(Self { hwnd }) + } + + pub extern "system" fn callback( + window: HWND, + message: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + unsafe { + match message { + WM_POWERBROADCAST => { + match wparam.0 as u32 { + // Automatic: System resumed itself from sleep or hibernation + // Suspend: User resumed system from sleep or hibernation + PBT_APMRESUMEAUTOMATIC | PBT_APMRESUMESUSPEND => { + tracing::debug!( + "WM_POWERBROADCAST event received - resume from suspend" + ); + let _ = monitor_reconciliator::event_tx().send( + monitor_reconciliator::Notification::ResumingFromSuspendedState, + ); + LRESULT(0) + } + // Computer is entering a suspended state + PBT_APMSUSPEND => { + tracing::debug!( + "WM_POWERBROADCAST event received - entering suspended state" + ); + let _ = monitor_reconciliator::event_tx() + .send(monitor_reconciliator::Notification::EnteringSuspendedState); + LRESULT(0) + } + _ => LRESULT(0), + } + } + WM_WTSSESSION_CHANGE => { + match wparam.0 as u32 { + WTS_SESSION_LOCK => { + tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked"); + + let _ = monitor_reconciliator::event_tx() + .send(monitor_reconciliator::Notification::SessionLocked); + } + WTS_SESSION_UNLOCK => { + tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked"); + + let _ = monitor_reconciliator::event_tx() + .send(monitor_reconciliator::Notification::SessionUnlocked); + } + _ => {} + } + + LRESULT(0) + } + // This event gets sent when: + // - The scaling factor on a display changes + // - The resolution on a display changes + // - A monitor is added + // - A monitor is removed + // Since WM_DEVICECHANGE also notifies on monitor changes, we only handle scaling + // and resolution changes here + WM_DISPLAYCHANGE => { + tracing::debug!( + "WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed", wparam.0 + ); + + let _ = monitor_reconciliator::event_tx() + .send(monitor_reconciliator::Notification::ResolutionScalingChanged); + LRESULT(0) + } + // Unfortunately this is the event sent with ButteryTaskbar which I use a lot + // Original idea from https://stackoverflow.com/a/33762334 + WM_SETTINGCHANGE => { + #[allow(clippy::cast_possible_truncation)] + if wparam.0 as u32 == SPI_SETWORKAREA.0 { + tracing::debug!( + "WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)" + ); + + let _ = monitor_reconciliator::event_tx() + .send(monitor_reconciliator::Notification::WorkAreaChanged); + } + LRESULT(0) + } + // This event + wparam combo is sent 4 times when a monitor is added based on my testing on win11 + // Original idea from https://stackoverflow.com/a/33762334 + WM_DEVICECHANGE => { + #[allow(clippy::cast_possible_truncation)] + if wparam.0 as u32 == DBT_DEVNODES_CHANGED { + tracing::debug!( + "WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed" + ); + let _ = monitor_reconciliator::event_tx() + .send(monitor_reconciliator::Notification::DisplayConnectionChange); + } + + LRESULT(0) + } + _ => DefWindowProcW(window, message, wparam, lparam), + } + } + } +} diff --git a/komorebi/src/monitor_reconciliator/mod.rs b/komorebi/src/monitor_reconciliator/mod.rs new file mode 100644 index 000000000..7a444bc2d --- /dev/null +++ b/komorebi/src/monitor_reconciliator/mod.rs @@ -0,0 +1,404 @@ +#![deny(clippy::unwrap_used, clippy::expect_used)] + +use crate::border_manager; +use crate::monitor; +use crate::monitor::Monitor; +use crate::monitor_reconciliator::hidden::Hidden; +use crate::MonitorConfig; +use crate::WindowManager; +use crate::WindowsApi; +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use crossbeam_utils::atomic::AtomicConsume; +use komorebi_core::Rect; +use parking_lot::Mutex; +use std::collections::HashMap; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::sync::OnceLock; + +pub mod hidden; + +pub enum Notification { + ResolutionScalingChanged, + WorkAreaChanged, + DisplayConnectionChange, + EnteringSuspendedState, + ResumingFromSuspendedState, + SessionLocked, + SessionUnlocked, +} + +static ACTIVE: AtomicBool = AtomicBool::new(true); + +static CHANNEL: OnceLock<(Sender, Receiver)> = OnceLock::new(); + +static MONITOR_CACHE: OnceLock>> = OnceLock::new(); + +pub fn channel() -> &'static (Sender, Receiver) { + CHANNEL.get_or_init(|| crossbeam_channel::bounded(1)) +} + +pub fn event_tx() -> Sender { + channel().0.clone() +} + +pub fn event_rx() -> Receiver { + channel().1.clone() +} + +pub fn insert_in_monitor_cache(device_id: &str, config: MonitorConfig) { + let mut monitor_cache = MONITOR_CACHE + .get_or_init(|| Mutex::new(HashMap::new())) + .lock(); + + monitor_cache.insert(device_id.to_string(), config); +} + +pub fn attached_display_devices() -> color_eyre::Result> { + Ok(win32_display_data::connected_displays() + .flatten() + .map(|display| { + let path = display.device_path; + let mut split: Vec<_> = path.split('#').collect(); + split.remove(0); + split.remove(split.len() - 1); + let device = split[0].to_string(); + let device_id = split.join("-"); + + let name = display.device_name.trim_start_matches(r"\\.\").to_string(); + let name = name.split('\\').collect::>()[0].to_string(); + + monitor::new( + display.hmonitor, + display.size.into(), + display.work_area_size.into(), + name, + device, + device_id, + ) + }) + .collect::>()) +} +pub fn listen_for_notifications(wm: Arc>) -> color_eyre::Result<()> { + #[allow(clippy::expect_used)] + Hidden::create("komorebi-hidden")?; + + tracing::info!("created hidden window to listen for monitor-related events"); + + std::thread::spawn(move || loop { + match handle_notifications(wm.clone()) { + Ok(()) => { + tracing::warn!("restarting finished thread"); + } + Err(error) => { + if cfg!(debug_assertions) { + tracing::error!("restarting failed thread: {:?}", error) + } else { + tracing::error!("restarting failed thread: {}", error) + } + } + } + }); + + Ok(()) +} +pub fn handle_notifications(wm: Arc>) -> color_eyre::Result<()> { + tracing::info!("listening"); + + let receiver = event_rx(); + + 'receiver: for notification in receiver { + if !ACTIVE.load_consume() { + if matches!( + notification, + Notification::ResumingFromSuspendedState | Notification::SessionUnlocked + ) { + tracing::debug!( + "reactivating reconciliator - system has resumed from suspended state or session has been unlocked" + ); + + ACTIVE.store(true, Ordering::SeqCst); + } + + continue 'receiver; + } + + let mut wm = wm.lock(); + + match notification { + Notification::EnteringSuspendedState | Notification::SessionLocked => { + tracing::debug!( + "deactivating reconciliator until system resumes from suspended state or session is unlocked" + ); + ACTIVE.store(false, Ordering::SeqCst); + } + Notification::ResumingFromSuspendedState | Notification::SessionUnlocked => { + // this is only handled above if the reconciliator is paused + } + Notification::WorkAreaChanged => { + tracing::debug!("handling work area changed notification"); + let offset = wm.work_area_offset; + for monitor in wm.monitors_mut() { + let mut should_update = false; + + // Update work areas as necessary + if let Ok(reference) = WindowsApi::monitor(monitor.id()) { + if reference.work_area_size() != monitor.work_area_size() { + monitor.set_work_area_size(Rect { + left: reference.work_area_size().left, + top: reference.work_area_size().top, + right: reference.work_area_size().right, + bottom: reference.work_area_size().bottom, + }); + + should_update = true; + } + } + + if should_update { + tracing::info!("updated work area for {}", monitor.device_id()); + monitor.update_focused_workspace(offset)?; + border_manager::event_tx().send(border_manager::Notification)?; + } else { + tracing::debug!( + "work areas match, reconciliation not required for {}", + monitor.device_id() + ); + } + } + } + Notification::ResolutionScalingChanged => { + tracing::debug!("handling resolution/scaling changed notification"); + let offset = wm.work_area_offset; + for monitor in wm.monitors_mut() { + let mut should_update = false; + + // Update sizes and work areas as necessary + if let Ok(reference) = WindowsApi::monitor(monitor.id()) { + if reference.work_area_size() != monitor.work_area_size() { + monitor.set_work_area_size(Rect { + left: reference.work_area_size().left, + top: reference.work_area_size().top, + right: reference.work_area_size().right, + bottom: reference.work_area_size().bottom, + }); + + should_update = true; + } + + if reference.size() != monitor.size() { + monitor.set_size(Rect { + left: reference.size().left, + top: reference.size().top, + right: reference.size().right, + bottom: reference.size().bottom, + }); + + should_update = true; + } + } + + if should_update { + tracing::info!( + "updated monitor resolution/scaling for {}", + monitor.device_id() + ); + + monitor.update_focused_workspace(offset)?; + border_manager::event_tx().send(border_manager::Notification)?; + } else { + tracing::debug!( + "resolutions match, reconciliation not required for {}", + monitor.device_id() + ); + } + } + } + Notification::DisplayConnectionChange => { + tracing::debug!("handling display connection change notification"); + let mut monitor_cache = MONITOR_CACHE + .get_or_init(|| Mutex::new(HashMap::new())) + .lock(); + + let initial_monitor_count = wm.monitors().len(); + + // Get the currently attached display devices + let attached_devices = attached_display_devices()?; + + // Make sure that in our state any attached displays have the latest Win32 data + for monitor in wm.monitors_mut() { + for attached in &attached_devices { + if attached.device_id().eq(monitor.device_id()) { + monitor.set_id(attached.id()); + monitor.set_name(attached.name().clone()); + monitor.set_size(*attached.size()); + monitor.set_work_area_size(*attached.work_area_size()); + } + } + } + + if initial_monitor_count == attached_devices.len() { + tracing::debug!("monitor counts match, reconciliation not required"); + continue 'receiver; + } + + if attached_devices.is_empty() { + tracing::debug!( + "no devices found, skipping reconciliation to avoid breaking state" + ); + continue 'receiver; + } + + if initial_monitor_count > attached_devices.len() { + tracing::info!( + "monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors", + attached_devices.len() + ); + + // Gather all the containers that will be orphaned from disconnected and invalid displays + let mut orphaned_containers = vec![]; + + // Collect the ids in our state which aren't in the current attached display ids + // These are monitors that have been removed + let mut newly_removed_displays = vec![]; + + for m in wm.monitors().iter() { + if !attached_devices + .iter() + .any(|attached| attached.device_id().eq(m.device_id())) + { + newly_removed_displays.push(m.device_id().clone()); + for workspace in m.workspaces() { + for container in workspace.containers() { + // Save the orphaned containers from the removed monitor + orphaned_containers.push(container.clone()); + } + } + + // Let's add their state to the cache for later + monitor_cache.insert(m.device_id().clone(), m.into()); + } + } + + if !orphaned_containers.is_empty() { + tracing::info!( + "removed orphaned containers from: {newly_removed_displays:?}" + ); + } + + if !newly_removed_displays.is_empty() { + // After we have cached them, remove them from our state + wm.monitors_mut() + .retain(|m| !newly_removed_displays.contains(m.device_id())); + } + + let post_removal_monitor_count = wm.monitors().len(); + let focused_monitor_idx = wm.focused_monitor_idx(); + if focused_monitor_idx >= post_removal_monitor_count { + wm.focus_monitor(0)?; + } + + if !orphaned_containers.is_empty() { + if let Some(primary) = wm.monitors_mut().front_mut() { + if let Some(focused_ws) = primary.focused_workspace_mut() { + let focused_container_idx = focused_ws.focused_container_idx(); + + // Put the orphaned containers somewhere visible + for container in orphaned_containers { + focused_ws.add_container(container); + } + + // Gotta reset the focus or the movement will feel "off" + if initial_monitor_count != post_removal_monitor_count { + focused_ws.focus_container(focused_container_idx); + } + } + } + } + + let offset = wm.work_area_offset; + + for monitor in wm.monitors_mut() { + // If we have lost a monitor, update everything to filter out any jank + if initial_monitor_count != post_removal_monitor_count { + monitor.update_focused_workspace(offset)?; + } + } + } + + let post_removal_monitor_count = wm.monitors().len(); + + // This is the list of device ids after we have removed detached displays + let post_removal_device_ids = wm + .monitors() + .iter() + .map(Monitor::device_id) + .cloned() + .collect::>(); + + // Check for and add any new monitors that may have been plugged in + // Monitor and display index preferences get applied in this function + WindowsApi::load_monitor_information(&mut wm.monitors)?; + + let post_addition_monitor_count = wm.monitors().len(); + + if post_addition_monitor_count > post_removal_monitor_count { + tracing::info!( + "monitor count mismatch ({post_removal_monitor_count} vs {post_addition_monitor_count}), adding connected monitors", + ); + + // Look in the updated state for new monitors + for m in wm.monitors_mut() { + let device_id = m.device_id().clone(); + // We identify a new monitor when we encounter a new device id + if !post_removal_device_ids.contains(&device_id) { + let mut cache_hit = false; + // Check if that device id exists in the cache for this session + if let Some(cached) = monitor_cache.get(&device_id) { + cache_hit = true; + + tracing::info!("found monitor and workspace configuration for {device_id} in the monitor cache, applying"); + + // If it does, load all the monitor settings from the cache entry + m.ensure_workspace_count(cached.workspaces.len()); + m.set_work_area_offset(cached.work_area_offset); + m.set_window_based_work_area_offset( + cached.window_based_work_area_offset, + ); + m.set_window_based_work_area_offset_limit( + cached.window_based_work_area_offset_limit.unwrap_or(1), + ); + + for (w_idx, workspace) in m.workspaces_mut().iter_mut().enumerate() + { + if let Some(cached_workspace) = cached.workspaces.get(w_idx) { + workspace.load_static_config(cached_workspace)?; + } + } + } + + // Entries in the cache should only be used once; remove the entry there was a cache hit + if cache_hit { + monitor_cache.remove(&device_id); + } + } + } + } + + let final_count = wm.monitors().len(); + + if post_removal_monitor_count != final_count { + wm.retile_all(true)?; + // Second retile to fix DPI/resolution related jank + wm.retile_all(true)?; + // Border updates to fix DPI/resolution related jank + border_manager::event_tx().send(border_manager::Notification)?; + } + } + } + } + + Ok(()) +} diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 34a23b4e8..0a2e99ba0 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -774,13 +774,10 @@ impl WindowManager { SocketMessage::VisibleWindows => { let mut monitor_visible_windows = HashMap::new(); - for (index, monitor) in self.monitors().iter().enumerate() { + for monitor in self.monitors() { if let Some(ws) = monitor.focused_workspace() { monitor_visible_windows.insert( - monitor - .device_id() - .clone() - .unwrap_or_else(|| format!("{index}")), + monitor.device_id().clone(), ws.visible_window_details().clone(), ); } diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 698c58055..821ee4b26 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -74,7 +74,7 @@ impl WindowManager { // All event handlers below this point should only be processed if the event is // related to a window that should be managed by the WindowManager. - if !should_manage && !matches!(event, WindowManagerEvent::DisplayChange(_)) { + if !should_manage { return Ok(()); } @@ -94,31 +94,29 @@ impl WindowManager { match event { WindowManagerEvent::FocusChange(_, window) | WindowManagerEvent::Show(_, window) - | WindowManagerEvent::DisplayChange(window) | WindowManagerEvent::MoveResizeEnd(_, window) => { - self.reconcile_monitors()?; - - let monitor_idx = self.monitor_idx_from_window(window) - .ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?; - - // This is a hidden window apparently associated with COM support mechanisms (based - // on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm) - // - // The hidden window, OLEChannelWnd, associated with this class (spawned by - // explorer.exe), after some debugging, is observed to always be tied to the primary - // display monitor, or (usually) monitor 0 in the WindowManager state. - // - // Due to this, at least one user in the Discord has witnessed behaviour where, when - // a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets - // set repeatedly to 0, regardless of where the current foreground window is actually - // located. - // - // This check ensures that we only update the focused monitor when the window - // triggering monitor reconciliation is known to not be tied to a specific monitor. - if window.class()? != "OleMainThreadWndClass" - && self.focused_monitor_idx() != monitor_idx - { - self.focus_monitor(monitor_idx)?; + if let Some(monitor_idx) = self.monitor_idx_from_window(window) { + // This is a hidden window apparently associated with COM support mechanisms (based + // on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm) + // + // The hidden window, OLEChannelWnd, associated with this class (spawned by + // explorer.exe), after some debugging, is observed to always be tied to the primary + // display monitor, or (usually) monitor 0 in the WindowManager state. + // + // Due to this, at least one user in the Discord has witnessed behaviour where, when + // a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets + // set repeatedly to 0, regardless of where the current foreground window is actually + // located. + // + // This check ensures that we only update the focused monitor when the window + // triggering monitor reconciliation is known to not be tied to a specific monitor. + if let Ok(class) = window.class() { + if class != "OleMainThreadWndClass" + && self.focused_monitor_idx() != monitor_idx + { + self.focus_monitor(monitor_idx)?; + } + } } } _ => {} @@ -606,9 +604,7 @@ impl WindowManager { WindowManagerEvent::ForceUpdate(_) => { self.update_focused_workspace(false, true)?; } - WindowManagerEvent::DisplayChange(..) - | WindowManagerEvent::MouseCapture(..) - | WindowManagerEvent::Cloak(..) => {} + WindowManagerEvent::MouseCapture(..) | WindowManagerEvent::Cloak(..) => {} }; // If we unmanaged a window, it shouldn't be immediately hidden behind managed windows diff --git a/komorebi/src/stackbar.rs b/komorebi/src/stackbar.rs index 5a5d0e167..5d15fa5ee 100644 --- a/komorebi/src/stackbar.rs +++ b/komorebi/src/stackbar.rs @@ -67,7 +67,7 @@ use crate::WINDOWS_BY_BAR_HWNDS; #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct Stackbar { - pub(crate) hwnd: isize, + pub hwnd: isize, } impl Stackbar { diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index b1b338495..4c73b4109 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -5,6 +5,7 @@ use crate::border_manager::Z_ORDER; use crate::colour::Colour; use crate::current_virtual_desktop; use crate::monitor::Monitor; +use crate::monitor_reconciliator; use crate::ring::Ring; use crate::window_manager::WindowManager; use crate::window_manager_event::WindowManagerEvent; @@ -82,7 +83,7 @@ pub struct BorderColours { pub unfocused: Option, } -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct WorkspaceConfig { /// Name pub name: String, @@ -196,7 +197,7 @@ impl From<&Workspace> for WorkspaceConfig { } } -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct MonitorConfig { /// Workspace configurations pub workspaces: Vec, @@ -648,7 +649,6 @@ impl StaticConfig { let mut wm = WindowManager { monitors: Ring::default(), - monitor_cache: HashMap::new(), incoming_events: incoming, command_listener: listener, is_paused: false, @@ -706,6 +706,13 @@ impl StaticConfig { if let Some(monitors) = value.monitors { for (i, monitor) in monitors.iter().enumerate() { + { + let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock(); + if let Some(device_id) = display_index_preferences.get(&i) { + monitor_reconciliator::insert_in_monitor_cache(device_id, monitor.clone()); + } + } + if let Some(m) = wm.monitors_mut().get_mut(i) { m.ensure_workspace_count(monitor.workspaces.len()); m.set_work_area_offset(monitor.work_area_offset); diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 2ac3ba71a..99fcc69ef 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -81,7 +81,6 @@ use komorebi_core::StackbarMode; #[derive(Debug)] pub struct WindowManager { pub monitors: Ring, - pub monitor_cache: HashMap, pub incoming_events: Receiver, pub command_listener: UnixListener, pub is_paused: bool, @@ -262,7 +261,6 @@ impl WindowManager { Ok(Self { monitors: Ring::default(), - monitor_cache: HashMap::new(), incoming_events: incoming, command_listener: listener, is_paused: false, @@ -385,155 +383,6 @@ impl WindowManager { None } - #[tracing::instrument(skip(self))] - pub fn reconcile_monitors(&mut self) -> Result<()> { - if self.pending_move_op.is_some() { - return Ok(()); - } - - let valid_hmonitors = WindowsApi::valid_hmonitors()?; - let mut valid_names = vec![]; - let before_count = self.monitors().len(); - - for monitor in self.monitors_mut() { - for (valid_name, valid_id) in &valid_hmonitors { - let actual_name = monitor.name().clone(); - if actual_name == *valid_name { - monitor.set_id(*valid_id); - valid_names.push(actual_name); - } - } - } - - let mut orphaned_containers = vec![]; - let mut invalid_indices = vec![]; - - for (i, invalid) in self - .monitors() - .iter() - .enumerate() - .filter(|(_, m)| !valid_names.contains(m.name())) - { - invalid_indices.push(i); - for workspace in invalid.workspaces() { - for container in workspace.containers() { - // Save the orphaned containers from an invalid monitor - // (which has most likely been disconnected) - orphaned_containers.push(container.clone()); - } - } - } - - for i in invalid_indices { - if let Some(monitor) = self.monitors().get(i) { - self.monitor_cache.insert(i, monitor.clone()); - } - } - - // Remove any invalid monitors from our state - self.monitors_mut() - .retain(|m| valid_names.contains(m.name())); - - let after_count = self.monitors().len(); - - if let Some(primary) = self.monitors_mut().front_mut() { - if let Some(focused_ws) = primary.focused_workspace_mut() { - let focused_container_idx = focused_ws.focused_container_idx(); - - // Put the orphaned containers somewhere visible - for container in orphaned_containers { - focused_ws.add_container(container); - } - - // Gotta reset the focus or the movement will feel "off" - if before_count != after_count { - focused_ws.focus_container(focused_container_idx); - } - } - } - - let offset = self.work_area_offset; - - for monitor in self.monitors_mut() { - // If we have lost a monitor, update everything to filter out any jank - let mut should_update = before_count != after_count; - - let reference = WindowsApi::monitor(monitor.id())?; - if reference.work_area_size() != monitor.work_area_size() { - monitor.set_work_area_size(Rect { - left: reference.work_area_size().left, - top: reference.work_area_size().top, - right: reference.work_area_size().right, - bottom: reference.work_area_size().bottom, - }); - - should_update = true; - } - - if reference.size() != monitor.size() { - monitor.set_size(Rect { - left: reference.size().left, - top: reference.size().top, - right: reference.size().right, - bottom: reference.size().bottom, - }); - - should_update = true; - } - - if should_update { - monitor.update_focused_workspace(offset)?; - } - } - - #[allow(clippy::needless_collect)] - let old_sizes = self - .monitors() - .iter() - .map(Monitor::size) - .copied() - .collect::>(); - - // Check for and add any new monitors that may have been plugged in - WindowsApi::load_monitor_information(&mut self.monitors)?; - - let mut check_cache = vec![]; - - for (i, m) in self.monitors().iter().enumerate() { - if !old_sizes.contains(m.size()) { - check_cache.push(i); - } - } - - for i in check_cache { - if let Some(cached) = self.monitor_cache.get(&i).cloned() { - if let Some(monitor) = self.monitors_mut().get_mut(i) { - for (w_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() { - if let Some(cached_workspace) = cached.workspaces().get(w_idx) { - workspace.set_layout(cached_workspace.layout().clone()); - workspace.set_layout_rules(cached_workspace.layout_rules().clone()); - workspace.set_layout_flip(cached_workspace.layout_flip()); - workspace.set_workspace_padding(cached_workspace.workspace_padding()); - workspace.set_container_padding(cached_workspace.container_padding()); - } - } - } - } - } - - let final_count = self.monitors().len(); - if after_count != final_count { - self.retile_all(true)?; - // Second retile to fix DPI/resolution related jank when a window - // moves between monitors with different resolutions - this doesn't - // really get seen by the user since the screens are flickering anyway - // as a result of the display connections / disconnections - self.retile_all(true)?; - } - - Ok(()) - } - #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip(self), level = "debug")] fn add_window_handle_to_move_based_on_workspace_rule( @@ -2382,10 +2231,21 @@ impl WindowManager { } } + // our hmonitor might be stale, so if we didn't return above, try querying via the latest + // info taken from win32_display_data and update our hmonitor while we're at it + if let Ok(latest) = WindowsApi::monitor(hmonitor) { + for (i, monitor) in self.monitors_mut().iter_mut().enumerate() { + if monitor.device_id() == latest.device_id() { + monitor.set_id(latest.id()); + return Option::from(i); + } + } + } + None } - pub fn monitor_idx_from_current_pos(&self) -> Option { + pub fn monitor_idx_from_current_pos(&mut self) -> Option { let hmonitor = WindowsApi::monitor_from_point(WindowsApi::cursor_pos().ok()?); for (i, monitor) in self.monitors().iter().enumerate() { @@ -2394,6 +2254,17 @@ impl WindowManager { } } + // our hmonitor might be stale, so if we didn't return above, try querying via the latest + // info taken from win32_display_data and update our hmonitor while we're at it + if let Ok(latest) = WindowsApi::monitor(hmonitor) { + for (i, monitor) in self.monitors_mut().iter_mut().enumerate() { + if monitor.device_id() == latest.device_id() { + monitor.set_id(latest.id()); + return Option::from(i); + } + } + } + None } diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index 2726bc3d6..42d91ae5d 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -27,7 +27,6 @@ pub enum WindowManagerEvent { Manage(Window), Unmanage(Window), Raise(Window), - DisplayChange(Window), ForceUpdate(Window), } @@ -76,9 +75,6 @@ impl Display for WindowManagerEvent { Self::Raise(window) => { write!(f, "Raise (Window: {window})") } - Self::DisplayChange(window) => { - write!(f, "DisplayChange (Window: {window})") - } Self::ForceUpdate(window) => { write!(f, "ForceUpdate (Window: {window})") } @@ -101,7 +97,6 @@ impl WindowManagerEvent { | Self::MouseCapture(_, window) | Self::Raise(window) | Self::Manage(window) - | Self::DisplayChange(window) | Self::Unmanage(window) | Self::ForceUpdate(window) => window, } diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index 01cfca920..38212df1d 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -4,9 +4,9 @@ use std::ffi::c_void; use std::mem::size_of; use color_eyre::eyre::anyhow; +use color_eyre::eyre::bail; use color_eyre::eyre::Error; use color_eyre::Result; -use widestring::U16CStr; use windows::core::Result as WindowsCrateResult; use windows::core::PCWSTR; use windows::core::PWSTR; @@ -30,14 +30,12 @@ use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP; use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED; use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL; use windows::Win32::Graphics::Gdi::CreateSolidBrush; -use windows::Win32::Graphics::Gdi::EnumDisplayDevicesW; use windows::Win32::Graphics::Gdi::EnumDisplayMonitors; use windows::Win32::Graphics::Gdi::GetMonitorInfoW; 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; use windows::Win32::Graphics::Gdi::HMONITOR; @@ -46,6 +44,7 @@ use windows::Win32::Graphics::Gdi::MONITORINFOEXW; use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST; use windows::Win32::System::LibraryLoader::GetModuleHandleW; use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId; +use windows::Win32::System::RemoteDesktop::WTSRegisterSessionNotification; use windows::Win32::System::Threading::GetCurrentProcessId; use windows::Win32::System::Threading::OpenProcess; use windows::Win32::System::Threading::QueryFullProcessImageNameW; @@ -95,7 +94,6 @@ use windows::Win32::UI::WindowsAndMessaging::ShowWindow; use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW; use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint; use windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT; -use windows::Win32::UI::WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME; use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE; use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE; use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT; @@ -140,6 +138,8 @@ use crate::ring::Ring; use crate::set_window_position::SetWindowPosition; use crate::windows_callbacks; use crate::Window; +use crate::DISPLAY_INDEX_PREFERENCES; +use crate::MONITOR_INDEX_PREFERENCES; pub enum WindowsResult { Err(E), @@ -220,58 +220,74 @@ impl WindowsApi { } pub fn valid_hmonitors() -> Result> { - let mut monitors: Vec<(String, isize)> = vec![]; - let monitors_ref: &mut Vec<(String, isize)> = monitors.as_mut(); - Self::enum_display_monitors( - Some(windows_callbacks::valid_display_monitors), - monitors_ref as *mut Vec<(String, isize)> as isize, - )?; + Ok(win32_display_data::connected_displays() + .flatten() + .map(|d| { + let name = d.device_name.trim_start_matches(r"\\.\").to_string(); + let name = name.split('\\').collect::>()[0].to_string(); - Ok(monitors) + (name, d.hmonitor) + }) + .collect::>()) } pub fn load_monitor_information(monitors: &mut Ring) -> Result<()> { - Self::enum_display_monitors( - Some(windows_callbacks::enum_display_monitor), - monitors as *mut Ring as isize, - )?; + 'read: for display in win32_display_data::connected_displays().flatten() { + let path = display.device_path.clone(); + let mut split: Vec<_> = path.split('#').collect(); + split.remove(0); + split.remove(split.len() - 1); + let device = split[0].to_string(); + let device_id = split.join("-"); + + let name = display.device_name.trim_start_matches(r"\\.\").to_string(); + let name = name.split('\\').collect::>()[0].to_string(); + + for monitor in monitors.elements() { + if device_id.eq(monitor.device_id()) { + continue 'read; + } + } - Ok(()) - } + let m = monitor::new( + display.hmonitor, + display.size.into(), + display.work_area_size.into(), + name, + device, + device_id, + ); - pub fn enum_display_devices( - index: u32, - lp_device: Option<*const u16>, - ) -> Result { - #[allow(clippy::option_if_let_else)] - let lp_device = match lp_device { - None => PCWSTR::null(), - Some(lp_device) => PCWSTR(lp_device), - }; + let mut index_preference = None; + let monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock(); + for (index, monitor_size) in &*monitor_index_preferences { + if m.size() == monitor_size { + index_preference = Option::from(index); + } + } - let mut display_device = DISPLAY_DEVICEW { - cb: u32::try_from(std::mem::size_of::())?, - ..Default::default() - }; + let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock(); + for (index, id) in &*display_index_preferences { + if id.eq(m.device_id()) { + index_preference = Option::from(index); + } + } - match unsafe { - EnumDisplayDevicesW( - lp_device, - index, - std::ptr::addr_of_mut!(display_device), - EDD_GET_DEVICE_INTERFACE_NAME, - ) - } - .ok() - { - Ok(()) => {} - Err(error) => { - tracing::error!("enum_display_devices: {}", error); - return Err(error.into()); + if monitors.elements().is_empty() { + monitors.elements_mut().push_back(m); + } else if let Some(preference) = index_preference { + let current_len = monitors.elements().len(); + if *preference > current_len { + monitors.elements_mut().reserve(1); + } + + monitors.elements_mut().insert(*preference, m); + } else { + monitors.elements_mut().push_back(m); } } - Ok(display_device) + Ok(()) } pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> { @@ -775,20 +791,32 @@ impl WindowsApi { } pub fn monitor(hmonitor: isize) -> Result { - let ex_info = Self::monitor_info_w(HMONITOR(hmonitor))?; - let name = U16CStr::from_slice_truncate(&ex_info.szDevice) - .expect("monitor name was not a valid u16 c string") - .to_ustring() - .to_string_lossy() - .trim_start_matches(r"\\.\") - .to_string(); - - Ok(monitor::new( - hmonitor, - ex_info.monitorInfo.rcMonitor.into(), - ex_info.monitorInfo.rcWork.into(), - name, - )) + for display in win32_display_data::connected_displays().flatten() { + if display.hmonitor == hmonitor { + let path = display.device_path; + let mut split: Vec<_> = path.split('#').collect(); + split.remove(0); + split.remove(split.len() - 1); + let device = split[0].to_string(); + let device_id = split.join("-"); + + let name = display.device_name.trim_start_matches(r"\\.\").to_string(); + let name = name.split('\\').collect::>()[0].to_string(); + + let monitor = monitor::new( + hmonitor, + display.size.into(), + display.work_area_size.into(), + name, + device, + device_id, + ); + + return Ok(monitor); + } + } + + bail!("could not find device_id for hmonitor: {hmonitor}"); } pub fn set_process_dpi_awareness_context() -> Result<()> { @@ -1031,4 +1059,8 @@ impl WindowsApi { SendInput(&inputs, std::mem::size_of::() as i32) } } + + pub fn wts_register_session_notification(hwnd: isize) -> Result<()> { + unsafe { WTSRegisterSessionNotification(HWND(hwnd), 1) }.process() + } } diff --git a/komorebi/src/windows_callbacks.rs b/komorebi/src/windows_callbacks.rs index 89ddd5ee8..1eb24085d 100644 --- a/komorebi/src/windows_callbacks.rs +++ b/komorebi/src/windows_callbacks.rs @@ -1,129 +1,17 @@ use std::collections::VecDeque; -use widestring::U16CStr; use windows::Win32::Foundation::BOOL; use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::LPARAM; -use windows::Win32::Foundation::LRESULT; -use windows::Win32::Foundation::RECT; -use windows::Win32::Foundation::WPARAM; -use windows::Win32::Graphics::Gdi::HDC; -use windows::Win32::Graphics::Gdi::HMONITOR; use windows::Win32::UI::Accessibility::HWINEVENTHOOK; -use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW; -use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED; -use windows::Win32::UI::WindowsAndMessaging::SPI_ICONVERTICALSPACING; -use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA; -use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE; -use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE; -use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE; use crate::container::Container; -use crate::monitor::Monitor; -use crate::ring::Ring; use crate::window::RuleDebug; use crate::window::Window; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::winevent::WinEvent; use crate::winevent_listener; -use crate::DISPLAY_INDEX_PREFERENCES; -use crate::MONITOR_INDEX_PREFERENCES; - -pub extern "system" fn valid_display_monitors( - hmonitor: HMONITOR, - _: HDC, - _: *mut RECT, - lparam: LPARAM, -) -> BOOL { - let monitors = unsafe { &mut *(lparam.0 as *mut Vec<(String, isize)>) }; - if let Ok(m) = WindowsApi::monitor(hmonitor.0) { - monitors.push((m.name().to_string(), hmonitor.0)); - } - - true.into() -} - -pub extern "system" fn enum_display_monitor( - hmonitor: HMONITOR, - _: HDC, - _: *mut RECT, - lparam: LPARAM, -) -> BOOL { - let monitors = unsafe { &mut *(lparam.0 as *mut Ring) }; - - // Don't duplicate a monitor that is already being managed - for monitor in monitors.elements() { - if monitor.id() == hmonitor.0 { - return true.into(); - } - } - - let current_index = monitors.elements().len(); - - if let Ok(mut m) = WindowsApi::monitor(hmonitor.0) { - #[allow(clippy::cast_possible_truncation)] - if let Ok(d) = WindowsApi::enum_display_devices(current_index as u32, None) { - let name = U16CStr::from_slice_truncate(d.DeviceName.as_ref()) - .expect("display device name was not a valid u16 c string") - .to_ustring() - .to_string_lossy() - .trim_start_matches(r"\\.\") - .to_string(); - - if name.eq(m.name()) { - if let Ok(device) = WindowsApi::enum_display_devices(0, Some(d.DeviceName.as_ptr())) - { - let id = U16CStr::from_slice_truncate(device.DeviceID.as_ref()) - .expect("display device id was not a valid u16 c string") - .to_ustring() - .to_string_lossy() - .trim_start_matches(r"\\?\") - .to_string(); - - let mut split: Vec<_> = id.split('#').collect(); - split.remove(0); - split.remove(split.len() - 1); - - m.set_device(Option::from(split[0].to_string())); - m.set_device_id(Option::from(split.join("-"))); - } - } - } - - let monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock(); - let mut index_preference = None; - for (index, monitor_size) in &*monitor_index_preferences { - if m.size() == monitor_size { - index_preference = Option::from(index); - } - } - - let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock(); - for (index, device) in &*display_index_preferences { - if let Some(known_device) = m.device_id() { - if device == known_device { - index_preference = Option::from(index); - } - } - } - - if monitors.elements().is_empty() { - monitors.elements_mut().push_back(m); - } else if let Some(preference) = index_preference { - let current_len = monitors.elements().len(); - if *preference > current_len { - monitors.elements_mut().reserve(1); - } - - monitors.elements_mut().insert(*preference, m); - } else { - monitors.elements_mut().push_back(m); - } - } - - true.into() -} pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL { let containers = unsafe { &mut *(lparam.0 as *mut VecDeque) }; @@ -201,48 +89,3 @@ pub extern "system" fn win_event_hook( .send(event_type) .expect("could not send message on winevent_listener::event_tx"); } - -pub extern "system" fn hidden_window( - window: HWND, - message: u32, - wparam: WPARAM, - lparam: LPARAM, -) -> LRESULT { - unsafe { - match message { - WM_DISPLAYCHANGE => { - let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 }); - winevent_listener::event_tx() - .send(event_type) - .expect("could not send message on winevent_listener::event_tx"); - - LRESULT(0) - } - // Added based on this https://stackoverflow.com/a/33762334 - WM_SETTINGCHANGE => { - #[allow(clippy::cast_possible_truncation)] - if wparam.0 as u32 == SPI_SETWORKAREA.0 - || wparam.0 as u32 == SPI_ICONVERTICALSPACING.0 - { - let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 }); - winevent_listener::event_tx() - .send(event_type) - .expect("could not send message on winevent_listener::event_tx"); - } - LRESULT(0) - } - // Added based on this https://stackoverflow.com/a/33762334 - WM_DEVICECHANGE => { - #[allow(clippy::cast_possible_truncation)] - if wparam.0 as u32 == DBT_DEVNODES_CHANGED { - let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 }); - winevent_listener::event_tx() - .send(event_type) - .expect("could not send message on winevent_listener::event_tx"); - } - LRESULT(0) - } - _ => DefWindowProcW(window, message, wparam, lparam), - } - } -}