Skip to content

Commit 400d5b9

Browse files
authored
fix(window): handle window movement across monitors with different scaling (#3675)
1 parent 512aab5 commit 400d5b9

File tree

5 files changed

+289
-4
lines changed

5 files changed

+289
-4
lines changed

backend/tauri/src/ipc.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,38 @@ pub async fn get_clash_ws_connections_state(
985985
Ok(ws_connector.state())
986986
}
987987

988+
/// Move window to another monitor
989+
#[tauri::command]
990+
#[specta::specta]
991+
pub fn move_window_to_other_monitor(
992+
window: tauri::Window,
993+
target_monitor_index: usize,
994+
) -> Result<()> {
995+
crate::utils::window_manager::move_window_to_other_monitor(window, target_monitor_index)?;
996+
Ok(())
997+
}
998+
999+
/// Center window on current monitor
1000+
#[tauri::command]
1001+
#[specta::specta]
1002+
pub fn center_window(window: tauri::Window) -> Result<()> {
1003+
crate::utils::window_manager::center_window(&window)?;
1004+
Ok(())
1005+
}
1006+
1007+
/// Get available monitors
1008+
#[tauri::command]
1009+
#[specta::specta]
1010+
pub fn get_available_monitors(
1011+
window: tauri::Window,
1012+
) -> Result<Vec<crate::utils::window_manager::MonitorInfo>> {
1013+
let monitors = crate::utils::window_manager::get_available_monitors(window)?;
1014+
Ok(monitors
1015+
.iter()
1016+
.map(|(id, monitor)| ((*id, monitor)).into())
1017+
.collect())
1018+
}
1019+
9881020
// Updater block
9891021

9901022
#[derive(Default, Clone, serde::Serialize, serde::Deserialize, specta::Type)]

backend/tauri/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ pub fn run() -> std::io::Result<()> {
272272
ipc::get_core_dir,
273273
// clash layer
274274
ipc::get_clash_ws_connections_state,
275+
// window management
276+
ipc::move_window_to_other_monitor,
277+
ipc::center_window,
278+
ipc::get_available_monitors,
275279
// updater layer
276280
ipc::check_update,
277281
])

backend/tauri/src/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod open;
1717

1818
pub mod dock;
1919
pub mod sudo;
20+
pub mod window_manager;
2021

