floem/
window_id.rs

1use crate::{
2    screen_layout::screen_layout_for_window,
3    window_tracking::{force_window_repaint, with_window},
4    ScreenLayout, ViewId,
5};
6use std::{cell::RefCell, collections::HashMap};
7
8use super::window_tracking::{
9    monitor_bounds, root_view_id, window_inner_screen_bounds, window_inner_screen_position,
10    window_outer_screen_bounds, window_outer_screen_position,
11};
12use floem_winit::{
13    dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel},
14    window::{UserAttentionType, Window, WindowId},
15};
16use peniko::kurbo::{Point, Rect, Size};
17
18// Using thread_local for consistency with static vars in updates.rs, but I suspect these
19// are thread_local not because thread-locality is desired, but only because static mutability is
20// desired - but that's a patch for another day.
21thread_local! {
22    /// Holding pen for window state changes, processed as part of the event loop cycle
23    pub(crate) static WINDOW_UPDATE_MESSAGES: RefCell<HashMap<WindowId, Vec<WindowUpdate>>> = Default::default();
24}
25
26/// Enum of state updates that can be requested on a window which are processed
27/// asynchronously after event processing.
28#[allow(dead_code)] // DocumentEdited is seen as unused on non-mac builds
29enum WindowUpdate {
30    Visibility(bool),
31    InnerBounds(Rect),
32    OuterBounds(Rect),
33    // Since both inner bounds and outer bounds require some fudgery because winit
34    // only supports setting outer location and *inner* bounds, it is a good idea
35    // also to support setting the two things winit supports directly:
36    OuterLocation(Point),
37    InnerSize(Size),
38    RequestAttention(Option<UserAttentionType>),
39    Minimize(bool),
40    Maximize(bool),
41    // macOS only
42    #[allow(unused_variables)] // seen as unused on linux, etc.
43    DocumentEdited(bool),
44}
45
46/// Delegate enum for `winit`'s [`UserAttentionType`](https://docs.rs/winit/latest/winit/window/enum.UserAttentionType.html)
47///
48/// This is used for making the window's icon bounce in the macOS dock or the equivalent of that on
49/// other platforms.
50#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
51pub enum Urgency {
52    Critical,
53    Informational,
54
55    /// The default attention type (equivalent of passing `None` to `winit::Window::request_user_attention())`).
56    /// On some platforms (X11), it is necessary to call `WindowId.request_attention(Urgency::Default)` to stop
57    /// the attention-seeking behavior of the window.
58    #[default]
59    Default,
60}
61
62impl From<Urgency> for Option<UserAttentionType> {
63    fn from(urgency: Urgency) -> Self {
64        match urgency {
65            Urgency::Critical => Some(UserAttentionType::Critical),
66            Urgency::Informational => Some(UserAttentionType::Informational),
67            Urgency::Default => None,
68        }
69    }
70}
71
72/// Ensures `WindowIdExt` cannot be implemented on arbitrary types.
73trait WindowIdExtSealed: Sized + Copy {
74    fn add_window_update(&self, msg: WindowUpdate);
75}
76
77impl WindowIdExtSealed for WindowId {
78    fn add_window_update(&self, msg: WindowUpdate) {
79        WINDOW_UPDATE_MESSAGES.with_borrow_mut(|map| match map.entry(*self) {
80            std::collections::hash_map::Entry::Occupied(updates) => {
81                updates.into_mut().push(msg);
82            }
83            std::collections::hash_map::Entry::Vacant(v) => {
84                v.insert(vec![msg]);
85            }
86        });
87    }
88}
89
90/// Extends `WindowId` to give instances methods to retrieve properties of the associated window,
91/// much as `ViewId` does.
92///
93/// Methods may return None if the view is not realized on-screen, or
94/// if information needed to compute the result is not available on the current platform or
95/// available on the current platform but not from the calling thread.
96///
97/// **Platform support notes:**
98///  * macOS: Many of the methods here, if called from a thread other than `main`, are
99///    blocking because accessing most window properties may only be done from the main
100///    thread on that OS.
101///  * Android & Wayland: Getting the outer position of a window is not supported by `winit` and
102///    methods whose return value have that as a prerequisite will return `None` or return a
103///    reasonable default.
104///  * X11: Some window managers (Openbox was one such which was tested) *appear* to support
105///    retrieving separate window-with-frame and window-content positions and sizes, but in
106///    fact report the same values for both.
107#[allow(private_bounds)]
108pub trait WindowIdExt: WindowIdExtSealed {
109    /// Get the bounds of the content of this window, including
110    /// titlebar and native window borders.
111    fn bounds_on_screen_including_frame(&self) -> Option<Rect>;
112    /// Get the bounds of the content of this window, excluding
113    /// titlebar and native window borders.
114    fn bounds_of_content_on_screen(&self) -> Option<Rect>;
115    /// Get the location of the window including any OS titlebar.
116    fn position_on_screen_including_frame(&self) -> Option<Point>;
117    /// Get the location of the window's content on the monitor where
118    /// it currently resides, **excluding** any OS titlebar.
119    fn position_of_content_on_screen(&self) -> Option<Point>;
120    /// Get the logical bounds of the monitor this window is on.
121    fn monitor_bounds(&self) -> Option<Rect>;
122    /// Determine if this window is currently visible.  Note that if a
123    /// call to set a window visible which is invisible has happened within
124    /// the current event loop cycle, the state returned will not reflect that.
125    fn is_visible(&self) -> bool;
126    /// Determine if this window is currently minimized. Note that if a
127    /// call to minimize or unminimize this window, and it is currently in the
128    /// opposite state, has happened the current event loop cycle, the state
129    /// returned will not reflect that.
130    fn is_minimized(&self) -> bool;
131
132    /// Determine if this window is currently maximize. Note that if a
133    /// call to maximize or unmaximize this window, and it is currently in the
134    /// opposite state, has happened the current event loop cycle, the state
135    /// returned will not reflect that.
136    fn is_maximized(&self) -> bool;
137
138    /// Determine if the window decorations should indicate an edited, unsaved
139    /// document.  Platform-dependent: Will only ever return `true` on macOS.
140    fn is_document_edited(&self) -> bool;
141
142    /// Instruct the window manager to indicate in the window's decorations
143    /// that the window contains an unsaved, edited document.  Only has an
144    /// effect on macOS.
145    #[allow(unused_variables)] // edited unused on non-mac builds
146    fn set_document_edited(&self, edited: bool) {
147        #[cfg(target_os = "macos")]
148        self.add_window_update(WindowUpdate::DocumentEdited(edited))
149    }
150
151    /// Set this window's visible state, hiding or showing it if it has been
152    /// hidden
153    fn set_visible(&self, visible: bool) {
154        self.add_window_update(WindowUpdate::Visibility(visible))
155    }
156
157    /// Update the bounds of this window.
158    fn set_window_inner_bounds(&self, bounds: Rect) {
159        self.add_window_update(WindowUpdate::InnerBounds(bounds))
160    }
161
162    /// Update the bounds of this window.
163    fn set_window_outer_bounds(&self, bounds: Rect) {
164        self.add_window_update(WindowUpdate::OuterBounds(bounds))
165    }
166
167    /// Change this window's maximized state.
168    fn maximized(&self, maximized: bool) {
169        self.add_window_update(WindowUpdate::Maximize(maximized))
170    }
171
172    /// Change this window's minimized state.
173    fn minimized(&self, minimized: bool) {
174        self.add_window_update(WindowUpdate::Minimize(minimized))
175    }
176
177    /// Change this window's minimized state.
178    fn set_outer_location(&self, location: Point) {
179        self.add_window_update(WindowUpdate::OuterLocation(location))
180    }
181
182    /// Ask the OS's windowing framework to update the size of the window
183    /// based on the passed size for its *content* (excluding titlebar, frame
184    /// or other decorations).
185    fn set_content_size(&self, size: Size) {
186        self.add_window_update(WindowUpdate::InnerSize(size))
187    }
188
189    /// Cause the desktop to perform some attention-drawing behavior that draws
190    /// the user's attention specifically to this window - e.g. bouncing in
191    /// the dock on macOS.  On X11, after calling this method with some urgency
192    /// other than `None`, it is necessary to *clear* the attention-seeking state
193    /// by calling this method again with `Urgency::None`.
194    fn request_attention(&self, urgency: Urgency) {
195        self.add_window_update(WindowUpdate::RequestAttention(urgency.into()))
196    }
197
198    /// Force a repaint of this window through the native window's repaint mechanism,
199    /// bypassing floem's normal repaint mechanism.
200    ///
201    /// This method may be removed or deprecated in the future, but has been needed
202    /// in [some situations](https://github.com/lapce/floem/issues/463), and to
203    /// address a few ongoing issues in `winit` (window unmaximize is delayed until
204    /// an external event triggers a repaint of the requesting window), and may
205    /// be needed as a workaround if other such issues are discovered until they
206    /// can be addressed.
207    ///
208    /// Returns true if the repaint request was issued successfully (i.e. there is
209    /// an actual system-level window corresponding to this `WindowId`).
210    fn force_repaint(&self) -> bool;
211
212    /// Get the root view of this window.
213    fn root_view(&self) -> Option<ViewId>;
214
215    /// Get a layout of this window in relation to the monitor on which it currently
216    /// resides, if any.
217    fn screen_layout(&self) -> Option<ScreenLayout>;
218
219    /// Get the dots-per-inch scaling of this window or 1.0 if the platform does not
220    /// support it (Android).
221    fn scale(&self) -> f64;
222}
223
224impl WindowIdExt for WindowId {
225    fn bounds_on_screen_including_frame(&self) -> Option<Rect> {
226        window_outer_screen_bounds(self)
227    }
228
229    fn bounds_of_content_on_screen(&self) -> Option<Rect> {
230        window_inner_screen_bounds(self)
231    }
232
233    fn position_on_screen_including_frame(&self) -> Option<Point> {
234        window_outer_screen_position(self)
235    }
236
237    fn position_of_content_on_screen(&self) -> Option<Point> {
238        window_inner_screen_position(self)
239    }
240
241    fn monitor_bounds(&self) -> Option<Rect> {
242        monitor_bounds(self)
243    }
244
245    fn is_visible(&self) -> bool {
246        with_window(self, |window| window.is_visible().unwrap_or(false)).unwrap_or(false)
247    }
248
249    fn is_minimized(&self) -> bool {
250        with_window(self, |window| window.is_minimized().unwrap_or(false)).unwrap_or(false)
251    }
252
253    fn is_maximized(&self) -> bool {
254        with_window(self, Window::is_maximized).unwrap_or(false)
255    }
256
257    #[cfg(target_os = "macos")]
258    #[allow(dead_code)]
259    fn is_document_edited(&self) -> bool {
260        with_window(
261            self,
262            floem_winit::platform::macos::WindowExtMacOS::is_document_edited,
263        )
264        .unwrap_or(false)
265    }
266
267    #[cfg(not(target_os = "macos"))]
268    #[allow(dead_code)]
269    fn is_document_edited(&self) -> bool {
270        false
271    }
272
273    fn force_repaint(&self) -> bool {
274        force_window_repaint(self)
275    }
276
277    fn root_view(&self) -> Option<ViewId> {
278        root_view_id(self)
279    }
280
281    fn screen_layout(&self) -> Option<ScreenLayout> {
282        with_window(self, move |window| screen_layout_for_window(*self, window)).unwrap_or(None)
283    }
284
285    fn scale(&self) -> f64 {
286        with_window(self, Window::scale_factor).unwrap_or(1.0)
287    }
288}
289
290/// Called by `ApplicationHandle` at the end of the event loop callback.
291pub(crate) fn process_window_updates(id: &WindowId) -> bool {
292    let mut result = false;
293    if let Some(items) = WINDOW_UPDATE_MESSAGES.with_borrow_mut(|map| map.remove(id)) {
294        result = !items.is_empty();
295        for update in items {
296            match update {
297                WindowUpdate::Visibility(visible) => {
298                    with_window(id, |window| {
299                        window.set_visible(visible);
300                    });
301                }
302                #[allow(unused_variables)] // non mac - edited is unused
303                WindowUpdate::DocumentEdited(edited) => {
304                    #[cfg(target_os = "macos")]
305                    with_window(id, |window| {
306                        use floem_winit::platform::macos::WindowExtMacOS;
307                        window.set_document_edited(edited);
308                    });
309                }
310                WindowUpdate::OuterBounds(bds) => {
311                    with_window(id, |window| {
312                        let params =
313                            bounds_to_logical_outer_position_and_inner_size(window, bds, true);
314                        window.set_outer_position(params.0);
315                        // XXX log any returned error?
316                        let _ = window.request_inner_size(params.1);
317                    });
318                }
319                WindowUpdate::InnerBounds(bds) => {
320                    with_window(id, |window| {
321                        let params =
322                            bounds_to_logical_outer_position_and_inner_size(window, bds, false);
323                        window.set_outer_position(params.0);
324                        // XXX log any returned error?
325                        let _ = window.request_inner_size(params.1);
326                    });
327                }
328                WindowUpdate::RequestAttention(att) => {
329                    with_window(id, |window| {
330                        window.request_user_attention(att);
331                    });
332                }
333                WindowUpdate::Minimize(minimize) => {
334                    with_window(id, |window| {
335                        window.set_minimized(minimize);
336                        if !minimize {
337                            // If we don't trigger a repaint on macOS,
338                            // unminimize doesn't happen until an input
339                            // event arrives. Unrelated to
340                            // https://github.com/lapce/floem/issues/463 -
341                            // this is in winit or below.
342                            maybe_yield_with_repaint(window);
343                        }
344                    });
345                }
346                WindowUpdate::Maximize(maximize) => {
347                    with_window(id, |window| window.set_maximized(maximize));
348                }
349                WindowUpdate::OuterLocation(outer) => {
350                    with_window(id, |window| {
351                        window.set_outer_position(LogicalPosition::new(outer.x, outer.y));
352                    });
353                }
354                WindowUpdate::InnerSize(size) => {
355                    with_window(id, |window| {
356                        window.request_inner_size(LogicalSize::new(size.width, size.height))
357                    });
358                }
359            }
360        }
361    }
362    result
363}
364
365/// Compute a new logical position and size, given a window, a rectangle and whether the
366/// rectangle represents the desired inner or outer bounds of the window.
367///
368/// This is complex because winit offers us two somewhat contradictory ways of setting
369/// the bounds:
370///
371///  * You can set the **outer** position with `window.set_outer_position(position)`
372///  * You can set the **inner** size with `window.request_inner_size(size)`
373///  * You can obtain inner and outer sizes and positions, but you can only set outer
374///    position and *inner* size
375///
376/// So we must take the delta of the inner and outer size and/or positions (position
377/// availability is more limited by platform), and from that, create an appropriate
378/// inner size and outer position based on a `Rect` that represents either inner or
379/// outer.
380fn bounds_to_logical_outer_position_and_inner_size(
381    window: &Window,
382    target_bounds: Rect,
383    target_is_outer: bool,
384) -> (LogicalPosition<f64>, LogicalSize<f64>) {
385    if !window.is_decorated() {
386        // For undecorated windows, the inner and outer location and size are always identical
387        // so no further work is needed
388        return (
389            LogicalPosition::new(target_bounds.x0, target_bounds.y0),
390            LogicalSize::new(target_bounds.width(), target_bounds.height()),
391        );
392    }
393
394    let scale = window.scale_factor();
395    if target_is_outer {
396        // We need to reduce the size we are requesting by the width and height of the
397        // OS-added decorations to get the right target INNER size:
398        let inner_to_outer_size_delta = delta_size(window.inner_size(), window.outer_size(), scale);
399
400        (
401            LogicalPosition::new(target_bounds.x0, target_bounds.y0),
402            LogicalSize::new(
403                (target_bounds.width() + inner_to_outer_size_delta.0).max(0.),
404                (target_bounds.height() + inner_to_outer_size_delta.1).max(0.),
405            ),
406        )
407    } else {
408        // We need to shift the x/y position we are requesting up and left (negatively)
409        // to come up with an *outer* location that makes sense with the passed rectangle's
410        // size as an *inner* size
411        let size_delta = delta_size(window.inner_size(), window.outer_size(), scale);
412        let inner_to_outer_delta: (f64, f64) = if let Some(delta) =
413            delta_position(window.inner_position(), window.outer_position(), scale)
414        {
415            // This is the more accurate way, but may be unavailable on some platforms
416            delta
417        } else {
418            // We have to make a few assumptions here, one of which is that window
419            // decorations are horizontally symmetric - the delta-x / 2 equals a position
420            // on the perimeter of the window's frame.  A few ancient XWindows window
421            // managers (Enlightenment) might violate that assumption, but it is a rarity.
422            (
423                size_delta.0 / 2.0,
424                size_delta.1, // assume vertical is titlebar and give it full weight
425            )
426        };
427        (
428            LogicalPosition::new(
429                target_bounds.x0 - inner_to_outer_delta.0,
430                target_bounds.y0 - inner_to_outer_delta.1,
431            ),
432            LogicalSize::new(target_bounds.width(), target_bounds.height()),
433        )
434    }
435}
436
437/// Some operations - notably minimize and restoring visibility - don't take
438/// effect on macOS until something triggers a repaint in the target window - the
439/// issue is below the level of floem's event loops and seems to be in winit or
440/// deeper.  Workaround is to force the window to repaint.
441#[allow(unused_variables)] // non mac builds see `window` as unused
442fn maybe_yield_with_repaint(window: &Window) {
443    #[cfg(target_os = "macos")]
444    {
445        window.request_redraw();
446        let main = Some("main") != std::thread::current().name();
447        if !main {
448            // attempt to get out of the way of the main thread
449            std::thread::yield_now();
450        }
451    }
452}
453
454fn delta_size(inner: PhysicalSize<u32>, outer: PhysicalSize<u32>, window_scale: f64) -> (f64, f64) {
455    let inner = winit_phys_size_to_size(inner, window_scale);
456    let outer = winit_phys_size_to_size(outer, window_scale);
457    (outer.width - inner.width, outer.height - inner.height)
458}
459
460type PositionResult =
461    Result<floem_winit::dpi::PhysicalPosition<i32>, floem_winit::error::NotSupportedError>;
462
463fn delta_position(
464    inner: PositionResult,
465    outer: PositionResult,
466    window_scale: f64,
467) -> Option<(f64, f64)> {
468    if let Ok(inner) = inner {
469        if let Ok(outer) = outer {
470            let outer = winit_phys_position_to_point(outer, window_scale);
471            let inner = winit_phys_position_to_point(inner, window_scale);
472
473            return Some((inner.x - outer.x, inner.y - outer.y));
474        }
475    }
476    None
477}
478
479// Conversion functions for winit's size and point types:
480
481fn winit_position_to_point<I: Into<f64> + Pixel>(pos: LogicalPosition<I>) -> Point {
482    Point::new(pos.x.into(), pos.y.into())
483}
484
485fn winit_size_to_size<I: Into<f64> + Pixel>(size: LogicalSize<I>) -> Size {
486    Size::new(size.width.into(), size.height.into())
487}
488
489fn winit_phys_position_to_point<I: Into<f64> + Pixel>(
490    pos: PhysicalPosition<I>,
491    window_scale: f64,
492) -> Point {
493    winit_position_to_point::<I>(pos.to_logical(window_scale))
494}
495
496fn winit_phys_size_to_size<I: Into<f64> + Pixel>(size: PhysicalSize<I>, window_scale: f64) -> Size {
497    winit_size_to_size::<I>(size.to_logical(window_scale))
498}