docking: viewer panels float as normal windows (#296)

* docking: viewer panels float as normal windows

DockContent::window_kind marks panels as palettes (the default) or normal
windows. A native floating window holding any Normal panel gets the normal
window level and stays shown while the app is in the background; drags
treat it as stacked with the main window by focus.

* docs: a normal window behind the main window takes drags outside it
This commit is contained in:
aiongg authored and GitHub committed 2026-10-24 17:13:37 -04:00
1 parent c98ee15a7f
commit c75bf618f0
8 files changed
+168 -58

No files matched your search

+26 -20
View File
@@ -16,19 +16,21 @@ pub(super) enum Over {
}
/// The window under `point`: the first of `windows` (floating native windows by id and desktop
/// rect, front first) that holds it, then `main` (the main window's content). The window
/// `dragged` follows the pointer by its bar and is never its own target.
pub(super) fn window_at(point: Pos2, windows: &[(u64, Rect)], main: Option<Rect>, dragged: Option<u64>) -> Over {
/// rect, front first) that holds it, where the first `front` of them lie in front of `main`
/// (the main window's content) and the rest behind it. The window `dragged` follows the pointer
/// by its bar and is never its own target.
pub(super) fn window_at(point: Pos2, windows: &[(u64, Rect)], front: usize, main: Option<Rect>, dragged: Option<u64>) -> Over {
if !point.is_finite() {
return Over::Desktop;
}
if let Some((id, rect)) = windows.iter().find(|(id, rect)| Some(*id) != dragged && rect.contains(point)) {
return Over::Window(*id, point - rect.min.to_vec2());
}
match main {
Some(main) if main.contains(point) => Over::Main(point - main.min.to_vec2()),
_ => Over::Desktop,
}
let (before, behind) = windows.split_at(front.min(windows.len()));
let hit = |windows: &[(u64, Rect)]| {
windows.iter().find(|(id, rect)| Some(*id) != dragged && rect.contains(point)).map(|(id, rect)| Over::Window(*id, point - rect.min.to_vec2()))
};
hit(before)
.or_else(|| main.filter(|main| main.contains(point)).map(|main| Over::Main(point - main.min.to_vec2())))
.or_else(|| hit(behind))
.unwrap_or(Over::Desktop)
}
#[cfg(test)]
@@ -46,24 +48,28 @@ mod tests {
let main = Some(rect(100.0, 50.0, 800.0, 600.0));
// Window 2 lies over window 1, and both over the main window.
let windows = [(2, rect(300.0, 100.0, 200.0, 200.0)), (1, rect(250.0, 80.0, 200.0, 200.0))];
assert_eq!(window_at(pos2(350.0, 150.0), &windows, main, None), Over::Window(2, pos2(50.0, 50.0)));
assert_eq!(window_at(pos2(260.0, 90.0), &windows, main, None), Over::Window(1, pos2(10.0, 10.0)));
assert_eq!(window_at(pos2(120.0, 60.0), &windows, main, None), Over::Main(pos2(20.0, 10.0)));
assert_eq!(window_at(pos2(20.0, 20.0), &windows, main, None), Over::Desktop);
assert_eq!(window_at(pos2(350.0, 150.0), &windows, 2, main, None), Over::Window(2, pos2(50.0, 50.0)));
assert_eq!(window_at(pos2(260.0, 90.0), &windows, 2, main, None), Over::Window(1, pos2(10.0, 10.0)));
assert_eq!(window_at(pos2(120.0, 60.0), &windows, 2, main, None), Over::Main(pos2(20.0, 10.0)));
assert_eq!(window_at(pos2(20.0, 20.0), &windows, 2, main, None), Over::Desktop);
// A floating window outside the main window.
let windows = [(3, rect(1000.0, 0.0, 100.0, 100.0))];
assert_eq!(window_at(pos2(1050.0, 40.0), &windows, main, None), Over::Window(3, pos2(50.0, 40.0)));
assert_eq!(window_at(pos2(1050.0, 40.0), &windows, 2, main, None), Over::Window(3, pos2(50.0, 40.0)));
// Unknown main window, a point that is not one.
assert_eq!(window_at(pos2(120.0, 60.0), &[], None, None), Over::Desktop);
assert_eq!(window_at(pos2(f32::NAN, 60.0), &windows, main, None), Over::Desktop);
assert_eq!(window_at(pos2(120.0, 60.0), &[], 0, None, None), Over::Desktop);
assert_eq!(window_at(pos2(f32::NAN, 60.0), &windows, 2, main, None), Over::Desktop);
// A normal window behind the main window is reached only past the main window's edge.
let windows = [(4, rect(50.0, 40.0, 200.0, 200.0))];
assert_eq!(window_at(pos2(120.0, 60.0), &windows, 0, main, None), Over::Main(pos2(20.0, 10.0)));
assert_eq!(window_at(pos2(60.0, 45.0), &windows, 0, main, None), Over::Window(4, pos2(10.0, 5.0)));
}
#[test]
fn the_dragged_window_is_looked_through() {
let main = Some(rect(0.0, 0.0, 800.0, 600.0));
let windows = [(2, rect(300.0, 100.0, 200.0, 200.0)), (1, rect(250.0, 80.0, 200.0, 200.0))];
assert_eq!(window_at(pos2(350.0, 150.0), &windows, main, Some(2)), Over::Window(1, pos2(100.0, 70.0)));
assert_eq!(window_at(pos2(350.0, 150.0), &windows, main, Some(1)), Over::Window(2, pos2(50.0, 50.0)));
assert_eq!(window_at(pos2(480.0, 280.0), &windows, main, Some(2)), Over::Main(pos2(480.0, 280.0)));
assert_eq!(window_at(pos2(350.0, 150.0), &windows, 2, main, Some(2)), Over::Window(1, pos2(100.0, 70.0)));
assert_eq!(window_at(pos2(350.0, 150.0), &windows, 2, main, Some(1)), Over::Window(2, pos2(50.0, 50.0)));
assert_eq!(window_at(pos2(480.0, 280.0), &windows, 2, main, Some(2)), Over::Main(pos2(480.0, 280.0)));
}
}
+2 -2
View File
@@ -31,8 +31,8 @@ pub use hits::{Divider, Hit, ResizeEdge};
pub use model::{Action, DockError, Floating, Layout, Location, MAX_DEPTH, MAX_PANELS, Monitor, Node, Permissions, Placement, StackEntry, Zone};
pub use render::{close_flyout, flyout, open_flyout};
pub use surface::{
DockArea, DockContent, DockEvent, DockHeights, DockOutput, DockStyle, DropFeedback, DropRequest, FloatSpace, HidePanels, PanelLimits, begin_drag,
cancel_drag, float_space, is_dragging,
DockArea, DockContent, DockEvent, DockHeights, DockOutput, DockStyle, DropFeedback, DropRequest, FloatSpace, HidePanels, PanelLimits, WindowKind,
begin_drag, cancel_drag, float_space, is_dragging,
};
pub use targets::{DropProposal, DropTarget, drop_zone, resolve_drop, tab_insertion_marker, zone_rect, zone_shape};
pub use workspace::Workspace;
+38 -1
View File
@@ -5,7 +5,7 @@ use egui::{Color32, Event, Key, Modifiers, PointerButton, Pos2, Rect, ViewportCo
use super::render::{APPKIT_CORNER, DragPointer, MACOS_CLOSE, WindowDrag, appkit_resize_areas, close_rects, floats_shown, resize_bands, wayland};
use super::testing::DockDriver;
use super::{Action, DockContent, Floating, Hit, Layout, Monitor, Node, Permissions, Placement, Target, Workspace};
use super::{Action, DockContent, Floating, Hit, Layout, Monitor, Node, Permissions, Placement, Target, WindowKind, Workspace};
use crate::layout::{SplitAxis, SplitSize};
/// Desktop position of the main window's content.
@@ -17,6 +17,8 @@ struct Content {
fixed: Vec<u32>,
/// Panels that stay in-window.
in_window: Vec<u32>,
/// Viewer panels: their windows are normal windows.
normal: Vec<u32>,
/// The panel with a text field, what it holds, and whether it takes focus next frame.
text: Option<u32>,
typed: String,
@@ -50,6 +52,10 @@ impl DockContent<u32> for Content {
fn allow_native(&self, panel: &u32) -> bool {
!self.in_window.contains(panel)
}
fn window_kind(&self, panel: &u32) -> WindowKind {
if self.normal.contains(panel) { WindowKind::Normal } else { WindowKind::Palette }
}
}
fn tabs(panels: &[u32]) -> Node<u32> {
@@ -348,6 +354,37 @@ fn floating_windows_hide_while_the_app_is_inactive() {
assert!(d.host().windows[&viewport].visible);
}
#[test]
fn viewer_windows_stay_shown_at_normal_level() {
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]),
// A palette in front of a viewer's tab: the viewer makes the window normal.
Floating::single(vec![7, 8], 0, [820.0, 80.0, 240.0, 300.0]),
];
let mut d = driver(layout);
d.content.normal = vec![5, 8];
d.step(vec![]);
let [viewer, palette, mixed] = [5, 6, 7].map(|p| d.viewport(window(&d, p).id));
let level = |d: &DockDriver<u32, Content>, v| d.host().windows[&v].level;
assert_eq!(level(&d, viewer), Some(egui::WindowLevel::Normal));
assert_eq!(level(&d, palette), Some(egui::WindowLevel::AlwaysOnTop));
assert_eq!(level(&d, mixed), Some(egui::WindowLevel::Normal));
// Another application: only the palette hides, and the others get no visibility command.
d.host().focus = None;
for _ in 0..20 {
d.step(vec![]);
}
let shown = |d: &DockDriver<u32, Content>| [viewer, palette, mixed].map(|v| d.host().windows[&v].visible);
assert_eq!(shown(&d), [true, false, true]);
let told = |d: &DockDriver<u32, Content>, v| d.host().commands.iter().any(|(w, c)| *w == v && matches!(c, ViewportCommand::Visible(_)));
assert!(!told(&d, viewer) && !told(&d, mixed));
d.host().focus = Some(egui::ViewportId::ROOT);
d.step(vec![]);
assert_eq!(shown(&d), [true, true, true]);
}
#[test]
fn unused_keys_in_a_native_window_reach_the_app() {
let mut d = driver(layout(true));
+4 -1
View File
@@ -85,6 +85,8 @@ pub(in crate::docking) struct Cross<P> {
pub main: Option<Rect>,
/// Shown native windows' desktop rects, front first.
pub windows: Vec<(u64, Rect)>,
/// How many of `windows` lie in front of the main window.
pub front: usize,
/// Targets of the native windows drawn this frame.
pub surfaces: Vec<Surface<P>>,
/// The drag's pointer, as its owner wrote it this frame.
@@ -106,6 +108,7 @@ impl<P> Default for Cross<P> {
focus_lost: false,
main: None,
windows: Vec::new(),
front: 0,
surfaces: Vec::new(),
pointer: None,
release: None,
@@ -152,7 +155,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// known, and a point outside it is on the desktop. `current` is a window drawing now.
fn destination(&self, pointer: &DragPointer, current: Option<&Surface<P>>) -> Option<Dest> {
if let (Some(point), Some(main)) = (pointer.desktop(), self.cross.main) {
return Some(match window_at(point, &self.cross.windows, Some(main), pointer.window) {
return Some(match window_at(point, &self.cross.windows, self.cross.front, Some(main), pointer.window) {
Over::Main(local) => Dest::Main(local),
Over::Window(id, local) => Dest::Window(id, local),
Over::Desktop => Dest::Desktop(Some(point)),
+48 -27
View File
@@ -18,7 +18,7 @@ use super::cross::{DragPointer, Surface};
use crate::docking::chrome::{ChromeStyle, TitleBar};
pub(in crate::docking) use crate::docking::placement::BAR_HEIGHT;
use crate::docking::placement::{kept_reachable, monitor_of, restore, sanitized};
use crate::docking::{Action, Drag, Floating, GroupDrag, GroupGesture, Node};
use crate::docking::{Action, Drag, Floating, GroupDrag, GroupGesture, Node, WindowKind};
/// Smallest native window.
const MIN_SIZE: [f32; 2] = [160.0, 100.0];
@@ -143,6 +143,8 @@ struct Place {
pending: Option<f64>,
/// The window system put the frame, not the content, where it opened: corrected once.
corrected: bool,
/// Shown, as it opened or was last told.
shown: bool,
}
/// Whether two rects differ by more than rounding.
@@ -202,7 +204,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 visibility = if native { self.float_visibility(ctx) } else { (true, false) };
let visible = !native || self.float_visibility(ctx);
self.cross.on = native;
let mut shown = Vec::new();
let mut in_window = Vec::new();
@@ -233,7 +235,7 @@ 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, visibility.0);
self.map_desktop(ctx, &natives, visible);
// The main window owns drags of the panels it draws.
let id = self.id;
let dragged = ctx.data(|d| {
@@ -245,7 +247,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
}
for (window, rect, viewport) in natives {
if let Some(key) = self.native_window(ctx, window, rect, viewport, visibility) {
if let Some(key) = self.native_window(ctx, window, rect, viewport, visible) {
shown.push(key);
}
}
@@ -281,37 +283,47 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
ctx.data_mut(|d| d.insert_temp(key, (runs, current)));
}
/// The main window and the shown native windows on the desktop, front first: the window
/// focused most recently first. A window whose position is unknown is left out.
/// 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) {
let key = self.id.with("native-order");
let mut order = ctx.data(|d| d.get_temp::<Vec<u64>>(key)).unwrap_or_default();
order.retain(|id| natives.iter().any(|(w, ..)| w.id == *id));
// 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) {
let front = order.contains(&None);
order.retain(Option::is_some);
if front { order.insert(0, None) } else { order.push(None) }
}
let mut windows = Vec::new();
for (window, rect, viewport) in natives {
let info = ctx.input(|i| i.raw.viewports.get(viewport).cloned());
// A new window opens in front.
if info.as_ref().is_some_and(|i| i.focused == Some(true)) || !order.contains(&window.id) {
order.retain(|id| *id != window.id);
order.insert(0, window.id);
if info.as_ref().is_some_and(|i| i.focused == Some(true)) || !order.contains(&Some(window.id)) {
order.retain(|id| *id != Some(window.id));
order.insert(0, Some(window.id));
}
let [x, y, w, h] = *rect;
let placed = match info {
None => Some(Rect::from_min_size(pos2(x, y), vec2(w, h))),
Some(info) => placed(&info).map(|[x, y, w, h]| Rect::from_min_size(pos2(x, y), vec2(w, h))),
};
if let Some(placed) = placed.filter(|r| r.is_finite() && visible) {
windows.push((window.id, placed));
let normal = self.normal_window(window);
if let Some(placed) = placed.filter(|r| r.is_finite() && (visible || normal)) {
windows.push((window.id, placed, normal));
}
}
windows.sort_by_key(|(id, _)| order.iter().position(|o| o == id));
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();
self.cross.windows = windows.into_iter().map(|(id, rect, _)| (id, rect)).collect();
ctx.data_mut(|d| d.insert_temp(key, order));
self.cross.windows = windows;
self.cross.main = ctx.input(|i| i.viewport().outer_rect.and(i.viewport().inner_rect)).filter(|r| r.is_finite());
}
/// Whether floating windows show this frame, and whether that changed since the last.
fn float_visibility(&mut self, ctx: &Context) -> (bool, bool) {
/// Whether palettes show this frame.
fn float_visibility(&mut self, ctx: &Context) -> bool {
let (focused, now) = ctx.input(|i| {
let infos = || i.raw.viewports.values();
// A backend that reports no focus at all keeps the windows.
@@ -320,19 +332,26 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
});
self.cross.focus_lost = crate::focus::turned_false(ctx, self.id.with("app-focus"), focused);
let key = self.id.with("native-visibility");
let (last, was) = ctx.data(|d| d.get_temp::<(f64, bool)>(key)).unwrap_or((now, true));
let last = if focused { now } else { last };
let last = if focused { now } else { ctx.data(|d| d.get_temp::<f64>(key)).unwrap_or(now) };
let visible = floats_shown(focused, now, last);
if visible && !focused {
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)));
(visible, visible != was)
ctx.data_mut(|d| d.insert_temp(key, last));
visible
}
/// One floating window in its own viewport. Returns the key of its [`Place`].
fn native_window(&mut self, ctx: &Context, window: &Floating<P>, rect: [f32; 4], viewport: ViewportId, visibility: (bool, bool)) -> Option<Id> {
let (visible, toggled) = visibility;
/// A window holding a viewer is a normal window, so it neither vanishes nor sits over other
/// applications; the rest are palettes.
fn normal_window(&self, window: &Floating<P>) -> bool {
window.panels().any(|p| self.content.window_kind(p) == WindowKind::Normal)
}
/// One floating window in its own viewport. `visible`: palettes show. Returns the key of
/// its [`Place`].
fn native_window(&mut self, ctx: &Context, window: &Floating<P>, rect: [f32; 4], viewport: ViewportId, visible: bool) -> Option<Id> {
let normal = self.normal_window(window);
let visible = visible || normal;
let first = window.panels().next()?.clone();
let key = self.id.with(("native-place", viewport));
let now = ctx.input(|i| i.time);
@@ -366,7 +385,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
};
seen.insert(window.id);
ctx.data_mut(|d| d.insert_temp(seen_key, seen));
Place { spawn, known: spawn, pending: Some(now + MOVE_WAIT), corrected: false }
Place { spawn, known: spawn, pending: Some(now + MOVE_WAIT), corrected: false, shown: true }
}
};
let spawn = place.spawn;
@@ -376,7 +395,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
.with_position([spawn[0], spawn[1]])
.with_inner_size([spawn[2], spawn[3]])
.with_min_inner_size(MIN_SIZE)
.with_window_level(WindowLevel::AlwaysOnTop)
.with_window_level(if normal { WindowLevel::Normal } else { WindowLevel::AlwaysOnTop })
.with_taskbar(false);
// The bar replaces the OS title bar. macOS keeps the frame, with its title bar and
// buttons hidden: AppKit resizes a movable window from its edges and corners with its
@@ -390,8 +409,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
.with_resizable(window.panels().all(|p| self.permissions(p).movable));
#[cfg(not(target_os = "macos"))]
let builder = builder.with_decorations(false);
if toggled || (info.is_none() && !visible) {
// Only on a change: showing a shown window raises it and, on macOS, takes focus.
if place.shown != visible {
ctx.send_viewport_cmd_to(viewport, ViewportCommand::Visible(visible));
place.shown = visible;
}
if visible && self.activation.as_ref().is_some_and(|panel| window.contains(panel)) {
ctx.send_viewport_cmd_to(viewport, ViewportCommand::Focus);
+22
View File
@@ -509,6 +509,20 @@ impl DockHeights {
}
}
/// How a floating window in its own OS window behaves (feature `native-windows`), see
/// [`DockContent::window_kind`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum WindowKind {
/// A tool palette: it stays above other windows and hides while none of the app's windows
/// has focus.
#[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.
Normal,
}
#[derive(Clone)]
pub(super) struct Drag<P> {
pub(super) panel: P,
@@ -637,6 +651,11 @@ pub trait DockContent<P> {
fn allow_native(&self, _panel: &P) -> bool {
true
}
/// How the panel's native floating window behaves (feature `native-windows`). A window
/// holding any [`WindowKind::Normal`] panel is a normal window; the rest are palettes.
fn window_kind(&self, _panel: &P) -> WindowKind {
WindowKind::Palette
}
/// Host-owned drop resolution for targets the renderer cannot know about. Returning
/// feedback consumes the gesture, including no-op targets; the renderer then emits no move.
fn resolve_drag(&mut self, _ctx: &Context, _style: &DockStyle, _request: &DropRequest<'_, P>) -> Option<DropFeedback> {
@@ -722,6 +741,9 @@ impl<P, C: DockContent<P> + ?Sized> DockContent<P> for Overrides<'_, P, C> {
fn allow_native(&self, panel: &P) -> bool {
self.inner.allow_native(panel)
}
fn window_kind(&self, panel: &P) -> WindowKind {
self.inner.window_kind(panel)
}
fn resolve_drag(&mut self, ctx: &Context, style: &DockStyle, request: &DropRequest<'_, P>) -> Option<DropFeedback> {
self.inner.resolve_drag(ctx, style, request)
}
+13 -1
View File
@@ -388,6 +388,8 @@ pub struct HostWindow {
/// `InnerSize`.
pub rect: Rect,
pub visible: bool,
/// The window level its builder asked for on its last frame.
pub level: Option<egui::WindowLevel>,
/// Frames it drew.
pub frames: usize,
/// Events for its next frame, in its own coordinates.
@@ -443,11 +445,21 @@ impl NativeHost {
let builder = &viewport.builder;
let rect = Rect::from_min_size(builder.position.unwrap_or_default() + host.frame, builder.inner_size.unwrap_or(vec2(300.0, 200.0)));
let visible = builder.visible.unwrap_or(true);
host.windows.entry(id).or_insert_with(|| HostWindow { rect, visible, frames: 0, events: Vec::new(), close: false, shapes: Vec::new() });
let level = builder.window_level;
host.windows.entry(id).or_insert_with(|| HostWindow {
rect,
visible,
level,
frames: 0,
events: Vec::new(),
close: false,
shapes: Vec::new(),
});
let mut viewports = host.infos();
let (time, focused, modifiers) = (host.time, host.focus == Some(id), host.modifiers);
let Some(window) = host.windows.get_mut(&id) else { return };
window.frames += 1;
window.level = level;
if std::mem::take(&mut window.close)
&& let Some(info) = viewports.get_mut(&id)
{
+15 -6
View File
@@ -374,10 +374,18 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho
`Floating::with_id` sets one; a layout
loads with unique ids; a window that merges into another takes the receiver's id, and a whole
window that moves (floated again, or its only group dragged) keeps its own.
- **Level.** The windows stay above other windows (`WindowLevel::AlwaysOnTop`), so clicking the
document does not bury them. While none of the app's windows has focus they hide
(`ViewportCommand::Visible(false)`), and they come back when one has it again. The hide waits
250 ms, so focus moving between the app's own windows does not flicker.
- **Kind and level.** `DockContent::window_kind` sorts panels into palettes
(`WindowKind::Palette`, the default) and viewers (`WindowKind::Normal`), such as a video
monitor on a second display. A window holding any `Normal` panel is a normal window, whichever
of its tabs shows; the rest are palettes.
- Palettes stay above other windows (`WindowLevel::AlwaysOnTop`), so clicking the document
does not bury them. While none of the app's windows has focus they hide
(`ViewportCommand::Visible(false)`), and they come back when one has it again. The hide
waits 250 ms, so focus moving between the app's own windows does not flicker.
- Normal windows have `WindowLevel::Normal` and stay shown while the app is in the
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.
- **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
@@ -451,8 +459,9 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho
- *Hit-testing.* The owner adds its desktop origin to the pointer: a native window's reported
`inner_rect.min`, or while it moves by its bar, the place it was last put less the pointer's
offset from the pressed point. The window under that
desktop point is a floating native window first, most recently focused first, then the main
window. A window moving by its bar is never its own target. A point in no window is on the
desktop point is a floating native window in front of the main window first (palettes, then
normal windows, most recently focused first), then the main window, then a normal window
behind it. A window moving by its bar is never its own target. A point in no window is on the
desktop.
- *Targets and previews.* Each window draws its drop targets in its own coordinates, and the
window under the pointer draws the drop preview (zone, dock line, ghost) in its own frame.