2122
#[cfg(test)]
2223
#[cfg(windows)]
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
//! Window management utilities for multi-monitor setups with different scaling factors
2+
use display_info::DisplayInfo;
3+
use serde::{Deserialize, Serialize};
4+
use specta::Type;
5+
use tauri::{Monitor, PhysicalPosition, PhysicalSize, Position, Size, Window};
6+
7+
/// Simplified monitor information for IPC
8+
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
9+
pub struct MonitorInfo {
10+
pub id: usize,
11+
pub name: String,
12+
pub position: (i32, i32),
13+
pub size: (u32, u32),
14+
pub scale_factor: f64,
15+
}
16+
17+
impl From<(usize, &Monitor)> for MonitorInfo {
18+
fn from((id, monitor): (usize, &Monitor)) -> Self {
19+
let position = monitor.position();
20+
let size = monitor.size();
21+
Self {
22+
id,
23+
name: format!("Monitor {}", id),
24+
position: (position.x, position.y),
25+
size: (size.width, size.height),
26+
scale_factor: monitor.scale_factor(),
27+
}
28+
}
29+
}
30+
31+
/// Get the scale factor for a specific monitor
32+
fn get_monitor_scale_factor(monitor: &Monitor) -> f64 {
33+
// Try to get the scale factor from the display info
34+
if let Ok(displays) = DisplayInfo::all() {
35+
for display in displays {
36+
// Match the monitor by position and size
37+
let monitor_pos = monitor.position();
38+
let monitor_size = monitor.size();
39+
40+
if display.x == monitor_pos.x
41+
&& display.y == monitor_pos.y
42+
&& display.width as u32 == monitor_size.width
43+
&& display.height as u32 == monitor_size.height
44+
{
45+
return display.scale_factor as f64;
46+
}
47+
}
48+
}
49+
50+
// Fallback to the monitor's scale factor if we can't find it in display info
51+
monitor.scale_factor()
52+
}
53+
54+
/// Move window to another monitor while correctly handling different scaling factors
55+
pub fn move_window_to_other_monitor(
56+
window: Window,
57+
target_monitor_index: usize,
58+
) -> tauri::Result<()> {
59+
let monitors = get_available_monitors(window.clone())?;
60+
61+
let (_index, target_monitor) =
62+
monitors
63+
.get(target_monitor_index)
64+
.ok_or(tauri::Error::InvalidArgs(
65+
"target_monitor_index",
66+
"Index out of bounds",
67+
serde_json::Error::io(std::io::Error::new(
68+
std::io::ErrorKind::InvalidInput,
69+
"Index out of bounds",
70+
)),
71+
))?;
72+
73+
// Get current monitor
74+
let current_monitor = window
75+
.current_monitor()?
76+
.unwrap_or_else(|| target_monitor.clone());
77+
78+
// Get scale factors for current and target monitors
79+
let current_scale_factor = get_monitor_scale_factor(&current_monitor);
80+
let target_scale_factor = get_monitor_scale_factor(target_monitor);
81+
82+
// Get current window size
83+
let window_size = window.outer_size()?;
84+
85+
// Calculate scaled size for target monitor
86+
let scale_ratio = current_scale_factor / target_scale_factor;
87+
let target_width = (window_size.width as f64 * scale_ratio).round() as u32;
88+
let target_height = (window_size.height as f64 * scale_ratio).round() as u32;
89+
90+
// Set window size first to prevent flickering
91+
window.set_size(Size::Physical(PhysicalSize {
92+
width: target_width,
93+
height: target_height,
94+
}))?;
95+
96+
// Move window to target monitor position
97+
let pos = target_monitor.position();
98+
window.set_position(Position::Physical(PhysicalPosition { x: pos.x, y: pos.y }))?;
99+
100+
Ok(())
101+
}
102+
103+
/// Resize window while correctly handling monitor scaling factors
104+
fn resize_window(window: &Window, screen_share: f64) -> tauri::Result<()> {
105+
let monitor = window.current_monitor().unwrap().unwrap();
106+
let monitor_size = monitor.size();
107+
108+
// Get the monitor's scale factor
109+
let scale_factor = get_monitor_scale_factor(&monitor);
110+
111+
// Calculate size accounting for scale factor
112+
let scaled_size: PhysicalSize<u32> = PhysicalSize {
113+
width: ((monitor_size.width as f64 * screen_share) / scale_factor).round() as u32,
114+
height: ((monitor_size.height as f64 * screen_share) / scale_factor).round() as u32,
115+
};
116+
117+
window.set_size(Size::Physical(scaled_size))?;
118+
Ok(())
119+
}
120+
121+
/// Center window on current monitor while correctly handling scaling factors
122+
#[cfg(windows)]
123+
pub fn center_window(window: &Window) -> tauri::Result<()> {
124+
use windows_sys::Win32::{
125+
Foundation::RECT,
126+
UI::WindowsAndMessaging::{SPI_GETWORKAREA, SystemParametersInfoW},
127+
};
128+
129+
// Get current monitor
130+
let monitor = window.current_monitor()?.ok_or(tauri::Error::InvalidArgs(
131+
"current_monitor",
132+
"No current monitor",
133+
serde_json::Error::io(std::io::Error::new(
134+
std::io::ErrorKind::InvalidInput,
135+
"No current monitor",
136+
)),
137+
))?;
138+
let scale_factor = get_monitor_scale_factor(&monitor);
139+
140+
// Get work area
141+
let mut work_area = RECT {
142+
left: 0,
143+
top: 0,
144+
right: 0,
145+
bottom: 0,
146+
};
147+
unsafe {
148+
SystemParametersInfoW(SPI_GETWORKAREA, 0, &mut work_area as *mut _ as *mut _, 0);
149+
}
150+
151+
let work_area_width = (work_area.right - work_area.left) as u32;
152+
let work_area_height = (work_area.bottom - work_area.top) as u32;
153+
let work_area_x = work_area.left as i32;
154+
let work_area_y = work_area.top as i32;
155+
156+
let window_size = window.outer_size()?;
157+
158+
// Adjust for scale factor
159+
let adjusted_window_width = (window_size.width as f64 / scale_factor).round() as i32;
160+
let adjusted_window_height = (window_size.height as f64 / scale_factor).round() as i32;
161+
162+
let new_x = work_area_x + (work_area_width as i32 - adjusted_window_width) / 2;
163+
let new_y = work_area_y + (work_area_height as i32 - adjusted_window_height) / 2;
164+
165+
window.set_position(Position::Physical(PhysicalPosition { x: new_x, y: new_y }))?;
166+
Ok(())
167+
}
168+
169+
#[cfg(target_os = "macos")]
170+
pub fn center_window(window: &Window) -> tauri::Result<()> {
171+
window.center();
172+
Ok(())
173+
}
174+
175+
/// Get available monitors sorted by position
176+
pub fn get_available_monitors(
177+
window: tauri::Window,
178+
) -> tauri::Result<Vec<(usize, tauri::Monitor)>> {
179+
let mut monitors = window.available_monitors()?;
180+
monitors.sort_by(|a, b| {
181+
let a_pos = a.position();
182+
let b_pos = b.position();
183+
let a_size = a.size();
184+
let b_size = b.size();
185+
let a_val =
186+
(a_pos.y + 200) * 10 / a_size.height as i32 + (a_pos.x + 300) / a_size.width as i32;
187+
let b_val =
188+
(b_pos.y + 200) * 10 / b_size.height as i32 + (b_pos.x + 300) / b_size.width as i32;
189+
190+
a_val.cmp(&b_val)
191+
});
192+
193+
monitors
194+
.iter()
195+
.enumerate()
196+
.map(|(i, m)| Ok((i, m.clone())))
197+
.collect()
198+
}

