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