mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 15:02:12 +00:00
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.
This commit is contained in:
1 parent
0ac8f80f64
commit
98e05997a0
8 files changed
+558
-101
No files matched your search
@@ -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![]);
|
||||
|
||||
@@ -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<u32>,
|
||||
/// Panels that stay in-window.
|
||||
in_window: 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 panel with a button that takes focus.
|
||||
focused_button: Option<u32>,
|
||||
}
|
||||
|
||||
impl DockContent<u32> 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<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");
|
||||
}
|
||||
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::<u32>(&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::<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);
|
||||
}
|
||||
@@ -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<u64>,
|
||||
/// 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<P> {
|
||||
pub bar: bool,
|
||||
/// A target was offered while `quiet`.
|
||||
pub offered: Cell<bool>,
|
||||
/// Where the native window drawing now was told to move its content this frame.
|
||||
pub moved: Option<Pos2>,
|
||||
/// Resolving a window move that docks whatever `title_bar_docks` says.
|
||||
pub docks: bool,
|
||||
}
|
||||
|
||||
impl<P> Default for Cross<P> {
|
||||
@@ -108,14 +115,16 @@ impl<P> Default for Cross<P> {
|
||||
quiet: false,
|
||||
bar: false,
|
||||
offered: Cell::new(false),
|
||||
moved: None,
|
||||
docks: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<u64>, origin: Option<Pos2>, window: Option<u64>) -> DragPointer {
|
||||
/// origin for this frame's input; `window` the bar drag it is.
|
||||
pub(super) fn record_pointer(&mut self, ctx: &Context, owner: Option<u64>, origin: Option<Pos2>, window: Option<&WindowDrag>) -> DragPointer {
|
||||
let key = self.id.with(POINTER);
|
||||
let kept = ctx.data(|d| d.get_temp::<DragPointer>(key)).filter(|p| p.owner == owner).and_then(|p| p.local);
|
||||
let pointer = ctx.input(|i| DragPointer {
|
||||
@@ -126,7 +135,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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;
|
||||
}
|
||||
|
||||
@@ -220,11 +220,21 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P>, 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;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<f64>,
|
||||
/// 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<Rect>, inner: Option<Rect>) -> 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<P> {
|
||||
closed: bool,
|
||||
placed: Option<[f32; 4]>,
|
||||
/// From the window's outer position to its content.
|
||||
frame: Vec2,
|
||||
monitor_size: Option<Vec2>,
|
||||
/// Its drop targets, for drags from other windows.
|
||||
surface: Option<Surface<P>>,
|
||||
@@ -70,7 +106,7 @@ struct Report<P> {
|
||||
|
||||
impl<P> Default for Report<P> {
|
||||
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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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::<WindowDrag>(key)).is_some_and(|d| !natives.iter().any(|(w, ..)| w.id == d.window)) {
|
||||
ctx.data_mut(|d| d.remove::<WindowDrag>(key));
|
||||
}
|
||||
self.map_desktop(ctx, &natives, visibility.0);
|
||||
// The main window owns drags of the panels it draws.
|
||||
let id = self.id;
|
||||
@@ -124,7 +163,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let previous = ctx.data_mut(|d| d.remove_temp::<Vec<Id>>(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::<Place>(*id);
|
||||
}
|
||||
d.insert_temp(key, shown);
|
||||
});
|
||||
@@ -137,7 +176,6 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let key = self.id.with("native-order");
|
||||
let mut order = ctx.data(|d| d.get_temp::<Vec<u64>>(key)).unwrap_or_default();
|
||||
order.retain(|id| natives.iter().any(|(w, ..)| w.id == *id));
|
||||
let moving = ctx.data(|d| d.get_temp::<WindowDrag>(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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P>, rect: [f32; 4], viewport: ViewportId, visibility: (bool, bool)) -> Option<Id> {
|
||||
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::<WindowDrag>(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::<Place>(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::<std::collections::HashSet<u64>>(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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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<P>) -> Option<Pos2> {
|
||||
let ctx = ui.ctx();
|
||||
let (drag_key, group_key) = (self.id.with("drag"), self.id.with("group-drag"));
|
||||
@@ -319,60 +408,79 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
d.remove::<Drag<P>>(drag_key);
|
||||
d.remove::<GroupDrag<P>>(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<P>>(drag_key).map(|d| d.panel).or(d.get_temp::<GroupDrag<P>>(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<Pos2> {
|
||||
/// 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::<WindowDrag>(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::<WindowDrag>(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::<WindowDrag>(key));
|
||||
return Some(origin);
|
||||
return true;
|
||||
}
|
||||
if !pointer.down {
|
||||
ui.ctx().data_mut(|d| d.remove::<WindowDrag>(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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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| {
|
||||
|
||||
@@ -250,7 +250,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> 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) {
|
||||
|
||||
@@ -116,6 +116,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> 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<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> 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<egui::ViewportId>,
|
||||
/// 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<egui::ViewportId, HostWindow>,
|
||||
/// 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<egui::ViewportInfo> {
|
||||
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::<Vec<_>, _>(|(.., 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,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user