frontend/interface/src/ipc/bindings.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
/** tauri-specta globals **/
22

3-
import {
4-
Channel as TAURI_CHANNEL,
5-
invoke as TAURI_INVOKE,
6-
} from '@tauri-apps/api/core'
3+
import { invoke as TAURI_INVOKE } from '@tauri-apps/api/core'
74
import * as TAURI_API_EVENT from '@tauri-apps/api/event'
85
import { type WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow'
96

@@ -720,6 +717,49 @@ export const commands = {
720717
else return { status: 'error', error: e as any }
721718
}
722719
},
720+
/**
721+
* Move window to another monitor
722+
*/
723+
async moveWindowToOtherMonitor(
724+
targetMonitorIndex: number,
725+
): Promise<Result<null, string>> {
726+
try {
727+
return {
728+
status: 'ok',
729+
data: await TAURI_INVOKE('move_window_to_other_monitor', {
730+
targetMonitorIndex,
731+
}),
732+
}
733+
} catch (e) {
734+
if (e instanceof Error) throw e
735+
else return { status: 'error', error: e as any }
736+
}
737+
},
738+
/**
739+
* Center window on current monitor
740+
*/
741+
async centerWindow(): Promise<Result<null, string>> {
742+
try {
743+
return { status: 'ok', data: await TAURI_INVOKE('center_window') }
744+
} catch (e) {
745+
if (e instanceof Error) throw e
746+
else return { status: 'error', error: e as any }
747+
}
748+
},
749+
/**
750+
* Get available monitors
751+
*/
752+
async getAvailableMonitors(): Promise<Result<MonitorInfo[], string>> {
753+
try {
754+
return {
755+
status: 'ok',
756+
data: await TAURI_INVOKE('get_available_monitors'),
757+
}
758+
} catch (e) {
759+
if (e instanceof Error) throw e
760+
else return { status: 'error', error: e as any }
761+
}
762+
},
723763
async checkUpdate(): Promise<Result<UpdateWrapper | null, string>> {
724764
try {
725765
return { status: 'ok', data: await TAURI_INVOKE('check_update') }
@@ -1168,6 +1208,16 @@ export type MergeProfileBuilder = {
11681208
*/
11691209
updated: number | null
11701210
}
1211+
/**
1212+
* Simplified monitor information for IPC
1213+
*/
1214+
export type MonitorInfo = {
1215+
id: number
1216+
name: string
1217+
position: [number, number]
1218+
size: [number, number]
1219+
scale_factor: number
1220+
}
11711221
export type NetworkStatisticWidgetConfig =
11721222
| { kind: 'disabled' }
11731223
| { kind: 'enabled'; value: StatisticWidgetVariant }

0 commit comments

Comments
 (0)