diff --git a/crates/ui/src/docking/desktop.rs b/crates/ui/src/docking/desktop.rs index 2047a641..7fb47211 100644 --- a/crates/ui/src/docking/desktop.rs +++ b/crates/ui/src/docking/desktop.rs @@ -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, dragged: Option) -> 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, dragged: Option) -> 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))); } } diff --git a/crates/ui/src/docking/mod.rs b/crates/ui/src/docking/mod.rs index 79457375..fd24506f 100644 --- a/crates/ui/src/docking/mod.rs +++ b/crates/ui/src/docking/mod.rs @@ -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; diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index 03e71876..351aeab9 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -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, /// Panels that stay in-window. in_window: Vec, + /// Viewer panels: their windows are normal windows. + normal: Vec, /// The panel with a text field, what it holds, and whether it takes focus next frame. text: Option, typed: String, @@ -50,6 +52,10 @@ impl DockContent 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 { @@ -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, 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| [viewer, palette, mixed].map(|v| d.host().windows[&v].visible); + assert_eq!(shown(&d), [true, false, true]); + let told = |d: &DockDriver, 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)); diff --git a/crates/ui/src/docking/render/cross.rs b/crates/ui/src/docking/render/cross.rs index bd7b767d..fca134e7 100644 --- a/crates/ui/src/docking/render/cross.rs +++ b/crates/ui/src/docking/render/cross.rs @@ -85,6 +85,8 @@ pub(in crate::docking) struct Cross

{ pub main: Option, /// 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>, /// The drag's pointer, as its owner wrote it this frame. @@ -106,6 +108,7 @@ impl

Default for Cross

{ focus_lost: false, main: None, windows: Vec::new(), + front: 0, surfaces: Vec::new(), pointer: None, release: None, @@ -152,7 +155,7 @@ impl 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

>) -> Option { 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)), diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs index 993c3ead..94a5069f 100644 --- a/crates/ui/src/docking/render/native.rs +++ b/crates/ui/src/docking/render/native.rs @@ -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, /// 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 Renderer<'_, P> { pub(super) fn native_windows<'w>(&mut self, ctx: &Context, windows: Vec<&'w Floating

>) -> Vec<&'w Floating

> { 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 Renderer<'_, P> { if ctx.data(|d| d.get_temp::(key)).is_some_and(|d| !natives.iter().any(|(w, ..)| w.id == d.window)) { ctx.data_mut(|d| d.remove::(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 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 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

, [f32; 4], ViewportId)], visible: bool) { let key = self.id.with("native-order"); - let mut order = ctx.data(|d| d.get_temp::>(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::>>(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| 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 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::(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

, rect: [f32; 4], viewport: ViewportId, visibility: (bool, bool)) -> Option { - 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

) -> 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

, rect: [f32; 4], viewport: ViewportId, visible: bool) -> Option { + 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 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 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 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); diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs index 6acd40dd..37028586 100644 --- a/crates/ui/src/docking/surface.rs +++ b/crates/ui/src/docking/surface.rs @@ -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

{ pub(super) panel: P, @@ -637,6 +651,11 @@ pub trait DockContent

{ 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 { @@ -722,6 +741,9 @@ impl + ?Sized> DockContent

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 { self.inner.resolve_drag(ctx, style, request) } diff --git a/crates/ui/src/docking/testing.rs b/crates/ui/src/docking/testing.rs index b6e1b107..c8873dd0 100644 --- a/crates/ui/src/docking/testing.rs +++ b/crates/ui/src/docking/testing.rs @@ -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, /// 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) { diff --git a/docs/ui-docking.md b/docs/ui-docking.md index 3034a125..dfcb5be7 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -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.