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()?.unwrap();
156    // Only use the screen_position for the Tray independent positioning,
157    // because a tray event may not be called on the currently active monitor.
158    let screen_position = screen.position();
159    let screen_size = PhysicalSize::<i32> {
160        width: screen.size().width as i32,
161        height: screen.size().height as i32,
162    };
163    let window_size = PhysicalSize::<i32> {
164        width: window.outer_size()?.width as i32,
165        height: window.outer_size()?.height as i32,
166    };
167    #[cfg(feature = "tray-icon")]
168    let (tray_position, tray_size) = window
169        .state::<Tray>()
170        .0
171        .lock()
172        .unwrap()
173        .map(|(pos, size)| {
174            (
175                Some((pos.x as i32, pos.y as i32)),
176                Some((size.width as i32, size.height as i32)),
177            )
178        })
179        .unwrap_or_default();
180
181    let physical_pos = match pos {
182        TopLeft => *screen_position,
183        TopRight => PhysicalPosition {
184            x: screen_position.x + (screen_size.width - window_size.width),
185            y: screen_position.y,
186        },
187        BottomLeft => PhysicalPosition {
188            x: screen_position.x,
189            y: screen_size.height - (window_size.height - screen_position.y),
190        },
191        BottomRight => PhysicalPosition {
192            x: screen_position.x + (screen_size.width - window_size.width),
193            y: screen_size.height - (window_size.height - screen_position.y),
194        },
195        TopCenter => PhysicalPosition {
196            x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
197            y: screen_position.y,
198        },
199        BottomCenter => PhysicalPosition {
200            x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
201            y: screen_size.height - (window_size.height - screen_position.y),
202        },
203        LeftCenter => PhysicalPosition {
204            x: screen_position.x,
205            y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
206        },
207        RightCenter => PhysicalPosition {
208            x: screen_position.x + (screen_size.width - window_size.width),
209            y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
210        },
211        Center => PhysicalPosition {
212            x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
213            y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
214        },
215        #[cfg(feature = "tray-icon")]
216        TrayLeft => {
217            if let (Some((tray_x, tray_y)), Some((_, _tray_height))) = (tray_position, tray_size) {
218                let y = tray_y - window_size.height;
219                // Choose y value based on the target OS
220                #[cfg(target_os = "windows")]
221                let y = if y < 0 { tray_y + _tray_height } else { y };
222
223                #[cfg(target_os = "macos")]
224                let y = if y < 0 { tray_y } else { y };
225
226                PhysicalPosition { x: tray_x, y }
227            } else {
228                panic!("Tray position not set");
229            }
230        }
231        #[cfg(feature = "tray-icon")]
232        TrayBottomLeft => {
233            if let Some((tray_x, tray_y)) = tray_position {
234                PhysicalPosition {
235                    x: tray_x,
236                    y: tray_y,
237                }
238            } else {
239                panic!("Tray position not set");
240            }
241        }
242        #[cfg(feature = "tray-icon")]
243        TrayRight => {
244            if let (Some((tray_x, tray_y)), Some((tray_width, _tray_height))) =
245                (tray_position, tray_size)
246            {
247                let y = tray_y - window_size.height;
248                // Choose y value based on the target OS
249                #[cfg(target_os = "windows")]
250                let y = if y < 0 { tray_y + _tray_height } else { y };
251
252                #[cfg(target_os = "macos")]
253                let y = if y < 0 { tray_y } else { y };
254
255                PhysicalPosition {
256                    x: tray_x + tray_width,
257                    y,
258                }
259            } else {
260                panic!("Tray position not set");
261            }
262        }
263        #[cfg(feature = "tray-icon")]
264        TrayBottomRight => {
265            if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size) {
266                PhysicalPosition {
267                    x: tray_x + tray_width,
268                    y: tray_y,
269                }
270            } else {
271                panic!("Tray position not set");
272            }
273        }
274        #[cfg(feature = "tray-icon")]
275        TrayCenter => {
276            if let (Some((tray_x, tray_y)), Some((tray_width, _tray_height))) =
277                (tray_position, tray_size)
278            {
279                let x = tray_x + tray_width / 2 - window_size.width / 2;
280                let y = tray_y - window_size.height;
281                // Choose y value based on the target OS
282                #[cfg(target_os = "windows")]
283                let y = if y < 0 { tray_y + _tray_height } else { y };
284
285                #[cfg(target_os = "macos")]
286                let y = if y < 0 { tray_y } else { y };
287
288                PhysicalPosition { x, y }
289            } else {
290                panic!("Tray position not set");
291            }
292        }
293        #[cfg(feature = "tray-icon")]
294        TrayBottomCenter => {
295            if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size) {
296                PhysicalPosition {
297                    x: tray_x + (tray_width / 2) - (window_size.width / 2),
298                    y: tray_y,
299                }
300            } else {
301                panic!("Tray position not set");
302            }
303        }
304    };
305
306    Ok(physical_pos)
307}