diff --git a/src/app_handle.rs b/src/app_handle.rs index 888c6471..56c7c2e2 100644 --- a/src/app_handle.rs +++ b/src/app_handle.rs @@ -17,6 +17,7 @@ use crate::{ view::View, window::WindowConfig, window_handle::WindowHandle, + window_id::process_window_updates, }; pub(crate) struct ApplicationHandle { @@ -391,8 +392,9 @@ impl ApplicationHandle { } fn handle_updates_for_all_windows(&mut self) { - for (_, handle) in self.window_handles.iter_mut() { + for (window_id, handle) in self.window_handles.iter_mut() { handle.process_update(); + while process_window_updates(window_id) {} } } diff --git a/src/id.rs b/src/id.rs index 626bd055..becc3c7c 100644 --- a/src/id.rs +++ b/src/id.rs @@ -6,6 +6,7 @@ use std::{any::Any, cell::RefCell, rc::Rc}; +use floem_winit::window::WindowId; use peniko::kurbo::{Insets, Point, Rect, Size}; use slotmap::new_key_type; use taffy::{Display, Layout, NodeId, TaffyTree}; @@ -21,6 +22,8 @@ use crate::{ view::{IntoView, View}, view_state::{ChangeFlags, StackOffset, ViewState}, view_storage::VIEW_STORAGE, + window_tracking::window_id_for_root, + ScreenLayout, }; new_key_type! { @@ -252,6 +255,11 @@ impl ViewId { self.request_changes(ChangeFlags::LAYOUT) } + /// Get the window id of the window containing this view, if there is one. + pub fn window_id(&self) -> Option { + self.root().and_then(window_id_for_root) + } + pub fn request_paint(&self) { self.add_update_message(UpdateMessage::RequestPaint); } @@ -417,6 +425,12 @@ impl ViewId { self.add_update_message(UpdateMessage::Draggable { id: *self }); } + /// Alter the visibility of the current window the view represented by this ID + /// is in. + pub fn window_visible(&self, visible: bool) { + self.add_update_message(UpdateMessage::WindowVisible(visible)); + } + fn add_update_message(&self, msg: UpdateMessage) { CENTRAL_UPDATE_MESSAGES.with_borrow_mut(|msgs| { msgs.push((*self, msg)); @@ -428,4 +442,9 @@ impl ViewId { msgs.push((*self, Box::new(state))); }); } + + /// Get a layout in screen-coordinates for this view, if possible. + pub fn screen_layout(&self) -> Option { + crate::screen_layout::try_create_screen_layout(self) + } } diff --git a/src/lib.rs b/src/lib.rs index 2f00a1ab..4f66f132 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,6 +169,7 @@ pub mod pointer; mod profiler; pub mod renderer; pub mod responsive; +mod screen_layout; pub mod style; pub(crate) mod theme; pub mod unit; @@ -180,6 +181,8 @@ pub mod view_tuple; pub mod views; pub mod window; mod window_handle; +mod window_id; +mod window_tracking; pub use app::{launch, quit_app, AppEvent, Application}; pub use app_state::AppState; @@ -190,6 +193,8 @@ pub use floem_renderer::Renderer; pub use id::ViewId; pub use peniko; pub use peniko::kurbo; +pub use screen_layout::ScreenLayout; pub use taffy; pub use view::{recursively_layout_view, AnyView, IntoView, View}; pub use window::{close_window, new_window}; +pub use window_id::{Urgency, WindowIdExt}; diff --git a/src/screen_layout.rs b/src/screen_layout.rs new file mode 100644 index 00000000..2451d8c5 --- /dev/null +++ b/src/screen_layout.rs @@ -0,0 +1,280 @@ +//! Tools for computing screen locations from locations within a View and +//! vice-versa. +use crate::ViewId; +use floem_winit::window::{Window, WindowId}; +use peniko::kurbo::{Point, Rect, Size}; + +use crate::window_tracking::{ + monitor_bounds_for_monitor, rect_from_physical_bounds_for_window, with_window_id_and_window, +}; + +/// Create a ScreenLayout for a view. This can fail if the view or an +/// ancestor of it has no parent and is not realized on-screen, or if the +/// platform does not support reading window inner or outer bounds. ScreenLayout +/// is useful when needing to convert locations within a view into absolute +/// positions on-screen, such as for creating a window at position relative +/// to that view. +pub fn try_create_screen_layout(view: &ViewId) -> Option { + with_window_id_and_window(view, |window_id, window| { + window + .current_monitor() + .map(|monitor| { + window + .inner_position() + .map(|inner_position| { + window + .outer_position() + .map(|outer_position| { + let monitor_bounds = monitor_bounds_for_monitor(window, &monitor); + let inner_size = window.inner_size(); + let outer_size = window.outer_size(); + + let window_bounds = rect_from_physical_bounds_for_window( + window, + outer_position, + outer_size, + ); + + let window_content_bounds = rect_from_physical_bounds_for_window( + window, + inner_position, + inner_size, + ); + + let view_origin_in_window = find_window_origin(view); + let monitor_scale = window.scale_factor(); + + ScreenLayout { + monitor_scale, + monitor_bounds, + window_content_bounds, + window_bounds, + view_origin_in_window: Some(view_origin_in_window), + window_id: *window_id, + } + }) + .ok() + }) + .ok() + }) + .unwrap_or(None) + .unwrap_or(None) + }) + .unwrap_or(None) +} + +pub fn screen_layout_for_window(window_id: WindowId, window: &Window) -> Option { + window + .current_monitor() + .map(|monitor| { + window + .inner_position() + .map(|inner_position| { + window + .outer_position() + .map(|outer_position| { + let monitor_bounds = monitor_bounds_for_monitor(window, &monitor); + let inner_size = window.inner_size(); + let outer_size = window.outer_size(); + + let window_bounds = rect_from_physical_bounds_for_window( + window, + outer_position, + outer_size, + ); + + let window_content_bounds = rect_from_physical_bounds_for_window( + window, + inner_position, + inner_size, + ); + + let view_origin_in_window = None; + let monitor_scale = window.scale_factor(); + + ScreenLayout { + monitor_scale, + monitor_bounds, + window_content_bounds, + window_bounds, + view_origin_in_window, + window_id, + } + }) + .ok() + }) + .ok() + }) + .unwrap_or(None) + .unwrap_or(None) +} + +/// Relates a realized `View` to the bounds of the window that contains it, +/// and the window to the bounds of the monitor that contains it. All fields +/// are in logical coordinates (if the OS scales physical coordinates for high +/// DPI displays, the scaling is already applied). +/// +/// Instances are a snapshot in time of the location of the view and window +/// at the time of creation, and are not updated if view or window or monitor +/// in use is. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct ScreenLayout { + /// The window id + pub window_id: WindowId, + /// The scaling of the monitor, if any + pub monitor_scale: f64, + /// The logical bounds of the monitor + pub monitor_bounds: Rect, + /// The bounds of the view content within the monitor's bounds + pub window_content_bounds: Rect, + /// The bounds of the window within the monitor's bounds + pub window_bounds: Rect, + /// The origin of the view within the window, if this ScreenLayout was + /// created from a `View` rather than a `WindowId` - needed for computing + /// relative offsets from, e.g., the location of a mouse click within + /// a `View`. + pub view_origin_in_window: Option, +} + +impl ScreenLayout { + /// Unscales this Screen to physical device coordinates, less any DPI + /// scaling done by hardware. + pub fn to_physical_scale(&self) -> Self { + // The bounds we have are pre-scaled by 1.0/window.scale(), so + // inverting them is multiplication, not division. + Self { + monitor_scale: 1.0, + monitor_bounds: scale_rect(self.monitor_scale, self.monitor_bounds), + window_bounds: scale_rect(self.monitor_scale, self.window_bounds), + window_content_bounds: scale_rect(self.monitor_scale, self.window_content_bounds), + view_origin_in_window: self + .view_origin_in_window + .map(|origin| scale_point(self.monitor_scale, origin)), + window_id: self.window_id, + } + } + + /// Get the insets required to transform the outer rectangle into + /// the inner one in the form `(left, top, right, bottom)` + pub fn window_frame_insets(&self) -> (f64, f64, f64, f64) { + // Kurbo contains an Insets type obtainable from + // self.window_bounds - self.window_content_bounds + // but uses a definition of "insets" that is has nothing + // to do with what any UI toolkit has ever meant by the word. + ( + self.window_content_bounds.x0 - self.window_bounds.x0, + self.window_content_bounds.y0 - self.window_bounds.y0, + self.window_bounds.x1 - self.window_content_bounds.x1, + self.window_bounds.y1 - self.window_content_bounds.y1, + ) + } + + /// If true, this instance has scaling applied. + pub fn is_scaled(&self) -> bool { + self.monitor_scale != 0_f64 + } + + /// Convert a screen position to a position within the view that created + /// this one. + pub fn view_location_from_screen(&self, screen_point: Point) -> Point { + let mut result = screen_point; + if let Some(origin) = self.view_origin_in_window { + result.x -= origin.x + self.window_content_bounds.x0; + result.y -= origin.y + self.window_content_bounds.y0; + } + result + } + + /// Determine if this `ScreenBounds` has a different bounding rectangle for + /// the content and frame bounds. Some X11 window managers (Openbox, for one) + /// appear to support getting frame position separately from content position, + /// but in fact report the same bounds for both. + pub fn contains_frame_decoration_insets(&self) -> bool { + self.window_content_bounds != self.window_bounds + } + + /// Compute a position, in screen coordinates, relative to the view this layout + /// was created from. If a target size is passed, the implementation will attempt + /// to adjust the resulting point so that a rectangle of the required size fits + /// entirely on-screen. + pub fn screen_location_from_view( + &self, + relative_position: Option, + target_size: Option, + ) -> Point { + let mut result = Point::new(self.window_content_bounds.x0, self.window_content_bounds.y0); + if let Some(offset) = relative_position { + result.x += offset.x; + result.y += offset.y; + } + + if let Some(origin) = self.view_origin_in_window { + result.x += origin.x; + result.y += origin.y; + } + + // If we have a size, adjust the resulting point to ensure the resulting + // bounds will fit on screen (if it is possible) + if let Some(size) = target_size { + let mut target_bounds = Rect::new( + result.x, + result.y, + result.x + size.width, + result.y + size.height, + ); + if target_bounds.x1 > self.monitor_bounds.x1 { + let offset = target_bounds.x1 - self.monitor_bounds.x1; + target_bounds.x0 -= offset; + target_bounds.x1 -= offset; + } + if target_bounds.y1 > self.monitor_bounds.y1 { + let offset = target_bounds.y1 - self.monitor_bounds.y1; + target_bounds.y0 -= offset; + target_bounds.y1 -= offset; + } + if target_bounds.x0 < self.monitor_bounds.x0 { + let offset = self.monitor_bounds.x0 - target_bounds.x0; + target_bounds.x0 += offset; + target_bounds.x1 += offset; + } + if target_bounds.y0 < self.monitor_bounds.y0 { + let offset = self.monitor_bounds.y0 - target_bounds.y0; + target_bounds.y0 += offset; + target_bounds.y1 += offset + } + result.x = target_bounds.x0; + result.y = target_bounds.y0; + } + result + } +} + +fn find_window_origin(view: &ViewId) -> Point { + let mut pt = Point::ZERO; + recursively_find_window_origin(*view, &mut pt); + pt +} + +fn recursively_find_window_origin(view: ViewId, point: &mut Point) { + if let Some(layout) = view.get_layout() { + point.x += layout.location.x as f64; + point.y += layout.location.y as f64; + if let Some(parent) = view.parent() { + recursively_find_window_origin(parent, point); + } + } +} + +fn scale_point(by: f64, mut pt: Point) -> Point { + pt.x *= by; + pt.y *= by; + pt +} + +fn scale_rect(by: f64, mut bds: Rect) -> Rect { + bds.x0 *= by; + bds.y0 *= by; + bds.x1 *= by; + bds.y1 *= by; + bds +} diff --git a/src/update.rs b/src/update.rs index f9ce3c67..941be91a 100644 --- a/src/update.rs +++ b/src/update.rs @@ -94,4 +94,5 @@ pub(crate) enum UpdateMessage { position: Point, size: Size, }, + WindowVisible(bool), } diff --git a/src/window_handle.rs b/src/window_handle.rs index 1a894cc5..1b7c3c3b 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -45,6 +45,7 @@ use crate::{ view::{default_compute_layout, view_tab_navigation, IntoView, View}, view_state::ChangeFlags, views::Decorators, + window_tracking::{remove_window_id_mapping, store_window_id_mapping}, }; /// The top-level window handle that owns the winit Window. @@ -120,6 +121,7 @@ impl WindowHandle { id.set_view(view.into_any()); let window = Arc::new(window); + store_window_id_mapping(id, window_id, &window); let paint_state = PaintState::new(window.clone(), scale, size.get_untracked() * scale); let mut window_handle = Self { window: Some(window), @@ -928,6 +930,11 @@ impl WindowHandle { cx.app_state.remove_view(id); self.id.request_all(); } + UpdateMessage::WindowVisible(visible) => { + if let Some(window) = self.window.as_ref() { + window.set_visible(visible); + } + } } } } @@ -1127,6 +1134,7 @@ impl WindowHandle { pub(crate) fn destroy(&mut self) { self.event(Event::WindowClosed); self.scope.dispose(); + remove_window_id_mapping(&self.id, &self.window_id); } #[cfg(target_os = "macos")] diff --git a/src/window_id.rs b/src/window_id.rs new file mode 100644 index 00000000..d7c6a626 --- /dev/null +++ b/src/window_id.rs @@ -0,0 +1,496 @@ +use crate::{ + screen_layout::screen_layout_for_window, + window_tracking::{force_window_repaint, with_window}, + ScreenLayout, ViewId, +}; +use std::{cell::RefCell, collections::HashMap}; + +use super::window_tracking::{ + monitor_bounds, root_view_id, window_inner_screen_bounds, window_inner_screen_position, + window_outer_screen_bounds, window_outer_screen_position, +}; +use floem_winit::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel}, + window::{UserAttentionType, Window, WindowId}, +}; +use peniko::kurbo::{Point, Rect, Size}; + +// Using thread_local for consistency with static vars in updates.rs, but I suspect these +// are thread_local not because thread-locality is desired, but only because static mutability is +// desired - but that's a patch for another day. +thread_local! { + /// Holding pen for window state changes, processed as part of the event loop cycle + pub(crate) static WINDOW_UPDATE_MESSAGES: RefCell>> = Default::default(); +} + +/// Enum of state updates that can be requested on a window which are processed +/// asynchronously after event processing. +#[allow(dead_code)] // DocumentEdited is seen as unused on non-mac builds +enum WindowUpdate { + Visibility(bool), + InnerBounds(Rect), + OuterBounds(Rect), + // Since both inner bounds and outer bounds require some fudgery because winit + // only supports setting outer location and *inner* bounds, it is a good idea + // also to support setting the two things winit supports directly: + OuterLocation(Point), + InnerSize(Size), + RequestAttention(Option), + Minimize(bool), + Maximize(bool), + // Mac OS only + #[allow(unused_variables)] // seen as unused on linux, etc. + DocumentEdited(bool), +} + +/// Delegate enum for `winit`'s +/// [`UserAttentionType`](https://docs.rs/winit/latest/winit/window/enum.UserAttentionType.html), +/// used for making the window's icon bounce in the Mac OS dock or the equivalent of that on +/// other platforms. +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] +pub enum Urgency { + Critical, + Informational, + + /// The default attention type (equivalent of passing `None` to `winit::Window::request_user_attention())`). + /// On some platforms (X11), it is necessary to call `WindowId.request_attention(Urgency::Default)` to stop + /// the attention-seeking behavior of the window. + #[default] + Default, +} + +impl From for Option { + fn from(urgency: Urgency) -> Self { + match urgency { + Urgency::Critical => Some(UserAttentionType::Critical), + Urgency::Informational => Some(UserAttentionType::Informational), + Urgency::Default => None, + } + } +} + +/// Ensures `WindowIdExt` cannot be implemented on arbitrary types. +trait WindowIdExtSealed: Sized + Copy { + fn add_window_update(&self, msg: WindowUpdate); +} + +impl WindowIdExtSealed for WindowId { + fn add_window_update(&self, msg: WindowUpdate) { + WINDOW_UPDATE_MESSAGES.with_borrow_mut(|map| match map.entry(*self) { + std::collections::hash_map::Entry::Occupied(updates) => { + updates.into_mut().push(msg); + } + std::collections::hash_map::Entry::Vacant(v) => { + v.insert(vec![msg]); + } + }); + } +} + +/// Extends `WindowId` to give instances methods to retrieve properties of the associated window, +/// much as `ViewId` does. Methods may return None if the view is not realized on-screen, or +/// if information needed to compute the result is not available on the current platform or +/// available on the current platform but not from the calling thread. +/// +/// **Platform support notes:** +/// * Mac OS: Many of the methods here, if called from a thread other than `main`, are +/// blocking because accessing most window properties may only be done from the main +/// thread on that OS. +/// * Android & Wayland: Getting the outer position of a window is not supported by `winit` and +/// methods whose return value have that as a prerequisite will return `None` or return a +/// reasonable default. +/// * X11: Some window managers (Openbox was one such which was tested) *appear* to support +/// retreiving separate window-with-frame and window-content positions and sizes, but in +/// fact report the same values for both. +#[allow(private_bounds)] +pub trait WindowIdExt: WindowIdExtSealed { + /// Get the bounds of the content of this window, including + /// titlebar and native window borders. + fn bounds_on_screen_including_frame(&self) -> Option; + /// Get the bounds of the content of this window, excluding + /// titlebar and native window borders. + fn bounds_of_content_on_screen(&self) -> Option; + /// Get the location of the window including any OS titlebar. + fn position_on_screen_including_frame(&self) -> Option; + /// Get the location of the window's content on the monitor where + /// it currently resides, **excluding** any OS titlebar. + fn position_of_content_on_screen(&self) -> Option; + /// Get the logical bounds of the monitor this window is on. + fn monitor_bounds(&self) -> Option; + /// Determine if this window is currently visible. Note that if a + /// call to set a window visible which is invisible has happened within + /// the current event loop cycle, the state returned will not reflect that. + fn is_visible(&self) -> bool; + /// Determine if this window is currently minimized. Note that if a + /// call to minimize or unminimize this window, and it is currently in the + /// opposite state, has happened the current event loop cycle, the state + /// returned will not reflect that. + fn is_minimized(&self) -> bool; + + /// Determine if this window is currently maximize. Note that if a + /// call to maximize or unmaximize this window, and it is currently in the + /// opposite state, has happened the current event loop cycle, the state + /// returned will not reflect that. + fn is_maximized(&self) -> bool; + + /// Determine if the window decorations should indicate an edited, unsaved + /// document. Platform-dependent: Will only ever return `true` on Mac OS. + fn is_document_edited(&self) -> bool; + + /// Instruct the window manager to indicate in the window's decorations + /// the the window contains an unsaved, edited document. Only has an + /// effect on Mac OS. + #[allow(unused_variables)] // edited unused on non-mac builds + fn set_document_edited(&self, edited: bool) { + #[cfg(target_os = "macos")] + self.add_window_update(WindowUpdate::DocumentEdited(edited)) + } + + /// Set this window's visible state, hiding or showing it if it has been + /// hidden + fn set_visible(&self, visible: bool) { + self.add_window_update(WindowUpdate::Visibility(visible)) + } + + /// Update the bounds of this window. + fn set_window_inner_bounds(&self, bounds: Rect) { + self.add_window_update(WindowUpdate::InnerBounds(bounds)) + } + + /// Update the bounds of this window. + fn set_window_outer_bounds(&self, bounds: Rect) { + self.add_window_update(WindowUpdate::OuterBounds(bounds)) + } + + /// Change this window's maximized state. + fn maximized(&self, maximized: bool) { + self.add_window_update(WindowUpdate::Maximize(maximized)) + } + + /// Change this window's minimized state. + fn minimized(&self, minimized: bool) { + self.add_window_update(WindowUpdate::Minimize(minimized)) + } + + /// Change this window's minimized state. + fn set_outer_location(&self, location: Point) { + self.add_window_update(WindowUpdate::OuterLocation(location)) + } + + /// Ask the OS's windowing framework to update the size of the window + /// based on the passed size for its *content* (excluding titlebar, frame + /// or other decorations). + fn set_content_size(&self, size: Size) { + self.add_window_update(WindowUpdate::InnerSize(size)) + } + + /// Cause the desktop to perform some attention-drawing behavior that draws + /// the user's attention specifically to this window - e.g. bouncing in + /// the dock on Mac OS. On X11, after calling this method with some urgency + /// other than `None`, it is necessary to *clear* the attention-seeking state + /// by calling this method again with `Urgency::None`. + fn request_attention(&self, urgency: Urgency) { + self.add_window_update(WindowUpdate::RequestAttention(urgency.into())) + } + + /// Force a repaint of this window through the native window's repaint mechanism, + /// bypassing floem's normal repaint mechanism. + /// + /// This method may be removed or deprecated in the future, but has been needed + /// in [some situations](https://github.com/lapce/floem/issues/463), and to + /// address a few ongoing issues in `winit` (window unmaximize is delayed until + /// an external event triggers a repaint of the requesting window), and may + /// be needed as a workaround if other such issues are discovered until they + /// can be addressed. + /// + /// Returns true if the repaint request was issued successfully (i.e. there is + /// an actual system-level window corresponding to this `WindowId`). + fn force_repaint(&self) -> bool; + + /// Get the root view of this window. + fn root_view(&self) -> Option; + + /// Get a layout of this window in relation to the monitor on which it currently + /// resides, if any. + fn screen_layout(&self) -> Option; + + /// Get the dots-per-inch scaling of this window or 1.0 if the platform does not + /// support it (Android). + fn scale(&self) -> f64; +} + +impl WindowIdExt for WindowId { + fn bounds_on_screen_including_frame(&self) -> Option { + window_outer_screen_bounds(self) + } + + fn bounds_of_content_on_screen(&self) -> Option { + window_inner_screen_bounds(self) + } + + fn position_on_screen_including_frame(&self) -> Option { + window_outer_screen_position(self) + } + + fn position_of_content_on_screen(&self) -> Option { + window_inner_screen_position(self) + } + + fn monitor_bounds(&self) -> Option { + monitor_bounds(self) + } + + fn is_visible(&self) -> bool { + with_window(self, |window| window.is_visible().unwrap_or(false)).unwrap_or(false) + } + + fn is_minimized(&self) -> bool { + with_window(self, |window| window.is_minimized().unwrap_or(false)).unwrap_or(false) + } + + fn is_maximized(&self) -> bool { + with_window(self, Window::is_maximized).unwrap_or(false) + } + + #[cfg(target_os = "macos")] + #[allow(dead_code)] + fn is_document_edited(&self) -> bool { + with_window( + self, + floem_winit::platform::macos::WindowExtMacOS::is_document_edited, + ) + .unwrap_or(false) + } + + #[cfg(not(target_os = "macos"))] + #[allow(dead_code)] + fn is_document_edited(&self) -> bool { + false + } + + fn force_repaint(&self) -> bool { + force_window_repaint(self) + } + + fn root_view(&self) -> Option { + root_view_id(self) + } + + fn screen_layout(&self) -> Option { + with_window(self, move |window| screen_layout_for_window(*self, window)).unwrap_or(None) + } + + fn scale(&self) -> f64 { + with_window(self, Window::scale_factor).unwrap_or(1.0) + } +} + +/// Called by `ApplicationHandle` at the end of the event loop callback. +pub(crate) fn process_window_updates(id: &WindowId) -> bool { + let mut result = false; + if let Some(items) = WINDOW_UPDATE_MESSAGES.with_borrow_mut(|map| map.remove(id)) { + result = !items.is_empty(); + for update in items { + match update { + WindowUpdate::Visibility(visible) => { + with_window(id, |window| { + window.set_visible(visible); + }); + } + #[allow(unused_variables)] // non mac - edited is unused + WindowUpdate::DocumentEdited(edited) => { + #[cfg(target_os = "macos")] + with_window(id, |window| { + use floem_winit::platform::macos::WindowExtMacOS; + window.set_document_edited(edited); + }); + } + WindowUpdate::OuterBounds(bds) => { + with_window(id, |window| { + let params = + bounds_to_logical_outer_position_and_inner_size(window, bds, true); + window.set_outer_position(params.0); + // XXX log any returned error? + let _ = window.request_inner_size(params.1); + }); + } + WindowUpdate::InnerBounds(bds) => { + with_window(id, |window| { + let params = + bounds_to_logical_outer_position_and_inner_size(window, bds, false); + window.set_outer_position(params.0); + // XXX log any returned error? + let _ = window.request_inner_size(params.1); + }); + } + WindowUpdate::RequestAttention(att) => { + with_window(id, |window| { + window.request_user_attention(att); + }); + } + WindowUpdate::Minimize(minimize) => { + with_window(id, |window| { + window.set_minimized(minimize); + if !minimize { + // If we don't trigger a repaint on Mac OS, + // unminimize doesn't happen until an input + // event arrives. Unrelated to + // https://github.com/lapce/floem/issues/463 - + // this is in winit or below. + maybe_yield_with_repaint(window); + } + }); + } + WindowUpdate::Maximize(maximize) => { + with_window(id, |window| window.set_maximized(maximize)); + } + WindowUpdate::OuterLocation(outer) => { + with_window(id, |window| { + window.set_outer_position(LogicalPosition::new(outer.x, outer.y)); + }); + } + WindowUpdate::InnerSize(size) => { + with_window(id, |window| { + window.request_inner_size(LogicalSize::new(size.width, size.height)) + }); + } + } + } + } + result +} + +/// Compute a new logical position and size, given a window, a rectangle and whether the +/// rectangle represents the desired inner or outer bounds of the window. +/// +/// This is complex because winit offers us two somewhat contradictory ways of setting +/// the bounds: +/// +/// * You can set the **outer** position with `window.set_outer_position(position)` +/// * You can set the **inner** size with `window.request_inner_size(size)` +/// * You can obtain inner and outer sizes and positions, but you can only set outer +/// position and *inner* size +/// +/// So we must take the delta of the inner and outer size and/or positions (position +/// availability is more limited by platform), and from that, create an appropriate +/// inner size and outer position based on a `Rect` that represents either inner or +/// outer. +fn bounds_to_logical_outer_position_and_inner_size( + window: &Window, + target_bounds: Rect, + target_is_outer: bool, +) -> (LogicalPosition, LogicalSize) { + if !window.is_decorated() { + // For undecorated windows, the inner and outer location and size are always identical + // so no further work is needed + return ( + LogicalPosition::new(target_bounds.x0, target_bounds.y0), + LogicalSize::new(target_bounds.width(), target_bounds.height()), + ); + } + + let scale = window.scale_factor(); + if target_is_outer { + // We need to reduce the size we are requesting by the width and height of the + // OS-added decorations to get the right target INNER size: + let inner_to_outer_size_delta = delta_size(window.inner_size(), window.outer_size(), scale); + + ( + LogicalPosition::new(target_bounds.x0, target_bounds.y0), + LogicalSize::new( + (target_bounds.width() + inner_to_outer_size_delta.0).max(0.), + (target_bounds.height() + inner_to_outer_size_delta.1).max(0.), + ), + ) + } else { + // We need to shift the x/y position we are requesting up and left (negatively) + // to come up with an *outer* location that makes sense with the passed rectangle's + // size as an *inner* size + let size_delta = delta_size(window.inner_size(), window.outer_size(), scale); + let inner_to_outer_delta: (f64, f64) = if let Some(delta) = + delta_position(window.inner_position(), window.outer_position(), scale) + { + // This is the more accurate way, but may be unavailable on some platforms + delta + } else { + // We have to make a few assumptions here, one of which is that window + // decorations are horizontally symmetric - the delta-x / 2 equals a position + // on the perimeter of the window's frame. A few ancient XWindows window + // managers (Enlightenment) might violate that assumption, but it is a rarity. + ( + size_delta.0 / 2.0, + size_delta.1, // assume vertical is titlebar and give it full weight + ) + }; + ( + LogicalPosition::new( + target_bounds.x0 - inner_to_outer_delta.0, + target_bounds.y0 - inner_to_outer_delta.1, + ), + LogicalSize::new(target_bounds.width(), target_bounds.height()), + ) + } +} + +/// Some operations - notably minimize and restoring visibility - don't take +/// effect on Mac OS until something triggers a repaint in the target window - the +/// issue is below the level of floem's event loops and seems to be in winit or +/// deeper. Workaround is to force the window to repaint. +#[allow(unused_variables)] // non mac builds see `window` as unused +fn maybe_yield_with_repaint(window: &Window) { + #[cfg(target_os = "macos")] + { + window.request_redraw(); + let main = Some("main") != std::thread::current().name(); + if !main { + // attempt to get out of the way of the main thread + std::thread::yield_now(); + } + } +} + +fn delta_size(inner: PhysicalSize, outer: PhysicalSize, window_scale: f64) -> (f64, f64) { + let inner = winit_phys_size_to_size(inner, window_scale); + let outer = winit_phys_size_to_size(outer, window_scale); + (outer.width - inner.width, outer.height - inner.height) +} + +type PositionResult = + Result, floem_winit::error::NotSupportedError>; + +fn delta_position( + inner: PositionResult, + outer: PositionResult, + window_scale: f64, +) -> Option<(f64, f64)> { + if let Ok(inner) = inner { + if let Ok(outer) = outer { + let outer = winit_phys_position_to_point(outer, window_scale); + let inner = winit_phys_position_to_point(inner, window_scale); + + return Some((inner.x - outer.x, inner.y - outer.y)); + } + } + None +} + +// Conversion functions for winit's size and point types: + +fn winit_position_to_point + Pixel>(pos: LogicalPosition) -> Point { + Point::new(pos.x.into(), pos.y.into()) +} + +fn winit_size_to_size + Pixel>(size: LogicalSize) -> Size { + Size::new(size.width.into(), size.height.into()) +} + +fn winit_phys_position_to_point + Pixel>( + pos: PhysicalPosition, + window_scale: f64, +) -> Point { + winit_position_to_point::(pos.to_logical(window_scale)) +} + +fn winit_phys_size_to_size + Pixel>(size: PhysicalSize, window_scale: f64) -> Size { + winit_size_to_size::(size.to_logical(window_scale)) +} diff --git a/src/window_tracking.rs b/src/window_tracking.rs new file mode 100644 index 00000000..6ef01607 --- /dev/null +++ b/src/window_tracking.rs @@ -0,0 +1,259 @@ +//! Provides tracking to map window ids to windows in order to force repaints +//! on inactive windows (which would otherwise not receive messages), and so +//! that views can retrieve the `WindowId` of the window that contains them +//! and use the methods that look up the `Window` for that id to retrieve information +//! such as screen position. +use crate::ViewId; +use floem_winit::{ + dpi::{PhysicalPosition, PhysicalSize}, + monitor::MonitorHandle, + window::{Window, WindowId}, +}; +use peniko::kurbo::{Point, Rect}; +use std::{ + collections::HashMap, + sync::{Arc, OnceLock, RwLock}, +}; + +static WINDOW_FOR_WINDOW_AND_ROOT_IDS: OnceLock> = OnceLock::new(); + +/// Add a mapping from `root_id` -> `window_id` -> `window` for the given triple. +pub fn store_window_id_mapping( + root_id: ViewId, + window_id: WindowId, + window: &Arc, +) { + with_window_map_mut(move |m| m.add(root_id, window_id, window.clone())); +} + +/// Remove the mapping from `root_id` -> `window_id` -> `window` for the given triple. +pub fn remove_window_id_mapping(root_id: &ViewId, window_id: &WindowId) { + with_window_map_mut(move |m| m.remove(root_id, window_id)); +} + +/// Maps root-id:window-id:window triples, so a view can get its root and +/// from that locate the window-id (if any) that it belongs to. +#[derive(Default, Debug)] +struct WindowMapping { + window_for_window_id: HashMap>, + window_id_for_root_view_id: HashMap, +} + +impl WindowMapping { + fn add(&mut self, root: ViewId, window_id: WindowId, window: Arc) { + self.window_for_window_id.insert(window_id, window); + self.window_id_for_root_view_id.insert(root, window_id); + } + + fn remove(&mut self, root: &ViewId, window_id: &WindowId) { + let root_found = self.window_id_for_root_view_id.remove(root).is_some(); + let window_found = self.window_for_window_id.remove(window_id).is_some(); + debug_assert!(root_found == window_found, + "Window mapping state inconsistent. Remove root {:?} success was {} but remove {:?} success was {}", + root, root_found, window_id, window_found); + } + + fn with_window_id_and_window T, T>( + &self, + root_view_id: ViewId, + f: F, + ) -> Option { + self.window_id_for_root_view_id + .get(&root_view_id) + .and_then(|window_id| { + self.window_for_window_id + .get(window_id) + .map(|window| f(window_id, window)) + }) + } + + fn with_window) -> T, T>(&self, window: &WindowId, f: F) -> Option { + self.window_for_window_id.get(window).map(f) + } + + fn window_id_for_root(&self, id: &ViewId) -> Option { + self.window_id_for_root_view_id.get(id).copied() + } + + fn root_view_id_for(&self, window_id: &WindowId) -> Option { + for (k, v) in self.window_id_for_root_view_id.iter() { + if v == window_id { + return Some(*k); + } + } + None + } +} + +pub fn with_window_id_and_window T, T>( + view: &ViewId, + f: F, +) -> Option { + view.root() + .and_then(|root_view_id| with_window_map(|m| m.with_window_id_and_window(root_view_id, f))) + .unwrap_or(None) +} + +fn with_window_map_mut(mut f: F) -> bool { + let map = WINDOW_FOR_WINDOW_AND_ROOT_IDS.get_or_init(|| RwLock::new(Default::default())); + if let Ok(mut map) = map.write() { + f(&mut map); + true + } else { + false + } +} + +fn with_window_map T, T>(f: F) -> Option { + let map = WINDOW_FOR_WINDOW_AND_ROOT_IDS.get_or_init(|| RwLock::new(Default::default())); + if let Ok(map) = map.read() { + Some(f(&map)) + } else { + None + } +} + +pub fn with_window T, T>(window: &WindowId, f: F) -> Option { + with_window_map(|m| m.with_window(window, |w| f(w.as_ref()))).unwrap_or(None) +} + +pub fn root_view_id(window: &WindowId) -> Option { + with_window_map(|m| m.root_view_id_for(window)).unwrap_or(None) +} + +/// Force a single window to repaint - this is necessary in cases where the +/// window is not the active window and otherwise would not process update +/// messages sent to it. +pub fn force_window_repaint(id: &WindowId) -> bool { + with_window_map(|m| { + m.with_window(id, |window| window.request_redraw()) + .is_some() + }) + .unwrap_or(false) +} + +pub fn window_id_for_root(root_id: ViewId) -> Option { + with_window_map(|map| map.window_id_for_root(&root_id)).unwrap_or(None) +} + +pub fn monitor_bounds(id: &WindowId) -> Option { + with_window_map(|m| { + m.with_window(id, |window| { + window + .current_monitor() + .map(|monitor| monitor_bounds_for_monitor(window, &monitor)) + }) + .unwrap_or(None) + }) + .unwrap_or(None) +} + +pub fn monitor_bounds_for_monitor(window: &Window, monitor: &MonitorHandle) -> Rect { + let scale = 1.0 / window.scale_factor(); + let pos = monitor.position(); + let sz = monitor.size(); + let x = pos.x as f64 * scale; + let y = pos.y as f64 * scale; + Rect::new( + x, + y, + x + sz.width as f64 * scale, + y + sz.height as f64 * scale, + ) +} + +fn scale_rect(window: &Window, mut rect: Rect) -> Rect { + let scale = 1.0 / window.scale_factor(); + rect.x0 *= scale; + rect.y0 *= scale; + rect.x1 *= scale; + rect.y1 *= scale; + rect +} + +fn scale_point(window: &Window, mut rect: Point) -> Point { + let scale = 1.0 / window.scale_factor(); + rect.x *= scale; + rect.y *= scale; + rect +} + +pub fn window_inner_screen_position(id: &WindowId) -> Option { + with_window_map(|m| { + m.with_window(id, |window| { + window + .inner_position() + .map(|pos| Some(scale_point(window, Point::new(pos.x as f64, pos.y as f64)))) + .unwrap_or(None) + }) + .unwrap_or(None) + }) + .unwrap_or(None) +} + +pub fn window_inner_screen_bounds(id: &WindowId) -> Option { + with_window_map(|m| { + m.with_window(id, |window| { + window + .inner_position() + .map(|pos| { + Some(rect_from_physical_bounds_for_window( + window, + pos, + window.inner_size(), + )) + }) + .unwrap_or(None) + }) + .unwrap_or(None) + }) + .unwrap_or(None) +} + +pub fn rect_from_physical_bounds_for_window( + window: &Window, + pos: PhysicalPosition, + sz: PhysicalSize, +) -> Rect { + scale_rect( + window, + Rect::new( + pos.x as f64, + pos.y as f64, + pos.x as f64 + sz.width.max(0) as f64, + pos.y as f64 + sz.height.max(0) as f64, + ), + ) +} + +pub fn window_outer_screen_position(id: &WindowId) -> Option { + with_window_map(|m| { + m.with_window(id, |window| { + window + .outer_position() + .map(|pos| Some(scale_point(window, Point::new(pos.x as f64, pos.y as f64)))) + .unwrap_or(None) + }) + .unwrap_or(None) + }) + .unwrap_or(None) +} + +pub fn window_outer_screen_bounds(id: &WindowId) -> Option { + with_window_map(|m| { + m.with_window(id, |window| { + window + .outer_position() + .map(|pos| { + Some(rect_from_physical_bounds_for_window( + window, + pos, + window.outer_size(), + )) + }) + .unwrap_or(None) + }) + .unwrap_or(None) + }) + .unwrap_or(None) +}