mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-09 20:12:57 +00:00
docking: normal floating windows come forward when the app is activated (#301)
* docking: normal floating windows come forward when the app is activated * docking: drop windows that became palettes from the raise queue * docking: the main window comes forward with the normal windows, in focus order
This commit is contained in:
1 parent
f9181cc7b9
commit
acb7290804
5 files changed
+172
-15
No files matched your search
@@ -391,6 +391,81 @@ fn viewer_windows_stay_shown_at_normal_level() {
|
||||
assert_eq!(shown(&d), [true, true, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_app_comes_forward_with_its_viewers_when_it_is_activated() {
|
||||
use egui::{ViewportId, WindowLevel};
|
||||
let start = || {
|
||||
let mut layout = layout(false);
|
||||
layout.floating = vec![
|
||||
Floating::single(vec![5], 0, [300.0, 80.0, 240.0, 300.0]),
|
||||
Floating::single(vec![6], 0, [560.0, 80.0, 240.0, 300.0]),
|
||||
Floating::single(vec![7], 0, [820.0, 80.0, 240.0, 300.0]),
|
||||
];
|
||||
let mut d = driver(layout);
|
||||
d.content.normal = vec![5, 7];
|
||||
d.step(vec![]);
|
||||
let [back, palette, front] = [5, 6, 7].map(|p| d.viewport(window(&d, p).id));
|
||||
(d, back, palette, front)
|
||||
};
|
||||
let focus = |d: &mut DockDriver<u32, Content>, window: Option<ViewportId>, frames: usize| {
|
||||
d.host().focus = window;
|
||||
for _ in 0..frames {
|
||||
d.step(vec![]);
|
||||
}
|
||||
};
|
||||
let levels = |d: &mut DockDriver<u32, Content>| -> Vec<(ViewportId, WindowLevel)> {
|
||||
let mut host = d.host();
|
||||
assert!(!host.commands.iter().any(|(_, c)| matches!(c, ViewportCommand::Focus)), "no window takes focus");
|
||||
let levels = host.commands.iter().filter_map(|(v, c)| if let ViewportCommand::WindowLevel(l) = c { Some((*v, *l)) } else { None }).collect();
|
||||
host.commands.clear();
|
||||
levels
|
||||
};
|
||||
// Each window once, in this order: the main window's commands are the root's.
|
||||
let raised = |order: &[ViewportId]| -> Vec<(ViewportId, WindowLevel)> {
|
||||
if cfg!(target_os = "macos") {
|
||||
return Vec::new();
|
||||
}
|
||||
order.iter().flat_map(|v| [(*v, WindowLevel::AlwaysOnTop), (*v, WindowLevel::Normal)]).collect()
|
||||
};
|
||||
const MAIN: ViewportId = ViewportId::ROOT;
|
||||
|
||||
let (mut d, back, palette, front) = start();
|
||||
// Focus moving between the app's own windows, with a frame of none between two, raises
|
||||
// nothing.
|
||||
focus(&mut d, Some(back), 2);
|
||||
focus(&mut d, None, 1);
|
||||
focus(&mut d, Some(front), 2);
|
||||
focus(&mut d, Some(MAIN), 2);
|
||||
assert_eq!(levels(&mut d), []);
|
||||
// Another application, then back through the main window: the viewers are raised back to
|
||||
// front and the main window last, without focus; the palette only shows again.
|
||||
focus(&mut d, None, 20);
|
||||
focus(&mut d, Some(MAIN), 10);
|
||||
assert_eq!(levels(&mut d), raised(&[back, front, MAIN]));
|
||||
assert!(d.host().windows[&palette].visible);
|
||||
// Back through the back viewer: it is raised last, so it stays in front, and the main
|
||||
// window comes forward behind it.
|
||||
focus(&mut d, None, 20);
|
||||
focus(&mut d, Some(back), 10);
|
||||
assert_eq!(levels(&mut d), raised(&[front, MAIN, back]));
|
||||
// A queued viewer that turns into a palette before its turn is not raised: the raise
|
||||
// would leave it at the normal level.
|
||||
focus(&mut d, None, 20);
|
||||
focus(&mut d, Some(MAIN), 1);
|
||||
d.content.normal = vec![7];
|
||||
focus(&mut d, Some(MAIN), 10);
|
||||
assert_eq!(levels(&mut d), raised(&[front, MAIN]));
|
||||
|
||||
// A press in the main window brings it in front of the viewers that opened over it, though
|
||||
// it kept focus.
|
||||
let (mut d, back, _, front) = start();
|
||||
d.click(pos2(100.0, 300.0));
|
||||
levels(&mut d);
|
||||
focus(&mut d, None, 20);
|
||||
focus(&mut d, Some(back), 10);
|
||||
assert_eq!(levels(&mut d), raised(&[front, MAIN, back]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_keys_in_a_native_window_reach_the_app() {
|
||||
let mut d = driver(layout(true));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Floating windows in their own OS windows (feature `native-windows`). Where the backend has
|
||||
//! viewports, every floating window whose panels allow it is drawn with
|
||||
//! `show_viewport_immediate`, always on top of other windows and hidden while none of the
|
||||
//! application's windows has focus. Where egui embeds viewports (wasm, tests) windows stay
|
||||
//! `show_viewport_immediate`. Palettes stay on top of other windows and hide while none of the
|
||||
//! application's windows has focus; viewers are normal windows that come forward when the
|
||||
//! application is activated. Where egui embeds viewports (wasm, tests) windows stay
|
||||
//! in-window at their in-window rect, and so do they on Wayland, unless the app turns native
|
||||
//! windows on ([`crate::docking::DockArea::native_windows`]). Tabs, groups and whole windows
|
||||
//! drag between OS windows (see `cross.rs`).
|
||||
@@ -25,6 +26,10 @@ const MIN_SIZE: [f32; 2] = [160.0, 100.0];
|
||||
/// How long floating windows stay after the application's last window lost focus, so focus
|
||||
/// moving between our own windows does not hide them.
|
||||
const HIDE_DELAY: f64 = 0.25;
|
||||
/// Whether normal windows are raised when the application is activated. macOS brings an
|
||||
/// application's windows forward itself: all of them on activation from the Dock or the
|
||||
/// application switcher, the clicked one alone on a click, as its own windows do.
|
||||
const RAISE_ON_ACTIVATION: bool = !cfg!(target_os = "macos");
|
||||
/// How long reports of a native window's place may lag a move the dock area commanded.
|
||||
const MOVE_WAIT: f64 = 0.5;
|
||||
/// How far the resize bands reach into a native window from its edges, where the window has
|
||||
@@ -204,7 +209,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
pub(super) fn native_windows<'w>(&mut self, ctx: &Context, windows: Vec<&'w Floating<P>>) -> Vec<&'w Floating<P>> {
|
||||
let native = !ctx.embed_viewports() && self.native_windows.unwrap_or_else(|| !wayland(ctx, env_session()));
|
||||
let origin = ctx.input(|i| i.viewport().inner_rect.map(|r| r.min)).filter(|p| p.is_finite()).unwrap_or(Pos2::ZERO);
|
||||
let visible = !native || self.float_visibility(ctx);
|
||||
let (visible, activated) = if native { self.float_visibility(ctx) } else { (true, false) };
|
||||
self.cross.on = native;
|
||||
let mut shown = Vec::new();
|
||||
let mut in_window = Vec::new();
|
||||
@@ -235,7 +240,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if ctx.data(|d| d.get_temp::<WindowDrag>(key)).is_some_and(|d| !natives.iter().any(|(w, ..)| w.id == d.window)) {
|
||||
ctx.data_mut(|d| d.remove::<WindowDrag>(key));
|
||||
}
|
||||
self.map_desktop(ctx, &natives, visible);
|
||||
self.map_desktop(ctx, &natives, visible, activated);
|
||||
self.raise_next(ctx, &natives);
|
||||
// The main window owns drags of the panels it draws.
|
||||
let id = self.id;
|
||||
let dragged = ctx.data(|d| {
|
||||
@@ -285,13 +291,21 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
|
||||
/// The main window and the shown native windows on the desktop, front first: palettes
|
||||
/// above the rest, each the window focused most recently first. A window whose position is
|
||||
/// unknown is left out. `visible`: palettes show.
|
||||
fn map_desktop(&mut self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)], visible: bool) {
|
||||
/// unknown is left out. `visible`: palettes show. `activated`: the application was just
|
||||
/// activated, so the main window and its normal windows are raised (see
|
||||
/// [`Self::raise_next`]).
|
||||
fn map_desktop(&mut self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)], visible: bool, activated: bool) {
|
||||
let key = self.id.with("native-order");
|
||||
// Focus order, front first; `None` is the main window.
|
||||
let mut order = ctx.data(|d| d.get_temp::<Vec<Option<u64>>>(key)).unwrap_or_default();
|
||||
order.retain(|id| id.is_none_or(|id| natives.iter().any(|(w, ..)| w.id == id)));
|
||||
if ctx.input(|i| i.viewport().focused == Some(true)) || !order.contains(&None) {
|
||||
// The main window comes to the front when it gains focus, and on a press in it: the
|
||||
// window system raises it then, though it may have kept focus.
|
||||
let main_key = self.id.with("native-main-focused");
|
||||
let (main_focused, pressed) = ctx.input(|i| (i.viewport().focused == Some(true), i.pointer.any_pressed()));
|
||||
let gained = main_focused && !ctx.data(|d| d.get_temp::<bool>(main_key)).unwrap_or(false);
|
||||
ctx.data_mut(|d| d.insert_temp(main_key, main_focused));
|
||||
if gained || pressed || !order.contains(&None) {
|
||||
let front = order.contains(&None);
|
||||
order.retain(Option::is_some);
|
||||
if front { order.insert(0, None) } else { order.push(None) }
|
||||
@@ -314,6 +328,19 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
windows.push((window.id, placed, normal));
|
||||
}
|
||||
}
|
||||
if activated && RAISE_ON_ACTIVATION {
|
||||
// The back one first, so each lands in front of those raised before it, and the
|
||||
// window just focused, at the front of the order, last.
|
||||
let queue: Vec<Option<u64>> = order
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|id| id.is_none_or(|id| natives.iter().any(|(w, ..)| w.id == id && self.normal_window(w))))
|
||||
.copied()
|
||||
.collect();
|
||||
if queue.iter().any(Option::is_some) {
|
||||
ctx.data_mut(|d| d.insert_temp(self.id.with("native-raise"), queue));
|
||||
}
|
||||
}
|
||||
let rank = |id: Option<u64>| order.iter().position(|o| *o == id);
|
||||
windows.sort_by_key(|(id, _, normal)| (*normal, rank(Some(*id))));
|
||||
self.cross.front = windows.iter().filter(|(id, _, normal)| !normal || rank(Some(*id)) < rank(None)).count();
|
||||
@@ -322,8 +349,40 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.cross.main = ctx.input(|i| i.viewport().outer_rect.and(i.viewport().inner_rect)).filter(|r| r.is_finite());
|
||||
}
|
||||
|
||||
/// Whether palettes show this frame.
|
||||
fn float_visibility(&mut self, ctx: &Context) -> bool {
|
||||
/// Raise the next window [`Self::map_desktop`] queued when the application was activated,
|
||||
/// the main window (`None`) or a normal window: one per pass, since egui sends the commands
|
||||
/// of different viewports in no fixed order. Toggling the window level raises a window without focusing it: on Windows
|
||||
/// `HWND_NOTOPMOST` puts the window in front of every window that is not topmost, and on
|
||||
/// X11 the window manager raises a window as it takes `_NET_WM_STATE_ABOVE`. winit has no
|
||||
/// plain raise. `ViewportCommand::Focus` would take focus from the window the user
|
||||
/// activated, and egui-winit ignores it for a window that has focus, so focus could not be
|
||||
/// handed back. `Visible(true)` does nothing to a shown window.
|
||||
fn raise_next(&self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)]) {
|
||||
let key = self.id.with("native-raise");
|
||||
let Some(mut queue) = ctx.data_mut(|d| d.remove_temp::<Vec<Option<u64>>>(key)) else { return };
|
||||
// A window that became a palette since activation keeps its `AlwaysOnTop` level.
|
||||
let viewport = |id: Option<u64>| match id {
|
||||
None => Some(ViewportId::ROOT),
|
||||
Some(id) => natives.iter().find(|(w, ..)| w.id == id && self.normal_window(w)).map(|(.., v)| *v),
|
||||
};
|
||||
queue.retain(|id| viewport(*id).is_some());
|
||||
if queue.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(viewport) = viewport(queue.remove(0)) {
|
||||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
|
||||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::WindowLevel(WindowLevel::Normal));
|
||||
}
|
||||
if !queue.is_empty() {
|
||||
ctx.data_mut(|d| d.insert_temp(key, queue));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether palettes show this frame, and whether the application was just activated: one
|
||||
/// of its windows has focus after none had it for long enough to hide the palettes.
|
||||
/// Losing focus again stops the raise of normal windows that activation started.
|
||||
fn float_visibility(&mut self, ctx: &Context) -> (bool, bool) {
|
||||
let (focused, now) = ctx.input(|i| {
|
||||
let infos = || i.raw.viewports.values();
|
||||
// A backend that reports no focus at all keeps the windows.
|
||||
@@ -338,7 +397,15 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64((HIDE_DELAY - (now - last)).max(0.0)));
|
||||
}
|
||||
ctx.data_mut(|d| d.insert_temp(key, last));
|
||||
visible
|
||||
let away_key = self.id.with("native-away");
|
||||
let away = ctx.data(|d| d.get_temp::<bool>(away_key)).unwrap_or(false);
|
||||
ctx.data_mut(|d| {
|
||||
d.insert_temp(away_key, !focused && (away || !visible));
|
||||
if !focused {
|
||||
d.remove::<Vec<Option<u64>>>(self.id.with("native-raise"));
|
||||
}
|
||||
});
|
||||
(visible, focused && away)
|
||||
}
|
||||
|
||||
/// A window holding a viewer is a normal window, so it neither vanishes nor sits over other
|
||||
|
||||
@@ -640,7 +640,9 @@ pub enum WindowKind {
|
||||
#[default]
|
||||
Palette,
|
||||
/// A window of its own, such as a viewer on a second display: normal window level, and it
|
||||
/// stays shown while the app is in the background.
|
||||
/// stays shown while the app is in the background. When the app is activated, these
|
||||
/// windows and the main window come forward, and the main window is left at the normal
|
||||
/// level.
|
||||
Normal,
|
||||
}
|
||||
|
||||
|
||||
@@ -389,7 +389,7 @@ pub struct NativeHost {
|
||||
/// frame still reports the old place, and gets pointer events relative to it.
|
||||
pub lag: bool,
|
||||
pub windows: std::collections::HashMap<egui::ViewportId, HostWindow>,
|
||||
/// Every command sent to a native window, in order.
|
||||
/// Every command sent to a native window or the main window, in order.
|
||||
pub commands: Vec<(egui::ViewportId, egui::ViewportCommand)>,
|
||||
/// Moves waiting to land under `lag`: window, content position, frame sent.
|
||||
late: Vec<(egui::ViewportId, Pos2, usize)>,
|
||||
@@ -527,9 +527,6 @@ impl NativeHost {
|
||||
/// Carry out the commands a frame sent to native windows.
|
||||
fn apply(&mut self, full: &egui::FullOutput) {
|
||||
for (id, output) in &full.viewport_output {
|
||||
if *id == egui::ViewportId::ROOT {
|
||||
continue;
|
||||
}
|
||||
for command in &output.commands {
|
||||
self.commands.push((*id, command.clone()));
|
||||
let Some(window) = self.windows.get_mut(id) else { continue };
|
||||
|
||||
@@ -393,6 +393,22 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho
|
||||
background. They stack with the main window by focus, below every palette; a drag over a
|
||||
normal window that lies behind the main window targets the main window where the two
|
||||
overlap, and the normal window outside the main window.
|
||||
- Like palettes, normal windows have no taskbar entry; they come forward with the app
|
||||
instead. When one of the app's windows gains focus after none had it for the hide delay,
|
||||
the main window and each normal window get `WindowLevel::AlwaysOnTop` and then
|
||||
`WindowLevel::Normal`, one window per frame, in their focus order from back to front.
|
||||
They keep their order, nothing takes focus, and the window the user activated is raised
|
||||
last, so it stays in front. The focus order counts a press in the main window as focusing
|
||||
it, since the window system raises it then. The main window is left at the normal level,
|
||||
so an app that keeps its main window always on top should not use normal windows. On
|
||||
Windows the toggle puts the window in front of every window that is not topmost, without
|
||||
activating it; on X11 the window manager raises a window as it takes
|
||||
`_NET_WM_STATE_ABOVE`. egui 0.36 cannot make the main window a native window's owner,
|
||||
which on Windows would bring the window forward with the main window, and
|
||||
`ViewportCommand::Focus` would take focus. On
|
||||
macOS nothing is raised: activation from the Dock or the application switcher brings all
|
||||
of the app's windows forward, and a click brings the clicked window alone, as with the
|
||||
app's other windows.
|
||||
- **Bar.** Each window has a bar at the top, 18 points high, in the floating title bar's style:
|
||||
the title (the shown panel of its first group) and ×. Dragging the bar moves the window. On
|
||||
Wayland, where a window cannot be placed, the bar hands the move to the compositor. On
|
||||
|
||||
Reference in new issue
Block a user