Files
craft-libs/crates/ui/src/docking/native_tests.rs
T

2703 lines
121 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Floating windows in their own OS windows (feature `native-windows`), drawn by the driver's
//! stand-in for OS windows.
use egui::{Color32, Event, Key, Modifiers, PointerButton, Pos2, Rect, Vec2, ViewportCommand, epaint::ClippedShape, pos2, vec2};
use super::render::{
APPKIT_CORNER, DragPointer, INACTIVE_PALETTE, MACOS_CLOSE, WindowDrag, appkit_resize_areas, close_rects, drag_opacity, resize_bands, wayland,
};
use super::testing::DockDriver;
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.
const ORIGIN: Pos2 = pos2(100.0, 50.0);
#[derive(Default)]
struct Content {
/// Panels that may not close.
fixed: Vec<u32>,
/// The protected canvas that takes splits beside it, as PhotoCraft's does.
canvas: Option<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,
focus_text: bool,
/// The text field gives up keyboard focus on the next frame.
blur_text: bool,
/// The panel with a button that takes focus.
focused_button: Option<u32>,
/// The panel with a counter button, its clicks, and where it was drawn.
counter: Option<u32>,
count: usize,
counter_rect: Option<Rect>,
/// Pointer buttons pressed in each panel's body.
pressed: Vec<(u32, PointerButton)>,
/// A panel body saw hovered or dropped files.
saw_files: bool,
/// Input visible to a selected panel, including native control events while blocked.
input_probe: Option<u32>,
events: Vec<Event>,
raw_events: Vec<Event>,
/// The panel with an app drag source, and where it was drawn.
source: Option<u32>,
/// How the source marks its drag as one that carries something.
marks: Marks,
source_rect: Option<Rect>,
/// The source's drag so far, and how often its drag stopped.
source_delta: egui::Vec2,
source_stopped: usize,
/// How often the source's body saw a primary-button release in its input.
source_releases: usize,
/// The source asks egui to run its window's pass again, once, the next time it draws.
discard_next: bool,
/// The source asks for that many more passes, drawn or not, before the frame completes.
discard_passes: usize,
/// A drop site asks for its window's pass again when it reads a release.
discard_drop: bool,
/// Panels whose bodies are app drop sites: where each saw the pointer, and where a release
/// came with the pointer inside.
drop_sites: Vec<u32>,
hovers: Vec<(u32, Pos2)>,
drops: Vec<(u32, Pos2)>,
/// Drop sites whose release found egui's drag-and-drop payload.
payload_drops: Vec<u32>,
/// Key and clipboard events panel 1 saw in the main window, and the text field's panel in
/// its own window.
main_keys: Vec<Event>,
field_keys: Vec<Event>,
}
/// How a test's app drag source marks its drag.
#[derive(Default)]
enum Marks {
/// It does not: a plain egui drag, such as a slider's.
#[default]
Nothing,
/// [`super::route_app_drag`] for this area, each frame while it drags.
Route(egui::Id),
/// An egui drag-and-drop payload, its panel.
Payload,
}
impl DockContent<u32> for Content {
/// Test body that probes native input: records events/raw events, text focus and typing, button
/// focus, a click counter, drag source state, drop sites and payloads, file hover/drop, and
/// primary/secondary presses inside the body.
fn body(&mut self, ui: &mut egui::Ui, panel: &u32) {
ui.label(format!("panel {panel}"));
if self.input_probe == Some(*panel) {
(self.events, self.raw_events) = ui.input(|i| (i.events.clone(), i.raw.events.clone()));
}
if self.text == Some(*panel) {
let field = ui.text_edit_singleline(&mut self.typed);
if std::mem::take(&mut self.focus_text) {
field.request_focus();
}
if std::mem::take(&mut self.blur_text) {
field.surrender_focus();
}
}
if self.focused_button == Some(*panel) {
ui.button("OK").request_focus();
}
if self.counter == Some(*panel) {
let response = ui.button("Add");
self.count += usize::from(response.clicked());
self.counter_rect = Some(response.rect);
}
if self.source == Some(*panel) {
let response = ui.allocate_response(vec2(120.0, 60.0), egui::Sense::drag());
self.source_rect = Some(response.rect);
self.source_delta += response.drag_delta();
self.source_stopped += usize::from(response.drag_stopped());
self.source_releases += usize::from(ui.input(|i| i.pointer.primary_released()));
let discard = std::mem::take(&mut self.discard_next);
let more = self.discard_passes > 0;
if more {
self.discard_passes -= 1;
}
if discard || more {
ui.ctx().request_discard("the source's pass runs twice");
}
match self.marks {
Marks::Route(area) if response.dragged() => super::route_app_drag(ui.ctx(), area),
Marks::Payload => response.dnd_set_drag_payload(*panel),
_ => {}
}
}
// As apps' drop sites do: the pointer over the body, and a release there.
if self.drop_sites.contains(panel)
&& let Some(pos) = ui.ctx().pointer_hover_pos().filter(|p| ui.max_rect().contains(*p))
{
self.hovers.push((*panel, pos));
if ui.input(|i| i.pointer.any_released()) {
self.drops.push((*panel, pos));
if egui::DragAndDrop::has_payload_of_type::<u32>(ui.ctx()) {
self.payload_drops.push(*panel);
}
if std::mem::take(&mut self.discard_drop) {
ui.ctx().request_discard("a drop site's pass runs twice");
}
}
}
self.saw_files |= ui.input(|i| !i.raw.hovered_files.is_empty() || !i.raw.dropped_files.is_empty());
let main = ui.ctx().viewport_id() == egui::ViewportId::ROOT;
if (main && *panel == 1) || (!main && self.text == Some(*panel)) {
let keys = ui.input(|i| {
i.events.iter().filter(|e| matches!(e, Event::Key { .. } | Event::Copy | Event::Cut | Event::Paste(_))).cloned().collect::<Vec<_>>()
});
(if main { &mut self.main_keys } else { &mut self.field_keys }).extend(keys);
}
if ui.rect_contains_pointer(ui.max_rect()) {
let pressed = [PointerButton::Primary, PointerButton::Secondary].into_iter().filter(|b| ui.input(|i| i.pointer.button_pressed(*b)));
self.pressed.extend(pressed.map(|b| (*panel, b)));
}
}
fn logical_label(&self, panel: &u32) -> String {
format!("Panel {panel}")
}
fn permissions(&self, panel: &u32) -> Permissions {
if self.canvas == Some(*panel) {
return Permissions { accepts_splits: true, ..Permissions::PROTECTED };
}
Permissions { closable: !self.fixed.contains(panel), ..Permissions::default() }
}
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> {
Node::Tabs { panels: panels.to_vec(), active: 0 }
}
/// Frames `[1, 2]` and `[3, 4]`, and window `[5, 6]` when `floating`.
fn layout(floating: bool) -> Layout<u32> {
Layout {
root: Some(Node::Split {
axis: SplitAxis::Horizontal,
size: SplitSize::Ratio(0.5),
first: Box::new(tabs(&[1, 2])),
second: Box::new(tabs(&[3, 4])),
}),
floating: if floating { vec![Floating::single(vec![5, 6], 0, [300.0, 80.0, 240.0, 300.0])] } else { Vec::new() },
}
}
fn driver(layout: Layout<u32>) -> DockDriver<u32, Content> {
DockDriver::new(layout, Content::default()).with_style(|s| s.command_press_floats = true).with_native_host(ORIGIN)
}
#[allow(clippy::expect_used)]
fn window(d: &DockDriver<u32, Content>, panel: u32) -> &Floating<u32> {
d.layout.floating.iter().find(|w| w.contains(&panel)).expect("floating")
}
/// The window's OS window rect, and how many frames it drew.
fn os_window(d: &DockDriver<u32, Content>, panel: u32) -> Option<(Rect, usize)> {
let viewport = d.viewport(window(d, panel).id);
d.host().windows.get(&viewport).map(|w| (w.rect, w.frames))
}
fn button(pos: Pos2, pressed: bool) -> Event {
Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }
}
#[test]
fn every_float_opens_in_its_own_os_window() {
// Undock Panel.
let mut d = driver(layout(false));
d.layout.apply(Action::Float { panel: 2, rect: [200.0, 100.0, 300.0, 200.0] }).unwrap();
let out = d.step(vec![]);
assert!(out.actions.contains(&Action::SetNative { panel: 2, rect: Some([300.0, 150.0, 300.0, 200.0]) }), "{:?}", out.actions);
assert_eq!(window(&d, 2).native, Some([300.0, 150.0, 300.0, 200.0]));
assert_eq!(os_window(&d, 2).map(|(rect, _)| rect), Some(Rect::from_min_size(pos2(300.0, 150.0), vec2(300.0, 200.0))));
assert!(d.hit(&Hit::Title(2)).is_none(), "not drawn in the main window");
// Undock Panel Group.
let mut d = driver(layout(false));
d.layout.apply(Action::MoveGroup { panel: 3, to: Target::Float { rect: Some([20.0, 30.0, 250.0, 200.0]) } }).unwrap();
d.step(vec![]);
assert_eq!(window(&d, 3).native, Some([120.0, 80.0, 250.0, 200.0]));
assert!(os_window(&d, 4).is_some());
// Ctrl/Cmd-drag.
let mut d = driver(layout(false));
let (tab, to) = (d.hit(&Hit::Tab(1)).unwrap().center(), pos2(600.0, 400.0));
d.modifiers = Modifiers::COMMAND;
d.hold(tab, to);
d.modifiers = Modifiers::NONE;
let out = d.release(to);
assert!(matches!(out.actions.as_slice(), [Action::Float { panel: 1, .. }]), "{:?}", out.actions);
d.step(vec![]);
let [x, y, ..] = window(&d, 1).rect;
assert_eq!(window(&d, 1).native.map(|r| [r[0], r[1]]), Some([x + ORIGIN.x, y + ORIGIN.y]));
assert!(os_window(&d, 1).is_some());
// A restored layout.
let mut d = driver(layout(true));
d.settle();
assert_eq!(window(&d, 5).native, Some([400.0, 130.0, 240.0, 300.0]));
assert!(os_window(&d, 5).is_some_and(|(_, frames)| frames > 0));
// A panel that must stay in-window keeps its window there.
let mut d = driver(layout(false));
d.content.in_window = vec![5, 6];
d.layout = layout(true);
d.settle();
assert_eq!(window(&d, 5).native, None);
assert!(d.hit(&Hit::Title(5)).is_some());
assert!(d.host().windows.is_empty());
}
#[test]
fn closing_the_os_window_closes_its_panels() {
let mut d = driver(layout(true));
let viewport = d.viewport(window(&d, 5).id);
d.host().windows.get_mut(&viewport).unwrap().close = true;
let out = d.step(vec![]);
assert!(out.actions.contains(&Action::CloseWindow { panel: 5 }), "{:?}", out.actions);
assert!(!d.layout.contains(&5) && !d.layout.contains(&6));
let frames = d.host().windows[&viewport].frames;
d.settle();
assert_eq!(d.host().windows[&viewport].frames, frames, "no longer drawn");
// The bar's × does the same.
let mut d = driver(layout(true));
let id = window(&d, 5).id;
let out = d.window_click(id, pos2(230.0, 9.0));
assert!(out.actions.contains(&Action::CloseWindow { panel: 5 }), "{:?}", out.actions);
}
#[test]
fn the_os_close_of_a_window_that_may_not_close_is_cancelled() {
// A panel that may not close, or a locked area.
for locked in [false, true] {
let mut d = driver(layout(true));
if locked {
d.locked = true;
} else {
d.content.fixed = vec![6];
}
let viewport = d.viewport(window(&d, 5).id);
let sent = d.host().commands.len();
d.host().windows.get_mut(&viewport).unwrap().close = true;
let out = d.step(vec![]);
assert!(!out.actions.iter().any(|a| matches!(a, Action::CloseWindow { .. })), "locked {locked}: {:?}", out.actions);
assert!(out.error.is_none(), "locked {locked}: {:?}", out.error);
assert!(d.host().commands[sent..].contains(&(viewport, ViewportCommand::CancelClose)), "locked {locked}: cancelled");
assert!(d.layout.contains(&5) && d.layout.contains(&6));
}
}
#[test]
fn a_window_keeps_its_id_and_os_window_when_its_first_panel_moves_out() {
let mut d = driver(layout(true));
let id = window(&d, 5).id;
let (_, frames) = os_window(&d, 6).unwrap();
d.layout.apply(Action::Move { panel: 5, anchor: 1, placement: Placement::Tab { before: None } }).unwrap();
d.settle();
assert_eq!(window(&d, 6).id, id);
assert_eq!(d.host().windows.len(), 1, "the same OS window");
assert!(os_window(&d, 6).unwrap().1 > frames);
// Moving a whole window keeps its id; merging into another keeps the receiver's.
let mut layout = layout(true);
layout.floating.push(Floating::single(vec![7], 0, [10.0, 10.0, 200.0, 200.0]));
let (first, second) = (layout.floating[0].id, layout.floating[1].id);
assert_ne!(first, second);
layout.apply(Action::MoveWindow { panel: 5, to: Target::Float { rect: Some([0.0, 0.0, 300.0, 300.0]) } }).unwrap();
assert_eq!(layout.floating.iter().find(|w| w.contains(&5)).unwrap().id, first);
layout.apply(Action::Float { panel: 7, rect: [50.0, 50.0, 200.0, 200.0] }).unwrap();
assert_eq!(layout.floating.iter().find(|w| w.contains(&7)).unwrap().id, second);
layout.apply(Action::MoveWindow { panel: 7, to: Target::At { anchor: 5, placement: Placement::Tab { before: None } } }).unwrap();
assert_eq!(layout.floating.len(), 1);
assert_eq!(layout.floating[0].id, first);
// A group split out of a window makes a new one.
layout.apply(Action::MovePanel { panel: 7, to: Target::Float { rect: Some([0.0, 0.0, 200.0, 200.0]) } }).unwrap();
let ids: Vec<u64> = layout.floating.iter().map(|w| w.id).collect();
assert!(ids.contains(&first) && ids.len() == 2 && ids[0] != ids[1], "{ids:?}");
}
#[test]
fn a_moved_window_reports_its_place_and_monitor() {
let mut d = driver(layout(true));
d.monitors = vec![Monitor { name: Some("left".into()), rect: [0.0, 0.0, 1920.0, 1080.0], scale: 1.0 }];
let viewport = d.viewport(window(&d, 5).id);
d.host().windows.get_mut(&viewport).unwrap().rect = Rect::from_min_size(pos2(700.0, 200.0), vec2(240.0, 300.0));
let out = d.step(vec![]);
assert!(out.actions.contains(&Action::SetNative { panel: 5, rect: Some([700.0, 200.0, 240.0, 300.0]) }), "{:?}", out.actions);
assert_eq!(window(&d, 5).rect, [600.0, 150.0, 240.0, 300.0], "the in-window rect follows");
assert_eq!(window(&d, 5).monitor.as_ref().and_then(|m| m.name.as_deref()), Some("left"));
// Without a monitor list, the size egui reports.
let mut d = driver(layout(true));
d.host().monitor_size = Some(vec2(2560.0, 1440.0));
d.settle();
assert_eq!((window(&d, 5).monitor.clone(), window(&d, 5).monitor_size), (None, Some([2560.0, 1440.0])));
}
#[test]
fn a_restored_window_opens_on_a_connected_monitor() {
let left = Monitor { name: Some("left".into()), rect: [0.0, 0.0, 1920.0, 1080.0], scale: 1.0 };
let right = Monitor { name: Some("right".into()), rect: [1920.0, 0.0, 2560.0, 1440.0], scale: 2.0 };
let mut saved = layout(true);
saved.floating[0].native = Some([3000.0, 100.0, 240.0, 300.0]);
saved.floating[0].monitor = Some(right.clone());
// Its monitor is connected: where it was.
let mut d = DockDriver::new(Layout::default(), Content::default()).with_native_host(ORIGIN);
d.monitors = vec![left.clone(), right];
d.layout = saved.clone();
d.settle();
assert_eq!(os_window(&d, 5).unwrap().0.min, pos2(3000.0, 100.0));
// Unplugged: the main window's monitor, at the same offset.
let mut d = DockDriver::new(Layout::default(), Content::default()).with_native_host(ORIGIN);
d.monitors = vec![left];
d.layout = saved;
d.settle();
assert_eq!(os_window(&d, 5).unwrap().0.min, pos2(1080.0, 100.0));
assert_eq!(window(&d, 5).native, Some([1080.0, 100.0, 240.0, 300.0]));
assert_eq!(window(&d, 5).monitor.as_ref().and_then(|m| m.name.as_deref()), Some("left"));
}
/// The last `InnerSize`, `MinInnerSize` and `MaxInnerSize` sent to `viewport`.
fn size_commands(d: &DockDriver<u32, Content>, viewport: egui::ViewportId) -> [Option<egui::Vec2>; 3] {
let mut sizes = [None; 3];
for (_, c) in d.host().commands.iter().filter(|(v, _)| *v == viewport) {
match c {
ViewportCommand::InnerSize(s) => sizes[0] = Some(*s),
ViewportCommand::MinInnerSize(s) => sizes[1] = Some(*s),
ViewportCommand::MaxInnerSize(s) => sizes[2] = Some(*s),
_ => {}
}
}
sizes
}
#[test]
fn a_minimized_native_window_shrinks_to_its_bar_and_keeps_its_expanded_size() {
let mut d = driver(layout(true));
let viewport = d.viewport(window(&d, 5).id);
let open = os_window(&d, 5).unwrap().0;
let (native, rect) = (window(&d, 5).native, window(&d, 5).rect);
assert_eq!(native, Some([open.left(), open.top(), 240.0, 300.0]));
// Its bar and its group's tab strip, at the same width and top-left corner; only the width
// resizes.
let collapsed = BAR + super::DockStyle::default().tab_height;
d.layout.apply(Action::SetMinimized { panel: 5, minimized: true }).unwrap();
d.settle();
assert_eq!(os_window(&d, 5).unwrap().0, Rect::from_min_size(open.min, vec2(240.0, collapsed)));
let [_, min, max] = size_commands(&d, viewport);
assert_eq!((min.map(|s| s.y), max.map(|s| s.y)), (Some(collapsed), Some(collapsed)));
// The layout keeps the expanded size after the window reports the collapsed one, past the
// wait for commanded moves, and takes a new width.
for _ in 0..40 {
d.step(vec![]);
}
assert_eq!((window(&d, 5).native, window(&d, 5).rect), (native, rect));
d.host().windows.get_mut(&viewport).unwrap().rect = Rect::from_min_size(open.min, vec2(300.0, collapsed));
for _ in 0..40 {
d.step(vec![]);
}
assert_eq!(window(&d, 5).native, Some([open.left(), open.top(), 300.0, 300.0]));
assert_eq!(window(&d, 5).rect[2..], [300.0, 300.0]);
// Expanded: the saved height comes back, and the height resizes again.
d.layout.apply(Action::SetMinimized { panel: 5, minimized: false }).unwrap();
d.settle();
assert_eq!(os_window(&d, 5).unwrap().0, Rect::from_min_size(open.min, vec2(300.0, 300.0)));
assert_eq!(size_commands(&d, viewport), [Some(vec2(300.0, 300.0)), Some(vec2(160.0, 100.0)), Some(egui::Vec2::INFINITY)]);
// The limits change before the size: the OS clamps a new size to the limits it has.
let commands: Vec<_> = d.host().commands.iter().filter(|(v, _)| *v == viewport).map(|(_, c)| c.clone()).collect();
let last = |f: fn(&ViewportCommand) -> bool| commands.iter().rposition(f);
assert!(last(|c| matches!(c, ViewportCommand::MaxInnerSize(_))) < last(|c| matches!(c, ViewportCommand::InnerSize(_))), "{commands:?}");
assert_eq!(window(&d, 5).native, Some([open.left(), open.top(), 300.0, 300.0]));
}
#[test]
fn a_locked_area_moves_no_native_window_and_records_where_the_os_put_it() {
let mut d = driver(layout(true));
d.locked = true;
let id = window(&d, 5).id;
let viewport = d.viewport(id);
let sent = d.host().commands.len();
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
let out = d.window_step(id, vec![Event::PointerMoved(pos2(70.0, 9.0)), button(pos2(70.0, 9.0), false)]);
assert!(out.actions.is_empty(), "{:?}", out.actions);
let moves = d.host().commands[sent..]
.iter()
.filter(|(v, c)| *v == viewport && matches!(c, ViewportCommand::OuterPosition(_) | ViewportCommand::StartDrag))
.count();
assert_eq!(moves, 0, "the bar is inert");
// The window system moves the window anyway: the locked workspace takes its new place.
let mut workspace = Workspace { locked: true, ..Workspace::new(d.layout.clone()) };
d.host().windows.get_mut(&viewport).unwrap().rect = Rect::from_min_size(pos2(700.0, 200.0), vec2(240.0, 300.0));
for _ in 0..60 {
let out = d.step(vec![]);
assert!(!out.actions.iter().any(|a| matches!(a, Action::MoveFloating { .. })), "{:?}", out.actions);
workspace.apply_all(out.actions, |_| Permissions::default()).unwrap();
}
assert_eq!(workspace.layout.floating[0].native, Some([700.0, 200.0, 240.0, 300.0]));
}
#[test]
fn dragging_the_bar_moves_the_os_window() {
let mut d = driver(layout(true));
let id = window(&d, 5).id;
let viewport = d.viewport(id);
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
// Each move is relative to where the window was last moved.
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
d.window_step(id, vec![Event::PointerMoved(pos2(70.0, 9.0))]);
let moves: Vec<Pos2> = d
.host()
.commands
.iter()
.filter_map(|(v, c)| match c {
ViewportCommand::OuterPosition(pos) if *v == viewport => Some(*pos),
_ => None,
})
.collect();
assert_eq!(moves.last(), Some(&pos2(450.0, 140.0)), "{moves:?}");
// Past the main window's right edge, the desktop: released there, it stays.
d.window_step(id, vec![Event::PointerMoved(pos2(540.0, 9.0))]);
let out = d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert!(out.actions.iter().all(|a| matches!(a, Action::SetNative { .. } | Action::MoveFloating { .. })), "{:?}", out.actions);
d.step(vec![]);
assert_eq!(window(&d, 5).native, Some([950.0, 140.0, 240.0, 300.0]));
// Without a reported position (Wayland) the window system moves it.
let mut d = driver(layout(true));
d.host().no_position = true;
d.step(vec![]);
d.window_step(id_of(&d), vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id_of(&d), vec![Event::PointerMoved(pos2(60.0, 19.0))]);
assert!(d.host().commands.iter().any(|(_, c)| *c == ViewportCommand::StartDrag));
}
fn id_of(d: &DockDriver<u32, Content>) -> u64 {
window(d, 5).id
}
#[test]
fn palettes_stay_shown_at_the_normal_level_while_the_app_is_inactive() {
use egui::{ViewportId, WindowLevel};
let mut d = driver(layout(true));
let palette = d.viewport(window(&d, 5).id);
let focus = |d: &mut DockDriver<u32, Content>, window: Option<ViewportId>, frames: usize| {
d.host().focus = window;
for _ in 0..frames {
d.step(vec![]);
}
};
let level = |d: &DockDriver<u32, Content>| d.host().windows[&palette].level;
let told = |d: &DockDriver<u32, Content>| d.host().commands.iter().any(|(v, c)| *v == palette && matches!(c, ViewportCommand::Visible(_)));
assert_eq!(level(&d), Some(WindowLevel::AlwaysOnTop));
// Focus moving to the palette, with a frame of none between, keeps it on top.
focus(&mut d, None, 1);
focus(&mut d, Some(palette), 30);
assert_eq!(level(&d), Some(WindowLevel::AlwaysOnTop));
// Another application: the palette drops behind it after the delay, and shows on, so
// files dragged from that application can reach it.
focus(&mut d, None, 10);
assert_eq!(level(&d), Some(WindowLevel::AlwaysOnTop), "not yet");
focus(&mut d, None, 10);
assert_eq!(level(&d), Some(INACTIVE_PALETTE));
// Back: on top again.
focus(&mut d, Some(ViewportId::ROOT), 1);
assert_eq!(level(&d), Some(WindowLevel::AlwaysOnTop));
// Blocked by a modal, it stays at the normal level, away and back.
d.input_blocked = true;
focus(&mut d, None, 20);
assert_eq!(level(&d), Some(WindowLevel::Normal));
focus(&mut d, Some(ViewportId::ROOT), 5);
assert_eq!(level(&d), Some(WindowLevel::Normal));
assert!(d.host().windows[&palette].visible);
assert!(!told(&d), "{:?}", d.host().commands);
}
#[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: the palette drops behind it; the viewers stay at the normal level.
d.host().focus = None;
for _ in 0..20 {
d.step(vec![]);
}
assert_eq!(
[viewer, palette, mixed].map(|v| level(&d, v)),
[Some(egui::WindowLevel::Normal), Some(INACTIVE_PALETTE), Some(egui::WindowLevel::Normal)]
);
d.host().focus = Some(egui::ViewportId::ROOT);
d.step(vec![]);
assert_eq!(level(&d, palette), Some(egui::WindowLevel::AlwaysOnTop));
assert_eq!(level(&d, viewer), Some(egui::WindowLevel::Normal));
assert!(!d.host().commands.iter().any(|(_, c)| matches!(c, ViewportCommand::Visible(_))), "no window hides");
}
#[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 goes back on top.
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]));
// While a modal blocks input, the main window with the modal is raised last, even when
// the app is activated through a viewer.
d.input_blocked = true;
focus(&mut d, None, 20);
focus(&mut d, Some(front), 10);
assert_eq!(levels(&mut d), raised(&[back, front, MAIN]));
}
#[test]
fn unused_keys_in_a_native_window_reach_the_app() {
let mut d = driver(layout(true));
let id = window(&d, 5).id;
let key = Event::Key { key: Key::S, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND };
d.modifiers = Modifiers::COMMAND;
let out = d.window_step(id, vec![key.clone(), Event::Text("s".into()), Event::Copy, Event::PointerMoved(pos2(50.0, 50.0))]);
d.modifiers = Modifiers::NONE;
assert_eq!(out.forwarded_events, vec![key, Event::Text("s".into()), Event::Copy]);
assert_eq!(out.forwarded_modifiers, Some(Modifiers::COMMAND));
// Keys of the main window stay its own.
let out = d.key(Key::A);
assert!(out.forwarded_events.is_empty());
}
#[cfg(feature = "serde")]
#[test]
fn a_native_window_saves_its_id_place_and_monitor() {
let mut layout = layout(true);
let monitor = Monitor { name: Some("DP-1".into()), rect: [-1920.0, 0.0, 1920.0, 1080.0], scale: 1.5 };
layout.floating[0].native = Some([-1500.0, 100.0, 240.0, 300.0]);
layout.floating[0].monitor = Some(monitor);
layout.floating.push(Floating {
monitor_size: Some([2560.0, 1440.0]),
native: Some([5.0, 5.0, 300.0, 200.0]),
..Floating::single(vec![7], 0, [0.0, 0.0, 300.0, 200.0])
});
let json = serde_json::to_string(&layout).unwrap();
let back: Layout<u32> = serde_json::from_str(&json).unwrap();
assert_eq!(back, layout);
assert_eq!(back.floating.iter().map(|w| w.id).collect::<Vec<_>>(), layout.floating.iter().map(|w| w.id).collect::<Vec<_>>());
// Old layouts get fresh ids, and copies of one id unique ones.
let old = r#"{"root": null, "floating": [{"panels": [5], "active": 0, "rect": [0, 0, 100, 100]}, {"panels": [6], "active": 0, "rect": [0, 0, 100, 100]}]}"#;
let old: Layout<u32> = serde_json::from_str(old).unwrap();
assert_ne!(old.floating[0].id, old.floating[1].id);
let mut copies = layout.clone();
copies.floating[1].id = copies.floating[0].id;
let copies: Layout<u32> = serde_json::from_str(&serde_json::to_string(&copies).unwrap()).unwrap();
assert_eq!(copies.floating[0].id, layout.floating[0].id);
assert_ne!(copies.floating[1].id, copies.floating[0].id);
// An in-window window writes its id too.
let in_window = Floating::single(vec![1u32], 0, [0.0, 0.0, 9.0, 9.0]).with_id(3);
assert!(serde_json::to_string(&in_window).unwrap().contains(r#""id":3"#));
}
/// Height of the bar at the top of a native window.
const BAR: f32 = 18.0;
/// A tab row's middle in a native window, below the bar.
const TAB_ROW: f32 = BAR + 13.0;
/// Window `[5, 6]` of [`layout`] and window `[7]` below it, apart on the desktop.
fn two_windows() -> Layout<u32> {
let mut layout = layout(true);
layout.floating.push(Floating::single(vec![7], 0, [20.0, 420.0, 240.0, 150.0]));
layout
}
fn in_root(d: &DockDriver<u32, Content>, panel: u32) -> bool {
d.layout.contains(&panel) && !d.layout.floating.iter().any(|w| w.contains(&panel))
}
#[test]
fn a_tab_dragged_past_the_main_windows_edge_floats_at_the_desktop_point() {
let mut d = driver(layout(false));
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
// Left of the main window, which starts at x = 100 on the desktop.
let out = pos2(-60.0, 120.0);
d.hold(tab, out);
// The pointer leaves the window: with native windows that is no release.
let gone = d.leave();
assert!(gone.actions.is_empty(), "{:?}", gone.actions);
let released = d.release(out);
assert!(matches!(released.actions.as_slice(), [Action::Float { panel: 1, .. }]), "{:?}", released.actions);
d.step(vec![]);
// The point grabbed (where the drag began, past the nudge) stays under the pointer: the
// tab's group starts at the window's top-left, below the bar.
let (desktop, grab) = (ORIGIN + out.to_vec2(), tab + vec2(8.0, 6.0));
let min = pos2(desktop.x - grab.x, desktop.y - grab.y - BAR);
let native = window(&d, 1).native.unwrap();
assert_eq!(pos2(native[0], native[1]), min);
assert_eq!(os_window(&d, 1).unwrap().0.min, min);
}
#[test]
fn a_tab_dragged_out_and_back_docks_in_the_main_window() {
let mut d = driver(layout(false));
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
d.hold(tab, pos2(-60.0, 120.0));
d.leave();
// Back over the middle of frame [3, 4].
let back = pos2(600.0, 300.0);
d.move_to(back);
let out = d.release(back);
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 1, anchor: 3 | 4, .. }]), "{:?}", out.actions);
assert!(d.layout.floating.is_empty());
}
#[test]
fn a_tab_from_a_native_window_docks_in_a_frame_of_the_main_window() {
let mut d = driver(layout(true));
let id = window(&d, 5).id;
// Frame [1, 2] at x = 100 in the main window, on the desktop.
let local = d.window_local(id, ORIGIN + vec2(100.0, 300.0)).unwrap();
d.window_hold(id, pos2(15.0, TAB_ROW), local);
let out = d.window_release(id, local);
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 5, anchor: 1 | 2, .. }]), "{:?}", out.actions);
assert!(in_root(&d, 5));
assert!(window(&d, 6).contains(&6));
}
#[test]
fn a_tab_from_a_native_window_joins_another_native_windows_tab_row() {
let mut d = driver(two_windows());
let (from, to) = (window(&d, 5).id, window(&d, 7).id);
let row = d.host().windows[&d.viewport(to)].rect.min + vec2(200.0, TAB_ROW);
let local = d.window_local(from, row).unwrap();
d.window_hold(from, pos2(15.0, TAB_ROW), local);
let out = d.window_release(from, local);
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 5, anchor: 7, placement: Placement::Tab { .. } }]), "{:?}", out.actions);
assert!(window(&d, 7).contains(&5));
}
#[test]
fn dragging_the_bar_onto_the_main_window_docks_the_whole_window() {
let mut d = driver(layout(true));
let id = window(&d, 5).id;
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
// The pointer over the tab row of frame [3, 4], which starts at x = 400 in the main window.
// A moving window joins a group there; it splits only groups of other floating windows.
let local = d.window_local(id, ORIGIN + vec2(450.0, 13.0)).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
let out = d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert!(out.actions.iter().any(|a| matches!(a, Action::MoveWindow { panel: 5, to: Target::At { anchor: 3 | 4, .. } })), "{:?}", out.actions);
assert!(d.layout.floating.is_empty());
assert!(in_root(&d, 5) && in_root(&d, 6));
}
#[test]
fn dragging_the_only_tab_of_a_native_window_moves_the_window() {
let mut layout = layout(false);
layout.floating.push(Floating::single(vec![7], 0, [300.0, 80.0, 240.0, 200.0]));
let mut d = driver(layout);
let id = window(&d, 7).id;
d.window_hold(id, pos2(15.0, TAB_ROW), pos2(1000.0, TAB_ROW));
let out = d.window_release(id, pos2(15.0, TAB_ROW));
assert!(!out.actions.iter().any(|a| matches!(a, Action::Float { .. } | Action::Move { .. })), "{:?}", out.actions);
d.step(vec![]);
assert_eq!(d.layout.floating.len(), 1);
assert_eq!(window(&d, 7).id, id, "the same window");
// It followed the pointer onto the desktop, the pressed point under it: the nudge moved it
// by (8, 6), then the pointer went 1000 - 15 points right of the press.
assert_eq!(window(&d, 7).native.map(|r| [r[0], r[1]]), Some([408.0 + 985.0, 136.0]));
}
#[test]
fn leaving_the_window_ends_a_drag_only_with_embedded_viewports() {
// Native viewports: the release still arrives, so leaving is no drop.
let mut d = driver(layout(false));
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
d.hold(tab, pos2(790.0, 300.0));
let out = d.leave();
assert!(out.actions.is_empty(), "{:?}", out.actions);
assert!(super::is_dragging::<u32>(&d.ctx, d.id));
d.step(vec![]);
assert!(super::is_dragging::<u32>(&d.ctx, d.id));
// Embedded: leaving floats the panel at the edge, as before.
let mut d = DockDriver::new(layout(false), Content::default());
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
d.hold(tab, pos2(790.0, 300.0));
let out = d.leave();
assert!(matches!(out.actions.as_slice(), [Action::Float { panel: 1, .. }]), "{:?}", out.actions);
}
#[test]
fn escape_cancels_a_drag_outside_the_window() {
let mut d = driver(layout(false));
let before = d.layout.clone();
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
d.hold(tab, pos2(-60.0, 120.0));
d.leave();
d.key(Key::Escape);
let out = d.release(pos2(-60.0, 120.0));
assert!(out.actions.is_empty(), "{:?}", out.actions);
assert_eq!(d.layout, before);
}
/// Shapes drawn in `color`: a drop preview's outline, or a fill.
fn accented(shapes: &[ClippedShape], color: Color32) -> usize {
shapes
.iter()
.filter(|clipped| match &clipped.shape {
egui::Shape::Rect(rect) => rect.stroke.color == color || rect.fill == color,
egui::Shape::Path(path) => path.fill == color.gamma_multiply(0.25),
_ => false,
})
.count()
}
#[test]
fn the_preview_is_drawn_in_the_window_under_the_pointer() {
let accent = Color32::from_rgb(250, 10, 200);
let mut d = DockDriver::new(two_windows(), Content::default())
.with_style(move |s| {
s.accent = accent;
s.float_hint = false;
})
.with_native_host(ORIGIN);
let to = window(&d, 7).id;
let viewport = d.viewport(to);
let (main_before, window_before) = (accented(d.shapes(), accent), accented(&d.host().windows[&viewport].shapes, accent));
// Tab 1 of the main window over window [7]'s body.
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let body = d.host().windows[&viewport].rect.center() - ORIGIN.to_vec2();
d.hold(tab, body);
d.move_to(body);
assert!(accented(&d.host().windows[&viewport].shapes, accent) > window_before, "drawn in the destination");
assert_eq!(accented(d.shapes(), accent), main_before, "not in the source");
let out = d.release(body);
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 1, anchor: 7, .. }]), "{:?}", out.actions);
}
/// The texts drawn in `shapes`.
fn texts(shapes: &[ClippedShape]) -> Vec<String> {
fn walk(shape: &egui::Shape, out: &mut Vec<String>) {
match shape {
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| walk(s, out)),
egui::Shape::Text(text) => out.push(text.galley.text().to_owned()),
_ => {}
}
}
let mut out = Vec::new();
shapes.iter().for_each(|clipped| walk(&clipped.shape, &mut out));
out
}
#[test]
fn the_undock_hint_is_drawn_in_the_window_under_the_pointer() {
let key = if egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac { "⌘" } else { "Ctrl" };
let hint = format!("Hold {key} to undock");
let hinted = |shapes: &[ClippedShape]| texts(shapes).contains(&hint);
let driver = || {
DockDriver::new(two_windows(), Content::default())
.with_style(|s| {
s.command_press_floats = true;
s.float_hint = true;
})
.with_native_host(ORIGIN)
};
// Tab 1 of the main window over window [7]'s body.
let mut d = driver();
let viewport = d.viewport(window(&d, 7).id);
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let body = d.host().windows[&viewport].rect.center() - ORIGIN.to_vec2();
d.hold(tab, body);
d.move_to(body);
assert!(hinted(&d.host().windows[&viewport].shapes), "{:?}", texts(&d.host().windows[&viewport].shapes));
assert!(!hinted(d.shapes()), "not in the source");
// Tab 5 of window [5, 6] over frame [1, 2] of the main window.
let mut d = driver();
let id = window(&d, 5).id;
let viewport = d.viewport(id);
let local = d.window_local(id, ORIGIN + vec2(100.0, 300.0)).unwrap();
d.window_hold(id, pos2(15.0, TAB_ROW), local);
assert!(hinted(d.shapes()), "{:?}", texts(d.shapes()));
assert!(!hinted(&d.host().windows[&viewport].shapes), "not in the source");
}
/// Zone outlines: closed 1 pt paths without a fill.
fn zone_outlines(shapes: &[ClippedShape]) -> usize {
shapes
.iter()
.filter(|clipped| matches!(&clipped.shape, egui::Shape::Path(path) if path.closed && path.fill == Color32::TRANSPARENT && path.stroke.width == 1.0))
.count()
}
#[test]
fn drop_zones_are_drawn_in_the_window_under_the_pointer() {
let mut d = DockDriver::new(two_windows(), Content::default())
.with_style(|s| {
s.side_edge_zones = true;
s.float_hint = false;
})
.with_native_host(ORIGIN);
let viewport = d.viewport(window(&d, 7).id);
// Tab 1 of the main window over window [7]'s body, too short to split top or bottom: its
// left and right sides, center and strip.
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let body = d.host().windows[&viewport].rect.center() - ORIGIN.to_vec2();
d.hold(tab, body);
d.move_to(body);
assert_eq!(zone_outlines(&d.host().windows[&viewport].shapes), 4, "drawn in the destination");
assert_eq!(zone_outlines(d.shapes()), 0, "not in the source");
}
#[test]
fn a_command_drag_floats_even_over_another_window() {
let mut d = driver(two_windows());
let viewport = d.viewport(window(&d, 7).id);
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let over = d.host().windows[&viewport].rect.center() - ORIGIN.to_vec2();
d.modifiers = Modifiers::COMMAND;
d.hold(tab, over);
let out = d.release(over);
assert!(matches!(out.actions.as_slice(), [Action::Float { panel: 1, .. }]), "{:?}", out.actions);
d.step(vec![]);
let native = window(&d, 1).native.unwrap();
let desktop = ORIGIN + over.to_vec2();
assert_eq!(pos2(native[0], native[1]), pos2(desktop.x - tab.x - 8.0, desktop.y - tab.y - 6.0 - BAR));
}
#[test]
fn without_window_positions_a_drag_out_of_a_native_window_floats() {
let mut d = driver(two_windows());
d.host().no_position = true;
d.settle();
let id = window(&d, 5).id;
// Over window [7] on the desktop, which cannot be known: a new window.
d.window_hold(id, pos2(15.0, TAB_ROW), pos2(-200.0, 400.0));
let out = d.window_release(id, pos2(-200.0, 400.0));
assert!(matches!(out.actions.as_slice(), [Action::Float { panel: 5, .. }]), "{:?}", out.actions);
// The only tab left moves its window, by handing the move to the window system. A press
// where the last one was, this soon, would be a double-click.
let id = window(&d, 6).id;
for _ in 0..40 {
d.step(vec![]);
}
d.window_hold(id, pos2(15.0, TAB_ROW), pos2(120.0, 200.0));
assert!(d.host().commands.iter().any(|(v, c)| *v == d.viewport(id) && *c == ViewportCommand::StartDrag));
let out = d.window_release(id, pos2(120.0, 200.0));
assert!(!out.actions.iter().any(|a| matches!(a, Action::Float { .. } | Action::Move { .. })), "{:?}", out.actions);
}
/// Drag window `panel`'s bar until the pointer is at `to` in the main window, and release.
fn drag_bar_to(d: &mut DockDriver<u32, Content>, panel: u32, to: Pos2) -> super::DockOutput<u32> {
let id = window(d, panel).id;
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
let Some(local) = d.window_local(id, ORIGIN + to.to_vec2()) else { return Default::default() };
d.window_step(id, vec![Event::PointerMoved(local)]);
d.window_step(id, vec![button(pos2(40.0, 9.0), false)])
}
fn window_move(out: &super::DockOutput<u32>) -> Option<&Target<u32>> {
out.actions.iter().find_map(|a| match a {
Action::MoveWindow { to, .. } => Some(to),
_ => None,
})
}
#[test]
fn a_native_windows_bar_docks_it_at_frame_edges_and_dock_lines_but_not_a_body_center() {
// The right edge zone of frame [3, 4], 40 pt in, clear of the new-dock band along the window's edge.
let mut d = driver(layout(true));
let out = drag_bar_to(&mut d, 5, pos2(760.0, 300.0));
assert!(matches!(window_move(&out), Some(Target::At { anchor: 3 | 4, placement: Placement::Split(super::Zone::Right) })), "{:?}", out.actions);
assert!(d.layout.floating.is_empty());
assert!(in_root(&d, 5) && in_root(&d, 6));
// The middle of the frame is no target for a whole window.
let mut d = driver(layout(true));
let out = drag_bar_to(&mut d, 5, pos2(600.0, 300.0));
assert_eq!(window_move(&out), None, "{:?}", out.actions);
assert!(window(&d, 5).contains(&6));
// The seam between the dock's two groups, which meet at y = 307.
let dock = Node::Dock {
side: super::Side::Start,
columns: vec![super::Column::new(vec![super::Group::new(vec![2], 0), super::Group::new(vec![3], 0)], 240.0)],
};
let mut docked = layout(true);
docked.root =
Some(Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedFirst(240.0), first: Box::new(dock), second: Box::new(tabs(&[1])) });
let mut d = driver(docked);
let out = drag_bar_to(&mut d, 5, pos2(120.0, 309.0));
assert_eq!(window_move(&out), Some(&Target::At { anchor: 2, placement: Placement::Group { below: true, height: None } }), "{:?}", out.actions);
assert!(d.layout.floating.is_empty());
assert!(in_root(&d, 5) && in_root(&d, 6));
}
#[test]
fn over_a_canvas_body_a_window_stays_floating_and_a_tab_floats_while_the_edge_bands_dock() {
// PhotoCraft: a dock column of [2] above [3, 4] beside canvas 0, which takes splits, and
// window [5, 6] floating.
let accent = Color32::from_rgb(250, 10, 200);
let dock = Node::Dock {
side: super::Side::Start,
columns: vec![super::Column::new(vec![super::Group::new(vec![2], 0), super::Group::new(vec![3, 4], 0)], 240.0)],
};
let root = Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedFirst(240.0), first: Box::new(dock), second: Box::new(tabs(&[0])) };
let layout = Layout { root: Some(root), floating: vec![Floating::single(vec![5, 6], 0, [300.0, 80.0, 240.0, 200.0])] };
let start = || {
DockDriver::new(layout.clone(), Content { canvas: Some(0), ..Content::default() })
.with_style(move |s| {
s.accent = accent;
s.float_hint = false;
})
.with_native_host(ORIGIN)
};
// The canvas spans x = 244 to 800 and the whole height: no tab strip.
for at in [pos2(520.0, 150.0), pos2(520.0, 450.0)] {
let mut d = start();
let before = accented(d.shapes(), accent);
let id = window(&d, 5).id;
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
let local = d.window_local(id, ORIGIN + at.to_vec2()).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
d.step(vec![]);
assert_eq!(accented(d.shapes(), accent), before, "{at:?}: no preview");
let out = d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert_eq!(window_move(&out), None, "{at:?}: {:?}", out.actions);
assert!(window(&d, 5).contains(&6), "{at:?}");
}
// A docked tab dropped there floats.
let mut d = start();
let tab = d.hit(&Hit::Tab(3)).unwrap().center();
let out = d.drag(tab, pos2(520.0, 450.0));
assert!(matches!(out.actions.as_slice(), [Action::Float { panel: 3, .. }]), "{:?}", out.actions);
// The window's right edge, where no dock is yet, and the bottom edge band beside the canvas
// still dock the window.
let mut d = start();
let out = drag_bar_to(&mut d, 5, pos2(795.0, 300.0));
assert_eq!(window_move(&out), Some(&Target::NewDock { side: super::Side::End }), "{:?}", out.actions);
let mut d = start();
let out = drag_bar_to(&mut d, 5, pos2(520.0, 590.0));
assert!(matches!(window_move(&out), Some(Target::At { placement: Placement::Edge(super::Zone::Bottom), .. })), "{:?}", out.actions);
}
#[test]
fn escape_during_a_bar_drag_puts_the_window_back() {
let mut d = driver(layout(true));
let id = window(&d, 5).id;
let viewport = d.viewport(id);
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
d.window_step(id, vec![Event::PointerMoved(pos2(140.0, 59.0))]);
assert_ne!(d.host().windows[&viewport].rect.min, pos2(400.0, 130.0), "moved");
let escape = Event::Key { key: Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE };
d.window_step(id, vec![escape]);
assert_eq!(d.host().windows[&viewport].rect.min, pos2(400.0, 130.0));
// The release that follows docks nothing, wherever it is.
let out = d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert_eq!(window_move(&out), None, "{:?}", out.actions);
d.step(vec![]);
assert_eq!(window(&d, 5).native.map(|r| [r[0], r[1]]), Some([400.0, 130.0]));
}
#[test]
fn the_window_focused_last_is_in_front_where_windows_overlap() {
// Window [7] overlaps window [5, 6] on the desktop, at (550, 280) among others.
let mut layout = layout(true);
layout.floating.push(Floating::single(vec![7], 0, [350.0, 150.0, 240.0, 150.0]));
let overlap = pos2(550.0, 280.0) - ORIGIN.to_vec2();
for (front, anchors) in [(7, vec![7]), (5, vec![5, 6]), (7, vec![7])] {
let mut d = driver(layout.clone());
let viewport = d.viewport(window(&d, front).id);
d.host().focus = Some(viewport);
d.step(vec![]);
d.host().focus = Some(egui::ViewportId::ROOT);
d.step(vec![]);
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let out = d.drag(tab, overlap);
let anchor = out.actions.iter().find_map(|a| match a {
Action::Move { panel: 1, anchor, .. } => Some(*anchor),
_ => None,
});
assert!(anchor.is_some_and(|a| anchors.contains(&a)), "front {front}: {:?}", out.actions);
}
}
/// Every `OuterPosition` sent to `viewport`, in order.
fn moves(d: &DockDriver<u32, Content>, viewport: egui::ViewportId) -> Vec<Pos2> {
d.host()
.commands
.iter()
.filter_map(|(v, c)| match c {
ViewportCommand::OuterPosition(pos) if *v == viewport => Some(*pos),
_ => None,
})
.collect()
}
fn escape() -> Event {
Event::Key { key: Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }
}
#[test]
fn a_window_made_this_session_opens_where_it_was_put() {
// egui reports the monitor's size but no monitor list. A tab dropped on the desktop right
// of the main window's monitor opens there.
let size = Some(vec2(1920.0, 1080.0));
let mut d = driver(layout(false));
d.host().monitor_size = size;
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let out = pos2(2400.0, 120.0);
d.hold(tab, out);
d.release(out);
d.settle();
let desktop = ORIGIN + out.to_vec2();
let min = pos2(desktop.x - tab.x - 8.0, desktop.y - tab.y - 6.0 - BAR);
let native = window(&d, 1).native.unwrap();
assert_eq!(pos2(native[0], native[1]), min);
assert_eq!(os_window(&d, 1).unwrap().0.min, min);
// Undock Panel, to a place off that monitor.
let mut d = driver(layout(false));
d.host().monitor_size = size;
d.layout.apply(Action::Float { panel: 2, rect: [2400.0, 100.0, 300.0, 200.0] }).unwrap();
d.settle();
assert_eq!(window(&d, 2).native, Some([2500.0, 150.0, 300.0, 200.0]));
// A loaded layout's window still opens on the main window's monitor.
let mut saved = layout(true);
saved.floating[0].native = Some([2500.0, 100.0, 240.0, 300.0]);
let mut d = driver(Layout::default());
d.host().monitor_size = size;
d.layout = saved;
d.settle();
assert_eq!(window(&d, 5).native, Some([1680.0, 100.0, 240.0, 300.0]));
assert_eq!(os_window(&d, 5).unwrap().0.min, pos2(1680.0, 100.0));
}
#[test]
fn a_changed_layout_moves_the_os_window() {
let mut d = driver(layout(true));
let viewport = d.viewport(id_of(&d));
assert_eq!(window(&d, 5).native, Some([400.0, 130.0, 240.0, 300.0]));
// An undo of a move, a reset, or a layout reloaded with the same ids.
d.layout.floating[0].native = Some([700.0, 200.0, 260.0, 320.0]);
let out = d.step(vec![]);
assert!(!out.actions.iter().any(|a| matches!(a, Action::SetNative { .. })), "{:?}", out.actions);
d.settle();
assert_eq!(d.host().windows[&viewport].rect, Rect::from_min_size(pos2(700.0, 200.0), vec2(260.0, 320.0)));
assert_eq!(window(&d, 5).native, Some([700.0, 200.0, 260.0, 320.0]));
// Off every monitor: onto the nearest, and the layout follows.
d.monitors = vec![Monitor { name: Some("left".into()), rect: [0.0, 0.0, 1920.0, 1080.0], scale: 1.0 }];
d.layout.floating[0].native = Some([5000.0, 200.0, 260.0, 320.0]);
d.settle();
assert_eq!(d.host().windows[&viewport].rect.min, pos2(1660.0, 200.0));
assert_eq!(window(&d, 5).native, Some([1660.0, 200.0, 260.0, 320.0]));
// A bar drag that leaves too little of the bar on a monitor ends with the window on it.
let id = id_of(&d);
let grab = pos2(40.0, 9.0);
d.window_step(id, vec![Event::PointerMoved(grab), button(grab, true)]);
let far = d.window_local(id, pos2(1950.0, 300.0)).unwrap();
d.window_step(id, vec![Event::PointerMoved(far)]);
let far = d.window_local(id, pos2(1950.0, 300.0)).unwrap();
d.window_step(id, vec![Event::PointerMoved(far), button(far, false)]);
d.settle();
assert_eq!(d.host().windows[&viewport].rect.min, pos2(1660.0, 291.0));
assert_eq!(window(&d, 5).native, Some([1660.0, 291.0, 260.0, 320.0]));
}
#[test]
fn a_bar_drag_follows_the_pointer_when_moves_land_late() {
let mut d = driver(layout(true));
d.host().lag = true;
let id = id_of(&d);
let viewport = d.viewport(id);
let grab = pos2(40.0, 9.0);
let mut pointer = d.host().windows[&viewport].rect.min + grab.to_vec2();
d.window_step(id, vec![Event::PointerMoved(grab), button(grab, true)]);
// On to the desktop right of the main window, where the release docks nothing.
for _ in 0..6 {
pointer += vec2(90.0, -20.0);
// The window system reports the pointer relative to where the window is now.
let local = d.window_local(id, pointer).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
assert_eq!(moves(&d, viewport).last(), Some(&(pointer - grab.to_vec2())), "no overshoot");
}
// Frames with no pointer move leave it, though the last move has landed since.
let sent = moves(&d, viewport).len();
for _ in 0..3 {
d.step(vec![]);
}
assert_eq!(moves(&d, viewport).len(), sent, "no creep");
assert_eq!(d.host().windows[&viewport].rect.min, pointer - grab.to_vec2());
let local = d.window_local(id, pointer).unwrap();
d.window_step(id, vec![button(local, false)]);
d.settle();
let to = pointer - grab.to_vec2();
assert_eq!(d.host().windows[&viewport].rect.min, to);
assert_eq!(window(&d, 5).native.map(|r| [r[0], r[1]]), Some([to.x, to.y]));
}
/// A bar drag at a steady pointer speed under AppKit's timing: the window's reported place is
/// fresh, while pointer events are measured against where the window was a frame back on some
/// frames. Returns the window's step each frame. Released, the layout keeps where it ended.
fn bar_drag_with_stale_pointer(tells_cursor: bool) -> Vec<Vec2> {
let mut d = driver(layout(true));
d.host().tells_cursor = tells_cursor;
d.host().stale = vec![1, 0, 1, 1, 0];
let id = id_of(&d);
let viewport = d.viewport(id);
let grab = pos2(40.0, 9.0);
let mut pointer = d.host().windows[&viewport].rect.min + grab.to_vec2();
d.window_step(id, vec![Event::PointerMoved(grab), button(grab, true)]);
let mut place = d.host().windows[&viewport].rect.min;
let mut steps = Vec::new();
// On to the desktop right of the main window, where the release docks nothing.
for _ in 0..24 {
pointer += vec2(20.0, 5.0);
d.window_pointer(id, pointer);
let now = d.host().windows[&viewport].rect.min;
steps.push(now - place);
place = now;
}
let local = pointer - place.to_vec2();
d.window_step(id, vec![button(local, false)]);
d.settle();
assert_eq!(window(&d, 5).native.map(|r| pos2(r[0], r[1])), Some(d.host().windows[&viewport].rect.min));
steps
}
#[test]
fn a_bar_drag_follows_the_cursor_where_pointer_events_trail_the_window() {
// Without the cursor the window overshoots and steps back.
let steps = bar_drag_with_stale_pointer(false);
assert!(steps.iter().any(|s| s.x < 0.0), "{steps:?}");
// With it, every frame moves the window by the pointer's step.
let steps = bar_drag_with_stale_pointer(true);
assert!(steps.iter().all(|s| *s == vec2(20.0, 5.0)), "{steps:?}");
}
#[test]
fn a_native_windows_content_is_its_origin_where_its_frame_differs() {
// A title bar's height between the window's outer position and its content.
let mut d = DockDriver::new(Layout::default(), Content::default()).with_native_host(ORIGIN);
d.host().frame = vec2(0.0, 28.0);
d.layout = layout(true);
d.settle();
// It opened with its frame there, then moved so its content is.
let id = id_of(&d);
let viewport = d.viewport(id);
assert_eq!(d.host().windows[&viewport].rect.min, pos2(400.0, 130.0));
assert_eq!(window(&d, 5).native, Some([400.0, 130.0, 240.0, 300.0]));
// Its bar moves it by its outer position, here onto the desktop right of the main window.
let grab = pos2(40.0, 9.0);
d.window_step(id, vec![Event::PointerMoved(grab), button(grab, true)]);
d.window_step(id, vec![Event::PointerMoved(grab + vec2(520.0, 20.0))]);
assert_eq!(moves(&d, viewport).last(), Some(&pos2(920.0, 122.0)));
d.window_step(id, vec![button(grab, false)]);
d.settle();
assert_eq!(d.host().windows[&viewport].rect.min, pos2(920.0, 150.0));
assert_eq!(window(&d, 5).native.map(|r| [r[0], r[1]]), Some([920.0, 150.0]));
// Its tab drops where the pointer is on the desktop: the main window's tab row of [3, 4].
let row = d.window_local(id, ORIGIN + vec2(450.0, 13.0)).unwrap();
d.window_hold(id, pos2(15.0, TAB_ROW), row);
let out = d.window_release(id, row);
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 5, anchor: 3 | 4, placement: Placement::Tab { .. } }]), "{:?}", out.actions);
}
#[test]
fn the_only_tab_of_a_native_window_docks_it_with_title_bar_docks_off() {
let mut layout = layout(false);
layout.floating.push(Floating::single(vec![7], 0, [300.0, 80.0, 240.0, 200.0]));
let driver = || DockDriver::new(layout.clone(), Content::default()).with_style(|s| s.title_bar_docks = false).with_native_host(ORIGIN);
let mut d = driver();
let id = window(&d, 7).id;
let row = ORIGIN + vec2(450.0, 13.0);
let from = pos2(15.0, TAB_ROW);
d.window_step(id, vec![Event::PointerMoved(from), button(from, true)]);
d.window_step(id, vec![Event::PointerMoved(from + vec2(8.0, 6.0))]);
for _ in 0..2 {
let local = d.window_local(id, row).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
}
let local = d.window_local(id, row).unwrap();
let out = d.window_step(id, vec![button(local, false)]);
assert!(matches!(window_move(&out), Some(Target::At { anchor: 3 | 4, .. })), "{:?}", out.actions);
assert!(in_root(&d, 7));
// Its bar keeps to `title_bar_docks`.
let mut d = driver();
let out = drag_bar_to(&mut d, 7, pos2(450.0, 13.0));
assert_eq!(window_move(&out), None, "{:?}", out.actions);
assert!(!in_root(&d, 7));
}
#[test]
fn the_only_tab_of_a_native_window_fills_and_joins_a_frame_bodys_center() {
let mut layout = layout(false);
layout.floating.push(Floating::single(vec![7], 0, [300.0, 80.0, 240.0, 200.0]));
let style = |s: &mut super::DockStyle| {
s.side_edge_zones = true;
s.title_bar_docks = false;
s.float_hint = false;
};
let mut d = DockDriver::new(layout, Content::default()).with_style(style).with_native_host(ORIGIN);
let id = window(&d, 7).id;
// The middle of frame [3, 4], in the main window.
let middle = pos2(600.0, 300.0);
let from = pos2(15.0, TAB_ROW);
d.window_step(id, vec![Event::PointerMoved(from), button(from, true)]);
d.window_step(id, vec![Event::PointerMoved(from + vec2(8.0, 6.0))]);
for _ in 0..2 {
let local = d.window_local(id, ORIGIN + middle.to_vec2()).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
}
// The center rectangle is the only zone around the pointer: outlined and filled.
let zone = |filled: bool| {
d.shapes().iter().any(|clipped| match &clipped.shape {
egui::Shape::Path(path) => {
path.closed && (path.fill != Color32::TRANSPARENT) == filled && Rect::from_points(&path.points).contains(middle)
}
_ => false,
})
};
assert!(zone(false), "outlined");
assert!(zone(true), "filled");
let local = d.window_local(id, ORIGIN + middle.to_vec2()).unwrap();
let out = d.window_step(id, vec![button(local, false)]);
assert!(matches!(window_move(&out), Some(Target::At { anchor: 3 | 4, placement: Placement::Split(super::Zone::Center) })), "{:?}", out.actions);
assert!(in_root(&d, 7));
}
#[test]
fn a_native_window_forwards_no_keys_while_one_of_its_widgets_has_focus() {
let mut d = driver(layout(true));
(d.content.text, d.content.focus_text) = (Some(5), true);
d.settle();
let key = Event::Key { key: Key::S, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND };
let out = d.window_step(id_of(&d), vec![key, Event::Text("s".into())]);
assert!(out.forwarded_events.is_empty(), "{:?}", out.forwarded_events);
assert_eq!(d.content.typed, "s");
// A focused button's Enter is its own.
let mut d = driver(layout(true));
d.content.focused_button = Some(5);
d.settle();
let enter = Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE };
let out = d.window_step(id_of(&d), vec![enter]);
assert!(out.forwarded_events.is_empty(), "{:?}", out.forwarded_events);
}
#[test]
fn the_input_hook_rewrites_each_native_windows_input_before_its_widgets_see_it() {
// As an app maps Ctrl+click to a secondary click on macOS.
let mut d = driver(layout(true)).with_native_input_hook(|_, input| {
for event in &mut input.events {
if let Event::PointerButton { button, .. } = event {
*button = PointerButton::Secondary;
}
}
});
let id = id_of(&d);
d.window_click(id, pos2(60.0, 150.0));
assert_eq!(d.content.pressed, vec![(5, PointerButton::Secondary)]);
// The main window's input is the app's own `raw_input_hook`'s.
d.content.pressed.clear();
d.click(pos2(100.0, 300.0));
assert_eq!(d.content.pressed, vec![(1, PointerButton::Primary)]);
}
#[derive(Debug)]
struct File(std::path::PathBuf);
impl egui::DroppedFile for File {
fn path(&self) -> &std::path::Path {
&self.0
}
fn bytes(&self) -> Result<Vec<u8>, String> {
Ok(Vec::new())
}
}
#[test]
fn files_dragged_onto_a_native_window_are_reported_with_its_panel() {
let mut d = driver(layout(true));
let (id, viewport) = (id_of(&d), d.viewport(id_of(&d)));
let hovered = egui::HoveredFile { path: Some("a.png".into()), mime: String::new() };
d.host().windows.get_mut(&viewport).unwrap().hovered_files = vec![hovered.clone()];
let out = d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 150.0))]);
let [files] = out.window_files.as_slice() else { panic!("{:?}", out.window_files) };
assert_eq!((files.window, files.viewport, files.panel, files.pos), (id, viewport, 5, Some(pos2(60.0, 150.0))));
assert_eq!(files.hovered, vec![hovered]);
assert!(files.dropped.is_empty());
{
let mut host = d.host();
let window = host.windows.get_mut(&viewport).unwrap();
window.hovered_files.clear();
window.dropped_files = vec![std::sync::Arc::new(File("a.png".into()))];
}
let out = d.window_step(id, Vec::new());
let [files] = out.window_files.as_slice() else { panic!("{:?}", out.window_files) };
assert_eq!(files.panel, 5);
assert_eq!(files.dropped.iter().map(|f| f.path().to_path_buf()).collect::<Vec<_>>(), vec![std::path::PathBuf::from("a.png")]);
assert!(d.step(Vec::new()).window_files.is_empty());
}
#[test]
fn a_blocked_native_window_reports_no_files() {
let mut d = driver(layout(true));
let (id, viewport) = (id_of(&d), d.viewport(id_of(&d)));
d.input_blocked = true;
d.step(Vec::new());
let drop = |d: &mut DockDriver<u32, Content>| {
let mut host = d.host();
let window = host.windows.get_mut(&viewport).unwrap();
window.hovered_files = vec![egui::HoveredFile { path: Some("a.png".into()), mime: String::new() }];
window.dropped_files = vec![std::sync::Arc::new(File("a.png".into()))];
};
drop(&mut d);
let out = d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 150.0))]);
assert!(out.window_files.is_empty(), "{:?}", out.window_files);
assert!(!d.content.saw_files);
d.input_blocked = false;
drop(&mut d);
let out = d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 150.0))]);
assert_eq!(out.window_files.len(), 1);
assert!(d.content.saw_files);
}
#[test]
fn escape_or_focus_loss_ends_a_drag_a_native_window_owns() {
// Escape cancels it, and is no shortcut.
let mut d = driver(layout(true));
let id = id_of(&d);
let over = d.window_local(id, ORIGIN + vec2(600.0, 300.0)).unwrap();
d.window_hold(id, pos2(15.0, TAB_ROW), over);
let out = d.window_step(id, vec![escape()]);
assert!(out.forwarded_events.is_empty(), "{:?}", out.forwarded_events);
let out = d.window_release(id, over);
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { .. } | Action::Float { .. })), "{:?}", out.actions);
assert!(window(&d, 5).contains(&6));
// Escape during a bar drag is no shortcut either.
let mut d = driver(layout(true));
d.window_step(id_of(&d), vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id_of(&d), vec![Event::PointerMoved(pos2(60.0, 19.0))]);
let out = d.window_step(id_of(&d), vec![escape()]);
assert!(out.forwarded_events.is_empty(), "{:?}", out.forwarded_events);
// Escape pressed in the main window, which has keyboard focus, cancels a drag the native
// window owns.
let mut d = driver(layout(true));
let id = id_of(&d);
let viewport = d.viewport(id);
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
d.window_step(id, vec![Event::PointerMoved(pos2(140.0, 59.0))]);
d.key(Key::Escape);
assert_eq!(d.host().windows[&viewport].rect.min, pos2(400.0, 130.0), "back where it was");
let out = d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert_eq!(window_move(&out), None, "{:?}", out.actions);
// Another application takes focus: cancelled.
let mut d = driver(layout(true));
let id = id_of(&d);
d.window_hold(id, pos2(15.0, TAB_ROW), over);
d.host().focus = None;
d.window_step(id, vec![Event::PointerMoved(over)]);
d.host().focus = Some(d.viewport(id));
let out = d.window_release(id, over);
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { .. } | Action::Float { .. })), "{:?}", out.actions);
assert!(!super::is_dragging::<u32>(&d.ctx, d.id));
}
#[test]
fn escape_in_a_focused_native_window_cancels_a_main_window_drag() {
let mut d = driver(two_windows());
let id = window(&d, 7).id;
let viewport = d.viewport(id);
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let body = d.host().windows[&viewport].rect.center() - ORIGIN.to_vec2();
d.hold(tab, body);
d.host().focus = Some(viewport);
let out = d.window_step(id, vec![escape()]);
assert!(out.forwarded_events.is_empty(), "the Escape is no shortcut: {:?}", out.forwarded_events);
d.step(vec![Event::PointerMoved(body)]);
let out = d.release(body);
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { .. } | Action::MovePanel { .. } | Action::Float { .. })), "{:?}", out.actions);
assert!(!super::is_dragging::<u32>(&d.ctx, d.id));
}
#[test]
fn a_main_window_without_focus_still_drags_a_tab_into_another_group() {
let mut d = driver(layout(false));
d.host().focus = None;
d.settle();
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let over = d.hit(&Hit::Tab(3)).unwrap().center();
d.hold(tab, over);
assert!(super::is_dragging::<u32>(&d.ctx, d.id));
let out = d.release(over);
assert!(out.actions.iter().any(|a| matches!(a, Action::Move { panel: 1, .. })), "{:?}", out.actions);
}
#[test]
fn focus_moving_to_a_native_window_mid_drag_keeps_the_drag() {
let mut d = driver(two_windows());
let viewport = d.viewport(window(&d, 7).id);
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let body = d.host().windows[&viewport].rect.center() - ORIGIN.to_vec2();
d.hold(tab, body);
d.host().focus = Some(viewport);
d.step(vec![Event::WindowFocused(false), Event::PointerMoved(body)]);
assert!(super::is_dragging::<u32>(&d.ctx, d.id));
let out = d.release(body);
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 1, anchor: 7, .. }]), "{:?}", out.actions);
}
#[test]
fn a_drag_ends_when_its_native_window_or_panel_goes() {
let pointer = |d: &DockDriver<u32, Content>| d.ctx.data(|data| data.get_temp::<DragPointer>(d.id.with("drag-pointer")));
let moving = |d: &DockDriver<u32, Content>| d.ctx.data(|data| data.get_temp::<WindowDrag>(d.id.with("window-drag")));
// The window closes during a tab drag it owns.
let mut d = driver(layout(true));
let id = id_of(&d);
let over = d.window_local(id, ORIGIN + vec2(600.0, 300.0)).unwrap();
d.window_hold(id, pos2(15.0, TAB_ROW), over);
let viewport = d.viewport(id);
d.host().windows.get_mut(&viewport).unwrap().close = true;
d.step(vec![]);
d.settle();
assert!(!d.layout.contains(&5));
assert!(!super::is_dragging::<u32>(&d.ctx, d.id));
assert_eq!(pointer(&d), None);
// The dragged panel is closed by the application.
let mut d = driver(layout(true));
let id = id_of(&d);
d.window_hold(id, pos2(15.0, TAB_ROW), over);
d.layout.apply(Action::Close { panel: 5 }).unwrap();
let out = d.window_release(id, over);
assert!(out.error.is_none() && !out.actions.iter().any(|a| matches!(a, Action::Move { .. } | Action::Float { .. })), "{:?}", out.actions);
d.settle();
assert!(!super::is_dragging::<u32>(&d.ctx, d.id));
assert_eq!(pointer(&d), None);
// The window closes during its own bar drag.
let mut d = driver(layout(true));
let id = id_of(&d);
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
assert!(moving(&d).is_some());
d.layout.apply(Action::CloseWindow { panel: 5 }).unwrap();
d.settle();
assert_eq!(moving(&d), None);
assert_eq!(pointer(&d), None);
// The main window's drags work as before.
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let out = d.drag(tab, pos2(600.0, 300.0));
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 1, anchor: 3 | 4, .. }]), "{:?}", out.actions);
}
/// Press at `from` in the native window of panel 5, move the pointer by `by` on the desktop
/// and release. Returns the window's OS rect, the resize the window handed to the window
/// system, and the driver.
#[cfg(not(target_os = "macos"))]
fn press_and_drag(from: Pos2, by: egui::Vec2) -> (Rect, Option<egui::ResizeDirection>, DockDriver<u32, Content>) {
let mut d = driver(layout(true));
let id = window(&d, 5).id;
let viewport = d.viewport(id);
let press = os_window(&d, 5).map_or(Pos2::ZERO, |(rect, _)| rect.min) + from.to_vec2();
d.window_step(id, vec![Event::PointerMoved(from), button(from, true)]);
// The pointer is where it was moved on the desktop, wherever the window is now.
let local = |d: &DockDriver<u32, Content>, step: egui::Vec2| d.window_local(id, press + step).unwrap_or_default();
for step in [vec2(3.0, 3.0), by] {
let pos = local(&d, step);
d.window_step(id, vec![Event::PointerMoved(pos)]);
}
let pos = local(&d, by);
d.window_step(id, vec![Event::PointerMoved(pos), button(pos, false)]);
d.step(vec![]);
let resize = d.host().commands.iter().find_map(|(v, c)| match c {
ViewportCommand::BeginResize(direction) if *v == viewport => Some(*direction),
_ => None,
});
(os_window(&d, 5).map_or(Rect::NOTHING, |(rect, _)| rect), resize, d)
}
// On macOS AppKit resizes the window and there are no bands; the macOS × layout has its own
// test.
#[cfg(not(target_os = "macos"))]
#[test]
fn the_bars_close_button_wins_over_the_resize_bands() {
use egui::ResizeDirection as D;
let mut d = driver(layout(true));
let id = window(&d, 5).id;
let viewport = d.viewport(id);
// The × of the 240 pt wide window spans 215..233 across and the whole bar down. Its top
// right is next to the window's top-right corner, but it closes the window.
let out = d.window_click(id, pos2(232.0, 1.0));
assert!(out.actions.contains(&Action::CloseWindow { panel: 5 }), "{:?}", out.actions);
assert!(!d.host().commands.iter().any(|(v, c)| *v == viewport && matches!(c, ViewportCommand::BeginResize(_))), "not resized");
// Beside it, at the window's edges, the bands hand the resize to the window system.
let resize = |from: Pos2| press_and_drag(from, vec2(20.0, 20.0)).1;
assert_eq!(resize(pos2(238.0, 2.0)), Some(D::NorthEast), "the top-right corner");
assert_eq!(resize(pos2(237.0, 15.0)), Some(D::NorthEast), "the corner beside the ×");
assert_eq!(resize(pos2(237.0, 100.0)), Some(D::East), "the right edge");
assert_eq!(resize(pos2(120.0, 1.0)), Some(D::North), "the top edge above the bar");
assert_eq!(resize(pos2(2.0, 298.0)), Some(D::SouthWest), "the bottom-left corner");
// Inside a corner's L, the bar moves the window, here to the middle of frame [1, 2].
let rect = |x0: f32, y0: f32, x1: f32, y1: f32| Rect::from_min_max(pos2(x0, y0), pos2(x1, y1));
let (moved, resize, _) = press_and_drag(pos2(10.0, 10.0), vec2(-110.0, 150.0));
assert_eq!((moved, resize), (rect(290.0, 280.0, 530.0, 580.0), None));
}
#[test]
/// Resize bands avoid the close button, are at most 6 pt deep, use 16 pt
/// corners along each edge, and leave the bar below the top band inert.
fn resize_bands_are_a_few_points_deep_and_leave_out_the_bar_buttons() {
let rect = Rect::from_min_size(Pos2::ZERO, vec2(240.0, 300.0));
let close = Rect::from_center_size(pos2(224.0, 9.0), vec2(18.0, 18.0));
let bands = resize_bands(rect, &[close]);
let at = |p: Pos2| bands.iter().find(|(_, band)| band.contains(p)).map(|(direction, _)| *direction);
for (_, band) in &bands {
assert!(!band.intersect(close).is_positive(), "{band:?} covers the ×");
assert!(band.width().min(band.height()) <= 6.0, "{band:?} is deeper than an edge band");
}
// Each edge resizes along its whole length, outside the ×, from 16 pt corners.
use egui::ResizeDirection as D;
// Which of an edge's three parts `v` is in: the first corner, the edge, the last corner.
let part = |v: f32, length: f32| usize::from(v >= 16.0) + usize::from(v > length - 16.0);
for i in 0..240 {
let x = i as f32 + 0.5;
let top = [D::NorthWest, D::North, D::NorthEast][part(x, 240.0)];
assert_eq!(at(pos2(x, 0.5)), (!close.contains(pos2(x, 0.5))).then_some(top), "top at {x}");
assert_eq!(at(pos2(x, 5.5)), (!close.contains(pos2(x, 5.5))).then_some(top), "6 pt down at {x}");
assert_eq!(at(pos2(x, 299.5)), Some([D::SouthWest, D::South, D::SouthEast][part(x, 240.0)]), "bottom at {x}");
}
for i in 0..300 {
let y = i as f32 + 0.5;
assert_eq!(at(pos2(0.5, y)), Some([D::NorthWest, D::West, D::SouthWest][part(y, 300.0)]), "left at {y}");
assert_eq!(at(pos2(239.5, y)), Some([D::NorthEast, D::East, D::SouthEast][part(y, 300.0)]), "right at {y}");
}
assert_eq!(at(pos2(10.0, 10.0)), None, "a corner is an L along its edges");
assert_eq!(at(pos2(120.0, 6.5)), None, "the bar below the top band");
}
#[test]
/// On macOS the close glyph sits 8 pt from the trailing corner in both directions; its hit area
/// extends 10 pt away from the corner, reaches the bar bottom, leaves the drag area clear, and stays
/// reachable outside AppKit's corner resize region.
fn on_macos_the_close_glyph_sits_by_the_corner_and_its_hit_area_reaches_away_from_it() {
use crate::docking::chrome::{ChromeStyle, TitleBar};
use crate::docking::placement::BAR_HEIGHT;
let ctx = egui::Context::default();
let full = Rect::from_min_size(Pos2::ZERO, vec2(240.0, 300.0));
let bar = Rect::from_min_size(full.min, vec2(full.width(), BAR_HEIGHT));
for right_to_left in [false, true] {
let (hit, glyph) = close_rects(bar, MACOS_CLOSE, right_to_left);
// Distances from the corner the × sits by: across from the trailing edge, and down.
let across = |x: f32| if right_to_left { x - full.left() } else { full.right() - x };
let (glyph_near, glyph_far) = (across(glyph.right()).min(across(glyph.left())), across(glyph.right()).max(across(glyph.left())));
assert_eq!((glyph_near, glyph.top(), glyph.width(), glyph.height()), (8.0, 4.5, 9.0, 9.0), "the glyph by the corner");
let (hit_near, hit_far) = (across(hit.right()).min(across(hit.left())), across(hit.right()).max(across(hit.left())));
assert_eq!(hit_near, glyph_near, "the hit area reaches no closer to the corner than the glyph");
assert_eq!(hit_far, glyph_far + 10.0, "the hit area reaches away from the corner");
assert_eq!((hit.top(), hit.bottom()), (glyph.top(), BAR_HEIGHT), "the hit area reaches down to the bar's bottom, not below");
// AppKit's corner area takes the presses nearest the corner; the rest reach the ×.
let [top_left, top_right, ..] = appkit_resize_areas(full);
let corner = if right_to_left { top_left } else { top_right };
assert_eq!(corner.width(), APPKIT_CORNER);
assert!(hit_far - APPKIT_CORNER >= 10.0, "{hit:?} has too little room outside {corner:?}");
let mut shown = None;
let mut output = ctx.run_ui(egui::RawInput { screen_rect: Some(full), ..Default::default() }, |ui| {
let style = ChromeStyle { right_to_left, ..ChromeStyle::from_ui(ui) };
let response = TitleBar::new(egui::Id::unique("bar"), "Move").title("Panel").close("Close").close_at(hit, glyph).show(ui, bar, &style);
shown = response.close.map(|close| (close.rect, response.bar.rect));
});
output.textures_delta.clear();
let (close, drag) = shown.unwrap_or((Rect::NOTHING, Rect::NOTHING));
assert_eq!(close, hit, "the bar takes clicks for the × in its hit area");
assert!(!drag.intersect(hit).is_positive(), "the bar's drag area {drag:?} leaves the × alone");
}
}
/// [`layout`] built anew, its window with id 7 when `with_id`.
fn rebuilt_layout(with_id: bool) -> Layout<u32> {
let mut layout = layout(true);
if with_id {
layout.floating = layout.floating.into_iter().map(|w| w.with_id(7)).collect();
}
layout
}
#[test]
fn a_layout_built_each_frame_with_ids_keeps_its_os_window() {
let mut d = driver(rebuilt_layout(true));
let viewport = d.viewport(7);
for _ in 0..4 {
d.layout = rebuilt_layout(true);
d.step(vec![]);
}
assert_eq!(d.host().windows.keys().copied().collect::<Vec<_>>(), [viewport], "one OS window, the same each frame");
}
#[cfg(debug_assertions)]
#[test]
fn a_layout_built_each_frame_without_ids_is_warned_about() {
let warned = |d: &DockDriver<u32, Content>| d.ctx.data(|m| m.get_temp::<bool>(d.id.with("native-rebuilt"))).unwrap_or(false);
let mut d = driver(rebuilt_layout(false));
for _ in 0..4 {
d.layout = rebuilt_layout(false);
d.step(vec![]);
}
assert!(warned(&d));
assert!(d.host().windows.len() > 1, "a new OS window each frame");
// A layout kept across frames, and one rebuilt with ids, are not.
let mut d = driver(layout(true));
d.layout = rebuilt_layout(false);
for _ in 0..4 {
d.step(vec![]);
}
assert!(!warned(&d), "loaded once");
let mut d = driver(rebuilt_layout(true));
for _ in 0..4 {
d.layout = rebuilt_layout(true);
d.step(vec![]);
}
assert!(!warned(&d));
}
/// Fill of the overlay over a blocked native window in [`blocked_driver`].
const DIM: Color32 = Color32::from_rgba_premultiplied(10, 20, 30, 120);
fn blocked_driver() -> DockDriver<u32, Content> {
let mut d = DockDriver::new(layout(true), Content { counter: Some(5), ..Content::default() })
.with_style(|s| s.blocked_dim = DIM)
.with_native_host(ORIGIN);
d.settle();
d
}
/// Whether the native window `viewport` drew the overlay over all of its content.
fn dimmed(d: &DockDriver<u32, Content>, viewport: egui::ViewportId) -> bool {
let full = Rect::from_min_size(Pos2::ZERO, d.host().windows[&viewport].rect.size());
d.host().windows[&viewport].shapes.iter().any(|c| matches!(&c.shape, egui::Shape::Rect(r) if r.fill == DIM && r.rect == full))
}
#[test]
/// While input is blocked, a native window is dimmed, loses always-on-top,
/// ignores clicks, drags, key presses and close requests, but still forwards key
/// releases; unblocking restores normal input and activation.
fn a_native_window_takes_no_input_while_the_app_blocks_it() {
let mut d = blocked_driver();
let id = id_of(&d);
let viewport = d.viewport(id);
let counter = d.content.counter_rect.unwrap().center();
let tab = pos2(90.0, TAB_ROW);
let key = Event::Key { key: Key::S, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND };
let level = |d: &DockDriver<u32, Content>| d.host().windows[&viewport].level;
assert!(!dimmed(&d, viewport));
assert_eq!(level(&d), Some(egui::WindowLevel::AlwaysOnTop));
d.input_blocked = true;
d.step(vec![]);
assert!(dimmed(&d, viewport), "drawn dimmed");
assert_eq!(level(&d), Some(egui::WindowLevel::Normal), "the main window can cover the palette");
let out = d.window_click(id, tab);
assert!(out.actions.is_empty(), "{:?}", out.actions);
d.window_click(id, counter);
assert_eq!(d.content.count, 0, "the body takes no click");
let out = d.window_step(id, vec![key.clone(), Event::Text("s".into())]);
assert!(out.forwarded_events.is_empty(), "{:?}", out.forwarded_events);
// A key released while blocked still reaches the app, which may have seen it go down.
let release = Event::Key { key: Key::S, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::COMMAND };
let out = d.window_step(id, vec![release.clone()]);
assert_eq!(out.forwarded_events, vec![release]);
// The bar starts no move, of the dock area's or the window system's.
let sent = d.host().commands.len();
d.window_hold(id, pos2(40.0, 9.0), pos2(140.0, 59.0));
assert!(d.ctx.data(|data| data.get_temp::<WindowDrag>(d.id.with("window-drag"))).is_none());
assert!(
!d.host().commands[sent..].iter().any(|(v, c)| *v == viewport && matches!(c, ViewportCommand::StartDrag | ViewportCommand::OuterPosition(_)))
);
let out = d.window_release(id, pos2(140.0, 59.0));
assert!(out.actions.is_empty(), "{:?}", out.actions);
// The OS close request is cancelled.
d.host().windows.get_mut(&viewport).unwrap().close = true;
let out = d.step(vec![]);
assert!(!out.actions.iter().any(|a| matches!(a, Action::CloseWindow { .. })), "{:?}", out.actions);
assert!(d.host().commands.contains(&(viewport, ViewportCommand::CancelClose)));
// Unblocked, the same input works again.
d.input_blocked = false;
d.step(vec![]);
assert!(!dimmed(&d, viewport));
assert_eq!(level(&d), Some(egui::WindowLevel::AlwaysOnTop));
d.window_click(id, counter);
assert_eq!(d.content.count, 1);
let out = d.window_click(id, tab);
assert!(out.actions.contains(&Action::Activate { panel: 6 }), "{:?}", out.actions);
let out = d.window_step(id, vec![key.clone()]);
assert_eq!(out.forwarded_events, vec![key]);
}
#[test]
/// A blocked native window rejects typing but preserves native control-event order: IME reset
/// completion and focus loss reach both event and raw-event readers, while a shortcut release is
/// still forwarded to the host.
fn a_blocked_native_window_preserves_ordered_ime_reset_completions() {
let mut d = blocked_driver();
let id = id_of(&d);
let viewport = d.viewport(id);
d.content.input_probe = Some(5);
d.input_blocked = true;
let request_id = d.ctx.request_ime_reset(viewport).unwrap();
let completed = Event::ImeResetCompleted { request_id };
let focus = Event::WindowFocused(false);
let release = Event::Key { key: Key::S, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::COMMAND };
let out = d.window_step(id, vec![Event::Text("blocked".into()), completed.clone(), release.clone(), focus.clone()]);
assert_eq!(d.content.events, vec![completed.clone(), focus.clone()], "panel input keeps control order while rejecting typing");
assert_eq!(d.content.raw_events, vec![completed, focus], "raw readers receive the same native controls");
assert_eq!(out.forwarded_events, vec![release], "shortcut releases still reach the host");
assert!(d.content.typed.is_empty());
}
#[test]
fn a_blocked_palette_stays_shown_and_behind_the_main_window_when_the_app_returns() {
use egui::{ViewportId, WindowLevel};
let mut d = blocked_driver();
let palette = d.viewport(id_of(&d));
d.input_blocked = true;
d.step(vec![]);
d.host().commands.clear();
let focus = |d: &mut DockDriver<u32, Content>, window: Option<ViewportId>| {
d.host().focus = window;
for _ in 0..20 {
d.step(vec![]);
}
};
// Away and back, it stays shown at the normal level: showing it or putting it on top could
// raise it over the modal, and showing it would, on macOS, give it focus.
focus(&mut d, None);
assert!(d.host().windows[&palette].visible);
focus(&mut d, Some(palette));
let host = d.host();
assert_eq!(host.windows[&palette].level, Some(WindowLevel::Normal));
assert!(!host.commands.iter().any(|(v, c)| *v == palette && matches!(c, ViewportCommand::Visible(_))), "{:?}", host.commands);
// Activated through the palette, the main window comes in front of it, with no viewers.
let levels: Vec<_> = host.commands.iter().filter(|(_, c)| matches!(c, ViewportCommand::WindowLevel(_))).cloned().collect();
let expected = if cfg!(target_os = "macos") {
Vec::new()
} else {
[WindowLevel::AlwaysOnTop, WindowLevel::Normal].map(|l| (ViewportId::ROOT, ViewportCommand::WindowLevel(l))).to_vec()
};
assert_eq!(levels, expected);
}
/// A menu's key equivalent, as an app's menu glue reports it in the main window's input.
fn menu_key(key: Key) -> Vec<Event> {
let press = |pressed| Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Modifiers::COMMAND };
vec![press(true), press(false)]
}
#[test]
fn menu_keys_go_to_the_focused_text_field_of_the_native_window_with_the_keyboard() {
let has = |events: &[Event], key: Key| events.iter().any(|e| matches!(e, Event::Key { key: k, .. } if *k == key));
let pasted = |d: &DockDriver<u32, Content>, viewport| d.host().commands.contains(&(viewport, ViewportCommand::RequestPaste));
let mut d = driver(layout(true));
let viewport = d.viewport(id_of(&d));
(d.content.text, d.content.focus_text) = (Some(5), true);
d.host().focus = Some(viewport);
d.settle();
assert_eq!(super::typing_window(&d.ctx), Some(viewport));
// Between frames, where an app's `raw_input_hook` asks, the answer is the last frame's.
d.content.blur_text = true;
d.step(vec![]);
assert_eq!(super::typing_window(&d.ctx), None, "right after the field loses focus");
d.content.focus_text = true;
d.step(vec![]);
assert_eq!(super::typing_window(&d.ctx), Some(viewport), "right after the field takes focus");
d.step(menu_key(Key::V));
assert!(!has(&d.content.main_keys, Key::V), "{:?}", d.content.main_keys);
assert!(pasted(&d, viewport), "the field's window asks for the clipboard text");
d.step(menu_key(Key::C));
assert!(!has(&d.content.main_keys, Key::C));
assert!(d.content.field_keys.contains(&Event::Copy), "{:?}", d.content.field_keys);
// Saving is the app's, from any window.
d.step(menu_key(Key::S));
assert!(has(&d.content.main_keys, Key::S));
assert!(!has(&d.content.field_keys, Key::S));
// Under a block the window's field is inert: the main window keeps every key.
d.input_blocked = true;
d.settle();
assert_eq!(super::typing_window(&d.ctx), None);
d.host().commands.clear();
d.step(menu_key(Key::V));
assert!(has(&d.content.main_keys, Key::V));
assert!(!pasted(&d, viewport));
// With no focused field, nothing moves.
let mut d = driver(layout(true));
let viewport = d.viewport(id_of(&d));
d.content.text = Some(5);
d.host().focus = Some(viewport);
d.settle();
d.step(menu_key(Key::V));
assert!(has(&d.content.main_keys, Key::V));
assert!(d.content.field_keys.is_empty(), "{:?}", d.content.field_keys);
assert!(!pasted(&d, viewport));
}
#[test]
fn a_focused_field_and_a_held_press_stay_inert_across_a_block() {
let mut d = blocked_driver();
let id = id_of(&d);
d.content.text = Some(5);
d.content.focus_text = true;
d.step(vec![]);
d.window_step(id, vec![Event::Text("a".into())]);
assert_eq!(d.content.typed, "a");
// The field loses focus when the block starts, and stays without it after.
d.input_blocked = true;
d.step(vec![]);
let out = d.window_step(id, vec![Event::Text("b".into())]);
assert!(out.forwarded_events.is_empty(), "{:?}", out.forwarded_events);
d.input_blocked = false;
d.step(vec![]);
let out = d.window_step(id, vec![Event::Text("c".into())]);
assert_eq!(d.content.typed, "a");
assert_eq!(out.forwarded_events, vec![Event::Text("c".into())]);
// A press made while blocked and released after is no click.
let counter = d.content.counter_rect.unwrap().center();
d.input_blocked = true;
d.step(vec![]);
d.window_step(id, vec![Event::PointerMoved(counter), button(counter, true)]);
d.input_blocked = false;
d.step(vec![]);
d.window_step(id, vec![button(counter, false)]);
assert_eq!(d.content.count, 0);
}
#[test]
fn blocking_input_hands_focus_from_a_native_window_to_the_main_window() {
let focus_main = |d: &DockDriver<u32, Content>| {
d.host().commands.iter().filter(|(v, c)| *v == egui::ViewportId::ROOT && matches!(c, ViewportCommand::Focus)).count()
};
let steps = |d: &mut DockDriver<u32, Content>, n: usize| {
for _ in 0..n {
d.step(vec![]);
}
};
let focused_float = || {
let mut d = blocked_driver();
let viewport = d.viewport(id_of(&d));
d.host().focus = Some(viewport);
d.step(vec![]);
d.input_blocked = true;
d
};
// A dialog opened from a floating panel needs the keys there, in the main window: once,
// after a short wait.
let mut d = focused_float();
steps(&mut d, 2);
assert_eq!(focus_main(&d), 0, "not yet: an OS dialog may still take focus");
steps(&mut d, 30);
assert_eq!(focus_main(&d), 1);
// An OS dialog took focus meanwhile: no window of the area has it, so nothing is sent.
let mut d = focused_float();
d.step(vec![]);
d.host().focus = None;
steps(&mut d, 30);
assert_eq!(focus_main(&d), 0);
// Blocking ended before the wait was over.
let mut d = focused_float();
d.step(vec![]);
d.input_blocked = false;
steps(&mut d, 30);
assert_eq!(focus_main(&d), 0);
// With the main window focused already, nothing is sent.
let mut d = blocked_driver();
d.input_blocked = true;
steps(&mut d, 30);
assert_eq!(focus_main(&d), 0);
}
#[test]
fn blocking_input_cancels_a_drag_in_progress() {
// A tab drag a native window owns.
let mut d = blocked_driver();
let id = id_of(&d);
let over = d.window_local(id, ORIGIN + vec2(600.0, 300.0)).unwrap();
d.window_hold(id, pos2(15.0, TAB_ROW), over);
assert!(super::is_dragging::<u32>(&d.ctx, d.id));
d.input_blocked = true;
d.step(vec![]);
assert!(!super::is_dragging::<u32>(&d.ctx, d.id));
let out = d.window_release(id, over);
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { .. } | Action::Float { .. })), "{:?}", out.actions);
// A bar drag.
let mut d = blocked_driver();
let id = id_of(&d);
d.window_hold(id, pos2(40.0, 9.0), pos2(140.0, 59.0));
assert!(d.ctx.data(|data| data.get_temp::<WindowDrag>(d.id.with("window-drag"))).is_some());
d.input_blocked = true;
d.step(vec![]);
assert!(d.ctx.data(|data| data.get_temp::<WindowDrag>(d.id.with("window-drag"))).is_none());
}
#[test]
fn replacing_the_layout_or_cancel_drag_ends_a_bar_drag() {
let moving = |d: &DockDriver<u32, Content>| d.ctx.data(|data| data.get_temp::<WindowDrag>(d.id.with("window-drag")));
for replace in [false, true] {
let mut d = driver(layout(true));
let id = id_of(&d);
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
assert!(moving(&d).is_some());
d.step(vec![]);
if replace {
// The same window where the OS has it, beside swapped frames.
let mut replacement = layout(true);
replacement.root = Some(Node::Split {
axis: SplitAxis::Horizontal,
size: SplitSize::Ratio(0.5),
first: Box::new(tabs(&[3, 4])),
second: Box::new(tabs(&[1, 2])),
});
replacement.floating[0] = Floating { native: window(&d, 5).native, ..replacement.floating[0].clone().with_id(id) };
d.layout = replacement;
} else {
super::cancel_drag::<u32>(&d.ctx, d.id);
}
d.settle();
assert_eq!(moving(&d), None);
// The OS window goes where the layout has it.
let before = os_window(&d, 5).unwrap().0;
let native = window(&d, 5).native.unwrap();
assert_eq!(before.min, pos2(native[0], native[1]));
d.window_step(id, vec![Event::PointerMoved(pos2(90.0, 39.0))]);
d.window_step(id, vec![Event::PointerMoved(pos2(120.0, 59.0))]);
assert_eq!(moving(&d), None);
assert_eq!(os_window(&d, 5).unwrap().0, before);
let out = d.window_step(id, vec![button(pos2(120.0, 59.0), false)]);
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { .. } | Action::MoveWindow { .. })), "{:?}", out.actions);
}
}
#[test]
fn replacing_the_layout_ends_a_divider_drag_in_a_native_window() {
let resizes = |out: &super::DockOutput<u32>| out.actions.iter().filter(|a| matches!(a, Action::ResizeWindowSplit { .. })).count();
let split = |d: &DockDriver<u32, Content>| match &window(d, 5).root {
Node::Split { size, .. } => *size,
root => panic!("{root:?}"),
};
for replace in [false, true] {
let mut start = layout(false);
let mut float = Floating::single(vec![5], 0, [300.0, 80.0, 240.0, 300.0]);
float.root =
Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::Ratio(0.5), first: Box::new(tabs(&[5])), second: Box::new(tabs(&[6])) };
start.floating.push(float);
let mut d = driver(start);
d.settle();
let id = id_of(&d);
let out = d.window_hold(id, pos2(120.0, 150.0), pos2(150.0, 150.0));
assert!(resizes(&out) > 0, "{:?}", out.actions);
if replace {
d.layout.root = Some(Node::Split {
axis: SplitAxis::Horizontal,
size: SplitSize::Ratio(0.5),
first: Box::new(tabs(&[3, 4])),
second: Box::new(tabs(&[1, 2])),
});
}
let before = split(&d);
let mut moved = 0;
for x in [160.0, 170.0, 180.0] {
moved += resizes(&d.window_step(id, vec![Event::PointerMoved(pos2(x, 150.0))]));
}
moved += resizes(&d.window_release(id, pos2(180.0, 150.0)));
if replace {
assert_eq!(moved, 0);
assert_eq!(split(&d), before);
} else {
assert!(moved > 0);
assert_ne!(split(&d), before);
}
}
}
/// [`layout`] with window `[5, 6]` saved as a native window at `NATIVE`.
fn saved_native() -> Layout<u32> {
let mut layout = layout(true);
layout.floating[0].native = Some(NATIVE);
layout
}
const NATIVE: [f32; 4] = [700.0, 400.0, 240.0, 300.0];
#[test]
fn with_native_windows_off_a_float_stays_in_the_app_window_and_keeps_its_native_rect() {
let mut d = DockDriver::new(saved_native(), Content::default());
d.native_windows = false;
let mut d = d.with_native_host(ORIGIN);
assert!(d.host().windows.is_empty() && d.host().commands.is_empty());
assert!(d.hit(&Hit::Title(5)).is_some(), "drawn in the main window");
assert_eq!(window(&d, 5).native, Some(NATIVE));
// Its tab docks again by drag, in the main window.
let tab = d.hit(&Hit::Tab(5)).unwrap().center();
let out = d.drag(tab, pos2(600.0, 300.0));
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 5, anchor: 3 | 4, .. }]), "{:?}", out.actions);
assert!(in_root(&d, 5));
assert_eq!(window(&d, 6).native, Some(NATIVE));
assert!(d.host().windows.is_empty() && d.host().commands.is_empty());
// The same workspace opens as a native window where native windows are on.
let mut d = driver(saved_native());
d.settle();
assert_eq!(os_window(&d, 5).map(|(rect, _)| rect), Some(Rect::from_min_size(pos2(700.0, 400.0), vec2(240.0, 300.0))));
}
#[test]
fn wayland_is_a_main_window_that_never_reported_a_position() {
use super::render::Session;
let mut d = driver(layout(false));
d.host().no_position = true;
d.step(vec![]);
assert!(wayland(&d.ctx, Session::Wayland));
assert!(wayland(&d.ctx, Session::Unknown), "a real window without a Wayland variable");
assert!(!wayland(&d.ctx, Session::Other), "X11");
// X11 (forced under a Wayland session) reports it; minimized, it reports none again.
d.host().no_position = false;
d.step(vec![]);
assert!(!wayland(&d.ctx, Session::Wayland));
d.host().no_position = true;
d.step(vec![]);
assert!(!wayland(&d.ctx, Session::Wayland));
// A context with no real window (a headless test) and no display variables at all.
let ctx = egui::Context::default();
ctx.run_ui(egui::RawInput::default(), |_| {}).textures_delta.clear();
assert!(!wayland(&ctx, Session::Unknown));
}
#[test]
fn the_session_follows_winits_environment_rule() {
use super::render::{Session, session};
let with = |vars: &'static [&'static str]| move |name: &str| vars.contains(&name);
assert_eq!(session(true, with(&["WAYLAND_DISPLAY", "DISPLAY"])), Session::Wayland);
assert_eq!(session(true, with(&["WAYLAND_SOCKET"])), Session::Wayland);
assert_eq!(session(true, with(&["DISPLAY"])), Session::Other);
assert_eq!(session(true, with(&[])), Session::Unknown);
assert_eq!(session(false, with(&["WAYLAND_DISPLAY"])), Session::Other);
}
/// A driver whose panel `source` has an app drag source and whose panels `sites` are drop sites.
fn app_drag(layout: Layout<u32>, source: u32, sites: &[u32]) -> DockDriver<u32, Content> {
let mut d = driver(layout);
(d.content.source, d.content.drop_sites, d.content.marks) = (Some(source), sites.to_vec(), Marks::Route(d.id));
d.settle();
d
}
#[allow(clippy::expect_used)]
fn source(d: &DockDriver<u32, Content>) -> Pos2 {
d.content.source_rect.expect("source drawn").center()
}
#[test]
fn an_app_drag_from_the_main_window_reaches_a_native_windows_panel() {
// Panel 3's frame lies under window [5, 6], which takes the drop.
let mut d = app_drag(layout(true), 1, &[3, 5]);
let id = window(&d, 5).id;
let local = pos2(120.0, 200.0);
let point = d.host().windows[&d.viewport(id)].rect.min + local.to_vec2() - ORIGIN.to_vec2();
d.hold(source(&d), point);
assert!(d.content.hovers.contains(&(5, local)), "{:?}", d.content.hovers);
d.release(point);
d.step(vec![]);
assert_eq!(d.content.drops, vec![(5, local)]);
assert!(!d.content.hovers.iter().any(|(panel, _)| *panel == 3), "{:?}", d.content.hovers);
}
#[test]
fn a_stop_withdraws_a_routed_release_no_window_read_but_not_the_owners() {
let mut d = app_drag(layout(true), 5, &[1]);
let id = window(&d, 5).id;
let local = d.window_local(id, ORIGIN + vec2(100.0, 300.0)).unwrap();
d.window_hold(id, source(&d), local);
// The release is queued for the main window's next pass, and the drag is cancelled first.
d.window_release(id, local);
super::cancel_drag::<u32>(&d.ctx, d.id);
d.step(vec![]);
d.step(vec![]);
assert_eq!(d.content.drops, vec![]);
// The owner still gets its own release.
assert_eq!(d.content.source_stopped, 1);
}
#[test]
fn a_plain_egui_drag_over_a_native_window_keeps_its_moves_and_is_not_routed() {
let mut d = app_drag(layout(true), 1, &[5]);
d.content.marks = Marks::Nothing;
let id = window(&d, 5).id;
let over = d.host().windows[&d.viewport(id)].rect.center() - ORIGIN.to_vec2();
let from = source(&d);
d.hold(from, over);
d.move_to(over + vec2(20.0, 10.0));
let moved = over + vec2(20.0, 10.0) - from;
assert!((d.content.source_delta - moved).length() < 0.01, "{:?}, not {moved:?}", d.content.source_delta);
d.release(over);
d.step(vec![]);
assert!(d.content.hovers.is_empty(), "{:?}", d.content.hovers);
assert!(d.content.drops.is_empty(), "{:?}", d.content.drops);
assert_eq!(d.content.source_stopped, 1);
}
#[test]
fn an_egui_payload_reaches_a_window_drawn_before_the_owner() {
// The main window draws before the native window that owns the drag.
let mut d = app_drag(layout(true), 5, &[1]);
d.content.marks = Marks::Payload;
let id = window(&d, 5).id;
let local = d.window_local(id, ORIGIN + vec2(100.0, 300.0)).unwrap();
d.window_hold(id, source(&d), local);
d.window_release(id, local);
d.step(vec![]);
d.step(vec![]);
assert_eq!(d.content.payload_drops, vec![1]);
assert!(!egui::DragAndDrop::has_any_payload(&d.ctx));
}
#[test]
fn an_owner_that_discards_its_release_pass_keeps_the_payload_for_the_target() {
let mut d = app_drag(layout(true), 5, &[1]);
d.content.marks = Marks::Payload;
let id = window(&d, 5).id;
let local = d.window_local(id, ORIGIN + vec2(100.0, 300.0)).unwrap();
d.window_hold(id, source(&d), local);
// The owner's pass that holds its release runs twice.
d.content.discard_next = true;
d.window_release(id, local);
d.step(vec![]);
d.step(vec![]);
assert_eq!(d.content.payload_drops, vec![1]);
}
#[test]
fn pointer_events_after_a_held_release_wait_behind_it() {
let mut d = app_drag(layout(true), 1, &[5]);
let id = window(&d, 5).id;
let point = d.host().windows[&d.viewport(id)].rect.min + vec2(120.0, 200.0) - ORIGIN.to_vec2();
d.hold(source(&d), point);
let delta = d.content.source_delta;
// The release over the native window, then the pointer back on the source and a new press,
// in one input.
let at = source(&d);
d.step(vec![button(point, false), Event::PointerMoved(at), button(at, true)]);
assert_eq!(d.content.source_delta, delta, "no move after the release reaches the source's drag");
d.step(vec![]);
assert!(d.ctx.input(|i| i.pointer.primary_down()), "the new press outlives the held release");
}
#[test]
fn the_owners_drag_stops_a_pass_after_its_release_over_another_window() {
let mut d = app_drag(layout(true), 1, &[5]);
let id = window(&d, 5).id;
let local = pos2(120.0, 200.0);
let point = d.host().windows[&d.viewport(id)].rect.min + local.to_vec2() - ORIGIN.to_vec2();
d.hold(source(&d), point);
d.release(point);
assert_eq!((d.content.drops.clone(), d.content.source_stopped), (vec![(5, local)], 0));
// The owner saw the pointer go, and holds its release and the pointer's going once.
assert_eq!(d.ctx.input(|i| i.pointer.hover_pos()), None);
let held = super::render::held_events(&d.ctx, egui::ViewportId::ROOT);
assert!(matches!(held.as_slice(), [Event::PointerButton { pressed: false, .. }, Event::PointerGone]), "{held:?}");
d.step(vec![]);
assert_eq!(d.content.source_stopped, 1);
assert_eq!(d.ctx.dragged_id(), None);
}
#[test]
fn a_press_right_after_a_routed_release_outlives_the_held_release() {
let mut d = app_drag(layout(true), 1, &[5]);
let id = window(&d, 5).id;
let point = d.host().windows[&d.viewport(id)].rect.min + vec2(120.0, 200.0) - ORIGIN.to_vec2();
d.hold(source(&d), point);
d.release(point);
// The owner's held release comes before a press in its next input.
let at = source(&d);
d.step(vec![Event::PointerMoved(at), button(at, true)]);
assert!(d.ctx.input(|i| i.pointer.primary_down()));
}
#[test]
fn a_routed_release_clicks_nothing_in_the_target() {
let mut d = app_drag(layout(true), 1, &[5]);
d.content.counter = Some(5);
d.step(vec![]);
let id = window(&d, 5).id;
let button = d.content.counter_rect.unwrap().center();
let point = d.host().windows[&d.viewport(id)].rect.min + button.to_vec2() - ORIGIN.to_vec2();
d.hold(source(&d), point);
d.release(point);
d.step(vec![]);
assert_eq!(d.content.drops, vec![(5, button)]);
assert_eq!(d.content.count, 0);
}
#[test]
fn an_app_drag_from_a_native_window_reaches_the_main_window() {
let mut d = app_drag(layout(true), 5, &[1]);
let id = window(&d, 5).id;
// Frame [1, 2], left of the native window on the desktop.
let point = pos2(100.0, 300.0);
let local = d.window_local(id, ORIGIN + point.to_vec2()).unwrap();
d.window_hold(id, source(&d), local);
d.window_release(id, local);
d.step(vec![]);
d.step(vec![]);
assert_eq!(d.content.drops, vec![(1, point)]);
assert!(d.content.hovers.contains(&(1, point)), "{:?}", d.content.hovers);
}
#[test]
fn an_app_drag_between_native_windows_drops_in_the_front_one() {
// Window [7] overlaps window [5, 6] at (550, 280) on the desktop; window [8] is apart.
let mut layout = layout(true);
layout.floating.push(Floating::single(vec![7], 0, [350.0, 150.0, 240.0, 150.0]));
layout.floating.push(Floating::single(vec![8], 0, [20.0, 420.0, 240.0, 150.0]));
let overlap = pos2(550.0, 280.0);
for front in [7, 5] {
let mut d = app_drag(layout.clone(), 8, &[5, 7]);
let viewport = d.viewport(window(&d, front).id);
d.host().focus = Some(viewport);
d.step(vec![]);
d.host().focus = Some(egui::ViewportId::ROOT);
d.step(vec![]);
let (owner, target) = (window(&d, 8).id, window(&d, front).id);
let local = d.window_local(owner, overlap).unwrap();
d.window_hold(owner, source(&d), local);
d.window_release(owner, local);
d.step(vec![]);
d.step(vec![]);
assert_eq!(d.content.drops, vec![(front, d.window_local(target, overlap).unwrap())], "front {front}");
}
}
#[test]
fn an_app_drag_released_outside_every_window_drops_nowhere() {
let mut d = app_drag(layout(true), 1, &[5]);
let id = window(&d, 5).id;
let over = d.host().windows[&d.viewport(id)].rect.center() - ORIGIN.to_vec2();
d.hold(source(&d), over);
assert!(d.content.hovers.iter().any(|(panel, _)| *panel == 5), "{:?}", d.content.hovers);
// Left of the main window, over no window of the app: the native window sees the pointer go.
let out = pos2(-60.0, 120.0);
d.move_to(out);
d.content.hovers.clear();
d.move_to(out);
d.release(out);
d.step(vec![]);
assert!(d.content.hovers.is_empty(), "{:?}", d.content.hovers);
assert!(d.content.drops.is_empty(), "{:?}", d.content.drops);
}
/// Hold an app drag from panel 1 over window [5, 6] and release it there.
fn drag_onto_window(d: &mut DockDriver<u32, Content>) {
let id = window(d, 5).id;
let over = d.host().windows[&d.viewport(id)].rect.center() - ORIGIN.to_vec2();
d.hold(source(d), over);
d.release(over);
d.step(vec![]);
d.step(vec![]);
}
#[test]
fn no_app_drag_is_routed_where_windows_report_no_positions() {
let mut d = app_drag(layout(true), 1, &[5]);
d.host().no_position = true;
d.step(vec![]);
drag_onto_window(&mut d);
assert!(d.content.hovers.is_empty(), "{:?}", d.content.hovers);
assert!(d.content.drops.is_empty(), "{:?}", d.content.drops);
}
#[test]
fn a_blocked_native_window_gets_no_app_drag() {
let mut d = app_drag(layout(true), 1, &[5]);
d.input_blocked = true;
d.settle();
drag_onto_window(&mut d);
assert!(d.content.hovers.is_empty(), "{:?}", d.content.hovers);
assert!(d.content.drops.is_empty(), "{:?}", d.content.drops);
}
#[test]
fn escape_and_cancel_drag_end_the_routing_of_an_app_drag() {
for cancel in [false, true] {
let mut d = app_drag(layout(true), 1, &[5]);
let id = window(&d, 5).id;
let over = d.host().windows[&d.viewport(id)].rect.center() - ORIGIN.to_vec2();
d.hold(source(&d), over);
assert!(!d.content.hovers.is_empty(), "cancel {cancel}");
if cancel {
super::cancel_drag::<u32>(&d.ctx, d.id);
d.step(vec![]);
} else {
d.key(Key::Escape);
}
d.content.hovers.clear();
d.move_to(over);
d.release(over);
d.step(vec![]);
assert!(d.content.hovers.is_empty(), "cancel {cancel}: {:?}", d.content.hovers);
assert!(d.content.drops.is_empty(), "cancel {cancel}: {:?}", d.content.drops);
}
}
#[test]
/// Dragging a tab onto a native window's body emits a tab `Move` rather than
/// an app body drag and leaves the content's drops empty.
fn a_tab_drag_onto_a_native_window_is_no_app_drag() {
let mut d = app_drag(layout(true), 1, &[5]);
let id = window(&d, 5).id;
let over = d.host().windows[&d.viewport(id)].rect.center() - ORIGIN.to_vec2();
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
let out = d.drag(tab, over);
d.step(vec![]);
assert!(matches!(out.actions.as_slice(), [Action::Move { panel: 1, anchor: 5 | 6, .. }]), "{:?}", out.actions);
assert!(d.content.drops.is_empty(), "{:?}", d.content.drops);
}
/// `cancel_drag` from an app's per-area native input hook (`DockArea::native_input_hook`)
/// runs while egui holds the `NativeInput` plugin lock. It must not try to lock that plugin
/// again: in a debug build the original deadlocks, and egui's debug mutex panics after its
/// 10 s deadlock bound.
#[test]
fn cancel_drag_from_a_native_input_hook_returns() {
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
let cancel = Arc::new(AtomicBool::new(false));
let flag = cancel.clone();
let mut d = driver(layout(true)).with_native_input_hook(move |ctx, _input| {
if flag.swap(false, Ordering::SeqCst) {
// `driver` uses this area id.
super::cancel_drag::<u32>(ctx, egui::Id::unique("dock-driver"));
}
});
let id = id_of(&d);
d.settle();
// The hook runs for the native window's input, inside `NativeInput::input_hook`.
cancel.store(true, Ordering::SeqCst);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 150.0))]);
assert!(!cancel.load(Ordering::SeqCst), "the hook did not run");
}
/// `send_keys` from the per-area native input hook, inside `Plugin::input_hook`, returns
/// without locking the plugin again, and the keys reach that window's next input.
#[test]
fn send_keys_from_a_native_input_hook_returns_and_delivers() {
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
let send = Arc::new(AtomicBool::new(false));
let flag = send.clone();
let mut d = driver(layout(true)).with_native_input_hook(move |ctx, input| {
if flag.swap(false, Ordering::SeqCst) {
super::send_keys(ctx, input.viewport_id, vec![Event::Copy]);
}
});
let id = id_of(&d);
d.content.text = Some(5);
d.settle();
send.store(true, Ordering::SeqCst);
d.window_step(id, vec![]);
assert!(!send.load(Ordering::SeqCst), "the hook did not run");
d.step(vec![]);
d.window_step(id, vec![]);
assert!(d.content.field_keys.contains(&Event::Copy), "{:?}", d.content.field_keys);
}
/// An app drag with a release queued for the main window: `cancel_drag` from the per-area
/// native input hook (inside `Plugin::input_hook`, with egui holding the `NativeInput` plugin
/// lock) returns without deadlocking, withdraws that release, and leaves the owner's own
/// release.
#[test]
fn cancel_drag_from_a_native_input_hook_withdraws_a_queued_release() {
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
let cancel = Arc::new(AtomicBool::new(false));
let flag = cancel.clone();
let mut d = app_drag(layout(true), 5, &[1]).with_native_input_hook(move |ctx, _input| {
if flag.swap(false, Ordering::SeqCst) {
// `driver` uses this area id.
super::cancel_drag::<u32>(ctx, egui::Id::unique("dock-driver"));
}
});
let id = window(&d, 5).id;
// The release is over the main window: it is queued for the main window's next pass.
let local = d.window_local(id, ORIGIN + vec2(100.0, 300.0)).unwrap();
d.window_hold(id, source(&d), local);
cancel.store(true, Ordering::SeqCst);
d.window_release(id, local);
assert!(!cancel.load(Ordering::SeqCst), "the hook did not run");
assert!(d.content.drops.is_empty(), "{:?}", d.content.drops);
assert_eq!(d.content.source_stopped, 0, "the owner's own release is still held");
d.step(vec![]);
d.step(vec![]);
assert!(d.content.drops.is_empty(), "{:?}", d.content.drops);
assert_eq!(d.content.source_stopped, 1, "the owner still gets its own release");
assert!(super::render::held_events(&d.ctx, egui::ViewportId::ROOT).is_empty());
assert!(super::render::held_events(&d.ctx, d.viewport(id)).is_empty());
}
/// A main-window owner that asks for its release pass to be discarded still gets its release
/// exactly once, and the native target reads the payload exactly once.
#[test]
fn a_root_owner_that_discards_its_release_pass_drops_the_payload_once() {
let mut d = app_drag(layout(true), 1, &[5]);
d.content.marks = Marks::Payload;
let id = window(&d, 5).id;
let local = pos2(120.0, 200.0);
let point = d.host().windows[&d.viewport(id)].rect.min + local.to_vec2() - ORIGIN.to_vec2();
d.hold(source(&d), point);
d.content.discard_next = true;
d.release(point);
assert_eq!(d.content.drops, vec![(5, local)]);
assert_eq!(d.content.payload_drops, vec![5]);
// The owner's release may arrive in the same frame's rerun.
assert_eq!(d.content.source_stopped, 1);
assert_eq!(d.content.source_releases, 1, "the owner receives its release once");
d.step(vec![]);
assert_eq!(d.content.drops, vec![(5, local)], "the target drops once");
assert_eq!(d.content.payload_drops, vec![5], "the payload drops once");
assert_eq!(d.content.source_stopped, 1, "the owner gets its release once");
assert_eq!(d.content.source_releases, 1, "the owner receives its release once");
assert!(!egui::DragAndDrop::has_any_payload(&d.ctx));
assert!(super::render::held_events(&d.ctx, egui::ViewportId::ROOT).is_empty());
}
/// A native target that asks for its release pass to be discarded reads the release and the
/// payload once; its rerun has no events, and the owner's held release follows a pass later.
#[test]
fn a_target_that_discards_its_release_pass_drops_the_payload_once() {
let mut d = app_drag(layout(true), 1, &[5]);
d.content.marks = Marks::Payload;
d.content.discard_drop = true;
let id = window(&d, 5).id;
let local = pos2(120.0, 200.0);
let point = d.host().windows[&d.viewport(id)].rect.min + local.to_vec2() - ORIGIN.to_vec2();
d.hold(source(&d), point);
d.release(point);
assert_eq!(d.content.drops, vec![(5, local)]);
assert_eq!(d.content.payload_drops, vec![5]);
assert_eq!(d.content.source_stopped, 0, "the owner's release is held for a later pass");
assert_eq!(d.content.source_releases, 0, "the owner's release is held for a later pass");
d.step(vec![]);
assert_eq!(d.content.drops, vec![(5, local)], "the target drops once");
assert_eq!(d.content.payload_drops, vec![5], "the payload drops once");
assert_eq!(d.content.source_stopped, 1, "the owner gets its release once");
assert_eq!(d.content.source_releases, 1, "the owner receives its release once");
assert!(!egui::DragAndDrop::has_any_payload(&d.ctx));
assert!(super::render::held_events(&d.ctx, egui::ViewportId::ROOT).is_empty());
}
/// Several discarded passes in one frame neither duplicate nor lose the routed release: the
/// target's drop and payload read, and the owner's release, happen exactly once.
#[test]
fn repeated_discards_deliver_the_release_and_drop_once() {
let mut d = app_drag(layout(true), 1, &[5]);
d.content.marks = Marks::Payload;
let id = window(&d, 5).id;
let local = pos2(120.0, 200.0);
let point = d.host().windows[&d.viewport(id)].rect.min + local.to_vec2() - ORIGIN.to_vec2();
d.hold(source(&d), point);
d.ctx.memory_mut(|memory| memory.options.max_passes = std::num::NonZeroUsize::new(4).unwrap());
d.content.discard_passes = 3;
d.release(point);
assert_eq!(d.content.drops, vec![(5, local)], "the target drops once");
assert_eq!(d.content.payload_drops, vec![5], "the payload drops once");
assert_eq!(d.content.source_stopped, 1, "the owner gets its release once");
assert_eq!(d.content.source_releases, 1, "the owner receives its release once");
// The frame ran its fourth, completing pass here; the release and payload are gone.
d.step(vec![]);
assert_eq!(d.content.drops, vec![(5, local)]);
assert_eq!(d.content.payload_drops, vec![5]);
assert_eq!(d.content.source_stopped, 1);
assert_eq!(d.content.source_releases, 1);
assert!(!egui::DragAndDrop::has_any_payload(&d.ctx));
assert!(super::render::held_events(&d.ctx, egui::ViewportId::ROOT).is_empty());
}
#[test]
fn a_native_window_fades_only_while_its_bar_drag_highlights_a_drop_target() {
let bar_to = |d: &mut DockDriver<u32, Content>, to: Pos2| {
let id = window(d, 5).id;
let local = d.window_local(id, ORIGIN + to.to_vec2()).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
drag_opacity(&d.ctx, d.id)
};
let grab = |d: &mut DockDriver<u32, Content>| {
let id = window(d, 5).id;
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
id
};
// A bar that only moves its window highlights nothing.
let mut d = driver(layout(true)).with_style(|s| {
s.command_press_floats = true;
s.title_bar_docks = false;
});
grab(&mut d);
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 1.0, "a move that cannot dock");
let mut d = driver(layout(true));
let id = grab(&mut d);
// Past the main window's right side: the desktop.
assert_eq!(bar_to(&mut d, pos2(1000.0, 300.0)), 1.0, "over empty space");
// The right edge zone of frame [3, 4].
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75, "over a drop target");
// The middle of the frame is no target for a whole window.
assert_eq!(bar_to(&mut d, pos2(600.0, 300.0)), 1.0, "after leaving the target");
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75);
// With Cmd/Ctrl held the drop floats and nothing is highlighted.
d.modifiers = Modifiers::COMMAND;
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 1.0, "a floating drop");
d.modifiers = Modifiers::NONE;
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75);
d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert_eq!(drag_opacity(&d.ctx, d.id), 1.0, "after the drop");
}