Skip to content

Commit

Permalink
Window management (#480)
Browse files Browse the repository at this point in the history
* Add methods to show and hide a window

* Fix #463 - broken inter-window update messages

This addresses a couple of issues:

 * Messages (including repaints) not getting processed when delivered a `View` in a window which is inactive
 * Ability to retrieve the window id of a view without the application having to pass that information down
   to whatever code needs it

and addresses a related issue - obtaining a `WindowId` for a realized `View` and its bounds on screen, in order
to convert view-relative locations into screen locations for purposes such as positioning windows relative
to a `View`.

This patch adds a tracking data structure, and a convenience data structure which contains sufficient
information to convert view-relative positions to screen-positions and back.

 * A static mapping of `root-id:WindowId`:`Arc<Window>` which is populated on window creation and cleared on destruction,
  which makes it possible to access `Window.request_redraw()` to force a redraw.  It is guarded by a read-write
  lock (but written to only during window creation and destruction).
 * `ScreenLayout` contains a snapshot of the window id, the window's content and framed bounds, the view's
  origin within the window and the monitor scaling.

So, a ViewId that wants its window-id looks up its root view's id, and looks up the WindowId associated
with that root.

We extend `WindowId` with methods to get the window bounds on screen with and without decorations and similar -
these methods could be added to `ViewId` instead, but it seems more intuitive that you ask a window id for
the window's bounds the same way you ask a view id for the view's bounds - that seems like what users will
expect.

Given the usage patterns (the mapping will only be locked for writing when a window is being created or destroyed, and will
not be read-locked except in the case of forcing a repaint or looking up window coordinates), it is not touched
in the critical path of rendering - unless the application and destroying many windows at the same time on
different threads, I would not expect the read-write lock to be contended at all in practice (famous last
words, I know).

A thread-local for the mapping, as is done in `update.rs` might work (or look like it works), but we would be
assuming no platform floem will ever support uses different event threads for different windows' event
loops. I know of two that do - BeOS and Java AWT (with its stack of event threads to allow modal dialogs
that work when their parent is disabled or blocked, which was a source of "interesting" deadlocks).

If there was some path that I missed that would let a `ViewId` reach down to the `WindowHandle` that
owns it without doing this sort of tracking, I would love to know about it - `PaintCx` has this information,
but it is the only thing that does, and the `ViewId` has no access to it.

* Move WindowIdExt to its own file; eliminate force-paint hack - better fix available

* Implement a number of window management features

 * Window show/hide (particularly needed for repeatedly shown popups where you don't want to allocate a native window on every call,
   but just reposition and reveal)
 * Window location / size / bounds management
 * Fetching the bounds of the monitor a window is on (needed for relative positioning of windows so they don't go offscreen)
 * Minimized / maximized management
 * Edited status management (MacOS only)

Tested on Mac OS Ventura 13.6.6 / Intel and Gentoo Linux 6.7.6 w/ Openbox WM.  Attempted to test on Wayland, but ran into
wgpu crashes with the OpenGL back-end and I don't have the DRM back-end built.
  • Loading branch information
timboudreau committed May 31, 2024
1 parent 959e292 commit dde5a1b
Show file tree
Hide file tree
Showing 8 changed files with 1,071 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/app_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::{
view::View,
window::WindowConfig,
window_handle::WindowHandle,
window_id::process_window_updates,
};

pub(crate) struct ApplicationHandle {
Expand Down Expand Up @@ -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) {}
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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! {
Expand Down Expand Up @@ -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<WindowId> {
self.root().and_then(window_id_for_root)
}

pub fn request_paint(&self) {
self.add_update_message(UpdateMessage::RequestPaint);
}
Expand Down Expand Up @@ -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));
Expand All @@ -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<ScreenLayout> {
crate::screen_layout::try_create_screen_layout(self)
}
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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};
280 changes: 280 additions & 0 deletions src/screen_layout.rs
Original file line number Diff line number Diff line change
@@ -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<ScreenLayout> {
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<ScreenLayout> {
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<Point>,
}

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<Point>,
target_size: Option<Size>,
) -> 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
}
1 change: 1 addition & 0 deletions src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,5 @@ pub(crate) enum UpdateMessage {
position: Point,
size: Size,
},
WindowVisible(bool),
}
Loading

0 comments on commit dde5a1b

Please sign in to comment.