From 98e05997a032bf5269263b431e0296d91769a81f Mon Sep 17 00:00:00 2001 From: aiongg Date: Fri, 23 Oct 2026 17:55:03 +0000 Subject: [PATCH] docking: native windows place by their content and follow the layout - A window made this session opens at its computed rect; only windows of a loaded layout are restored onto a connected monitor. - The content (inner rect) is each native window's desktop origin; only OuterPosition converts to the outer position. - A bar drag places the window from its reported position, so late moves do not overshoot. The layout follows once the drag ends, and a window left with its bar off every monitor goes back onto the nearest. - A changed Floating::native (undo, reset, reload) moves the OS window instead of being overwritten by its next report. - Dragging a native window's only tab or group docks it whatever title_bar_docks says. - Escape that cancels a drag is consumed; nothing is forwarded while a widget of the window has focus. - A bar drag whose window goes away ends. - An embedded fallback keeps its drop targets; floating windows get no taskbar entry. - The test host can give windows a frame inset and land moves a frame late. --- crates/ui/src/docking/floating_tests.rs | 7 +- crates/ui/src/docking/native_tests.rs | 294 ++++++++++++++++++++++- crates/ui/src/docking/render/cross.rs | 28 ++- crates/ui/src/docking/render/floating.rs | 12 +- crates/ui/src/docking/render/mod.rs | 2 +- crates/ui/src/docking/render/native.rs | 272 ++++++++++++++------- crates/ui/src/docking/render/preview.rs | 2 +- crates/ui/src/docking/testing.rs | 42 +++- 8 files changed, 558 insertions(+), 101 deletions(-) diff --git a/crates/ui/src/docking/floating_tests.rs b/crates/ui/src/docking/floating_tests.rs index 8e20d6991..1806c4298 100644 --- a/crates/ui/src/docking/floating_tests.rs +++ b/crates/ui/src/docking/floating_tests.rs @@ -537,9 +537,12 @@ fn native_window_close_closes_its_panels() { d.ctx.set_embed_viewports(false); d.content.native = true; d.layout = layout; - let out = d.step(vec![]); + d.step(vec![]); assert!(d.response(Id::new(AREA).with(("move-group", Some(&5u32)))).is_none(), "drawn in its own OS window"); - assert!(out.actions.contains(&Action::SetNative { panel: 5, rect: Some([1210.0, 905.0, 250.0, 300.0]) }), "{:?}", out.actions); + // Where the window system put its content, once a report that late is no longer the + // opening move landing. + let reported = (0..60).any(|_| d.step(vec![]).actions.contains(&Action::SetNative { panel: 5, rect: Some([1210.0, 935.0, 250.0, 300.0]) })); + assert!(reported, "{:?}", d.layout.floating[0].native); close.set(true); let out = d.step(vec![]); diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index 85e324890..74caf8ac1 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -3,7 +3,7 @@ use egui::{Color32, Event, Key, Modifiers, PointerButton, Pos2, Rect, ViewportCommand, epaint::ClippedShape, pos2, vec2}; -use super::render::floats_shown; +use super::render::{DragPointer, WindowDrag, floats_shown}; use super::testing::DockDriver; use super::{Action, DockContent, Floating, Hit, Layout, Monitor, Node, Permissions, Placement, Target}; use crate::layout::{SplitAxis, SplitSize}; @@ -17,11 +17,26 @@ struct Content { fixed: Vec, /// Panels that stay in-window. in_window: Vec, + /// The panel with a text field, what it holds, and whether it takes focus next frame. + text: Option, + typed: String, + focus_text: bool, + /// The panel with a button that takes focus. + focused_button: Option, } impl DockContent for Content { fn body(&mut self, ui: &mut egui::Ui, panel: &u32) { ui.label(format!("panel {panel}")); + 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 self.focused_button == Some(*panel) { + ui.button("OK").request_focus(); + } } fn logical_label(&self, panel: &u32) -> String { @@ -639,3 +654,280 @@ fn the_window_focused_last_is_in_front_where_windows_overlap() { assert!(anchor.is_some_and(|a| anchors.contains(&a)), "front {front}: {:?}", out.actions); } } + +/// Every `OuterPosition` sent to `viewport`, in order. +fn moves(d: &DockDriver, viewport: egui::ViewportId) -> Vec { + 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"); + } + 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])); +} + +#[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 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 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); + + // 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::(&d.ctx, d.id)); +} + +#[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::(&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| d.ctx.data(|data| data.get_temp::(d.id.with("drag-pointer"))); + let moving = |d: &DockDriver| d.ctx.data(|data| data.get_temp::(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::(&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::(&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); +} diff --git a/crates/ui/src/docking/render/cross.rs b/crates/ui/src/docking/render/cross.rs index 284787277..cfd08313a 100644 --- a/crates/ui/src/docking/render/cross.rs +++ b/crates/ui/src/docking/render/cross.rs @@ -42,6 +42,9 @@ pub(in crate::docking) struct DragPointer { pub command: bool, /// The window a bar drag moves. It follows the pointer and is never its own target. pub window: Option, + /// That drag began on the window's only tab or group: it docks whatever + /// [`crate::docking::DockStyle::title_bar_docks`] says. + pub docks: bool, } impl DragPointer { @@ -93,6 +96,10 @@ pub(in crate::docking) struct Cross

{ pub bar: bool, /// A target was offered while `quiet`. pub offered: Cell, + /// Where the native window drawing now was told to move its content this frame. + pub moved: Option, + /// Resolving a window move that docks whatever `title_bar_docks` says. + pub docks: bool, } impl

Default for Cross

{ @@ -108,14 +115,16 @@ impl

Default for Cross

{ quiet: false, bar: false, offered: Cell::new(false), + moved: None, + docks: false, } } } impl Renderer<'_, P> { /// Write the pointer of the drag this window owns. `origin` is the window's desktop - /// origin for this frame's input. - pub(super) fn record_pointer(&mut self, ctx: &Context, owner: Option, origin: Option, window: Option) -> DragPointer { + /// origin for this frame's input; `window` the bar drag it is. + pub(super) fn record_pointer(&mut self, ctx: &Context, owner: Option, origin: Option, window: Option<&WindowDrag>) -> DragPointer { let key = self.id.with(POINTER); let kept = ctx.data(|d| d.get_temp::(key)).filter(|p| p.owner == owner).and_then(|p| p.local); let pointer = ctx.input(|i| DragPointer { @@ -126,7 +135,8 @@ impl Renderer<'_, P> { released: i.pointer.primary_released(), escape: i.key_pressed(Key::Escape), command: i.modifiers.command || i.modifiers.ctrl || i.modifiers.mac_cmd, - window, + window: window.map(|w| w.window), + docks: window.is_some_and(|w| w.docks), }); ctx.data_mut(|d| d.insert_temp(key, pointer)); self.cross.pointer = Some(pointer); @@ -290,21 +300,21 @@ impl Renderer<'_, P> { let Some(pointer) = self.cross.pointer.filter(|p| p.window.is_some() && !p.released) else { return }; let Some(drag) = pointer.window.and_then(|w| self.window_group_drag(w)) else { return }; if let Some(Dest::Main(local)) = self.destination(&pointer, None) { - (self.cross.quiet, self.cross.bar) = (true, true); + (self.cross.quiet, self.cross.bar, self.cross.docks) = (true, true, pointer.docks); self.paint_group_drag(ui, &drag, local, pointer.command); - (self.cross.quiet, self.cross.bar) = (false, false); + (self.cross.quiet, self.cross.bar, self.cross.docks) = (false, false, false); } } /// Release of a bar drag: over a target in another window the whole window docks or /// joins there. Over the desktop, or with Ctrl/Cmd held, it stays where it was moved. fn drop_native_window(&mut self, window: u64, pointer: &DragPointer) { - let Some(drag) = self.window_group_drag(window).filter(|_| !pointer.command && self.style.title_bar_docks) else { return }; + let Some(drag) = self.window_group_drag(window).filter(|_| !pointer.command && (pointer.docks || self.style.title_bar_docks)) else { return }; match self.destination(pointer, None) { Some(Dest::Main(local)) => { - self.cross.bar = true; + (self.cross.bar, self.cross.docks) = (true, pointer.docks); self.drop_window(&drag, local, false); - self.cross.bar = false; + (self.cross.bar, self.cross.docks) = (false, false); } Some(Dest::Window(target, local)) => { let action = self.within_window(target, true, |r| { @@ -353,7 +363,9 @@ impl Renderer<'_, P> { let ctx = ui.ctx(); if let Some(window) = pointer.window { if let Some(drag) = self.window_group_drag(window) { + self.cross.docks = pointer.docks; self.paint_group_drag(ui, &drag, local, pointer.command); + self.cross.docks = false; } return; } diff --git a/crates/ui/src/docking/render/floating.rs b/crates/ui/src/docking/render/floating.rs index 72ec389c3..21eaf447d 100644 --- a/crates/ui/src/docking/render/floating.rs +++ b/crates/ui/src/docking/render/floating.rs @@ -220,11 +220,21 @@ impl Renderer<'_, P> { matches!(self.place(panel), Some(crate::docking::dock_model::Place::Floating { path, .. }) if path.is_empty()) } + /// Whether moving a whole window docks it: [`crate::docking::DockStyle::title_bar_docks`], + /// or a native window moved by its only tab or group. + pub(super) fn title_bar_docks(&self) -> bool { + #[cfg(feature = "native-windows")] + if self.cross.docks { + return true; + } + self.style.title_bar_docks + } + /// Release of a title bar drag. The window already sits where it was dragged; a dock or /// group target docks or merges every group in it, else hosts get the release in /// `group_drop`. pub(super) fn drop_window(&mut self, drag: &GroupDrag

, pointer: Pos2, float_only: bool) { - let float_only = float_only || !self.style.title_bar_docks; + let float_only = float_only || !self.title_bar_docks(); if !float_only && let Some((action, _)) = self.dock_group_proposal(drag, pointer) { self.output.actions.extend(action); return; diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs index 023461bff..823e30860 100644 --- a/crates/ui/src/docking/render/mod.rs +++ b/crates/ui/src/docking/render/mod.rs @@ -15,7 +15,7 @@ mod iconic; #[cfg(feature = "native-windows")] mod native; #[cfg(all(test, feature = "native-windows"))] -pub(in crate::docking) use native::floats_shown; +pub(in crate::docking) use {cross::DragPointer, native::WindowDrag, native::floats_shown}; /// The viewport of the native window `window` of the dock area `area`. #[cfg(feature = "native-windows")] diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs index eade8e533..09711bee4 100644 --- a/crates/ui/src/docking/render/native.rs +++ b/crates/ui/src/docking/render/native.rs @@ -7,21 +7,25 @@ use std::{fmt::Debug, hash::Hash}; -use egui::{Context, Event, Id, Pos2, Rect, Ui, Vec2, ViewportBuilder, ViewportClass, ViewportCommand, ViewportId, WindowLevel, pos2, vec2}; +use egui::{ + Context, Event, Id, Key, Modifiers, Pos2, Rect, Ui, Vec2, ViewportBuilder, ViewportClass, ViewportCommand, ViewportId, ViewportInfo, WindowLevel, pos2, + vec2, +}; use super::Renderer; use super::cross::{DragPointer, Surface}; use crate::docking::chrome::{ChromeStyle, TitleBar}; -use crate::docking::placement::{monitor_of, restore, sanitized}; +pub(in crate::docking) use crate::docking::placement::BAR_HEIGHT; +use crate::docking::placement::{kept_reachable, monitor_of, restore, sanitized}; use crate::docking::{Action, Drag, Floating, GroupDrag, GroupGesture, Node}; /// Smallest native window. const MIN_SIZE: [f32; 2] = [160.0, 100.0]; -/// Height of the bar drawn at the top of a native window. -pub(in crate::docking) const BAR_HEIGHT: f32 = 18.0; /// How long floating windows stay after the application's last window lost focus, so focus /// moving between our own windows does not hide them. const HIDE_DELAY: f64 = 0.25; +/// How long reports of a native window's place may lag a move the dock area commanded. +const MOVE_WAIT: f64 = 0.5; /// A drag of a native window's bar, in the context's data under the area's `"window-drag"`. #[derive(Clone, Copy, Debug, PartialEq)] @@ -30,11 +34,40 @@ pub(in crate::docking) struct WindowDrag { pub window: u64, /// Where the pointer pressed, in the window's local points. pub grab: Vec2, - /// The window's outer position on the desktop: reported when the drag began, then as last - /// commanded. The reported position lags and jitters while the window moves. - pub origin: Pos2, - /// Where the window was when the drag began, for Escape. + /// Desktop position of the window's content as last commanded. + pub to: Pos2, + /// Desktop position of the window's content when the drag began, for Escape. pub start: Pos2, + /// The drag began on the window's only tab or group, so it docks whatever + /// [`crate::docking::DockStyle::title_bar_docks`] says. + pub docks: bool, +} + +/// What the dock area knows of a shown native window's place, in the context's data under +/// the area's `("native-place", viewport)`. Rects are desktop rects of the window's content. +#[derive(Clone, Copy, Debug)] +struct Place { + /// Where it opened. The builder keeps it, so egui sends no move of its own. + spawn: [f32; 4], + /// As last reported or commanded. [`Floating::native`] follows it; a `native` that differs + /// was changed by the application (undo, reset, a reloaded layout), and the window moves. + known: [f32; 4], + /// Until when reports that differ from `known` are a commanded move that has not landed. + pending: Option, + /// The window system put the frame, not the content, where it opened: corrected once. + corrected: bool, +} + +/// Whether two rects differ by more than rounding. +fn differs(a: [f32; 4], b: [f32; 4]) -> bool { + a.iter().zip(b).any(|(a, b)| (a - b).abs() > 0.5) +} + +/// Desktop position of a viewport's content, and its offset from the window's outer +/// position, where the window system reports both. +fn content_of(info: &ViewportInfo) -> Option<(Pos2, Vec2)> { + let (outer, inner) = (info.outer_rect?, info.inner_rect?); + (outer.min.is_finite() && inner.min.is_finite()).then(|| (inner.min, inner.min - outer.min)) } /// The viewport of the native window `window` of the dock area `area`. The `n`th window with @@ -52,17 +85,20 @@ pub(in crate::docking) fn floats_shown(focused: bool, now: f64, last_focused: f6 focused || now - last_focused < HIDE_DELAY } -/// The desktop rect a native viewport reports: outer position, inner size. None when the -/// backend reports nothing usable. -fn placed(outer: Option, inner: Option) -> Option<[f32; 4]> { - let (outer, inner) = (outer?, inner?); - (outer.min.is_finite() && inner.size().is_finite()).then(|| sanitized([outer.min.x, outer.min.y, inner.width(), inner.height()])) +/// The desktop rect of a native viewport's content. None when the backend reports no +/// positions. +fn placed(info: &ViewportInfo) -> Option<[f32; 4]> { + let (min, _) = content_of(info)?; + let size = info.inner_rect?.size(); + size.is_finite().then(|| sanitized([min.x, min.y, size.x, size.y])) } /// What a native window's frame reports back to the main window. struct Report

{ closed: bool, placed: Option<[f32; 4]>, + /// From the window's outer position to its content. + frame: Vec2, monitor_size: Option, /// Its drop targets, for drags from other windows. surface: Option>, @@ -70,7 +106,7 @@ struct Report

{ impl

Default for Report

{ fn default() -> Self { - Self { closed: false, placed: None, monitor_size: None, surface: None } + Self { closed: false, placed: None, frame: Vec2::ZERO, monitor_size: None, surface: None } } } @@ -87,22 +123,25 @@ impl Renderer<'_, P> { let mut natives = Vec::new(); for window in windows { let allowed = window.panels().all(|p| self.content.allow_native(p) && self.permissions(p).floatable); - let Some(first) = window.panels().next().cloned().filter(|_| native && allowed) else { + if !native || !allowed || window.panels().next().is_none() { in_window.push(window); continue; - }; + } // Every float is native: one still in-window opens over its place in the app window. let rect = window.native.unwrap_or_else(|| { let [x, y, w, h] = window.rect; - let rect = sanitized([origin.x + x, origin.y + y, w, h]); - self.output.actions.push(Action::SetNative { panel: first, rect: Some(rect) }); - rect + sanitized([origin.x + x, origin.y + y, w, h]) }); let n = ids.iter().filter(|id| **id == window.id).count(); ids.push(window.id); natives.push((window, rect, viewport_id(self.id, window.id, n))); } if native { + // A window closed or docked during its own bar drag ends the drag. + let key = self.id.with("window-drag"); + if ctx.data(|d| d.get_temp::(key)).is_some_and(|d| !natives.iter().any(|(w, ..)| w.id == d.window)) { + ctx.data_mut(|d| d.remove::(key)); + } self.map_desktop(ctx, &natives, visibility.0); // The main window owns drags of the panels it draws. let id = self.id; @@ -124,7 +163,7 @@ impl Renderer<'_, P> { let previous = ctx.data_mut(|d| d.remove_temp::>(key)).unwrap_or_default(); ctx.data_mut(|d| { for id in previous.iter().filter(|id| !shown.contains(id)) { - d.remove::<[f32; 4]>(*id); + d.remove::(*id); } d.insert_temp(key, shown); }); @@ -137,7 +176,6 @@ impl Renderer<'_, P> { let key = self.id.with("native-order"); let mut order = ctx.data(|d| d.get_temp::>(key)).unwrap_or_default(); order.retain(|id| natives.iter().any(|(w, ..)| w.id == *id)); - let moving = ctx.data(|d| d.get_temp::(self.id.with("window-drag"))); let mut windows = Vec::new(); for (window, rect, viewport) in natives { let info = ctx.input(|i| i.raw.viewports.get(viewport).cloned()); @@ -149,10 +187,7 @@ impl Renderer<'_, P> { let [x, y, w, h] = *rect; let placed = match info { None => Some(Rect::from_min_size(pos2(x, y), vec2(w, h))), - Some(info) => info.outer_rect.zip(info.inner_rect).map(|(outer, inner)| { - let min = moving.filter(|m| m.window == window.id).map_or(outer.min, |m| m.origin); - Rect::from_min_size(min, inner.size()) - }), + Some(info) => placed(&info).map(|[x, y, w, h]| Rect::from_min_size(pos2(x, y), vec2(w, h))), }; if let Some(placed) = placed.filter(|r| r.is_finite() && visible) { windows.push((window.id, placed)); @@ -184,32 +219,61 @@ impl Renderer<'_, P> { (visible, visible != was) } - /// One floating window in its own viewport. Returns the key of its spawn rect. + /// One floating window in its own viewport. Returns the key of its [`Place`]. fn native_window(&mut self, ctx: &Context, window: &Floating

, rect: [f32; 4], viewport: ViewportId, visibility: (bool, bool)) -> Option { let (visible, toggled) = visibility; let first = window.panels().next()?.clone(); - // The window opens at its saved place, moved onto a connected monitor; after that the - // window system owns its position and size, which are read back below. - let spawn_key = self.id.with(("native-spawn", viewport)); - let spawned = ctx.data(|d| d.get_temp::<[f32; 4]>(spawn_key)); - let spawn = spawned.unwrap_or_else(|| { - let (main, size) = ctx.input(|i| (i.viewport().inner_rect, i.viewport().monitor_size)); - restore(rect, window.monitor.as_ref(), &self.monitors, main, size) - }); - ctx.data_mut(|d| d.insert_temp(spawn_key, spawn)); + let key = self.id.with(("native-place", viewport)); + let now = ctx.input(|i| i.time); + let drag_key = self.id.with("window-drag"); + let moving = |ctx: &Context| ctx.data(|d| d.get_temp::(drag_key)).is_some_and(|d| d.window == window.id); + let info = ctx.input(|i| i.raw.viewports.get(&viewport).cloned()); + let frame = info.as_ref().and_then(content_of).map_or(Vec2::ZERO, |(_, frame)| frame); + let mut place = match ctx.data(|d| d.get_temp::(key)) { + Some(mut place) => { + // The application changed the window's place (undo, reset, a reloaded layout): + // the OS window moves there, kept where its bar can be grabbed. + if !moving(ctx) + && let Some(native) = window.native.filter(|native| differs(*native, place.known)) + { + let target = kept_reachable(native, &self.monitors); + ctx.send_viewport_cmd_to(viewport, ViewportCommand::OuterPosition(pos2(target[0], target[1]) - frame)); + ctx.send_viewport_cmd_to(viewport, ViewportCommand::InnerSize(vec2(target[2], target[3]))); + (place.known, place.pending) = (target, Some(now + MOVE_WAIT)); + } + place + } + None => { + // A window of a loaded layout opens at its saved place, moved onto a connected + // monitor. One made this session opens where it was put. + let seen_key = self.id.with("native-seen"); + let mut seen = ctx.data(|d| d.get_temp::>(seen_key)).unwrap_or_default(); + let spawn = if window.native.is_some() && !seen.contains(&window.id) { + let (main, size) = ctx.input(|i| (i.viewport().inner_rect, i.viewport().monitor_size)); + restore(rect, window.monitor.as_ref(), &self.monitors, main, size) + } else { + sanitized(rect) + }; + seen.insert(window.id); + ctx.data_mut(|d| d.insert_temp(seen_key, seen)); + Place { spawn, known: spawn, pending: Some(now + MOVE_WAIT), corrected: false } + } + }; + let spawn = place.spawn; let title = crate::docking::dock_model::first_active(&window.root).map(|p| self.label_of(p)).unwrap_or_default(); let builder = ViewportBuilder::default() .with_title(title) .with_position([spawn[0], spawn[1]]) .with_inner_size([spawn[2], spawn[3]]) .with_min_inner_size(MIN_SIZE) - .with_window_level(WindowLevel::AlwaysOnTop); + .with_window_level(WindowLevel::AlwaysOnTop) + .with_taskbar(false); // The bar replaces the OS title bar. macOS keeps its frame for edge resizing. #[cfg(target_os = "macos")] let builder = builder.with_titlebar_shown(false).with_title_shown(false).with_fullsize_content_view(true).with_titlebar_buttons_shown(false); #[cfg(not(target_os = "macos"))] let builder = builder.with_decorations(false); - if toggled || (spawned.is_none() && !visible) { + if toggled || (info.is_none() && !visible) { ctx.send_viewport_cmd_to(viewport, ViewportCommand::Visible(visible)); } if visible && self.activation.as_ref().is_some_and(|panel| window.contains(panel)) { @@ -244,32 +308,57 @@ impl Renderer<'_, P> { self.forward_events(ui); ui.input(|i| Report { closed: i.viewport().close_requested(), - placed: placed(i.viewport().outer_rect, i.viewport().inner_rect), + placed: placed(i.viewport()), + frame: content_of(i.viewport()).map_or(Vec2::ZERO, |(_, frame)| frame), monitor_size: i.viewport().monitor_size, surface: Some(surface), }) }); - // Targets in another OS window are in its coordinates. - self.targets.truncate(targets); - self.dock_targets.truncate(docks); - self.frame.windows.truncate(windows); + // Targets in another OS window are in its coordinates. An embedded fallback drew its + // targets in the main window's. + if report.surface.is_some() { + self.targets.truncate(targets); + self.dock_targets.truncate(docks); + self.frame.windows.truncate(windows); + } self.cross.surfaces.extend(report.surface); + if let Some(to) = self.cross.moved.take() { + (place.known[0], place.known[1], place.pending) = (to.x, to.y, Some(now + MOVE_WAIT)); + } + let moving = moving(ctx); + if let Some(placed) = report.placed.filter(|_| !moving) { + let waiting = place.pending.is_some_and(|until| now < until); + if !differs(placed, place.known) { + place.pending = None; + } else if waiting { + // The window system put the window's frame where its content should be. + let outer = pos2(placed[0], placed[1]) - report.frame; + if !place.corrected && report.frame.length() > 0.5 && outer.distance(pos2(place.known[0], place.known[1])) < 0.5 { + place.corrected = true; + ctx.send_viewport_cmd_to(viewport, ViewportCommand::OuterPosition(pos2(place.known[0], place.known[1]) - report.frame)); + } + } else { + (place.known, place.pending) = (placed, None); + } + } + ctx.data_mut(|d| d.insert_temp(key, place)); if report.closed { // Like the bar's ×: its panels close and reopen from the application's menu. self.output.actions.push(Action::CloseWindow { panel: first }); - return Some(spawn_key); + return Some(key); } - let now = report.placed.unwrap_or(rect); - if let Some(placed) = report.placed.filter(|placed| placed.iter().zip(rect).any(|(a, b)| (a - b).abs() > 0.5)) { - self.output.actions.push(Action::SetNative { panel: first.clone(), rect: Some(placed) }); + // The layout follows the window's place, once a bar drag is over. + let known = place.known; + if !moving && window.native.is_none_or(|native| differs(native, known)) { + self.output.actions.push(Action::SetNative { panel: first.clone(), rect: Some(known) }); // The in-window rect follows, for builds that draw the window in-window. if window.panels().all(|p| self.permissions(p).movable) { let origin = ctx.input(|i| i.viewport().inner_rect.map(|r| r.min)).filter(|p| p.is_finite()).unwrap_or(Pos2::ZERO); - let [x, y, w, h] = placed; + let [x, y, w, h] = known; self.output.actions.push(Action::MoveFloating { panel: first.clone(), rect: [x - origin.x, y - origin.y, w, h] }); } } - let monitor = monitor_of(&self.monitors, now).cloned(); + let monitor = monitor_of(&self.monitors, known).cloned(); let size = match monitor { Some(_) => None, None => report.monitor_size.filter(|s| s.is_finite() && s.x > 0.0 && s.y > 0.0).map(|s| [s.x, s.y]), @@ -277,7 +366,7 @@ impl Renderer<'_, P> { if (monitor.is_some() || size.is_some()) && (monitor != window.monitor || size != window.monitor_size) { self.output.actions.push(Action::SetMonitor { panel: first, monitor, monitor_size: size }); } - Some(spawn_key) + Some(key) } /// The bar at the top of a native window: its title and ×. Dragging it moves the window. @@ -300,13 +389,13 @@ impl Renderer<'_, P> { self.output.actions.push(Action::CloseWindow { panel: first }); } if movable && response.bar.drag_started() { - self.start_window_drag(ui, window.id); + self.start_window_drag(ui, window.id, false); } } /// After a native window's tree drew: a drag of its only tab, or of its only group, moves /// the window as its bar does. Then the window follows a bar drag, and writes the pointer - /// of a drag it owns. Returns the window's desktop origin, where known. + /// of a drag it owns. Returns the desktop position of the window's content, where known. fn window_gestures(&mut self, ui: &Ui, window: &Floating

) -> Option { let ctx = ui.ctx(); let (drag_key, group_key) = (self.id.with("drag"), self.id.with("group-drag")); @@ -319,60 +408,79 @@ impl Renderer<'_, P> { d.remove::>(drag_key); d.remove::>(group_key); }); - self.start_window_drag(ui, window.id); + self.start_window_drag(ui, window.id, true); } - let outer = ui.input(|i| i.viewport().outer_rect).map(|r| r.min).filter(|p| p.is_finite()); - if let Some(origin) = self.follow_window_drag(ui, window.id) { - return Some(origin); + let origin = ui.input(|i| content_of(i.viewport())).map(|(origin, _)| origin); + if self.follow_window_drag(ui, window.id) { + return origin; } let owns = ctx.data(|d| d.get_temp::>(drag_key).map(|d| d.panel).or(d.get_temp::>(group_key).map(|d| d.panel))); - if owns.is_some_and(|panel| window.contains(&panel)) { - self.record_pointer(ctx, Some(window.id), outer, None); + if owns.is_some_and(|panel| window.contains(&panel)) && self.record_pointer(ctx, Some(window.id), origin, None).escape { + // The drag's cancel, not a shortcut. + ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape)); } - outer + origin } /// Start moving the window with the pointer, from the position the window system reports, - /// or, where it reports none (Wayland), by handing the move to it. - fn start_window_drag(&mut self, ui: &Ui, window: u64) { + /// or, where it reports none (Wayland), by handing the move to it. `docks`: the drag began + /// on the window's only tab or group. + fn start_window_drag(&mut self, ui: &Ui, window: u64, docks: bool) { let press = ui.input(|i| i.pointer.press_origin().or(i.pointer.latest_pos())); - match (ui.input(|i| i.viewport().outer_rect), press) { - (Some(outer), Some(press)) if outer.min.is_finite() => { - let drag = WindowDrag { window, grab: press.to_vec2(), origin: outer.min, start: outer.min }; + match (ui.input(|i| content_of(i.viewport())), press) { + (Some((origin, _)), Some(press)) => { + let drag = WindowDrag { window, grab: press.to_vec2(), to: origin, start: origin, docks }; ui.ctx().data_mut(|d| d.insert_temp(self.id.with("window-drag"), drag)); } _ => ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag), } } - /// Move the window by `OuterPosition` from the position last commanded, while the button - /// is held. Escape puts it back; losing focus leaves it. Returns the desktop origin of - /// this frame's input while the window is moving. - fn follow_window_drag(&mut self, ui: &Ui, window: u64) -> Option { + /// Move the window by `OuterPosition` while the button is held, so the pressed point stays + /// under the pointer. Escape puts it back; losing focus leaves it. Returns whether the + /// window is moving. + fn follow_window_drag(&mut self, ui: &Ui, window: u64) -> bool { let key = self.id.with("window-drag"); - let mut drag = ui.ctx().data(|d| d.get_temp::(key)).filter(|d| d.window == window)?; - let origin = drag.origin; - let pointer = self.record_pointer(ui.ctx(), Some(window), Some(origin), Some(window)); + let Some(mut drag) = ui.ctx().data(|d| d.get_temp::(key)).filter(|d| d.window == window) else { return false }; + let content = ui.input(|i| content_of(i.viewport())); + let pointer = self.record_pointer(ui.ctx(), Some(window), content.map(|(origin, _)| origin), Some(&drag)); + let frame = content.map_or(Vec2::ZERO, |(_, frame)| frame); + let move_to = |r: &mut Self, to: Pos2| { + ui.ctx().send_viewport_cmd(ViewportCommand::OuterPosition(to - frame)); + r.cross.moved = Some(to); + }; if pointer.escape || !self.cross.focused { if pointer.escape { - ui.ctx().send_viewport_cmd(ViewportCommand::OuterPosition(drag.start)); + move_to(self, drag.start); + ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape)); } ui.ctx().data_mut(|d| d.remove::(key)); - return Some(origin); + return true; } if !pointer.down { ui.ctx().data_mut(|d| d.remove::(key)); + // Left with too little of its bar on a monitor to grab it: back onto the nearest. + if let Some(size) = ui.input(|i| i.viewport().inner_rect.map(|r| r.size())) { + let [x, y, ..] = kept_reachable([drag.to.x, drag.to.y, size.x, size.y], &self.monitors); + if pos2(x, y).distance(drag.to) > 0.5 { + move_to(self, pos2(x, y)); + } + } self.end_window_drag(drag, pointer); - return Some(origin); + return true; } - // The pointer on the desktop is the commanded origin plus the local pointer. - let step = ui.input(|i| i.pointer.latest_pos()).filter(|p| p.is_finite()).map_or(Vec2::ZERO, |p| p.to_vec2() - drag.grab); - if step != Vec2::ZERO { - drag.origin += step; - ui.ctx().send_viewport_cmd(ViewportCommand::OuterPosition(drag.origin)); - ui.ctx().data_mut(|d| d.insert_temp(key, drag)); + // This frame's pointer is relative to where the window was when the window system sent + // it, which is where it reports the window now: a move commanded earlier may not have + // landed yet. + if let (Some((origin, _)), Some(local)) = (content, ui.input(|i| i.pointer.latest_pos()).filter(|p| p.is_finite())) { + let to = origin + (local.to_vec2() - drag.grab); + if to.distance(drag.to) > 0.0 { + drag.to = to; + move_to(self, to); + ui.ctx().data_mut(|d| d.insert_temp(key, drag)); + } } - Some(origin) + true } /// Release of a bar drag. The window already sits where it was dragged; over a target in @@ -382,9 +490,9 @@ impl Renderer<'_, P> { } /// Keys, text and clipboard events the window's widgets left over, for the application's - /// shortcuts. Nothing while a text field of the window has focus. + /// shortcuts. Nothing while a widget of the window has keyboard focus: its keys are its own. fn forward_events(&mut self, ui: &Ui) { - if ui.ctx().text_edit_focused() { + if ui.memory(|m| m.focused().is_some()) { return; } let events = ui.input(|i| { diff --git a/crates/ui/src/docking/render/preview.rs b/crates/ui/src/docking/render/preview.rs index 33b04a209..9562a4ae3 100644 --- a/crates/ui/src/docking/render/preview.rs +++ b/crates/ui/src/docking/render/preview.rs @@ -250,7 +250,7 @@ impl Renderer<'_, P> { x += advance; } } - if float_only || (drag.gesture == GroupGesture::Window && !self.style.title_bar_docks) { + if float_only || (drag.gesture == GroupGesture::Window && !self.title_bar_docks()) { return; } if let Some((action, line)) = self.dock_group_proposal(drag, pointer) { diff --git a/crates/ui/src/docking/testing.rs b/crates/ui/src/docking/testing.rs index a6c025abe..5ec3239eb 100644 --- a/crates/ui/src/docking/testing.rs +++ b/crates/ui/src/docking/testing.rs @@ -116,6 +116,7 @@ impl> Do if let Some(host) = &self.host { let mut host = host.borrow_mut(); (host.time, host.screen) = (self.time, self.screen); + host.frames += 1; input.viewports = host.infos(); input.focused = host.focus == Some(egui::ViewportId::ROOT); } @@ -142,7 +143,9 @@ impl> Do full.textures_delta.clear(); #[cfg(feature = "native-windows")] if let Some(host) = &self.host { - host.borrow_mut().apply(&full); + let mut host = host.borrow_mut(); + host.apply(&full); + host.land(); } self.shapes = full.shapes; output @@ -347,9 +350,19 @@ pub struct NativeHost { pub focus: Option, /// Windows report no position, as on Wayland. pub no_position: bool, + /// From a native window's outer position to its content, as a title bar puts it. The + /// window opens with its outer position where it was asked to, and `OuterPosition` moves + /// that. + pub frame: egui::Vec2, + /// `OuterPosition` lands a frame late, at the end of the frame after the command. That + /// frame still reports the old place, and gets pointer events relative to it. + pub lag: bool, pub windows: std::collections::HashMap, /// Every command sent to a native window, in order. pub commands: Vec<(egui::ViewportId, egui::ViewportCommand)>, + /// Moves waiting to land under `lag`: window, content position, frame sent. + late: Vec<(egui::ViewportId, Pos2, usize)>, + frames: usize, screen: Rect, time: f64, } @@ -358,7 +371,8 @@ pub struct NativeHost { #[cfg(feature = "native-windows")] #[derive(Debug)] pub struct HostWindow { - /// Desktop rect of its content: where it opened, then moved by `OuterPosition`. + /// Desktop rect of its content: where it opened, then moved by `OuterPosition` and + /// `InnerSize`. pub rect: Rect, pub visible: bool, /// Frames it drew. @@ -379,8 +393,12 @@ impl NativeHost { monitor_size: None, focus: Some(egui::ViewportId::ROOT), no_position: false, + frame: egui::Vec2::ZERO, + lag: false, windows: Default::default(), commands: Vec::new(), + late: Vec::new(), + frames: 0, screen: Rect::NOTHING, time: 0.0, } @@ -390,7 +408,7 @@ impl NativeHost { fn infos(&self) -> egui::ViewportIdMap { let info = |id: egui::ViewportId, rect: Rect| egui::ViewportInfo { inner_rect: Some(rect), - outer_rect: (!self.no_position).then_some(rect), + outer_rect: (!self.no_position).then(|| if id == egui::ViewportId::ROOT { rect } else { Rect::from_min_max(rect.min - self.frame, rect.max) }), focused: Some(self.focus == Some(id)), monitor_size: self.monitor_size, ..Default::default() @@ -407,7 +425,7 @@ impl NativeHost { let input = { let mut host = host.borrow_mut(); let builder = &viewport.builder; - let rect = Rect::from_min_size(builder.position.unwrap_or_default(), builder.inner_size.unwrap_or(vec2(300.0, 200.0))); + let rect = Rect::from_min_size(builder.position.unwrap_or_default() + host.frame, builder.inner_size.unwrap_or(vec2(300.0, 200.0))); let visible = builder.visible.unwrap_or(true); host.windows.entry(id).or_insert_with(|| HostWindow { rect, visible, frames: 0, events: Vec::new(), close: false, shapes: Vec::new() }); let mut viewports = host.infos(); @@ -438,6 +456,18 @@ impl NativeHost { } } + /// End a frame: moves sent in an earlier one land. + fn land(&mut self) { + let frames = self.frames; + let (landed, late) = std::mem::take(&mut self.late).into_iter().partition::, _>(|(.., sent)| *sent < frames); + self.late = late; + for (id, pos, _) in landed { + if let Some(window) = self.windows.get_mut(&id) { + window.rect = Rect::from_min_size(pos, window.rect.size()); + } + } + } + /// Carry out the commands a frame sent to native windows. fn apply(&mut self, full: &egui::FullOutput) { for (id, output) in &full.viewport_output { @@ -448,7 +478,9 @@ impl NativeHost { self.commands.push((*id, command.clone())); let Some(window) = self.windows.get_mut(id) else { continue }; match command { - egui::ViewportCommand::OuterPosition(pos) => window.rect = Rect::from_min_size(*pos, window.rect.size()), + egui::ViewportCommand::OuterPosition(pos) if self.lag => self.late.push((*id, *pos + self.frame, self.frames)), + egui::ViewportCommand::OuterPosition(pos) => window.rect = Rect::from_min_size(*pos + self.frame, window.rect.size()), + egui::ViewportCommand::InnerSize(size) => window.rect = Rect::from_min_size(window.rect.min, *size), egui::ViewportCommand::Visible(visible) => window.visible = *visible, _ => {} }