Skip to main content

floem/window/
id.rs

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