Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ pub struct PlacementOverride {
pub nest: String,
#[serde(default)]
pub monitor_index: usize,
/// GDI device name (e.g. `\\.\DISPLAY2`) of the saved monitor. Enumeration
/// order can change between boots, so this wins over `monitor_index`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub monitor_device: Option<String>,
#[serde(default)]
pub screen_x: i32,
#[serde(default)]
Expand Down
18 changes: 17 additions & 1 deletion src/native_interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use windows::core::BOOL;
use windows::core::PCWSTR;
use windows::Win32::Foundation::{HWND, LPARAM, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFO,
EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW,
};
use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK};
use windows::Win32::UI::Shell::ShellExecuteW;
Expand Down Expand Up @@ -128,6 +128,22 @@ pub fn find_monitors() -> Vec<DisplayMonitor> {
result
}

/// GDI device name of a monitor (e.g. `\\.\DISPLAY2`), stable across boots
/// unlike its position in `find_monitors`.
pub fn monitor_device_name(monitor: HMONITOR) -> Option<String> {
let mut info = MONITORINFOEXW::default();
info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
if !unsafe { GetMonitorInfoW(monitor, &mut info.monitorInfo).as_bool() } {
return None;
}
let len = info
.szDevice
.iter()
.position(|&c| c == 0)
.unwrap_or(info.szDevice.len());
(len > 0).then(|| String::from_utf16_lossy(&info.szDevice[..len]))
}

pub fn find_taskbars() -> Vec<TaskbarWindow> {
unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
let taskbars = &mut *(lparam.0 as *mut Vec<TaskbarWindow>);
Expand Down
16 changes: 10 additions & 6 deletions src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ fn theme_with_placement(state: &AppState, auto_ejected: bool) -> Option<ThemeDoc
.filter(|p| p.nest == "floating")
.map(|p| {
(
Some(p.monitor_index),
Some((p.monitor_index, p.monitor_device.clone())),
POINT {
x: p.screen_x,
y: p.screen_y,
Expand All @@ -655,6 +655,9 @@ fn theme_with_placement(state: &AppState, auto_ejected: bool) -> Option<ThemeDoc
if let Some((saved_monitor, point)) = floating {
let displays = native_interop::find_monitors();
let selected = saved_monitor
.and_then(|(index, device)| {
positioning::resolve_saved_monitor(&displays, index, device.as_deref())
})
.and_then(|index| displays.get(index).copied().map(|d| (index, d)))
.unwrap_or_else(|| positioning::monitor_for_point(&displays, point));
apply_floating_position(&mut theme, state, selected.0, selected.1, point);
Expand All @@ -664,11 +667,12 @@ fn theme_with_placement(state: &AppState, auto_ejected: bool) -> Option<ThemeDoc
.filter(|p| p.nest == "taskbar")
{
let displays = native_interop::find_monitors();
let index = if p.monitor_index < displays.len() {
p.monitor_index
} else {
0
};
let index = positioning::resolve_saved_monitor(
&displays,
p.monitor_index,
p.monitor_device.as_deref(),
)
.unwrap_or(0);
let horizontal = taskbar_is_horizontal(index);
let placement = if p.screen_x > 0 || p.screen_y > 0 {
let offset = if horizontal { p.screen_x } else { p.screen_y };
Expand Down
2 changes: 2 additions & 0 deletions src/window/message_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ pub(super) unsafe extern "system" fn wnd_proc(
s.placement_override = Some(PlacementOverride {
nest: "taskbar".into(),
monitor_index,
monitor_device: native_interop::monitor_device_name(handle),
screen_x,
screen_y,
tray_offset,
Expand Down Expand Up @@ -590,6 +591,7 @@ pub(super) unsafe extern "system" fn wnd_proc(
s.placement_override = Some(PlacementOverride {
nest: "floating".into(),
monitor_index: monitor_idx,
monitor_device: native_interop::monitor_device_name(display.handle),
screen_x: clamped_x,
screen_y: clamped_y,
tray_offset: 0,
Expand Down
1 change: 1 addition & 0 deletions src/window/placement_regression_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ fn placement(nest: &str) -> PlacementOverride {
PlacementOverride {
nest: nest.into(),
monitor_index: 0,
monitor_device: None,
screen_x: 0,
screen_y: 0,
tray_offset: 0,
Expand Down
1 change: 1 addition & 0 deletions src/window/placement_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ fn test_placement_override_serialization_and_normalization() {
let ov = app_settings::PlacementOverride {
nest: "floating".into(),
monitor_index: 1,
monitor_device: None,
screen_x: 250,
screen_y: 120,
tray_offset: 0,
Expand Down
63 changes: 63 additions & 0 deletions src/window/positioning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,34 @@ pub(super) fn monitor_index_for_handle(
displays.iter().position(|display| display.handle == handle)
}

/// Resolve a saved monitor: the device name wins, then the saved index. A
/// named monitor that is absent (e.g. not enumerated yet during boot) yields
/// None so callers fall back without the index silently picking another one.
pub(super) fn resolve_saved_monitor(
displays: &[native_interop::DisplayMonitor],
index: usize,
device: Option<&str>,
) -> Option<usize> {
let names: Vec<Option<String>> = displays
.iter()
.map(|display| native_interop::monitor_device_name(display.handle))
.collect();
resolve_saved_monitor_in(&names, index, device)
}

fn resolve_saved_monitor_in(
names: &[Option<String>],
index: usize,
device: Option<&str>,
) -> Option<usize> {
match device {
Some(device) => names
.iter()
.position(|name| name.as_deref() == Some(device)),
None => (index < names.len()).then_some(index),
}
}

pub(super) fn override_primary_placement(
theme: &mut ThemeDocument,
placement: theme_engine::Placement,
Expand Down Expand Up @@ -1064,3 +1092,38 @@ pub(super) fn taskbar_free_dock_slot(
.total_cmp(&calculate_rect_overlap_ratio(widget, *b))
})
}

#[cfg(test)]
mod resolve_saved_monitor_tests {
use super::resolve_saved_monitor_in;

fn names(list: &[&str]) -> Vec<Option<String>> {
list.iter().map(|name| Some((*name).to_string())).collect()
}

#[test]
fn device_name_follows_the_monitor_when_enumeration_order_changes() {
// Saved on DISPLAY2 at index 1; a third monitor now sorts before it.
let displays = names(&[r"\\.\DISPLAY1", r"\\.\DISPLAY3", r"\\.\DISPLAY2"]);
assert_eq!(
resolve_saved_monitor_in(&displays, 1, Some(r"\\.\DISPLAY2")),
Some(2)
);
}

#[test]
fn absent_named_monitor_does_not_fall_back_to_the_stale_index() {
let displays = names(&[r"\\.\DISPLAY1", r"\\.\DISPLAY3"]);
assert_eq!(
resolve_saved_monitor_in(&displays, 1, Some(r"\\.\DISPLAY2")),
None
);
}

#[test]
fn placements_saved_without_a_device_keep_using_the_index() {
let displays = names(&[r"\\.\DISPLAY1", r"\\.\DISPLAY2"]);
assert_eq!(resolve_saved_monitor_in(&displays, 1, None), Some(1));
assert_eq!(resolve_saved_monitor_in(&displays, 5, None), None);
}
}