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, sync::Arc};
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 peniko::kurbo::{Point, Rect, Size};
13use winit::{
14 dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel},
15 window::{UserAttentionType, Window, WindowId},
16};
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| 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 use winit::platform::macos::WindowExtMacOS;
261 with_window(self, |window| window.is_document_edited()).unwrap_or(false)
262 }
263
264 #[cfg(not(target_os = "macos"))]
265 #[allow(dead_code)]
266 fn is_document_edited(&self) -> bool {
267 false
268 }
269
270 fn force_repaint(&self) -> bool {
271 force_window_repaint(self)
272 }
273
274 fn root_view(&self) -> Option<ViewId> {
275 root_view_id(self)
276 }
277
278 fn screen_layout(&self) -> Option<ScreenLayout> {
279 with_window(self, move |window| screen_layout_for_window(*self, window)).unwrap_or(None)
280 }
281
282 fn scale(&self) -> f64 {
283 with_window(self, |window| window.scale_factor()).unwrap_or(1.0)
284 }
285}
286
287/// Called by `ApplicationHandle` at the end of the event loop callback.
288pub(crate) fn process_window_updates(id: &WindowId) -> bool {
289 let mut result = false;
290 if let Some(items) = WINDOW_UPDATE_MESSAGES.with_borrow_mut(|map| map.remove(id)) {
291 result = !items.is_empty();
292 for update in items {
293 match update {
294 WindowUpdate::Visibility(visible) => {
295 with_window(id, |window| {
296 window.set_visible(visible);
297 });
298 }
299 #[allow(unused_variables)] // non mac - edited is unused
300 WindowUpdate::DocumentEdited(edited) => {
301 #[cfg(target_os = "macos")]
302 with_window(id, |window| {
303 use winit::platform::macos::WindowExtMacOS;
304 window.set_document_edited(edited);
305 });
306 }
307 WindowUpdate::OuterBounds(bds) => {
308 with_window(id, |window| {
309 let params =
310 bounds_to_logical_outer_position_and_inner_size(window, bds, true);
311 window.set_outer_position(params.0.into());
312 // XXX log any returned error?
313 let _ = window.request_surface_size(params.1.into());
314 });
315 }
316 WindowUpdate::InnerBounds(bds) => {
317 with_window(id, |window| {
318 let params =
319 bounds_to_logical_outer_position_and_inner_size(window, bds, false);
320 window.set_outer_position(params.0.into());
321 // XXX log any returned error?
322 let _ = window.request_surface_size(params.1.into());
323 });
324 }
325 WindowUpdate::RequestAttention(att) => {
326 with_window(id, |window| {
327 window.request_user_attention(att);
328 });
329 }
330 WindowUpdate::Minimize(minimize) => {
331 with_window(id, |window| {
332 window.set_minimized(minimize);
333 if !minimize {
334 // If we don't trigger a repaint on macOS,
335 // unminimize doesn't happen until an input
336 // event arrives. Unrelated to
337 // https://github.com/lapce/floem/issues/463 -
338 // this is in winit or below.
339 maybe_yield_with_repaint(window);
340 }
341 });
342 }
343 WindowUpdate::Maximize(maximize) => {
344 with_window(id, |window| window.set_maximized(maximize));
345 }
346 WindowUpdate::OuterLocation(outer) => {
347 with_window(id, |window| {
348 window.set_outer_position(LogicalPosition::new(outer.x, outer.y).into());
349 });
350 }
351 WindowUpdate::InnerSize(size) => {
352 with_window(id, |window| {
353 window
354 .request_surface_size(LogicalSize::new(size.width, size.height).into())
355 });
356 }
357 }
358 }
359 }
360 result
361}
362
363/// Compute a new logical position and size, given a window, a rectangle and whether the
364/// rectangle represents the desired inner or outer bounds of the window.
365///
366/// This is complex because winit offers us two somewhat contradictory ways of setting
367/// the bounds:
368///
369/// * You can set the **outer** position with `window.set_outer_position(position)`
370/// * You can set the **inner** size with `window.request_inner_size(size)`
371/// * You can obtain inner and outer sizes and positions, but you can only set outer
372/// position and *inner* size
373///
374/// So we must take the delta of the inner and outer size and/or positions (position
375/// availability is more limited by platform), and from that, create an appropriate
376/// inner size and outer position based on a `Rect` that represents either inner or
377/// outer.
378fn bounds_to_logical_outer_position_and_inner_size(
379 window: &Arc<dyn Window>,
380 target_bounds: Rect,
381 target_is_outer: bool,
382) -> (LogicalPosition<f64>, LogicalSize<f64>) {
383 if !window.is_decorated() {
384 // For undecorated windows, the inner and outer location and size are always identical
385 // so no further work is needed
386 return (
387 LogicalPosition::new(target_bounds.x0, target_bounds.y0),
388 LogicalSize::new(target_bounds.width(), target_bounds.height()),
389 );
390 }
391
392 let scale = window.scale_factor();
393 if target_is_outer {
394 // We need to reduce the size we are requesting by the width and height of the
395 // OS-added decorations to get the right target INNER size:
396 let inner_to_outer_size_delta =
397 delta_size(window.surface_size(), window.outer_size(), scale);
398
399 (
400 LogicalPosition::new(target_bounds.x0, target_bounds.y0),
401 LogicalSize::new(
402 (target_bounds.width() + inner_to_outer_size_delta.0).max(0.),
403 (target_bounds.height() + inner_to_outer_size_delta.1).max(0.),
404 ),
405 )
406 } else {
407 // We need to shift the x/y position we are requesting up and left (negatively)
408 // to come up with an *outer* location that makes sense with the passed rectangle's
409 // size as an *inner* size
410 let size_delta = delta_size(window.surface_size(), window.outer_size(), scale);
411 let inner_to_outer_delta: (f64, f64) = if let Some(delta) =
412 delta_position(window.surface_position(), window.outer_position(), scale)
413 {
414 // This is the more accurate way, but may be unavailable on some platforms
415 delta
416 } else {
417 // We have to make a few assumptions here, one of which is that window
418 // decorations are horizontally symmetric - the delta-x / 2 equals a position
419 // on the perimeter of the window's frame. A few ancient XWindows window
420 // managers (Enlightenment) might violate that assumption, but it is a rarity.
421 (
422 size_delta.0 / 2.0,
423 size_delta.1, // assume vertical is titlebar and give it full weight
424 )
425 };
426 (
427 LogicalPosition::new(
428 target_bounds.x0 - inner_to_outer_delta.0,
429 target_bounds.y0 - inner_to_outer_delta.1,
430 ),
431 LogicalSize::new(target_bounds.width(), target_bounds.height()),
432 )
433 }
434}
435
436/// Some operations - notably minimize and restoring visibility - don't take
437/// effect on macOS until something triggers a repaint in the target window - the
438/// issue is below the level of floem's event loops and seems to be in winit or
439/// deeper. Workaround is to force the window to repaint.
440#[allow(unused_variables)] // non mac builds see `window` as unused
441fn maybe_yield_with_repaint(window: &Arc<dyn Window>) {
442 #[cfg(target_os = "macos")]
443 {
444 window.request_redraw();
445 let main = Some("main") != std::thread::current().name();
446 if !main {
447 // attempt to get out of the way of the main thread
448 std::thread::yield_now();
449 }
450 }
451}
452
453fn delta_size(inner: PhysicalSize<u32>, outer: PhysicalSize<u32>, window_scale: f64) -> (f64, f64) {
454 let inner = winit_phys_size_to_size(inner, window_scale);
455 let outer = winit_phys_size_to_size(outer, window_scale);
456 (outer.width - inner.width, outer.height - inner.height)
457}
458
459type PositionResult = Result<winit::dpi::PhysicalPosition<i32>, winit::error::RequestError>;
460
461fn delta_position(
462 inner: PhysicalPosition<i32>,
463 outer: PositionResult,
464 window_scale: f64,
465) -> Option<(f64, f64)> {
466 if let Ok(outer) = outer {
467 let outer = winit_phys_position_to_point(outer, window_scale);
468 let inner = winit_phys_position_to_point(inner, window_scale);
469
470 return Some((inner.x - outer.x, inner.y - outer.y));
471 }
472 None
473}
474
475// Conversion functions for winit's size and point types:
476
477fn winit_position_to_point<I: Into<f64> + Pixel>(pos: LogicalPosition<I>) -> Point {
478 Point::new(pos.x.into(), pos.y.into())
479}
480
481fn winit_size_to_size<I: Into<f64> + Pixel>(size: LogicalSize<I>) -> Size {
482 Size::new(size.width.into(), size.height.into())
483}
484
485fn winit_phys_position_to_point<I: Into<f64> + Pixel>(
486 pos: PhysicalPosition<I>,
487 window_scale: f64,
488) -> Point {
489 winit_position_to_point::<I>(pos.to_logical(window_scale))
490}
491
492fn winit_phys_size_to_size<I: Into<f64> + Pixel>(size: PhysicalSize<I>, window_scale: f64) -> Size {
493 winit_size_to_size::<I>(size.to_logical(window_scale))
494}