Skip to main content

tauri_plugin_positioner/
ext.rs

1// Copyright 2021 Jonas Kruckenberg
2// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6#[cfg(feature = "tray-icon")]
7use crate::Tray;
8use serde_repr::Deserialize_repr;
9#[cfg(feature = "tray-icon")]
10use tauri::Manager;
11#[cfg(feature = "tray-icon")]
12use tauri::Monitor;
13use tauri::{PhysicalPosition, PhysicalSize, Result, Runtime, WebviewWindow, Window};
14
15/// Well known window positions.
16#[derive(Debug, Deserialize_repr)]
17#[repr(u16)]
18pub enum Position {
19    TopLeft = 0,
20    TopRight,
21    BottomLeft,
22    BottomRight,
23    TopCenter,
24    BottomCenter,
25    LeftCenter,
26    RightCenter,
27    Center,
28    #[cfg(feature = "tray-icon")]
29    TrayLeft,
30    #[cfg(feature = "tray-icon")]
31    TrayBottomLeft,
32    #[cfg(feature = "tray-icon")]
33    TrayRight,
34    #[cfg(feature = "tray-icon")]
35    TrayBottomRight,
36    #[cfg(feature = "tray-icon")]
37    TrayCenter,
38    #[cfg(feature = "tray-icon")]
39    TrayBottomCenter,
40}
41
42/// A [`Window`] extension that provides extra methods related to positioning.
43pub trait WindowExt {
44    /// Moves the [`Window`] to the given [`Position`]
45    ///
46    /// All (non-tray) positions are relative to the **current** screen.
47    fn move_window(&self, position: Position) -> Result<()>;
48    #[cfg(feature = "tray-icon")]
49    /// Moves the [`Window`] to the given [`Position`] while constraining Tray Positions to the dimensions of the screen.
50    ///
51    /// All non-tray positions will not be constrained by this method.
52    ///
53    /// This method allows you to position your Tray Windows without having them
54    /// cut off on the screen borders.
55    fn move_window_constrained(&self, position: Position) -> Result<()>;
56}
57
58impl<R: Runtime> WindowExt for WebviewWindow<R> {
59    fn move_window(&self, pos: Position) -> Result<()> {
60        self.as_ref().window().move_window(pos)
61    }
62
63    #[cfg(feature = "tray-icon")]
64    fn move_window_constrained(&self, position: Position) -> Result<()> {
65        self.as_ref().window().move_window_constrained(position)
66    }
67}
68
69impl<R: Runtime> WindowExt for Window<R> {
70    #[cfg(feature = "tray-icon")]
71    fn move_window_constrained(&self, position: Position) -> Result<()> {
72        // Diverge to basic move_window, if the position is not a tray position
73        if !matches!(
74            position,
75            Position::TrayLeft
76                | Position::TrayBottomLeft
77                | Position::TrayRight
78                | Position::TrayBottomRight
79                | Position::TrayCenter
80                | Position::TrayBottomCenter
81        ) {
82            return self.move_window(position);
83        }
84
85        let window_position = calculate_position(self, position)?;
86        let monitor = get_monitor_for_tray_icon(self)?;
87        if let Some(monitor) = monitor {
88            let monitor_size = monitor.size();
89            let monitor_position = monitor.position();
90            let window_size = self.outer_size()?;
91
92            let right_border_monitor = monitor_position.x as f64 + monitor_size.width as f64;
93            let left_border_monitor = monitor_position.x as f64;
94            let right_border_window = window_position.x as f64 + window_size.width as f64;
95            let left_border_window = window_position.x as f64;
96
97            let constrained_x = if left_border_window < left_border_monitor {
98                left_border_monitor
99            } else if right_border_window > right_border_monitor {
100                right_border_monitor - window_size.width as f64
101            } else {
102                window_position.x as f64
103            };
104
105            let bottom_border_monitor = monitor_position.y as f64 + monitor_size.height as f64;
106            let top_border_monitor = monitor_position.y as f64;
107            let bottom_border_window = window_position.y as f64 + window_size.height as f64;
108            let top_border_window = window_position.y as f64;
109
110            let constrained_y = if top_border_window < top_border_monitor {
111                top_border_monitor
112            } else if bottom_border_window > bottom_border_monitor {
113                bottom_border_monitor - window_size.height as f64
114            } else {
115                window_position.y as f64
116            };
117
118            self.set_position(PhysicalPosition::new(constrained_x, constrained_y))?;
119        } else {
120            // Fallback on non constrained positioning
121            self.set_position(window_position)?;
122        }
123
124        Ok(())
125    }
126
127    fn move_window(&self, pos: Position) -> Result<()> {
128        let position = calculate_position(self, pos)?;
129        self.set_position(position)
130    }
131}
132
133#[cfg(feature = "tray-icon")]
134/// Retrieve the monitor, where the tray icon is located on.
135fn get_monitor_for_tray_icon<R: Runtime>(window: &Window<R>) -> Result<Option<Monitor>> {
136    let tray_position = window
137        .state::<Tray>()
138        .0
139        .lock()
140        .unwrap()
141        .map(|(pos, _)| pos)
142        .unwrap_or_default();
143
144    window.monitor_from_point(tray_position.x, tray_position.y)
145}
146
147/// Calculate the top-left position of the window based on the given
148/// [`Position`].
149fn calculate_position<R: Runtime>(
150    window: &Window<R>,
151    pos: Position,
152) -> Result<PhysicalPosition<i32>> {
153    use Position::*;
154
155    let screen = window.current_monitor()?.ok_or_else(|| {
156        tauri::Error::Io(std::io::Error::other("No monitor found for the window"))
157    })?;
158    // Only use the screen_position for the Tray independent positioning,
159    // because a tray event may not be called on the currently active monitor.
160    let screen_position = screen.position();
161    let screen_size = PhysicalSize::<i32> {
162        width: screen.size().width as i32,
163        height: screen.size().height as i32,
164    };
165    let window_size = PhysicalSize::<i32> {
166        width: window.outer_size()?.width as i32,
167        height: window.outer_size()?.height as i32,
168    };
169    #[cfg(feature = "tray-icon")]
170    let (tray_position, tray_size) = window
171        .state::<Tray>()
172        .0
173        .lock()
174        .unwrap()
175        .map(|(pos, size)| {
176            (
177                Some((pos.x as i32, pos.y as i32)),
178                Some((size.width as i32, size.height as i32)),
179            )
180        })
181        .unwrap_or_default();
182
183    let physical_pos = match pos {
184        TopLeft => *screen_position,
185        TopRight => PhysicalPosition {
186            x: screen_position.x + (screen_size.width - window_size.width),
187            y: screen_position.y,
188        },
189        BottomLeft => PhysicalPosition {
190            x: screen_position.x,
191            y: screen_size.height - (window_size.height - screen_position.y),
192        },
193        BottomRight => PhysicalPosition {
194            x: screen_position.x + (screen_size.width - window_size.width),
195            y: screen_size.height - (window_size.height - screen_position.y),
196        },
197        TopCenter => PhysicalPosition {
198            x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
199            y: screen_position.y,
200        },
201        BottomCenter => PhysicalPosition {
202            x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
203            y: screen_size.height - (window_size.height - screen_position.y),
204        },
205        LeftCenter => PhysicalPosition {
206            x: screen_position.x,
207            y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
208        },
209        RightCenter => PhysicalPosition {
210            x: screen_position.x + (screen_size.width - window_size.width),
211            y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
212        },
213        Center => PhysicalPosition {
214            x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
215            y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
216        },
217        #[cfg(feature = "tray-icon")]
218        TrayLeft => {
219            if let (Some((tray_x, tray_y)), Some((_, _tray_height))) = (tray_position, tray_size) {
220                let y = tray_y - window_size.height;
221                // Choose y value based on the target OS
222                #[cfg(target_os = "windows")]
223                let y = if y < 0 { tray_y + _tray_height } else { y };
224
225                #[cfg(target_os = "macos")]
226                let y = if y < 0 { tray_y } else { y };
227
228                PhysicalPosition { x: tray_x, y }
229            } else {
230                return Err(tauri::Error::Io(std::io::Error::other(
231                    "Tray position not set",
232                )));
233            }
234        }
235        #[cfg(feature = "tray-icon")]
236        TrayBottomLeft => {
237            if let Some((tray_x, tray_y)) = tray_position {
238                PhysicalPosition {
239                    x: tray_x,
240                    y: tray_y,
241                }
242            } else {
243                return Err(tauri::Error::Io(std::io::Error::other(
244                    "Tray position not set",
245                )));
246            }
247        }
248        #[cfg(feature = "tray-icon")]
249        TrayRight => {
250            if let (Some((tray_x, tray_y)), Some((tray_width, _tray_height))) =
251                (tray_position, tray_size)
252            {
253                let y = tray_y - window_size.height;
254                // Choose y value based on the target OS
255                #[cfg(target_os = "windows")]
256                let y = if y < 0 { tray_y + _tray_height } else { y };
257
258                #[cfg(target_os = "macos")]
259                let y = if y < 0 { tray_y } else { y };
260
261                PhysicalPosition {
262                    x: tray_x + tray_width,
263                    y,
264                }
265            } else {
266                return Err(tauri::Error::Io(std::io::Error::other(
267                    "Tray position not set",
268                )));
269            }
270        }
271        #[cfg(feature = "tray-icon")]
272        TrayBottomRight => {
273            if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size) {
274                PhysicalPosition {
275                    x: tray_x + tray_width,
276                    y: tray_y,
277                }
278            } else {
279                return Err(tauri::Error::Io(std::io::Error::other(
280                    "Tray position not set",
281                )));
282            }
283        }
284        #[cfg(feature = "tray-icon")]
285        TrayCenter => {
286            if let (Some((tray_x, tray_y)), Some((tray_width, _tray_height))) =
287                (tray_position, tray_size)
288            {
289                let x = tray_x + tray_width / 2 - window_size.width / 2;
290                let y = tray_y - window_size.height;
291                // Choose y value based on the target OS
292                #[cfg(target_os = "windows")]
293                let y = if y < 0 { tray_y + _tray_height } else { y };
294
295                #[cfg(target_os = "macos")]
296                let y = if y < 0 { tray_y } else { y };
297
298                PhysicalPosition { x, y }
299            } else {
300                return Err(tauri::Error::Io(std::io::Error::other(
301                    "Tray position not set",
302                )));
303            }
304        }
305        #[cfg(feature = "tray-icon")]
306        TrayBottomCenter => {
307            if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size) {
308                PhysicalPosition {
309                    x: tray_x + (tray_width / 2) - (window_size.width / 2),
310                    y: tray_y,
311                }
312            } else {
313                return Err(tauri::Error::Io(std::io::Error::other(
314                    "Tray position not set",
315                )));
316            }
317        }
318    };
319
320    Ok(physical_pos)
321}