Reconcile native docking fixes with egui 0.37 input routing

This commit is contained in:
skylar taylor-barrick committed 2026-10-25 17:41:23 +01:00
commit 42e30cd5b3
18 files changed
+1114 -282

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ serde = { version = "1", features = ["derive"], optional = true }
# Fading a native window while it is dragged (`NSWindow.alphaValue`); winit already uses these.
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "objc2-core-foundation", "NSApplication", "NSResponder", "NSWindow"] }
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "objc2-core-foundation", "NSApplication", "NSEvent", "NSResponder", "NSScreen", "NSWindow"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSArray", "NSEnumerator", "NSString"] }
[features]
+9 -1
View File
@@ -117,6 +117,7 @@ pub struct FloatingFrame<'a> {
limits: PanelLimits,
resizable: bool,
resize_label: &'a str,
opacity: f32,
}
/// What a [`FloatingFrame`] did this frame.
@@ -136,7 +137,7 @@ pub struct FrameResponse<R> {
impl<'a> FloatingFrame<'a> {
pub fn new(id: Id, rect: Rect) -> Self {
Self { id, rect, bounds: None, limits: PanelLimits::default(), resizable: true, resize_label: "Resize window" }
Self { id, rect, bounds: None, limits: PanelLimits::default(), resizable: true, resize_label: "Resize window", opacity: 1.0 }
}
/// The frame stays inside `bounds`; the default is the viewport's content rect.
@@ -163,6 +164,12 @@ impl<'a> FloatingFrame<'a> {
self
}
/// Opacity of everything drawn in the frame, contents included (default 1.0).
pub fn opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity;
self
}
pub fn layer(id: Id) -> LayerId {
LayerId::new(Order::Middle, id)
}
@@ -180,6 +187,7 @@ impl<'a> FloatingFrame<'a> {
.movable(false)
.constrain_to(bounds);
let shown = area.show(ctx, |ui| {
ui.multiply_opacity(self.opacity);
// Drawn at `rect` itself, also in egui's first (sizing) pass of a new area.
ui.set_min_size(rect.size());
ui.allocate_rect(rect, Sense::hover());
+98 -1
View File
@@ -243,6 +243,10 @@ fn hline(y: f32, x: std::ops::RangeInclusive<f32>) -> Rect {
Rect::from_x_y_ranges(x, (y - 2.0)..=(y + 2.0))
}
fn vline(x: f32, y: std::ops::RangeInclusive<f32>) -> Rect {
Rect::from_x_y_ranges((x - 2.0)..=(x + 2.0), y)
}
/// A floating group's drag bar.
fn bar(d: &Driver, panel: u32) -> Pos2 {
d.rect("drag-bar", panel).center()
@@ -467,12 +471,14 @@ fn group_dropped_on_the_window_edge_without_a_dock_makes_a_new_dock() {
let layout = with_floating(docked(side, &[&[&[2]]]), &[(5, [300.0, 300.0, 150.0, 150.0])]);
let mut d = Driver::new(layout);
d.hold(bar(&d, 5), at);
let x = if new == Side::End { 598.0 } else { 2.0 };
assert!(d.painted(vline(x, 0.0..=600.0)), "{new:?}: a vertical line at the window edge");
let held = d.shapes.clone();
let out = d.step(vec![button(at, false)]);
d.settle();
let rect = d.column_of(5);
let shapes = std::mem::replace(&mut d.shapes, held);
assert!(d.previewed(rect), "{new:?}: the new dock's column at the window edge, {rect:?}");
assert!(!d.previewed(rect), "{new:?}: no rect of the new dock's column");
d.shapes = shapes;
assert_eq!(moved(&out.actions), Some((5, &Target::NewDock { side: new })));
assert_eq!(columns(&d.layout, new), vec![vec![vec![5]]]);
@@ -962,3 +968,94 @@ fn defaults_give_way_to_the_filler_but_user_heights_stay() {
let d = sized_column(900.0, [Some(200.0), Some(340.0), None]);
assert_eq!(drawn_heights(&d), [200.0, 340.0, 900.0 - 14.0 - 540.0]);
}
/// Dock columns left and right of protected canvas 0, as in a creative app: group [1, 7] above
/// [2] at the left, [3] above [4] at the right, each column 200 wide, and a floating window
/// holding group [5, 6].
fn creative() -> testing::DockDriver<u32, TestContent> {
let dock = |side, groups: &[&[u32]]| Node::Dock { side, columns: vec![Column::new(groups.iter().map(|g| group(g)).collect(), 200.0)] };
let canvas = Node::Tabs { panels: vec![0], active: 0 };
let inner = Node::Split {
axis: SplitAxis::Horizontal,
size: SplitSize::FixedSecond(200.0),
first: Box::new(canvas),
second: Box::new(dock(Side::End, &[&[3], &[4]])),
};
let root = Node::Split {
axis: SplitAxis::Horizontal,
size: SplitSize::FixedFirst(200.0),
first: Box::new(dock(Side::Start, &[&[1, 7], &[2]])),
second: Box::new(inner),
};
let layout = Layout { root: Some(root), floating: vec![Floating::single(vec![5, 6], 0, [300.0, 200.0, 200.0, 160.0])] };
testing::DockDriver::new(layout, TestContent::default()).with_style(|s| {
s.side_edge_zones = false;
s.new_dock_zones = true;
s.float_on_empty_drop = true;
s.title_bar_docks = true;
})
}
/// The rect of the result preview the last frame painted: a 2 pt accent outline with a light
/// fill.
fn result_preview<C: DockContent<u32>>(d: &testing::DockDriver<u32, C>) -> Option<Rect> {
let fill = DockStyle::default().accent.gamma_multiply(0.08);
d.shapes()
.iter()
.find_map(|clipped| match &clipped.shape {
egui::Shape::Rect(shape) if shape.fill == fill && shape.stroke.width == 0.0 => Some(shape.rect),
_ => None,
})
.filter(|rect| d.outlined(*rect))
}
#[test]
fn a_group_dropped_on_the_window_edge_makes_a_column_there_and_previews_the_edge_line() {
// Group [1, 7] heads the left column, so the column's window edge is named by panel 1: the
// drop names the column by panel 2 instead and moves the whole group out.
let mut d = creative();
let at = pos2(3.0, 300.0);
d.hold(d.hit(&Hit::DragBar(1)).unwrap().center(), at);
assert!(d.filled(Rect::from_x_y_ranges(0.0..=4.0, 0.0..=600.0)), "the line along the window edge");
assert_eq!(result_preview(&d), None);
let out = d.release(at);
assert_eq!(out.actions, vec![Action::MoveGroup { panel: 1, to: Target::At { anchor: 2, placement: Placement::Column { after: false } } }]);
assert_eq!(columns(&d.layout, Side::Start), vec![vec![vec![1, 7]], vec![vec![2]]]);
// A floating window's group at the right window edge.
let mut d = creative();
let at = pos2(797.0, 300.0);
d.hold(d.hit(&Hit::DragBar(5)).unwrap().center(), at);
assert!(d.filled(Rect::from_x_y_ranges(796.0..=800.0, 0.0..=600.0)), "the line along the window edge");
assert_eq!(result_preview(&d), None);
let out = d.release(at);
assert_eq!(out.actions, vec![Action::MoveGroup { panel: 5, to: Target::At { anchor: 3, placement: Placement::Column { after: false } } }]);
assert_eq!(columns(&d.layout, Side::End), vec![vec![vec![5, 6]], vec![vec![3], vec![4]]]);
// The bottom edge band between the docks: the line along the canvas's bottom.
let mut d = creative();
let at = pos2(400.0, 597.0);
d.hold(d.hit(&Hit::DragBar(5)).unwrap().center(), at);
assert!(d.filled(Rect::from_x_y_ranges(204.0..=596.0, 596.0..=600.0)), "the line along the canvas's bottom edge");
assert_eq!(result_preview(&d), None);
let out = d.release(at);
assert!(
matches!(&out.actions[..], [Action::MoveGroup { panel: 5, to: Target::At { placement: Placement::Edge(Zone::Bottom), .. } }]),
"{:?}",
out.actions
);
}
#[test]
fn a_tab_dropped_beside_a_dock_group_previews_the_rect_it_takes() {
// The bottom third of group [2], the left column's last group: a new group below it.
let mut d = creative();
let at = pos2(100.0, 560.0);
d.hold(d.hit(&Hit::Tab(6)).unwrap().center(), at);
let preview = result_preview(&d);
let out = d.release(at);
assert_eq!(out.actions, vec![Action::Move { panel: 6, anchor: 2, placement: Placement::Group { below: true, height: None } }]);
d.settle();
let rect = Rect::from_x_y_ranges(0.0..=200.0, d.hit(&Hit::Tab(6)).unwrap().top()..=600.0);
assert_eq!(preview, Some(rect.shrink(1.0)), "group 6's rect below group 2");
}
+47 -36
View File
@@ -122,16 +122,17 @@ fn a_drop_on_a_groups_outer_third_docks_beside_it_with_its_rect_and_its_middle_j
}
#[test]
fn a_group_that_takes_no_tabs_maps_its_whole_body_to_its_edges() {
fn without_new_dock_zones_a_group_that_takes_no_tabs_maps_its_whole_body_to_its_edges() {
let canvas = Permissions { accepts_tabs: false, ..Permissions::default() };
let d = driver(&[(1, canvas)]);
let start = || driver(&[(1, canvas)]).with_style(|style| style.new_dock_zones = false);
let d = start();
let (body, rect) = group(&d, 1);
for (zone, at) in [(Zone::Left, body.center() - vec2(10.0, 0.0)), (Zone::Top, body.center() - vec2(0.0, 10.0))] {
let out = previews_result(driver(&[(1, canvas)]), tab(&d, 3), at, |d| group(d, 3).1);
let out = previews_result(start(), tab(&d, 3), at, |d| group(d, 3).1);
assert_eq!(out, vec![Action::Move { panel: 3, anchor: 1, placement: Placement::Split(zone) }], "{zone:?}");
}
// Its strip joins nothing.
let mut d = driver(&[(1, canvas)]);
let mut d = start();
let out = d.drag(tab(&d, 3), pos2(rect.center().x, rect.top() + 13.0));
assert!(!out.actions.iter().any(|a| matches!(a, Action::Move { anchor: 1, placement: Placement::Tab { .. }, .. })), "{:?}", out.actions);
}
@@ -164,17 +165,18 @@ fn a_side_without_room_joins_as_a_tab_or_offers_nothing() {
}
#[test]
fn the_root_edge_band_is_28_pt_and_previews_the_new_frame_along_the_layouts_edge() {
fn the_root_edge_band_is_28_pt_and_previews_a_line_along_the_layouts_edge() {
let d = driver(&[]);
let (body, _) = group(&d, 1);
let x = body.center().x;
let along_the_bottom = |d: &Driver| {
let (_, rect) = group(d, 3);
assert_eq!((rect.left(), rect.right(), rect.bottom()), (0.0, 800.0, 600.0), "along the layout's whole bottom edge");
rect
};
let out = previews_result(driver(&[]), tab(&d, 3), pos2(x, 573.0), along_the_bottom);
let window = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
let out = previews_line(driver(&[]), tab(&d, 3), pos2(x, 573.0), line(window, Zone::Bottom));
assert!(matches!(&out[..], [Action::Move { panel: 3, placement: Placement::Edge(Zone::Bottom), .. }]), "{out:?}");
let mut d = driver(&[]);
d.drag(tab(&d, 3), pos2(x, 573.0));
d.settle();
let (_, rect) = group(&d, 3);
assert_eq!((rect.left(), rect.right(), rect.bottom()), (0.0, 800.0, 600.0), "along the layout's whole bottom edge");
// Past the band, the group's own bottom third.
let mut d = driver(&[]);
let out = d.drag(tab(&d, 3), pos2(x, 571.0));
@@ -217,12 +219,8 @@ fn between_side_frames_the_outer_edge_band_covers_only_the_canvas_column() {
let (side, _) = group(&d, 1);
let at = pos2(canvas.center().x, 590.0);
let mut d = canvas_driver(Permissions::PROTECTED, |_| {});
let under_the_canvas = |d: &Driver| {
let (_, rect) = group(d, 3);
assert_eq!(rect.x_range(), canvas.x_range(), "the canvas column's width");
rect
};
let out = previews_result(canvas_driver(Permissions::PROTECTED, |_| {}), tab(&d, 3), at, under_the_canvas);
// The line along the canvas column's bottom.
let out = previews_line(canvas_driver(Permissions::PROTECTED, |_| {}), tab(&d, 3), at, line(canvas, Zone::Bottom));
assert_eq!(out, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Bottom) }]);
d.drag(tab(&d, 3), at);
// The side frames keep their full height.
@@ -248,12 +246,8 @@ fn between_docks_the_outer_edge_band_covers_the_part_between_them() {
let (timeline, _) = group(&d, 4);
let area = Rect::from_x_y_ranges(canvas.x_range(), (canvas.top() - 26.0)..=timeline.bottom());
let at = pos2(canvas.center().x, 590.0);
let between_the_docks = |d: &Driver| {
let (_, rect) = group(d, 3);
assert_eq!((rect.x_range(), rect.bottom()), (area.x_range(), area.bottom()), "along the bottom between the docks");
rect
};
let out = previews_result(driver(), tab(&d, 3), at, between_the_docks);
// The line along the bottom between the docks.
let out = previews_line(driver(), tab(&d, 3), at, line(area, Zone::Bottom));
assert!(matches!(&out[..], [Action::Move { panel: 3, placement: Placement::Edge(Zone::Bottom), .. }]), "{out:?}");
let mut d = driver();
d.drag(tab(&d, 3), at);
@@ -280,15 +274,12 @@ fn over_a_full_width_row_the_top_edge_band_covers_only_the_canvas_column_above_i
let d = driver();
let (canvas, _) = group(&d, 0);
let at = pos2(canvas.center().x, 5.0);
// The line along the canvas column's top, not the window's.
let out = previews_line(driver(), tab(&d, 3), at, line(canvas, Zone::Top));
assert_eq!(out, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Top) }]);
let mut d = driver();
d.hold(tab(&d, 3), at);
let preview = result_preview(&d).unwrap_or_else(|| panic!("no result preview"));
assert_eq!(preview.x_range(), canvas.shrink(1.0).x_range(), "the canvas column's width, not the window's");
let out = d.release(at);
d.settle();
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Top) }]);
d.drag(tab(&d, 3), at);
assert_eq!(wrapped(&d.layout, 3), Some(tabs(&[0])), "{:?}", d.layout);
assert_eq!(preview, group(&d, 3).1.shrink(1.0));
}
#[test]
@@ -304,8 +295,8 @@ fn an_outer_edge_drop_wraps_the_area_it_previews_when_the_app_applies_other_perm
let at = pos2(canvas.center().x, 5.0);
let mut d = driver();
d.hold(tab(&d, 3), at);
let preview = result_preview(&d).unwrap_or_else(|| panic!("no result preview"));
assert_eq!(preview.x_range(), Rect::from_x_y_ranges(0.0..=800.0, 0.0..=1.0).shrink2(egui::vec2(1.0, 0.0)).x_range(), "the window's width");
assert!(d.filled(line(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0)), Zone::Top)), "the line along the window's top");
assert_eq!(result_preview(&d), None);
let out = d.release(at);
let mut applied = layout.clone();
let apply = |p: &u32| if *p == 0 { Permissions::PROTECTED } else { Permissions::default() };
@@ -345,7 +336,7 @@ fn drop_edges_limit_the_outer_edges_and_the_canvas_sides() {
}
#[test]
fn a_canvas_that_takes_splits_takes_them_only_on_its_drop_edges() {
fn without_new_dock_zones_a_canvas_that_takes_splits_takes_them_only_on_its_drop_edges() {
let beside = Permissions { accepts_splits: true, ..Permissions::PROTECTED };
let d = canvas_driver(beside, |_| {});
let (body, _) = group(&d, 0);
@@ -358,12 +349,20 @@ fn a_canvas_that_takes_splits_takes_them_only_on_its_drop_edges() {
(Sides::NONE, [None, None]),
] {
for (at, zone) in [low, high].into_iter().zip(zones) {
let mut d = canvas_driver(beside, move |style| style.drop_edges = edges);
let split_frames = move |style: &mut DockStyle| {
style.new_dock_zones = false;
style.drop_edges = edges;
};
let mut d = canvas_driver(beside, split_frames);
let out = d.drag(tab(&d, 3), at);
match zone {
Some(zone) => assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Split(zone) }], "{edges:?}"),
None => assert!(!moves(&out), "{edges:?}: {:?}", out.actions),
}
// With new dock zones the canvas body takes no drop on any side.
let mut d = canvas_driver(beside, move |style| style.drop_edges = edges);
let out = d.drag(tab(&d, 3), at);
assert!(!moves(&out), "{edges:?}: {:?}", out.actions);
}
}
}
@@ -519,6 +518,17 @@ fn result_preview(d: &Driver) -> Option<Rect> {
.filter(|rect| d.outlined(*rect))
}
/// Drags `from` to `at` and releases: the preview while held is the drop line `line`, with no
/// result rect. Returns the drop's actions.
fn previews_line(mut d: Driver, from: Pos2, at: Pos2, line: Rect) -> Vec<Action<u32>> {
d.hold(from, at);
assert!(d.filled(line), "the line {line:?}");
assert_eq!(result_preview(&d), None);
let out = d.release(at);
d.settle();
out.actions
}
/// Drags `from` to `at` and back to rest: the preview while held is the rect `rect_of` reads
/// once the drop is applied, inset by its 1 pt margin. Returns the drop's actions.
fn previews_result(mut d: Driver, from: Pos2, at: Pos2, rect_of: impl Fn(&Driver) -> Rect) -> Vec<Action<u32>> {
@@ -570,7 +580,8 @@ fn a_dock_drop_previews_the_rect_the_group_or_column_takes() {
// The column's inner edge: a new column after it, the full height.
let out = previews_result(dock_driver(), tab(&d, 4), pos2(upper.right() + 6.0, 200.0), |d| column_of(d, 4));
assert_eq!(out, vec![Action::Move { panel: 4, anchor: 2, placement: Placement::Column { after: true } }]);
// The window's right edge: a new dock.
let out = previews_result(dock_driver(), tab(&d, 4), pos2(795.0, 200.0), |d| column_of(d, 4));
// The window's right edge: a new dock, previewed by the line along that edge.
let window = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
let out = previews_line(dock_driver(), tab(&d, 4), pos2(795.0, 200.0), line(window, Zone::Right));
assert_eq!(out, vec![Action::MovePanel { panel: 4, to: Target::NewDock { side: Side::End } }]);
}
+81
View File
@@ -51,6 +51,8 @@ struct Content {
/// Canvas 0 accepts side splits, as an app that opens panels beside it does.
beside: bool,
drawn: Vec<u32>,
/// A preview the host draws for every drag, from `resolve_drag`.
host_preview: Option<Rect>,
}
impl DockContent<u32> for Content {
@@ -71,6 +73,9 @@ impl DockContent<u32> for Content {
fn allow_native(&self, _: &u32) -> bool {
self.native
}
fn resolve_drag(&mut self, _: &Context, _: &DockStyle, _: &DropRequest<'_, u32>) -> Option<DropFeedback> {
self.host_preview.map(|rect| DropFeedback { preview: Some(rect), insertion: None })
}
}
#[derive(Clone, Copy, PartialEq)]
@@ -106,6 +111,8 @@ struct Driver {
hide: Hide,
modifiers: Modifiers,
time: f64,
/// What the last frame painted.
shapes: Vec<egui::epaint::ClippedShape>,
}
impl Driver {
@@ -120,6 +127,7 @@ impl Driver {
hide: Hide::None,
modifiers: Modifiers::NONE,
time: 0.0,
shapes: Vec::new(),
};
driver.step(vec![]);
driver.step(vec![]);
@@ -157,6 +165,7 @@ impl Driver {
});
let mut full = full;
full.textures_delta.clear();
self.shapes = full.shapes;
output
}
@@ -199,6 +208,22 @@ impl Driver {
self.step(vec![button(at, false)])
}
/// Opacity of the floating window that the last frame drew at `rect`: the alpha of its
/// opaque background fill.
fn window_opacity(&self, rect: [f32; 4]) -> Option<f32> {
let rect = Rect::from_min_size(pos2(rect[0], rect[1]), vec2(rect[2], rect[3]));
fn find(shape: &egui::Shape, rect: Rect) -> Option<f32> {
match shape {
egui::Shape::Vec(shapes) => shapes.iter().find_map(|s| find(s, rect)),
egui::Shape::Rect(r) if r.rect.min.distance(rect.min) < 0.5 && r.rect.max.distance(rect.max) < 0.5 && r.fill.a() > 0 => {
Some(f32::from(r.fill.a()) / 255.0)
}
_ => None,
}
}
self.shapes.iter().find_map(|s| find(&s.shape, rect))
}
fn wait(&mut self) {
for _ in 0..45 {
self.step(vec![]);
@@ -764,3 +789,59 @@ fn undock_panel_group_floats_one_group_of_a_floating_window() {
menu(&mut h, 5);
assert_eq!(h.query_all_by_label("Undock Panel Group").count(), 0);
}
#[test]
fn a_title_bar_drag_fades_over_a_preview_the_host_draws() {
let mut d = Driver::new(workspace(), false);
d.content.host_preview = Some(Rect::from_min_size(pos2(20.0, 20.0), vec2(100.0, 100.0)));
let title = d.title(8).center();
d.step(vec![Event::PointerMoved(title), button(title, true)]);
d.step(vec![Event::PointerMoved(title + vec2(8.0, 6.0))]);
d.step(vec![Event::PointerMoved(pos2(650.0, 150.0))]);
let rect = window_of(&d.layout, 8).unwrap().rect;
d.step(vec![]);
let shown = d.window_opacity(rect).unwrap();
assert!((shown - 0.75).abs() < 0.01, "{shown}");
}
#[test]
fn an_in_window_floating_window_fades_only_while_its_title_bar_drag_highlights_a_drop_target() {
let mut d = Driver::new(workspace(), false);
let strip = d.tab(4).unwrap().center();
let empty = pos2(650.0, 150.0);
// Opacity of window W2 (dragged) and W1 once the pointer rests at `at`: the window draws
// before the preview, so it fades a frame after the target lights up.
let over = |d: &mut Driver, at: egui::Pos2| {
d.step(vec![Event::PointerMoved(at)]);
let (dragged, other) = (window_of(&d.layout, 8).unwrap().rect, window_of(&d.layout, 5).unwrap().rect);
d.step(vec![]);
(d.window_opacity(dragged).unwrap(), d.window_opacity(other).unwrap())
};
let shown = |d: &Driver| d.window_opacity(window_of(&d.layout, 8).unwrap().rect).unwrap();
let grab = |d: &mut Driver| {
let title = d.title(8).center();
d.step(vec![Event::PointerMoved(title), button(title, true)]);
d.step(vec![Event::PointerMoved(title + vec2(8.0, 6.0))]);
};
assert_eq!(shown(&d), 1.0);
grab(&mut d);
assert_eq!(over(&mut d, empty), (1.0, 1.0), "over empty space");
let (dragged, other) = over(&mut d, strip);
assert!((dragged - 0.75).abs() < 0.01 && other == 1.0, "over a drop target: {dragged}, {other}");
assert_eq!(over(&mut d, empty).0, 1.0, "after leaving the target");
assert!((over(&mut d, strip).0 - 0.75).abs() < 0.01);
// Released off the target the window stays floating, opaque.
d.step(vec![Event::PointerMoved(empty), button(empty, false)]);
d.step(vec![]);
assert_eq!(shown(&d), 1.0, "after the release");
assert!(window_of(&d.layout, 8).is_some());
// Escape over a target puts the window back, opaque.
grab(&mut d);
assert!((over(&mut d, strip).0 - 0.75).abs() < 0.01);
d.step(vec![Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }]);
d.step(vec![]);
assert_eq!(shown(&d), 1.0, "after Escape");
d.step(vec![button(strip, false)]);
assert!(window_of(&d.layout, 8).is_some());
}
+1 -1
View File
@@ -32,7 +32,7 @@ pub use model::{Action, DockError, Floating, Layout, Location, MAX_DEPTH, MAX_PA
pub use render::{close_flyout, flyout, open_flyout};
pub use surface::{
DockArea, DockContent, DockEvent, DockHeights, DockOutput, DockStyle, DropFeedback, DropRequest, FloatSpace, HidePanels, PanelLimits,
WindowFiles, WindowKind, begin_drag, cancel_drag, float_space, is_dragging, route_app_drag,
WindowFiles, WindowKind, begin_drag, cancel_drag, float_space, is_dragging, route_app_drag, send_keys, typing_window,
};
pub use targets::{
DropProposal, DropTarget, SIDE_EDGE_ZONE_DEPTH, Sides, ZoneRule, body_zone, drop_zone, resolve_drop, resolve_drop_by, tab_insertion_marker,
+304 -51
View File
@@ -1,10 +1,10 @@
//! Floating windows in their own OS windows (feature `native-windows`), drawn by the driver's
//! stand-in for OS windows.
use egui::{Color32, Event, Key, Modifiers, PointerButton, Pos2, Rect, ViewportCommand, epaint::ClippedShape, pos2, vec2};
use egui::{Color32, Event, Key, Modifiers, PointerButton, Pos2, Rect, Vec2, ViewportCommand, epaint::ClippedShape, pos2, vec2};
use super::render::{
APPKIT_CORNER, DragPointer, MACOS_CLOSE, WindowDrag, appkit_resize_areas, close_rects, drag_opacity, floats_shown, resize_bands, wayland,
APPKIT_CORNER, DragPointer, INACTIVE_PALETTE, MACOS_CLOSE, WindowDrag, appkit_resize_areas, close_rects, drag_opacity, resize_bands, wayland,
};
use super::testing::DockDriver;
use super::{Action, DockContent, Floating, Hit, Layout, Monitor, Node, Permissions, Placement, Target, WindowKind, Workspace};
@@ -17,6 +17,8 @@ const ORIGIN: Pos2 = pos2(100.0, 50.0);
struct Content {
/// Panels that may not close.
fixed: Vec<u32>,
/// The protected canvas that takes splits beside it, as PhotoCraft's does.
canvas: Option<u32>,
/// Panels that stay in-window.
in_window: Vec<u32>,
/// Viewer panels: their windows are normal windows.
@@ -25,6 +27,8 @@ struct Content {
text: Option<u32>,
typed: String,
focus_text: bool,
/// The text field gives up keyboard focus on the next frame.
blur_text: bool,
/// The panel with a button that takes focus.
focused_button: Option<u32>,
/// The panel with a counter button, its clicks, and where it was drawn.
@@ -62,6 +66,10 @@ struct Content {
drops: Vec<(u32, Pos2)>,
/// Drop sites whose release found egui's drag-and-drop payload.
payload_drops: Vec<u32>,
/// Key and clipboard events panel 1 saw in the main window, and the text field's panel in
/// its own window.
main_keys: Vec<Event>,
field_keys: Vec<Event>,
}
/// How a test's app drag source marks its drag.
@@ -90,6 +98,9 @@ impl DockContent<u32> for Content {
if std::mem::take(&mut self.focus_text) {
field.request_focus();
}
if std::mem::take(&mut self.blur_text) {
field.surrender_focus();
}
}
if self.focused_button == Some(*panel) {
ui.button("OK").request_focus();
@@ -135,6 +146,13 @@ impl DockContent<u32> for Content {
}
}
self.saw_files |= ui.input(|i| !i.raw.hovered_files.is_empty() || !i.raw.dropped_files.is_empty());
let main = ui.ctx().viewport_id() == egui::ViewportId::ROOT;
if (main && *panel == 1) || (!main && self.text == Some(*panel)) {
let keys = ui.input(|i| {
i.events.iter().filter(|e| matches!(e, Event::Key { .. } | Event::Copy | Event::Cut | Event::Paste(_))).cloned().collect::<Vec<_>>()
});
(if main { &mut self.main_keys } else { &mut self.field_keys }).extend(keys);
}
if ui.rect_contains_pointer(ui.max_rect()) {
let pressed = [PointerButton::Primary, PointerButton::Secondary].into_iter().filter(|b| ui.input(|i| i.pointer.button_pressed(*b)));
self.pressed.extend(pressed.map(|b| (*panel, b)));
@@ -146,6 +164,9 @@ impl DockContent<u32> for Content {
}
fn permissions(&self, panel: &u32) -> Permissions {
if self.canvas == Some(*panel) {
return Permissions { accepts_splits: true, ..Permissions::PROTECTED };
}
Permissions { closable: !self.fixed.contains(panel), ..Permissions::default() }
}
@@ -353,6 +374,62 @@ fn a_restored_window_opens_on_a_connected_monitor() {
assert_eq!(window(&d, 5).monitor.as_ref().and_then(|m| m.name.as_deref()), Some("left"));
}
/// The last `InnerSize`, `MinInnerSize` and `MaxInnerSize` sent to `viewport`.
fn size_commands(d: &DockDriver<u32, Content>, viewport: egui::ViewportId) -> [Option<egui::Vec2>; 3] {
let mut sizes = [None; 3];
for (_, c) in d.host().commands.iter().filter(|(v, _)| *v == viewport) {
match c {
ViewportCommand::InnerSize(s) => sizes[0] = Some(*s),
ViewportCommand::MinInnerSize(s) => sizes[1] = Some(*s),
ViewportCommand::MaxInnerSize(s) => sizes[2] = Some(*s),
_ => {}
}
}
sizes
}
#[test]
fn a_minimized_native_window_shrinks_to_its_bar_and_keeps_its_expanded_size() {
let mut d = driver(layout(true));
let viewport = d.viewport(window(&d, 5).id);
let open = os_window(&d, 5).unwrap().0;
let (native, rect) = (window(&d, 5).native, window(&d, 5).rect);
assert_eq!(native, Some([open.left(), open.top(), 240.0, 300.0]));
// Its bar and its group's tab strip, at the same width and top-left corner; only the width
// resizes.
let collapsed = BAR + super::DockStyle::default().tab_height;
d.layout.apply(Action::SetMinimized { panel: 5, minimized: true }).unwrap();
d.settle();
assert_eq!(os_window(&d, 5).unwrap().0, Rect::from_min_size(open.min, vec2(240.0, collapsed)));
let [_, min, max] = size_commands(&d, viewport);
assert_eq!((min.map(|s| s.y), max.map(|s| s.y)), (Some(collapsed), Some(collapsed)));
// The layout keeps the expanded size after the window reports the collapsed one, past the
// wait for commanded moves, and takes a new width.
for _ in 0..40 {
d.step(vec![]);
}
assert_eq!((window(&d, 5).native, window(&d, 5).rect), (native, rect));
d.host().windows.get_mut(&viewport).unwrap().rect = Rect::from_min_size(open.min, vec2(300.0, collapsed));
for _ in 0..40 {
d.step(vec![]);
}
assert_eq!(window(&d, 5).native, Some([open.left(), open.top(), 300.0, 300.0]));
assert_eq!(window(&d, 5).rect[2..], [300.0, 300.0]);
// Expanded: the saved height comes back, and the height resizes again.
d.layout.apply(Action::SetMinimized { panel: 5, minimized: false }).unwrap();
d.settle();
assert_eq!(os_window(&d, 5).unwrap().0, Rect::from_min_size(open.min, vec2(300.0, 300.0)));
assert_eq!(size_commands(&d, viewport), [Some(vec2(300.0, 300.0)), Some(vec2(160.0, 100.0)), Some(egui::Vec2::INFINITY)]);
// The limits change before the size: the OS clamps a new size to the limits it has.
let commands: Vec<_> = d.host().commands.iter().filter(|(v, _)| *v == viewport).map(|(_, c)| c.clone()).collect();
let last = |f: fn(&ViewportCommand) -> bool| commands.iter().rposition(f);
assert!(last(|c| matches!(c, ViewportCommand::MaxInnerSize(_))) < last(|c| matches!(c, ViewportCommand::InnerSize(_))), "{commands:?}");
assert_eq!(window(&d, 5).native, Some([open.left(), open.top(), 300.0, 300.0]));
}
#[test]
fn a_locked_area_moves_no_native_window_and_records_where_the_os_put_it() {
let mut d = driver(layout(true));
@@ -421,37 +498,40 @@ fn id_of(d: &DockDriver<u32, Content>) -> u64 {
}
#[test]
fn floating_windows_hide_while_the_app_is_inactive() {
assert!(floats_shown(true, 10.0, 10.0));
assert!(floats_shown(false, 10.2, 10.0));
assert!(!floats_shown(false, 10.3, 10.0));
fn palettes_stay_shown_at_the_normal_level_while_the_app_is_inactive() {
use egui::{ViewportId, WindowLevel};
let mut d = driver(layout(true));
let viewport = d.viewport(window(&d, 5).id);
let hides =
|d: &DockDriver<u32, Content>| d.host().commands.iter().filter(|(v, c)| *v == viewport && *c == ViewportCommand::Visible(false)).count();
// Focus moving to the floating window keeps it.
d.host().focus = None;
d.step(vec![]);
d.host().focus = Some(viewport);
for _ in 0..30 {
d.step(vec![]);
}
assert_eq!(hides(&d), 0);
// Another application: hidden after the delay, shown again on return.
d.host().focus = None;
for _ in 0..10 {
d.step(vec![]);
}
assert_eq!(hides(&d), 0, "not yet");
for _ in 0..10 {
d.step(vec![]);
}
assert_eq!(hides(&d), 1);
assert!(!d.host().windows[&viewport].visible);
d.host().focus = Some(egui::ViewportId::ROOT);
d.step(vec![]);
assert!(d.host().windows[&viewport].visible);
let palette = d.viewport(window(&d, 5).id);
let focus = |d: &mut DockDriver<u32, Content>, window: Option<ViewportId>, frames: usize| {
d.host().focus = window;
for _ in 0..frames {
d.step(vec![]);
}
};
let level = |d: &DockDriver<u32, Content>| d.host().windows[&palette].level;
let told = |d: &DockDriver<u32, Content>| d.host().commands.iter().any(|(v, c)| *v == palette && matches!(c, ViewportCommand::Visible(_)));
assert_eq!(level(&d), Some(WindowLevel::AlwaysOnTop));
// Focus moving to the palette, with a frame of none between, keeps it on top.
focus(&mut d, None, 1);
focus(&mut d, Some(palette), 30);
assert_eq!(level(&d), Some(WindowLevel::AlwaysOnTop));
// Another application: the palette drops behind it after the delay, and shows on, so
// files dragged from that application can reach it.
focus(&mut d, None, 10);
assert_eq!(level(&d), Some(WindowLevel::AlwaysOnTop), "not yet");
focus(&mut d, None, 10);
assert_eq!(level(&d), Some(INACTIVE_PALETTE));
// Back: on top again.
focus(&mut d, Some(ViewportId::ROOT), 1);
assert_eq!(level(&d), Some(WindowLevel::AlwaysOnTop));
// Blocked by a modal, it stays at the normal level, away and back.
d.input_blocked = true;
focus(&mut d, None, 20);
assert_eq!(level(&d), Some(WindowLevel::Normal));
focus(&mut d, Some(ViewportId::ROOT), 5);
assert_eq!(level(&d), Some(WindowLevel::Normal));
assert!(d.host().windows[&palette].visible);
assert!(!told(&d), "{:?}", d.host().commands);
}
#[test]
@@ -471,18 +551,20 @@ fn viewer_windows_stay_shown_at_normal_level() {
assert_eq!(level(&d, viewer), Some(egui::WindowLevel::Normal));
assert_eq!(level(&d, palette), Some(egui::WindowLevel::AlwaysOnTop));
assert_eq!(level(&d, mixed), Some(egui::WindowLevel::Normal));
// Another application: only the palette hides, and the others get no visibility command.
// Another application: the palette drops behind it; the viewers stay at the normal level.
d.host().focus = None;
for _ in 0..20 {
d.step(vec![]);
}
let shown = |d: &DockDriver<u32, Content>| [viewer, palette, mixed].map(|v| d.host().windows[&v].visible);
assert_eq!(shown(&d), [true, false, true]);
let told = |d: &DockDriver<u32, Content>, v| d.host().commands.iter().any(|(w, c)| *w == v && matches!(c, ViewportCommand::Visible(_)));
assert!(!told(&d, viewer) && !told(&d, mixed));
assert_eq!(
[viewer, palette, mixed].map(|v| level(&d, v)),
[Some(egui::WindowLevel::Normal), Some(INACTIVE_PALETTE), Some(egui::WindowLevel::Normal)]
);
d.host().focus = Some(egui::ViewportId::ROOT);
d.step(vec![]);
assert_eq!(shown(&d), [true, true, true]);
assert_eq!(level(&d, palette), Some(egui::WindowLevel::AlwaysOnTop));
assert_eq!(level(&d, viewer), Some(egui::WindowLevel::Normal));
assert!(!d.host().commands.iter().any(|(_, c)| matches!(c, ViewportCommand::Visible(_))), "no window hides");
}
#[test]
@@ -532,7 +614,7 @@ fn the_app_comes_forward_with_its_viewers_when_it_is_activated() {
focus(&mut d, Some(MAIN), 2);
assert_eq!(levels(&mut d), []);
// Another application, then back through the main window: the viewers are raised back to
// front and the main window last, without focus; the palette only shows again.
// front and the main window last, without focus; the palette only goes back on top.
focus(&mut d, None, 20);
focus(&mut d, Some(MAIN), 10);
assert_eq!(levels(&mut d), raised(&[back, front, MAIN]));
@@ -951,6 +1033,55 @@ fn a_native_windows_bar_docks_it_at_frame_edges_and_dock_lines_but_not_a_body_ce
assert!(in_root(&d, 5) && in_root(&d, 6));
}
#[test]
fn over_a_canvas_body_a_window_stays_floating_and_a_tab_floats_while_the_edge_bands_dock() {
// PhotoCraft: a dock column of [2] above [3, 4] beside canvas 0, which takes splits, and
// window [5, 6] floating.
let accent = Color32::from_rgb(250, 10, 200);
let dock = Node::Dock {
side: super::Side::Start,
columns: vec![super::Column::new(vec![super::Group::new(vec![2], 0), super::Group::new(vec![3, 4], 0)], 240.0)],
};
let root = Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedFirst(240.0), first: Box::new(dock), second: Box::new(tabs(&[0])) };
let layout = Layout { root: Some(root), floating: vec![Floating::single(vec![5, 6], 0, [300.0, 80.0, 240.0, 200.0])] };
let start = || {
DockDriver::new(layout.clone(), Content { canvas: Some(0), ..Content::default() })
.with_style(move |s| {
s.accent = accent;
s.float_hint = false;
})
.with_native_host(ORIGIN)
};
// The canvas spans x = 244 to 800 and the whole height: no tab strip.
for at in [pos2(520.0, 150.0), pos2(520.0, 450.0)] {
let mut d = start();
let before = accented(d.shapes(), accent);
let id = window(&d, 5).id;
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
let local = d.window_local(id, ORIGIN + at.to_vec2()).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
d.step(vec![]);
assert_eq!(accented(d.shapes(), accent), before, "{at:?}: no preview");
let out = d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert_eq!(window_move(&out), None, "{at:?}: {:?}", out.actions);
assert!(window(&d, 5).contains(&6), "{at:?}");
}
// A docked tab dropped there floats.
let mut d = start();
let tab = d.hit(&Hit::Tab(3)).unwrap().center();
let out = d.drag(tab, pos2(520.0, 450.0));
assert!(matches!(out.actions.as_slice(), [Action::Float { panel: 3, .. }]), "{:?}", out.actions);
// The window's right edge, where no dock is yet, and the bottom edge band beside the canvas
// still dock the window.
let mut d = start();
let out = drag_bar_to(&mut d, 5, pos2(795.0, 300.0));
assert_eq!(window_move(&out), Some(&Target::NewDock { side: super::Side::End }), "{:?}", out.actions);
let mut d = start();
let out = drag_bar_to(&mut d, 5, pos2(520.0, 590.0));
assert!(matches!(window_move(&out), Some(Target::At { placement: Placement::Edge(super::Zone::Bottom), .. })), "{:?}", out.actions);
}
#[test]
fn escape_during_a_bar_drag_puts_the_window_back() {
let mut d = driver(layout(true));
@@ -1110,6 +1241,45 @@ fn a_bar_drag_follows_the_pointer_when_moves_land_late() {
assert_eq!(window(&d, 5).native.map(|r| [r[0], r[1]]), Some([to.x, to.y]));
}
/// A bar drag at a steady pointer speed under AppKit's timing: the window's reported place is
/// fresh, while pointer events are measured against where the window was a frame back on some
/// frames. Returns the window's step each frame. Released, the layout keeps where it ended.
fn bar_drag_with_stale_pointer(tells_cursor: bool) -> Vec<Vec2> {
let mut d = driver(layout(true));
d.host().tells_cursor = tells_cursor;
d.host().stale = vec![1, 0, 1, 1, 0];
let id = id_of(&d);
let viewport = d.viewport(id);
let grab = pos2(40.0, 9.0);
let mut pointer = d.host().windows[&viewport].rect.min + grab.to_vec2();
d.window_step(id, vec![Event::PointerMoved(grab), button(grab, true)]);
let mut place = d.host().windows[&viewport].rect.min;
let mut steps = Vec::new();
// On to the desktop right of the main window, where the release docks nothing.
for _ in 0..24 {
pointer += vec2(20.0, 5.0);
d.window_pointer(id, pointer);
let now = d.host().windows[&viewport].rect.min;
steps.push(now - place);
place = now;
}
let local = pointer - place.to_vec2();
d.window_step(id, vec![button(local, false)]);
d.settle();
assert_eq!(window(&d, 5).native.map(|r| pos2(r[0], r[1])), Some(d.host().windows[&viewport].rect.min));
steps
}
#[test]
fn a_bar_drag_follows_the_cursor_where_pointer_events_trail_the_window() {
// Without the cursor the window overshoots and steps back.
let steps = bar_drag_with_stale_pointer(false);
assert!(steps.iter().any(|s| s.x < 0.0), "{steps:?}");
// With it, every frame moves the window by the pointer's step.
let steps = bar_drag_with_stale_pointer(true);
assert!(steps.iter().all(|s| *s == vec2(20.0, 5.0)), "{steps:?}");
}
#[test]
fn a_native_windows_content_is_its_origin_where_its_frame_differs() {
// A title bar's height between the window's outer position and its content.
@@ -1720,11 +1890,13 @@ fn a_blocked_palette_stays_shown_and_behind_the_main_window_when_the_app_returns
d.step(vec![]);
}
};
// Showing it again would raise it over the modal and, on macOS, give it focus.
// Away and back, it stays shown at the normal level: showing it or putting it on top could
// raise it over the modal, and showing it would, on macOS, give it focus.
focus(&mut d, None);
assert!(d.host().windows[&palette].visible);
focus(&mut d, Some(palette));
let host = d.host();
assert_eq!(host.windows[&palette].level, Some(WindowLevel::Normal));
assert!(!host.commands.iter().any(|(v, c)| *v == palette && matches!(c, ViewportCommand::Visible(_))), "{:?}", host.commands);
// Activated through the palette, the main window comes in front of it, with no viewers.
let levels: Vec<_> = host.commands.iter().filter(|(_, c)| matches!(c, ViewportCommand::WindowLevel(_))).cloned().collect();
@@ -1736,6 +1908,61 @@ fn a_blocked_palette_stays_shown_and_behind_the_main_window_when_the_app_returns
assert_eq!(levels, expected);
}
/// A menu's key equivalent, as an app's menu glue reports it in the main window's input.
fn menu_key(key: Key) -> Vec<Event> {
let press = |pressed| Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Modifiers::COMMAND };
vec![press(true), press(false)]
}
#[test]
fn menu_keys_go_to_the_focused_text_field_of_the_native_window_with_the_keyboard() {
let has = |events: &[Event], key: Key| events.iter().any(|e| matches!(e, Event::Key { key: k, .. } if *k == key));
let pasted = |d: &DockDriver<u32, Content>, viewport| d.host().commands.contains(&(viewport, ViewportCommand::RequestPaste));
let mut d = driver(layout(true));
let viewport = d.viewport(id_of(&d));
(d.content.text, d.content.focus_text) = (Some(5), true);
d.host().focus = Some(viewport);
d.settle();
assert_eq!(super::typing_window(&d.ctx), Some(viewport));
// Between frames, where an app's `raw_input_hook` asks, the answer is the last frame's.
d.content.blur_text = true;
d.step(vec![]);
assert_eq!(super::typing_window(&d.ctx), None, "right after the field loses focus");
d.content.focus_text = true;
d.step(vec![]);
assert_eq!(super::typing_window(&d.ctx), Some(viewport), "right after the field takes focus");
d.step(menu_key(Key::V));
assert!(!has(&d.content.main_keys, Key::V), "{:?}", d.content.main_keys);
assert!(pasted(&d, viewport), "the field's window asks for the clipboard text");
d.step(menu_key(Key::C));
assert!(!has(&d.content.main_keys, Key::C));
assert!(d.content.field_keys.contains(&Event::Copy), "{:?}", d.content.field_keys);
// Saving is the app's, from any window.
d.step(menu_key(Key::S));
assert!(has(&d.content.main_keys, Key::S));
assert!(!has(&d.content.field_keys, Key::S));
// Under a block the window's field is inert: the main window keeps every key.
d.input_blocked = true;
d.settle();
assert_eq!(super::typing_window(&d.ctx), None);
d.host().commands.clear();
d.step(menu_key(Key::V));
assert!(has(&d.content.main_keys, Key::V));
assert!(!pasted(&d, viewport));
// With no focused field, nothing moves.
let mut d = driver(layout(true));
let viewport = d.viewport(id_of(&d));
d.content.text = Some(5);
d.host().focus = Some(viewport);
d.settle();
d.step(menu_key(Key::V));
assert!(has(&d.content.main_keys, Key::V));
assert!(d.content.field_keys.is_empty(), "{:?}", d.content.field_keys);
assert!(!pasted(&d, viewport));
}
#[test]
fn a_focused_field_and_a_held_press_stay_inert_across_a_block() {
let mut d = blocked_driver();
@@ -2408,15 +2635,41 @@ fn repeated_discards_deliver_the_release_and_drop_once() {
}
#[test]
fn a_native_window_fades_only_while_its_drag_can_dock() {
let drag = |docks| WindowDrag { window: 1, grab: vec2(10.0, 10.0), to: pos2(100.0, 100.0), start: pos2(100.0, 100.0), docks };
assert_eq!(drag_opacity(None, false, true), 1.0);
// A bar drag that only moves the window stays opaque.
assert_eq!(drag_opacity(Some(&drag(false)), false, false), 1.0);
assert_eq!(drag_opacity(Some(&drag(false)), false, true), 0.75);
// A drag by the window's only tab or group docks either way.
assert_eq!(drag_opacity(Some(&drag(true)), false, false), 0.75);
// With Cmd/Ctrl held the drag only moves the window.
assert_eq!(drag_opacity(Some(&drag(false)), true, true), 1.0);
assert_eq!(drag_opacity(Some(&drag(true)), true, false), 1.0);
fn a_native_window_fades_only_while_its_bar_drag_highlights_a_drop_target() {
let bar_to = |d: &mut DockDriver<u32, Content>, to: Pos2| {
let id = window(d, 5).id;
let local = d.window_local(id, ORIGIN + to.to_vec2()).unwrap();
d.window_step(id, vec![Event::PointerMoved(local)]);
drag_opacity(&d.ctx, d.id)
};
let grab = |d: &mut DockDriver<u32, Content>| {
let id = window(d, 5).id;
d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]);
d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]);
id
};
// A bar that only moves its window highlights nothing.
let mut d = driver(layout(true)).with_style(|s| {
s.command_press_floats = true;
s.title_bar_docks = false;
});
grab(&mut d);
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 1.0, "a move that cannot dock");
let mut d = driver(layout(true));
let id = grab(&mut d);
// Past the main window's right side: the desktop.
assert_eq!(bar_to(&mut d, pos2(1000.0, 300.0)), 1.0, "over empty space");
// The right edge zone of frame [3, 4].
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75, "over a drop target");
// The middle of the frame is no target for a whole window.
assert_eq!(bar_to(&mut d, pos2(600.0, 300.0)), 1.0, "after leaving the target");
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75);
// With Cmd/Ctrl held the drop floats and nothing is highlighted.
d.modifiers = Modifiers::COMMAND;
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 1.0, "a floating drop");
d.modifiers = Modifiers::NONE;
assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75);
d.window_step(id, vec![button(pos2(40.0, 9.0), false)]);
assert_eq!(drag_opacity(&d.ctx, d.id), 1.0, "after the drop");
}
+13 -3
View File
@@ -25,6 +25,9 @@ pub(super) const POINTER: &str = "drag-pointer";
/// Key of an Escape that reached one of the application's windows other than the drag's
/// owner. Keys go to the window with keyboard focus, which need not be the one dragging.
const ESCAPE: &str = "drag-escape";
/// Key of whether a native window's bar drag highlighted a drop target on the last frame, under
/// the area's id: [`Cross::highlighted`] as [`Renderer::end_cross`] left it.
pub(super) const HIGHLIGHTED: &str = "drag-highlighted";
/// The pointer of a drag, as the window that owns the drag last saw it.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -99,6 +102,9 @@ pub(in crate::docking) struct Cross<P> {
pub moved: Option<Pos2>,
/// Resolving a window move that docks whatever `title_bar_docks` says.
pub docks: bool,
/// A native window's bar drag highlighted a drop target this frame, in whichever window
/// drew its preview.
pub highlighted: bool,
/// A native window's dragged widget is one of the area's controls: egui keeps a drag per
/// viewport.
pub held: bool,
@@ -122,6 +128,7 @@ impl<P> Default for Cross<P> {
bar: false,
moved: None,
docks: false,
highlighted: false,
held: false,
pointed: None,
viewports: Vec::new(),
@@ -315,7 +322,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
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.bar, self.cross.docks) = (true, pointer.docks);
self.paint_group_drag(ui, &drag, local, pointer.command);
self.cross.highlighted |= self.paint_group_drag(ui, &drag, local, pointer.command);
(self.cross.bar, self.cross.docks) = (false, false);
}
}
@@ -371,7 +378,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
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.highlighted |= self.paint_group_drag(ui, &drag, local, pointer.command);
self.cross.docks = false;
}
return;
@@ -391,9 +398,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
}
/// End of the main window's frame: forget the pointer of a finished drag.
/// End of the main window's frame: forget the pointer of a finished drag, and keep whether
/// a bar drag highlighted a drop target. The dragged window sets its opacity from that on
/// the next frame: it draws before the main window paints its preview.
pub(in crate::docking) fn end_cross(&mut self, ctx: &Context) {
let id = self.id;
ctx.data_mut(|d| d.insert_temp(id.with(HIGHLIGHTED), self.cross.highlighted));
let active = ctx.data(|d| {
d.get_temp::<Drag<P>>(id.with("drag")).is_some()
|| d.get_temp::<GroupDrag<P>>(id.with("group-drag")).is_some()
+29 -16
View File
@@ -287,7 +287,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
outer.min(outer + inward * self.style.new_dock_band)..=outer.max(outer + inward * self.style.new_dock_band),
rect.y_range(),
));
self.dock_targets.push(DockDrop { target: DockTarget::NewDock { side }, band, line });
self.dock_targets.push(DockDrop { target: DockTarget::NewDock { side }, band, line, outer: true });
return;
}
if usable(rect) {
@@ -319,9 +319,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
group.sides[1] = Some(right.clone());
}
if index == 0 {
self.dock_targets.push(DockDrop { target: before.0, band: outer_band, line: before.1 });
// Inside the window, as the new-dock band's line is.
let line = vertical_line(x + inward * line_width * 0.5, rect.y_range(), line_width);
self.dock_targets.push(DockDrop { target: before.0, band: outer_band, line, outer: true });
}
self.dock_targets.push(DockDrop { target: after.0, band: inner_band, line: after.1 });
self.dock_targets.push(DockDrop { target: after.0, band: inner_band, line: after.1, outer: false });
drawn.push((index, column, first, width, inner));
}
// Registered after the columns, so the narrow hit areas win at the edges.
@@ -506,7 +508,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let push = |targets: &mut Vec<DockDrop<P>>, target: DockTarget<P>, band: Rect, line: Rect| {
let band = band.intersect(view);
if usable(band) {
targets.push(DockDrop { target, band, line });
targets.push(DockDrop { target, band, line, outer: false });
}
};
let mut previous: Option<&P> = None;
@@ -680,7 +682,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let band = Rect::from_x_y_ranges(x.min(x + inward)..=x.max(x + inward), ys);
let width = self.style.drop_line_width;
let line = vertical_line(x + inward.signum() * width * 0.5, ys, width);
candidates.push(DockDrop { target: DockTarget::NewDock { side }, band, line });
candidates.push(DockDrop { target: DockTarget::NewDock { side }, band, line, outer: true });
}
candidates
}
@@ -724,25 +726,36 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
body_zone(group.body, 0.0, self.style.side_zone_fraction, group.center, pointer)?
};
let (target, line) = group.side(zone)?.clone();
Some(DockDrop { target, band: group.body, line })
Some(DockDrop { target, band: group.body, line, outer: false })
}
/// The dock target a whole group or column drag would take at `pointer`, with the action
/// and its line. A target that leaves the layout unchanged is a no-op: `None` action.
pub(super) fn dock_group_proposal(&self, drag: &GroupDrag<P>, pointer: Pos2) -> Option<(Option<Action<P>>, Rect)> {
/// The dock target a whole group or column drag would take at `pointer`, with the action.
/// A target that leaves the layout unchanged is a no-op: `None` action. A target named by
/// one of the moving panels, such as the window edge of the moving group's own column, is
/// named by another panel of that column or group, as for a tab drag.
pub(super) fn dock_group_proposal(&self, drag: &GroupDrag<P>, pointer: Pos2) -> Option<(Option<Action<P>>, DockDrop<P>)> {
let through = self.looks_through(drag);
if self.dock_covered(pointer, through.then_some(&drag.panel)) {
return None;
}
let drop = self.dock_drop(pointer, if through { &drag.members } else { &[] })?;
let action = group_action(drag, drop.target.to());
self.checked(action).map(|action| (action, drop.line))
let to = match drop.target.to() {
Target::At { anchor, placement } if drag.members.contains(&anchor) => {
let unit = if matches!(placement, Placement::Column { .. }) { Unit::Column } else { Unit::Group };
match self.layout.members(&anchor, unit).ok()?.into_iter().find(|p| !drag.members.contains(p)) {
Some(anchor) => Target::At { anchor, placement },
None => return Some((None, drop)),
}
}
to => to,
};
self.checked(group_action(drag, to)).map(|action| (action, drop))
}
/// The dock target a single tab drag would take: a new group, column or dock of that panel.
/// A panel alone in its group takes the group's state to a new dock; one that shares its
/// group leaves the other tabs behind (P6).
pub(super) fn dock_tab_proposal(&self, panel: &P, pointer: Pos2) -> Option<(Option<Action<P>>, Rect)> {
pub(super) fn dock_tab_proposal(&self, panel: &P, pointer: Pos2) -> Option<(Option<Action<P>>, DockDrop<P>)> {
if self.dock_covered(pointer, None) {
return None;
}
@@ -754,7 +767,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let unit = if matches!(placement, Placement::Column { .. }) { Unit::Column } else { Unit::Group };
match self.layout.members(panel, unit).ok()?.into_iter().find(|p| p != panel) {
Some(anchor) => Action::Move { panel: panel.clone(), anchor, placement },
None => return Some((None, drop.line)),
None => return Some((None, drop)),
}
}
Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement },
@@ -764,7 +777,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
to @ Target::NewDock { .. } => Action::MovePanel { panel: panel.clone(), to },
Target::Float { .. } => return None,
};
self.checked(action).map(|action| (action, drop.line))
self.checked(action).map(|action| (action, drop))
}
/// `Some(None)` when the action is valid but changes nothing apart from sizes
@@ -775,8 +788,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
Some((!candidate.same_arrangement(self.layout)).then_some(action))
}
/// The line of a dock drop that shows no result rect: into an iconic column, or any dock
/// drop with [`DockStyle::side_edge_zones`].
/// The line of a drop that shows no result rect: on the window edge, into an iconic column,
/// or any dock drop with [`DockStyle::side_edge_zones`].
pub(super) fn paint_drop_line(&self, ctx: &Context, line: Rect) {
if usable(line) {
let painter = self.preview_painter(ctx);
+27 -9
View File
@@ -39,15 +39,19 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
pub(super) fn window_limits(&self, window: &Floating<P>) -> PanelLimits {
let chrome = self.window_chrome(window);
let tree = node_limits(&window.root, self.style, &*self.content);
let body = match (&window.root, window.minimized) {
(Node::Tabs { .. }, true) => (self.style.tab_height, self.style.tab_height),
(_, true) => (0.0, 0.0),
(_, false) => (tree.min.y, tree.max.y),
let body = match window.minimized {
true => (self.minimized_body(window), self.minimized_body(window)),
false => (tree.min.y, tree.max.y),
};
let min = vec2(tree.min.x, chrome + body.0).min(vec2(1_000_000.0, 1_000_000.0));
PanelLimits { min, max: vec2(tree.max.x, chrome + body.1).max(min).min(vec2(1_000_000.0, 1_000_000.0)) }
}
/// The height of a minimized window's body: the tab strip of its only group, or nothing.
pub(super) fn minimized_body(&self, window: &Floating<P>) -> f32 {
if matches!(window.root, Node::Tabs { .. }) { self.style.tab_height } else { 0.0 }
}
/// The title bar and, for a movable (resizable) window, the resize band above it.
fn window_chrome(&self, window: &Floating<P>) -> f32 {
let band = if window.panels().all(|panel| self.permissions(panel).movable) { RESIZE_BAND } else { 0.0 };
@@ -76,11 +80,13 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let movable = window.panels().all(|panel| self.permissions(panel).movable);
let style = ChromeStyle::from_dock(self.style);
let resize_label = self.style.resize_window_label.clone();
let response = FloatingFrame::new(id, rect).bounds(screen).limits(limits).resizable(movable).resize_label(&resize_label).show(
ctx,
&style,
|ui, contents| self.window_contents(ui, window, contents, rect, &style),
);
let opacity = self.drag_opacity(ctx, first);
let response =
FloatingFrame::new(id, rect).bounds(screen).limits(limits).resizable(movable).resize_label(&resize_label).opacity(opacity).show(
ctx,
&style,
|ui, contents| self.window_contents(ui, window, contents, rect, &style),
);
for handle in &response.handles {
self.register_interaction(ctx, handle.id);
}
@@ -100,6 +106,18 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
}
/// Opacity of the in-window floating window whose first panel is `first`: faded
/// ([`super::DRAG_OPACITY`]) while its title bar drag highlights a drop target, as the last
/// frame drew it ([`super::preview::GROUP_HIGHLIGHTED`]), else opaque. Its tabs and drag
/// bars only drag a ghost, so the window stays opaque for them.
fn drag_opacity(&self, ctx: &Context, first: &P) -> f32 {
let moving = ctx
.data(|d| d.get_temp::<GroupDrag<P>>(self.id.with("group-drag")))
.is_some_and(|drag| drag.gesture == GroupGesture::Window && drag.panel == *first);
let highlighted = ctx.data(|d| d.get_temp::<bool>(self.id.with(super::preview::GROUP_HIGHLIGHTED))).unwrap_or(false);
if moving && highlighted { super::DRAG_OPACITY } else { 1.0 }
}
/// The title bar, then the window's tree below it, in `contents`. Moves carry the whole
/// `frame`, resize band included.
fn window_contents(&mut self, ui: &mut Ui, window: &Floating<P>, contents: Rect, frame: Rect, style: &ChromeStyle) {
+1 -1
View File
@@ -183,7 +183,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
if usable(band) {
let width = self.style.drop_line_width;
let line = horizontal_line(line.clamp(view.top() + width * 0.5, view.bottom() - width * 0.5), xs, width);
self.dock_targets.push(DockDrop { target: DockTarget::GroupBand { anchor: anchor.clone(), below }, band, line });
self.dock_targets.push(DockDrop { target: DockTarget::GroupBand { anchor: anchor.clone(), below }, band, line, outer: false });
}
}
}
+9 -2
View File
@@ -21,7 +21,7 @@ pub(in crate::docking) use native::WindowDrag;
#[cfg(all(test, feature = "native-windows"))]
pub(in crate::docking) use {
cross::DragPointer,
native::{APPKIT_CORNER, MACOS_CLOSE, Session, appkit_resize_areas, close_rects, drag_opacity, floats_shown, resize_bands, session, wayland},
native::{APPKIT_CORNER, INACTIVE_PALETTE, MACOS_CLOSE, Session, appkit_resize_areas, close_rects, drag_opacity, resize_bands, session, wayland},
};
#[cfg(all(test, feature = "native-windows"))]
@@ -29,7 +29,7 @@ pub(in crate::docking) use body_drag::held_events;
#[cfg(feature = "native-windows")]
pub(in crate::docking) use body_drag::{cancel_body_route, mark_app_drag};
#[cfg(feature = "native-windows")]
pub(in crate::docking) use native::{InputHook, set_input_hook};
pub(in crate::docking) use native::{DesktopCursor, InputHook, send_keys, set_input_hook, typing_window};
/// How long after blocking starts a native window that still has the keyboard hands it to the
/// main window: an OS dialog the app opens with the modal (a file dialog) takes focus a little
@@ -37,6 +37,10 @@ pub(in crate::docking) use native::{InputHook, set_input_hook};
#[cfg(feature = "native-windows")]
const BLOCKED_FOCUS_WAIT: f64 = 0.2;
/// Opacity of a floating window, in-window or native, while its bar, only tab or only tab row
/// drags it over a drop target: the highlighted target under the window shows through it.
pub(in crate::docking) const DRAG_OPACITY: f32 = 0.75;
/// While input is blocked (`starts`: from this frame), a native window of the dock area `area`
/// with the keyboard would drop the keys the modal needs, so once, after
/// [`BLOCKED_FOCUS_WAIT`], the main window, where the modal is, takes the keyboard if one still
@@ -133,6 +137,9 @@ pub(super) struct Renderer<'a, P> {
/// The other OS windows a drag may cross into.
#[cfg(feature = "native-windows")]
pub(super) cross: cross::Cross<P>,
/// Where a native window that follows a bar drag goes.
#[cfg(feature = "native-windows")]
pub(super) cursor: DesktopCursor,
}
pub(super) struct DrawnDivider {
+263 -91
View File
@@ -1,7 +1,8 @@
//! Floating windows in their own OS windows (feature `native-windows`). Where the backend has
//! viewports, every floating window whose panels allow it is drawn with
//! `show_viewport_immediate`. Palettes stay on top of other windows and hide while none of the
//! application's windows has focus; viewers are normal windows that come forward when the
//! `show_viewport_immediate`. Palettes stay on top of the application's other windows, and
//! drop to the normal level while none of its windows has focus, so they stay shown without
//! covering other applications; viewers are normal windows that come forward when the
//! application is activated. Where egui embeds viewports (wasm, tests) windows stay
//! in-window at their in-window rect, and so do they on Wayland, unless the app turns native
//! windows on ([`crate::docking::DockArea::native_windows`]). Tabs, groups and whole windows
@@ -25,13 +26,19 @@ use crate::docking::{Action, Drag, Floating, GroupDrag, GroupGesture, Node, Wind
/// Smallest native window.
const MIN_SIZE: [f32; 2] = [160.0, 100.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 none of the application's windows has focus before the application counts as
/// inactive, so focus moving between our own windows does not.
const AWAY_DELAY: f64 = 0.25;
/// Whether normal windows are raised when the application is activated. macOS brings an
/// application's windows forward itself: all of them on activation from the Dock or the
/// application switcher, the clicked one alone on a click, as its own windows do.
const RAISE_ON_ACTIVATION: bool = !cfg!(target_os = "macos");
/// The level of a palette while the application is inactive, behind other applications'
/// windows. On Windows `WindowLevel::Normal` is `HWND_NOTOPMOST`, which puts the window in
/// front of every window that is not topmost, the activated application's too; `HWND_BOTTOM`
/// sends it behind them without activating it.
pub(in crate::docking) const INACTIVE_PALETTE: WindowLevel =
if cfg!(target_os = "windows") { WindowLevel::AlwaysOnBottom } else { WindowLevel::Normal };
/// How long reports of a native window's place may lag a move the dock area commanded.
const MOVE_WAIT: f64 = 0.5;
/// How far the resize bands reach into a native window from its edges, where the window has
@@ -88,21 +95,16 @@ pub(in crate::docking) const MACOS_CLOSE: CloseLayout =
pub(in crate::docking) const OTHER_CLOSE: CloseLayout =
CloseLayout { glyph_top: 4.5, glyph_trailing: 11.5, glyph_size: 9.0, hit_left: 4.5, hit_right: 4.5, hit_up: 4.5, hit_down: 4.5 };
/// Opacity of a native window while its bar or only tab drags it somewhere it can dock: the
/// window stays above the main window, so the drop preview under the pointer shows only
/// through it.
/// Opacity of the native window that a bar drag of the area `area` moves, by the rule for every
/// floating window ([`super::DRAG_OPACITY`]): faded while the drag highlights a drop target, as
/// the last frame drew it ([`super::cross::HIGHLIGHTED`]), opaque over empty space, for a move
/// that cannot dock and while Ctrl/Cmd keeps the drop floating. The window stays above the main
/// window, so the highlighted target shows only through it.
#[cfg(any(target_os = "macos", test))]
const DRAG_OPACITY: f64 = 0.75;
/// Opacity of the window `drag` moves: faded while the drag can dock (its only tab or group, or
/// a bar with [`crate::docking::DockStyle::title_bar_docks`]), opaque for a plain move and while
/// Ctrl/Cmd keeps the drop floating (`float_only`).
#[cfg(any(target_os = "macos", test))]
pub(in crate::docking) fn drag_opacity(drag: Option<&WindowDrag>, float_only: bool, title_bar_docks: bool) -> f64 {
match drag {
Some(drag) if !float_only && (drag.docks || title_bar_docks) => DRAG_OPACITY,
_ => 1.0,
}
pub(in crate::docking) fn drag_opacity(ctx: &Context, area: Id) -> f64 {
let dragging = ctx.data(|d| d.get_temp::<WindowDrag>(area.with("window-drag"))).is_some();
let highlighted = ctx.data(|d| d.get_temp::<bool>(area.with(super::cross::HIGHLIGHTED))).unwrap_or(false);
if dragging && highlighted { f64::from(super::DRAG_OPACITY) } else { 1.0 }
}
/// Sets the opacity of the AppKit windows titled `title`, never one titled `main` (the main
@@ -126,6 +128,48 @@ fn set_window_opacity(title: &str, main: Option<&str>, opacity: f64) {
}
}
/// Where the cursor is on the desktop, in egui's desktop points, for a native window that
/// follows a bar drag.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(in crate::docking) enum DesktopCursor {
/// The window system's: AppKit's on macOS, none elsewhere.
#[default]
System,
/// A stand-in window system's (`docking::testing::NativeHost`), or none.
#[cfg(any(test, feature = "testing"))]
Fixed(Option<Pos2>),
}
impl DesktopCursor {
fn at(self, ctx: &Context) -> Option<Pos2> {
match self {
Self::System => system_cursor(ctx),
#[cfg(any(test, feature = "testing"))]
Self::Fixed(at) => at,
}
.filter(|p| p.is_finite())
}
}
/// AppKit's cursor, in the desktop points egui reports window positions in. AppKit measures
/// from the bottom-left of the primary screen (the one with the menu bar, first in
/// `NSScreen::screens`), y up; winit flips window positions against that screen's height,
/// whichever screen a window is on, so the cursor is flipped the same way. egui's points are
/// AppKit's divided by the zoom factor.
#[cfg(target_os = "macos")]
fn system_cursor(ctx: &Context) -> Option<Pos2> {
let mtm = objc2::MainThreadMarker::new()?;
let primary = objc2_app_kit::NSScreen::screens(mtm).firstObject()?;
let at = objc2_app_kit::NSEvent::mouseLocation();
let height = primary.frame().size.height;
Some(pos2(at.x as f32, (height - at.y) as f32) / ctx.zoom_factor())
}
#[cfg(not(target_os = "macos"))]
fn system_cursor(_: &Context) -> Option<Pos2> {
None
}
/// Whether native windows outline their ×'s hit area, and on macOS AppKit's resize areas:
/// `CRAFT_DOCK_DEBUG_CLOSE=1`, read once.
fn debug_close() -> bool {
@@ -162,8 +206,9 @@ struct Place {
pending: Option<f64>,
/// The window system put the frame, not the content, where it opened: corrected once.
corrected: bool,
/// Shown, as it opened or was last told.
shown: bool,
/// The height of its bar while minimized, as it was last told. `known` keeps the expanded
/// size meanwhile.
collapsed: Option<f32>,
}
/// Whether two rects differ by more than rounding.
@@ -187,12 +232,6 @@ pub(in crate::docking) fn viewport_id(area: Id, window: u64, n: usize) -> Viewpo
}
}
/// Whether floating windows show: while one of the application's windows has focus, and until
/// [`HIDE_DELAY`] after `last_focused`, when one last had it.
pub(in crate::docking) fn floats_shown(focused: bool, now: f64, last_focused: f64) -> bool {
focused || now - last_focused < HIDE_DELAY
}
/// The desktop rect of a native viewport's content. None when the backend reports no
/// positions.
fn placed(info: &ViewportInfo) -> Option<[f32; 4]> {
@@ -223,7 +262,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
pub(super) fn native_windows<'w>(&mut self, ctx: &Context, windows: Vec<&'w Floating<P>>) -> Vec<&'w Floating<P>> {
let native = !ctx.embed_viewports() && self.native_windows.unwrap_or_else(|| !wayland(ctx, env_session()));
let origin = ctx.input(|i| i.viewport().inner_rect.map(|r| r.min)).filter(|p| p.is_finite()).unwrap_or(Pos2::ZERO);
let (visible, activated) = if native { self.float_visibility(ctx) } else { (true, false) };
let (active, activated) = if native { self.app_activity(ctx) } else { (true, false) };
self.cross.on = native;
let mut shown = Vec::new();
let mut in_window = Vec::new();
@@ -254,7 +293,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
if ctx.data(|d| d.get_temp::<WindowDrag>(key)).is_some_and(|d| !natives.iter().any(|(w, ..)| w.id == d.window)) {
ctx.data_mut(|d| d.remove::<WindowDrag>(key));
}
self.map_desktop(ctx, &natives, visible, activated);
self.map_desktop(ctx, &natives, activated);
self.raise_next(ctx, &natives);
// The main window owns drags of the panels it draws.
let id = self.id;
@@ -269,7 +308,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
self.map_body_route(ctx, native);
for (window, rect, viewport) in natives {
if let Some(key) = self.native_window(ctx, window, rect, viewport, visible) {
if let Some(key) = self.native_window(ctx, window, rect, viewport, active) {
shown.push(key);
}
}
@@ -307,10 +346,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// The main window and the shown native windows on the desktop, front first: palettes
/// above the rest, each the window focused most recently first. A window whose position is
/// unknown is left out. `visible`: palettes show. `activated`: the application was just
/// activated, so the main window and its normal windows are raised (see
/// [`Self::raise_next`]).
fn map_desktop(&mut self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)], visible: bool, activated: bool) {
/// unknown is left out. `activated`: the application was just activated, so the main window
/// and its normal windows are raised (see [`Self::raise_next`]).
fn map_desktop(&mut self, ctx: &Context, natives: &[(&Floating<P>, [f32; 4], ViewportId)], activated: bool) {
let key = self.id.with("native-order");
// Focus order, front first; `None` is the main window.
let mut order = ctx.data(|d| d.get_temp::<Vec<Option<u64>>>(key)).unwrap_or_default();
@@ -340,7 +378,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
Some(info) => placed(&info).map(|[x, y, w, h]| Rect::from_min_size(pos2(x, y), vec2(w, h))),
};
let normal = self.normal_window(window);
if let Some(placed) = placed.filter(|r| r.is_finite() && (visible || normal)) {
if let Some(placed) = placed.filter(|r| r.is_finite()) {
windows.push((window.id, placed, normal));
}
}
@@ -401,10 +439,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
}
/// Whether palettes show this frame, and whether the application was just activated: one
/// of its windows has focus after none had it for long enough to hide the palettes.
/// Losing focus again stops the raise of normal windows that activation started.
fn float_visibility(&mut self, ctx: &Context) -> (bool, bool) {
/// Whether the application is active this frame: one of its windows has focus, or one had it
/// less than [`AWAY_DELAY`] ago. And whether it was just activated: one of its windows has
/// focus after none had it for that long. Losing focus again stops the raise of normal
/// windows that activation started.
fn app_activity(&mut self, ctx: &Context) -> (bool, bool) {
let (focused, now) = ctx.input(|i| {
let infos = || i.raw.viewports.values();
// A backend that reports no focus at all keeps the windows.
@@ -412,35 +451,34 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
(focused, i.time)
});
self.cross.focus_lost = crate::focus::turned_false(ctx, self.id.with("app-focus"), focused);
let key = self.id.with("native-visibility");
let key = self.id.with("native-last-focused");
let last = if focused { now } else { ctx.data(|d| d.get_temp::<f64>(key)).unwrap_or(now) };
let visible = floats_shown(focused, now, last);
if visible && !focused {
ctx.request_repaint_after(std::time::Duration::from_secs_f64((HIDE_DELAY - (now - last)).max(0.0)));
let active = focused || now - last < AWAY_DELAY;
if active && !focused {
ctx.request_repaint_after(std::time::Duration::from_secs_f64((AWAY_DELAY - (now - last)).max(0.0)));
}
ctx.data_mut(|d| d.insert_temp(key, last));
let away_key = self.id.with("native-away");
let away = ctx.data(|d| d.get_temp::<bool>(away_key)).unwrap_or(false);
ctx.data_mut(|d| {
d.insert_temp(away_key, !focused && (away || !visible));
d.insert_temp(away_key, !focused && (away || !active));
if !focused {
d.remove::<Vec<Option<u64>>>(self.id.with("native-raise"));
}
});
(visible, focused && away)
(active, focused && away)
}
/// A window holding a viewer is a normal window, so it neither vanishes nor sits over other
/// applications; the rest are palettes.
/// A window holding a viewer is a normal window, so it stacks with the main window; the rest
/// are palettes.
fn normal_window(&self, window: &Floating<P>) -> bool {
window.panels().any(|p| self.content.window_kind(p) == WindowKind::Normal)
}
/// One floating window in its own viewport. `visible`: palettes show. Returns the key of
/// its [`Place`].
fn native_window(&mut self, ctx: &Context, window: &Floating<P>, rect: [f32; 4], viewport: ViewportId, visible: bool) -> Option<Id> {
/// One floating window in its own viewport. `active`: the application is active (see
/// [`Self::app_activity`]). Returns the key of its [`Place`].
fn native_window(&mut self, ctx: &Context, window: &Floating<P>, rect: [f32; 4], viewport: ViewportId, active: bool) -> Option<Id> {
let normal = self.normal_window(window);
let visible = visible || normal;
let first = window.panels().next()?.clone();
let key = self.id.with(("native-place", viewport));
let now = ctx.input(|i| i.time);
@@ -448,6 +486,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
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);
// A minimized window shows its bar and what `floating_body` draws below it.
let collapsed = window.minimized.then(|| BAR_HEIGHT + self.minimized_body(window));
let shown = |[x, y, w, h]: [f32; 4]| [x, y, w, collapsed.unwrap_or(h)];
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):
@@ -456,7 +497,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
&& let Some(native) = window.native.filter(|native| differs(*native, place.known))
{
let target = kept_reachable(native, &self.monitors);
place_window(ctx, viewport, target, frame);
place_window(ctx, viewport, shown(target), frame);
(place.known, place.pending) = (target, Some(now + MOVE_WAIT));
}
place
@@ -474,9 +515,22 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
};
seen.insert(window.id);
ctx.data_mut(|d| d.insert_temp(seen_key, seen));
Place { spawn, known: spawn, pending: Some(now + MOVE_WAIT), corrected: false, shown: true }
Place { spawn, known: spawn, pending: Some(now + MOVE_WAIT), corrected: false, collapsed: None }
}
};
// Minimized or expanded: the window keeps its top-left corner and width, and its
// height is fixed while it shows only its bar.
// The limits go first: the OS clamps the new size to the limits it has.
if place.collapsed != collapsed {
let (min, max) = match collapsed {
Some(height) => (vec2(MIN_SIZE[0], height), vec2(1_000_000.0, height)),
None => (Vec2::from(MIN_SIZE), Vec2::INFINITY),
};
ctx.send_viewport_cmd_to(viewport, ViewportCommand::MinInnerSize(min));
ctx.send_viewport_cmd_to(viewport, ViewportCommand::MaxInnerSize(max));
place_window(ctx, viewport, shown(place.known), frame);
(place.collapsed, place.pending) = (collapsed, Some(now + MOVE_WAIT));
}
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()
@@ -485,8 +539,14 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
.with_inner_size([spawn[2], spawn[3]])
.with_min_inner_size(MIN_SIZE)
// A blocked palette could cover the modal, and its bar moves nothing: the main
// window can come in front of it at the normal level.
.with_window_level(if normal || self.input_blocked { WindowLevel::Normal } else { WindowLevel::AlwaysOnTop })
// window can come in front of it at the normal level. While the application is
// inactive, its palettes stay shown behind other applications' windows
// (`INACTIVE_PALETTE`). egui sends the level only when it changes.
.with_window_level(match (normal || self.input_blocked, active) {
(true, _) => WindowLevel::Normal,
(false, true) => WindowLevel::AlwaysOnTop,
(false, false) => INACTIVE_PALETTE,
})
.with_taskbar(false);
// The bar replaces the OS title bar. macOS keeps the frame, with its title bar and
// buttons hidden: AppKit resizes a movable window from its edges and corners with its
@@ -500,15 +560,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
.with_resizable(!self.input_blocked && window.panels().all(|p| self.permissions(p).movable));
#[cfg(not(target_os = "macos"))]
let builder = builder.with_decorations(false);
// A blocked palette keeps showing while the app is away: showing it on the app's return
// would raise it over the modal and, on macOS, give it focus.
let visible = visible || (self.input_blocked && place.shown);
// Only on a change: showing a shown window raises it and, on macOS, takes focus.
if place.shown != visible {
ctx.send_viewport_cmd_to(viewport, ViewportCommand::Visible(visible));
place.shown = visible;
}
if visible && !self.input_blocked && self.activation.as_ref().is_some_and(|panel| window.contains(panel)) {
// While the application is inactive, raising a palette leaves focus with the application
// in use.
if (active || normal) && !self.input_blocked && self.activation.as_ref().is_some_and(|panel| window.contains(panel)) {
ctx.send_viewport_cmd_to(viewport, ViewportCommand::Focus);
}
let (targets, docks, windows) = (self.targets.len(), self.dock_targets.len(), self.frame.windows.len());
@@ -530,6 +584,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let body = Rect::from_min_max(pos2(full.left(), (full.top() + BAR_HEIGHT).min(full.bottom())), full.max);
self.cross.pointed = None;
self.floating_body(ui, window, body, full);
if own && !blocked && ui.ctx().text_edit_focused() {
let _ = body_input(ui.ctx()).lock().typing_next.insert(viewport);
}
self.cross.held |= ui.ctx().dragged_id().is_some_and(|id| self.owned_interactions.contains(&id));
if debug_close() {
debug_outlines(ui, full, &buttons);
@@ -586,20 +643,22 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
(place.known[0], place.known[1], place.pending) = (to.x, to.y, Some(now + MOVE_WAIT));
}
let moving = moving(ctx);
// Set every frame from the drag state, so any way a drag ends (a drop, Escape, a focus
// loss, the window closing) brings the window back to full opacity. AppKit finds the
// window by title, so a window sharing the dragged one's title stays faded with it.
// Set every frame from the drag state and the last frame's preview, so leaving a target
// or any way a drag ends (a drop, Escape, a focus loss, the window closing) brings the
// window back to full opacity. AppKit finds the window by title, so a window sharing
// the dragged one's title stays faded with it.
#[cfg(target_os = "macos")]
{
let main = ctx.input(|i| i.raw.viewports.get(&ViewportId::ROOT).and_then(|v| v.title.clone()));
let drag = ctx.data(|d| d.get_temp::<WindowDrag>(drag_key));
let dragged = drag.as_ref().and_then(|drag| self.layout.floating.iter().find(|w| w.id == drag.window));
let this = dragged.and_then(|w| crate::docking::dock_model::first_active(&w.root)).is_some_and(|p| self.label_of(p) == title);
let float_only = drag.is_some_and(|drag| self.cross.pointer.is_some_and(|p| p.window == Some(drag.window) && p.command));
let opacity = if this { drag_opacity(drag.as_ref(), float_only, self.style.title_bar_docks) } else { 1.0 };
let opacity = if this { drag_opacity(ctx, self.id) } else { 1.0 };
set_window_opacity(&title, main.as_deref(), opacity);
}
if let Some(placed) = report.placed.filter(|_| !moving) {
// The collapsed height a minimized window reports is not the size to keep.
let reported = report.placed.map(|[x, y, w, h]| [x, y, w, if place.collapsed.is_some() { place.known[3] } else { h }]);
if let Some(placed) = reported.filter(|_| !moving) {
let waiting = place.pending.is_some_and(|until| now < until);
if !differs(placed, place.known) {
place.pending = None;
@@ -727,21 +786,29 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
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 frame = content.map_or(Vec2::ZERO, |(_, frame)| frame);
// A pointer move of this frame 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. Without one the window stays, since the last position egui
// keeps (button events included) was measured against an older place.
let moved = ui.input(|i| i.events.iter().rev().find_map(|e| if let Event::PointerMoved(p) = e { Some(*p) } else { None }));
let mut step = None;
if let (Some((origin, _)), Some(local)) = (content, moved.filter(|p| p.is_finite())) {
let to = origin + (local.to_vec2() - drag.grab);
if to.distance(drag.to) > 0.0 {
(drag.to, step) = (to, Some(to));
ui.ctx().data_mut(|d| d.insert_temp(key, drag));
// Where the window system tells the cursor on the desktop (macOS), the window goes
// there: AppKit reports the window's place as soon as a move is commanded, but measures
// the pointer events it sends against where the window server had the window, which
// can be a move behind. Elsewhere a pointer move of this frame 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. Without one the window stays, since
// the last position egui keeps (button events included) was measured against an older
// place.
let to = match self.cursor.at(ui.ctx()) {
Some(cursor) => Some(cursor - drag.grab),
None => {
let moved = ui.input(|i| i.events.iter().rev().find_map(|e| if let Event::PointerMoved(p) = e { Some(*p) } else { None }));
content.zip(moved.filter(|p| p.is_finite())).map(|((origin, _), local)| origin + (local.to_vec2() - drag.grab))
}
};
let mut step = None;
if let Some(to) = to.filter(|to| to.distance(drag.to) > 0.0) {
(drag.to, step) = (to, Some(to));
ui.ctx().data_mut(|d| d.insert_temp(key, drag));
}
// The pointer on the desktop is the pressed point of the window where it was last
// put, so the origin is that place, less the local pointer.
// put (the cursor, where the window system tells it), so the origin is that place,
// less the local pointer. Targets are hit there.
let local = ui.input(|i| i.pointer.latest_pos()).filter(|p| p.is_finite());
let origin = local.map(|local| drag.to + drag.grab - local.to_vec2()).or(content.map(|(origin, _)| origin));
let pointer = self.record_pointer(ui.ctx(), Some(window), origin, Some(&drag));
@@ -830,10 +897,11 @@ pub(super) struct NativeInput {
windows: std::collections::HashMap<ViewportId, Id>,
}
/// Each dock area's app drag in a panel body, and the releases waiting for a window's next
/// input. Kept outside [`NativeInput`]: egui calls [`egui::Plugin::input_hook`] with the plugin
/// locked, and an app's input hook may call [`crate::docking::cancel_drag`], which must reach
/// the routes without locking the plugin again. The lock here is dropped before that hook runs.
/// Each dock area's app drag in a panel body, the events waiting for a window's next input, and
/// the windows whose text fields have the keyboard. Kept outside [`NativeInput`]: egui calls
/// [`egui::Plugin::input_hook`] with the plugin locked, and an app's input hook may call
/// [`crate::docking::cancel_drag`] or [`crate::docking::send_keys`], which must reach this state
/// without locking the plugin again. The lock here is dropped before that hook runs.
#[derive(Default)]
pub(super) struct NativeBodyInput {
/// Each dock area's app drag in a panel body.
@@ -844,6 +912,14 @@ pub(super) struct NativeBodyInput {
pub held: Held,
/// The main window's passes so far.
pub pass: u64,
/// The windows that had a focused text field in the last main-window pass, and those
/// noted so far in this one.
typing: std::collections::HashSet<ViewportId>,
typing_next: std::collections::HashSet<ViewportId>,
/// The main window's completed passes when `typing` was last taken from `typing_next`.
typing_at: u64,
/// Key and clipboard events for each window's next input, and the pass they were queued in.
keys: std::collections::HashMap<ViewportId, (u64, Vec<Event>)>,
}
/// The app drag routing and routed events shared by every native input hook of `ctx`.
@@ -854,8 +930,8 @@ pub(super) fn body_input(ctx: &Context) -> Arc<egui::mutex::Mutex<NativeBodyInpu
/// The temp-data key for the context's native body-routing state.
///
/// All dock areas in a context deliberately use this one container: its `routes` map is keyed by
/// dock-area `Id`, and queued and held events by target viewport. The key is unique only so this
/// state does not collide with unrelated temporary data.
/// dock-area `Id`, and queued, held and key events by target viewport. The key is unique only so
/// this state does not collide with unrelated temporary data.
fn body_input_key() -> Id {
Id::unique("craft-ui docking native body input")
}
@@ -870,7 +946,7 @@ impl egui::Plugin for NativeInput {
/// Route this viewport's input without deadlocking.
///
/// Only the root viewport advances the frame pass; queued and held entries older than the
/// Only the root viewport advances the frame pass; queued, held and key entries older than the
/// current pass are pruned. Events waiting for this viewport are taken before routing, so events
/// enqueued by routing wait for its next input. A held release is only released once a later
/// root pass exists. Earlier routed events are spliced before the window's own events, and any
@@ -891,18 +967,42 @@ impl egui::Plugin for NativeInput {
let pass = body.pass;
body.queued.retain(|_, (at, _)| *at + 1 >= pass);
body.held.retain(|_, (at, _)| *at + 1 >= pass);
body.keys.retain(|_, (at, _)| *at + 1 >= pass);
body.typing = std::mem::take(&mut body.typing_next);
body.typing_at = ctx.cumulative_pass_nr_for(ViewportId::ROOT);
// eframe has run `App::raw_input_hook`, where an app's native menu reports its key
// equivalents (macOS: AppKit's menu takes them before the key window sees them).
// While a native window has the keyboard, the main window gets no keys of its own,
// so these are the menu's: those a focused text field there acts on are its own.
if let Some(window) = typing_window_of(&body.typing, &input.viewports) {
let (moved, kept): (Vec<_>, Vec<_>) = std::mem::take(&mut input.events).into_iter().partition(text_key);
input.events = kept;
if !moved.is_empty() {
let entry = body.keys.entry(window).or_default();
entry.0 = pass;
entry.1.extend(moved);
ctx.request_repaint_of(window);
}
}
}
// Taken before routing, which may queue more for this window's next input. A held
// release waits for a later main-window pass: a discarded pass reruns within its own.
let waiting = body.queued.remove(&viewport);
let keys = body.keys.remove(&viewport).map(|(_, mut keys)| {
text_clipboard_keys(ctx, viewport, &mut keys);
keys
});
let held = if body.held.get(&viewport).is_some_and(|(at, _)| *at < body.pass) { body.held.remove(&viewport) } else { None };
for route in body.routes.values_mut() {
routed |= route.route(input, &mut body.queued, &mut body.held, body.pass);
}
// Before the window's own events: they happened earlier, and a held release must
// not end a press that follows it.
let earlier =
held.into_iter().flat_map(|(_, events)| events).chain(waiting.into_iter().flat_map(|(_, events)| events.into_iter().map(|(_, e)| e)));
let earlier = held
.into_iter()
.flat_map(|(_, events)| events)
.chain(waiting.into_iter().flat_map(|(_, events)| events.into_iter().map(|(_, e)| e)))
.chain(keys.into_iter().flatten());
input.events.splice(0..0, earlier);
}
if routed {
@@ -916,6 +1016,78 @@ impl egui::Plugin for NativeInput {
}
}
/// The native window in `typing` that has the keyboard, by the main window's `viewports`.
fn typing_window_of(typing: &std::collections::HashSet<ViewportId>, viewports: &egui::ViewportIdMap<ViewportInfo>) -> Option<ViewportId> {
typing.iter().copied().find(|viewport| viewports.get(viewport).and_then(|info| info.focused) == Some(true))
}
/// Whether a text field acts on `event`: the clipboard events, and the command keys that copy,
/// paste, select all, undo, redo, move or delete in text. Other command keys are the app's
/// (save, close, new window, its own shortcuts).
fn text_key(event: &Event) -> bool {
match event {
Event::Copy | Event::Cut | Event::Paste(_) => true,
Event::Key { key, modifiers: m, .. } if m.command && !m.alt => match key {
Key::C | Key::X | Key::V | Key::A | Key::Backspace | Key::Delete => !m.shift,
Key::Z | Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown => true,
_ => false,
},
_ => false,
}
}
/// A text field copies and pastes on `Copy`, `Cut` and `Paste` events, which egui-winit makes
/// from the window's own key presses. A command C, X or V press here is a menu's key
/// equivalent, which a text field ignores: give back C and X as `Copy` and `Cut`, and turn V
/// into a request for the clipboard text, which arrives in `viewport` as `Paste`.
fn text_clipboard_keys(ctx: &Context, viewport: ViewportId, events: &mut Vec<Event>) {
events.retain_mut(|e| {
let Event::Key { key, pressed: true, modifiers: m, .. } = *e else { return true };
if !m.command || m.alt || m.shift {
return true;
}
match key {
Key::C => *e = Event::Copy,
Key::X => *e = Event::Cut,
Key::V => {
ctx.send_viewport_cmd_to(viewport, ViewportCommand::RequestPaste);
return false;
}
_ => {}
}
true
});
}
/// See [`crate::docking::typing_window`].
pub(in crate::docking) fn typing_window(ctx: &Context) -> Option<ViewportId> {
// Between frames (in `App::raw_input_hook`) a main-window pass has ended since the swap, and
// `typing_next` is the whole of the last pass, which the next swap hands to the routing.
let passes = ctx.cumulative_pass_nr_for(ViewportId::ROOT);
let body = body_input(ctx);
let typing = {
let input = body.lock();
if passes > input.typing_at { input.typing_next.clone() } else { input.typing.clone() }
};
ctx.input_for(ViewportId::ROOT, |i| typing_window_of(&typing, &i.raw.viewports))
}
/// See [`crate::docking::send_keys`].
pub(in crate::docking) fn send_keys(ctx: &Context, viewport: ViewportId, events: Vec<Event>) {
if events.is_empty() {
return;
}
{
let body = body_input(ctx);
let mut input = body.lock();
let pass = input.pass;
let entry = input.keys.entry(viewport).or_default();
entry.0 = pass;
entry.1.extend(events);
}
ctx.request_repaint_of(viewport);
}
/// Set or clear the input hook of the dock area `area` for this frame, and forget its native
/// windows until they show again.
pub(in crate::docking) fn set_input_hook(ctx: &Context, area: Id, hook: Option<InputHook>) {
+43 -24
View File
@@ -8,7 +8,7 @@ use crate::docking::{
Action, Drag, DropProposal, DropRequest, DropTarget, GroupDrag, GroupGesture, Layout, Node, Placement, Target, Zone, cancel_drag,
dock_model::Place,
surface::TAB_PADDING,
targets::{SIDE_EDGE_ZONE_DEPTH, resolve_drop_on},
targets::{DockDrop, SIDE_EDGE_ZONE_DEPTH, resolve_drop_on},
};
use crate::layout::{SplitAxis, SplitSize};
@@ -33,6 +33,11 @@ const DOCKED_FLOAT_HEIGHT: f32 = 320.0;
/// Least height of a floating window or flyout fitted to a short panel's content.
pub(super) const MIN_FITTED_HEIGHT: f32 = 120.0;
/// Key of whether the area's group drag highlighted a drop target on the last frame, under the
/// area's id. An in-window floating window that its bar drag moves fades from it on the next
/// frame: the window draws before the preview.
pub(super) const GROUP_HIGHLIGHTED: &str = "group-drag-highlighted";
/// What finishing a drag reads of the pointer, in the coordinates of the window finishing it.
#[derive(Clone, Copy, Debug)]
pub(super) struct DragInput {
@@ -71,6 +76,7 @@ impl DragInput {
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
pub(in crate::docking) fn finish_group_drag(&mut self, ui: &Ui) {
let ctx = ui.ctx();
ctx.data_mut(|d| d.remove::<bool>(self.id.with(GROUP_HIGHLIGHTED)));
let key = self.id.with("group-drag");
let Some(drag) = ctx.data(|d| d.get_temp::<GroupDrag<P>>(key)) else { return };
if !drag.members.iter().all(|p| self.layout.contains(p) && self.permissions(p).movable) {
@@ -122,7 +128,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
} else if let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) {
self.output.group_drag = Some(drag.panel.clone());
self.paint_group_drag(ui, &drag, pointer, float_only);
if self.paint_group_drag(ui, &drag, pointer, float_only) {
ctx.data_mut(|d| d.insert_temp(self.id.with(GROUP_HIGHLIGHTED), true));
}
}
}
@@ -198,11 +206,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
/// [`crate::docking::resolve_drop_by`] with [`crate::docking::DockStyle::zone_rule`] and the
/// canvas sides of [`crate::docking::DockStyle::drop_edges`], where a split that would leave
/// a group without room becomes a join of the target group, or no target when the group
/// takes no tabs. A drop that would change only sizes is a no-op ([`Self::settled`]).
/// canvas sides of [`crate::docking::DockStyle::drop_edges`] (none in the docking profile),
/// where a split that would leave a group without room becomes a join of the target group,
/// or no target when the group takes no tabs. A drop that would change only sizes is a no-op ([`Self::settled`]).
fn resolve_roomy(&self, panel: &P, pointer: Pos2, targets: &[DropTarget<P>], action: impl Fn(Target<P>) -> Action<P>) -> Option<DropProposal<P>> {
let proposal = resolve_drop_on(panel, pointer, targets, self.style.zone_rule(), self.style.drop_sides())?;
let proposal = resolve_drop_on(panel, pointer, targets, self.style.zone_rule(), self.style.canvas_sides())?;
if !matches!(proposal.placement, Placement::Split(zone) if zone != Zone::Center) {
return Some(self.settled(proposal, action));
}
@@ -220,7 +228,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
pointer,
std::slice::from_ref(&DropTarget { allow_splits: false, ..target.clone() }),
self.style.zone_rule(),
self.style.drop_sides(),
self.style.canvas_sides(),
)?;
Some(self.settled(proposal, action))
}
@@ -396,7 +404,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// The ghost at the pointer (a window moving live is its own ghost) under one destination
/// preview. The ghost has a translucent fill, a 2 pt outline and the members' labels.
pub(super) fn paint_group_drag(&mut self, ui: &Ui, drag: &GroupDrag<P>, pointer: Pos2, float_only: bool) {
/// Returns whether it highlighted a drop target.
pub(super) fn paint_group_drag(&mut self, ui: &Ui, drag: &GroupDrag<P>, pointer: Pos2, float_only: bool) -> bool {
let ctx = ui.ctx();
let painter = self.preview_painter(ctx);
if drag.gesture != GroupGesture::Window {
@@ -418,16 +427,17 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
}
if float_only || (drag.gesture == GroupGesture::Window && !self.title_bar_docks()) {
return;
return false;
}
if let Some((action, line)) = self.dock_group_proposal(drag, pointer) {
if let Some((action, drop)) = self.dock_group_proposal(drag, pointer) {
let highlighted = action.is_some();
if let Some(action) = action {
self.paint_dock_preview(ctx, &painter, action, &drag.members, line);
self.paint_dock_preview(ctx, &painter, action, &drag.members, &drop);
if drag.gesture != GroupGesture::Window {
self.paint_float_hint(ctx, pointer);
}
}
return;
return highlighted;
}
let proposal = self.group_proposal(drag, pointer).filter(|p| !p.noop);
let zones = self.paint_group_zones(&painter, drag, pointer, proposal.as_ref());
@@ -441,7 +451,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
if drag.gesture != GroupGesture::Window {
self.paint_float_hint(ctx, pointer);
}
return true;
}
false
}
pub(in crate::docking) fn finish_drag(&mut self, ctx: &Context) {
@@ -485,7 +497,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
return;
}
let dock = if float_only || outside { None } else { self.dock_tab_proposal(&drag.panel, pointer) };
if let Some((action, line)) = dock {
if let Some((action, drop)) = dock {
if released {
cancel_drag::<P>(ctx, self.id);
if let Some(action) = action.filter(|_| pointer.distance(drag.origin) >= 2.0) {
@@ -494,7 +506,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
} else if let Some(action) = action {
let painter = self.preview_painter(ctx);
self.paint_dock_preview(ctx, &painter, action, std::slice::from_ref(&drag.panel), line);
self.paint_dock_preview(ctx, &painter, action, std::slice::from_ref(&drag.panel), &drop);
self.paint_float_hint(ctx, pointer);
}
return;
@@ -568,10 +580,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
/// The preview of a group target, for tab and group drags of `members`, which `action` moves
/// there: for a drop beside the group or the layout, the rect the members then take
/// ([`Self::result_rect`]), else the tab outline of the whole group, and the insertion marker
/// on a strip. With [`crate::docking::DockStyle::side_edge_zones`], the region the drop
/// takes, filled for a tab drag (`filled`) or a split.
/// there: for a drop beside the group, the rect the members then take
/// ([`Self::result_rect`]); on the layout's outer edge band, the line along that edge; else
/// the tab outline of the whole group, and the insertion marker on a strip. With
/// [`crate::docking::DockStyle::side_edge_zones`], the region the drop takes, filled for a
/// tab drag (`filled`) or a split.
fn paint_preview(
&self,
ctx: &Context,
@@ -582,9 +595,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
action: impl Fn(Target<P>) -> Action<P>,
) {
if crate::docking::targets::usable(proposal.preview) {
let beside = matches!(proposal.placement, Placement::Split(zone) | Placement::SplitSized { zone, .. } | Placement::Edge(zone) if zone != Zone::Center);
let beside = matches!(proposal.placement, Placement::Split(zone) | Placement::SplitSized { zone, .. } if zone != Zone::Center);
if self.style.side_edge_zones {
drop_preview(painter, proposal.preview.shrink(1.0), self.style.accent, filled);
} else if matches!(proposal.placement, Placement::Edge(_)) {
self.paint_drop_line(ctx, proposal.preview);
} else if beside {
let to = Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() };
match self.result_rect(action(to), members) {
@@ -599,12 +614,14 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
/// The preview of a dock drop that `action` makes of `members`: the rect they then take
/// ([`Self::result_rect`]). A drop into an iconic column, or any dock drop with
/// [`crate::docking::DockStyle::side_edge_zones`], shows its `line`.
fn paint_dock_preview(&self, ctx: &Context, painter: &egui::Painter, action: Action<P>, members: &[P], line: Rect) {
match (!self.style.side_edge_zones).then_some(action).and_then(|action| self.result_rect(action, members)) {
/// ([`Self::result_rect`]). A drop on the window edge ([`DockDrop::outer`]) or into an
/// iconic column, and any dock drop with [`crate::docking::DockStyle::side_edge_zones`],
/// shows the drop's line.
fn paint_dock_preview(&self, ctx: &Context, painter: &egui::Painter, action: Action<P>, members: &[P], drop: &DockDrop<P>) {
let rect = (!self.style.side_edge_zones && !drop.outer).then_some(action).and_then(|action| self.result_rect(action, members));
match rect {
Some(rect) => tab_outline(painter, rect.shrink(1.0), self.style.accent),
None => self.paint_drop_line(ctx, line),
None => self.paint_drop_line(ctx, drop.line),
}
}
@@ -870,6 +887,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let painter = self.preview_painter(ctx);
if let Some(rect) = feedback.preview.filter(|rect| crate::docking::targets::usable(*rect)) {
drop_preview(&painter, rect, self.style.accent, true);
// A window moving by its title bar fades over it, as over the renderer's own.
ctx.data_mut(|d| d.insert_temp(self.id.with(GROUP_HIGHLIGHTED), true));
}
if let Some(rect) = feedback.insertion.filter(|rect| crate::docking::targets::usable(*rect)) {
painter.rect_filled(rect, 0.0, self.style.accent);
+73 -12
View File
@@ -64,25 +64,30 @@ pub struct DockStyle {
/// bottom edges add a full-width row, a group's zones are [`Self::side_zone_fraction`] of its
/// body, and every preview is a line or a tab outline.
pub side_edge_zones: bool,
/// Which sides of a group that takes no tabs (a canvas), and of the layout's outer edge, take
/// a drop (all four). Without [`Self::side_edge_zones`] the outer left and right edges are
/// new docks, so only top and bottom apply there. A dock-column app whose palettes dock only
/// at the left and right sets [`crate::docking::Sides::NONE`] with [`Self::new_dock_zones`]
/// on. Not used with [`Self::side_edge_zones`]. Groups that take tabs keep all four sides.
/// Which sides of the layout's outer edge, and of a group that takes no tabs (a canvas)
/// without [`Self::new_dock_zones`], take a drop (all four). With [`Self::new_dock_zones`]
/// a canvas body takes no drop, and without [`Self::side_edge_zones`] the outer left and
/// right edges are new docks, so only top and bottom apply there. A dock-column app whose
/// palettes dock only at the left and right sets [`crate::docking::Sides::NONE`] with
/// [`Self::new_dock_zones`] on. Not used with [`Self::side_edge_zones`]. Groups that take
/// tabs keep all four sides.
pub drop_edges: super::Sides,
/// The share of a group's body, below the tab strip, that means "beside" on each side: of
/// its width from the left and right edges, of its height from the top and bottom edges
/// (one third, at most one half). The middle joins the group as a tab. Not used with
/// [`Self::side_edge_zones`].
pub side_zone_fraction: f32,
/// Width of the line that a drop between the icon groups of an iconic column, and every
/// dock drop with [`Self::side_edge_zones`], previews with, and of the line
/// Width of the line that a drop on a band along the layout's outer edge (a new dock, a new
/// outermost column or the outer edge band), a drop between the icon groups of an iconic
/// column, and every dock drop with [`Self::side_edge_zones`] preview with, and of the line
/// [`crate::docking::ZoneRule::Fraction`] gives hosts as a side's preview (4 pt, more than
/// zero). Other drops beside a group, column, dock or the layout's edge preview the rect the
/// moved panels take. Border bands reach 4 pt past this line into a tab strip.
/// zero). Other drops beside a group, column or dock preview the rect the moved panels
/// take. Border bands reach 4 pt past this line into a tab strip.
pub drop_line_width: f32,
/// [D] The docked area's left and right edges, where no dock is yet, take a drop as a new
/// dock (see [`Self::new_dock_band`]).
/// [D] The docking profile. The docked area's left and right edges, where no dock is yet,
/// take a drop as a new dock (see [`Self::new_dock_band`]). Without
/// [`Self::side_edge_zones`], the body of a frame or floating group that takes no tabs (a
/// canvas) takes no drop: a release there floats as over no target.
pub new_dock_zones: bool,
/// [D] Height of the bar above each dock column. It drags the column and holds its »/« button.
pub column_bar_height: f32,
@@ -237,12 +242,20 @@ impl DockStyle {
}
}
/// The canvas sides [`Self::drop_edges`] gives in this profile: all four with
/// The sides [`Self::drop_edges`] gives in this profile: all four with
/// [`Self::side_edge_zones`].
pub(super) fn drop_sides(&self) -> super::Sides {
if self.side_edge_zones { super::Sides::ALL } else { self.drop_edges }
}
/// The sides of a frame or floating group that takes no tabs (a canvas) whose body takes a
/// drop: none in the docking profile ([`Self::new_dock_zones`] without
/// [`Self::side_edge_zones`]), where only the bands along borders dock beside it, else
/// [`Self::drop_sides`].
pub(super) fn canvas_sides(&self) -> super::Sides {
if self.side_edge_zones || !self.new_dock_zones { self.drop_sides() } else { super::Sides::NONE }
}
/// The outer edges that take a drop: all four with [`Self::side_edge_zones`], else the top
/// and bottom of [`Self::drop_edges`].
pub(super) fn outer_edges(&self) -> super::Sides {
@@ -778,6 +791,41 @@ pub fn route_app_drag(ctx: &Context, area_id: Id) {
let _ = (ctx, area_id);
}
/// The native window that has the keyboard and a focused text field in its last pass (feature
/// `native-windows`; elsewhere `None`). Not a window drawn under [`DockArea::input_blocked`].
///
/// On macOS the menu bar takes key equivalents such as ⌘C and ⌘V before the key window sees
/// them, and an app's menu glue reports them in the main window's input. While this is `Some`,
/// the dock area moves the keys a text field acts on (⌘C, ⌘X, ⌘V, ⌘A, ⌘Z and ⇧⌘Z, ⌘ with the
/// arrows, ⌘Backspace and ⌘Delete, and `Copy`, `Cut` and `Paste` events) from the main window's
/// input to that window's, as [`send_keys`] does; the app's other command keys stay with the
/// main window. A menu that runs commands itself instead of reporting keys can send the text
/// keys to this window with [`send_keys`]. An app whose own `raw_input_hook` turns the main
/// window's ⌘V into `ViewportCommand::RequestPaste` for its text fields skips that while this
/// is `Some`, so the field's window pastes instead.
pub fn typing_window(ctx: &Context) -> Option<egui::ViewportId> {
#[cfg(feature = "native-windows")]
return super::render::typing_window(ctx);
#[cfg(not(feature = "native-windows"))]
{
let _ = ctx;
None
}
}
/// Give `events` to the native window `viewport` at the start of its next input, and repaint
/// it (feature `native-windows`; elsewhere it does nothing). A text field copies and pastes on
/// egui's `Copy`, `Cut` and `Paste` events, not on key presses, so ⌘C and ⌘X presses arrive as
/// `Copy` and `Cut`, and ⌘V asks the window for the clipboard text
/// (`ViewportCommand::RequestPaste`), which arrives as `Paste`. Other events arrive as they are.
/// See [`typing_window`].
pub fn send_keys(ctx: &Context, viewport: egui::ViewportId, events: Vec<egui::Event>) {
#[cfg(feature = "native-windows")]
super::render::send_keys(ctx, viewport, events);
#[cfg(not(feature = "native-windows"))]
let _ = (ctx, viewport, events);
}
/// One mutable application adapter owns bodies, menus, shaped text and per-panel policy,
/// avoiding competing mutable closures. Logical labels are used for tab accessibility.
/// [`DockArea::show_content`] reads every policy from here; the closures passed to the older
@@ -965,6 +1013,8 @@ pub struct DockArea<'a, P = ()> {
native_windows: Option<bool>,
#[cfg(feature = "native-windows")]
input_hook: Option<super::render::InputHook>,
#[cfg(feature = "native-windows")]
cursor: super::render::DesktopCursor,
}
impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
@@ -983,6 +1033,8 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
native_windows: None,
#[cfg(feature = "native-windows")]
input_hook: None,
#[cfg(feature = "native-windows")]
cursor: Default::default(),
}
}
@@ -1040,6 +1092,13 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
self
}
/// Where native windows that follow a bar drag go: a stand-in window system's cursor.
#[cfg(all(feature = "native-windows", any(test, feature = "testing")))]
pub(in crate::docking) fn desktop_cursor(mut self, cursor: super::render::DesktopCursor) -> Self {
self.cursor = cursor;
self
}
/// [D] Auto-Collapse Iconic Panels (default on): a flyout closes on Escape or a press
/// outside it. Off, it stays open until its icon or another icon is clicked.
pub fn auto_collapse_iconic(mut self, on: bool) -> Self {
@@ -1216,6 +1275,8 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
native_windows: self.native_windows,
#[cfg(feature = "native-windows")]
cross: Default::default(),
#[cfg(feature = "native-windows")]
cursor: self.cursor,
};
if let Some(root) = &view.root {
renderer.node(ui, root, rect, &mut Vec::new(), (root, rect));
+11 -5
View File
@@ -4,6 +4,8 @@ use super::{Placement, Side, Target, Zone};
/// One visible group, supplied in back-to-front order to [`resolve_drop`]. An empty tab strip
/// or a group that rejects tabs may still accept side splits, e.g. a protected document canvas.
/// The dock area's docking profile ([`crate::docking::DockStyle::new_dock_zones`]) gives the
/// body of a frame or floating group that rejects tabs no target at all.
#[derive(Clone, Debug)]
pub struct DropTarget<P> {
pub anchor: P,
@@ -75,9 +77,10 @@ pub fn drop_zone(rect: Rect, tab_height: f32, depth: f32, pointer: Pos2) -> Opti
/// In the body below it, the outer `fraction` of its width from the left or right edge, or of
/// its height from the top or bottom edge, is that side's zone; where two sides' zones meet, the
/// side the pointer is relatively nearer wins. The rest is the center. Without `center` (a group
/// that takes no tabs) the whole body goes to the relatively nearest side. A `fraction` of zero
/// or less leaves only the center; more than one half acts as one half. Outside, non-finite,
/// empty and inverted rectangles have no target.
/// that takes no tabs) the whole body goes to the relatively nearest side, as the dock area has
/// it without [`crate::docking::DockStyle::new_dock_zones`]; with it, such a body is no target.
/// A `fraction` of zero or less leaves only the center; more than one half acts as one half.
/// Outside, non-finite, empty and inverted rectangles have no target.
pub fn body_zone(rect: Rect, tab_height: f32, fraction: f32, center: bool, pointer: Pos2) -> Option<Zone> {
body_zone_on(rect, tab_height, fraction, center, Sides::ALL, pointer)
}
@@ -271,7 +274,7 @@ pub fn resolve_drop_by<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropT
}
/// [`resolve_drop_by`] where a group that takes no tabs splits only on `sides` with
/// [`ZoneRule::Fraction`].
/// [`ZoneRule::Fraction`], and with none is no target.
pub(super) fn resolve_drop_on<P: Clone + Eq>(
panel: &P,
pointer: Pos2,
@@ -354,12 +357,15 @@ pub(super) fn resolve_drop_on<P: Clone + Eq>(
}
/// [D] A dock drop target, built once per frame next to the [`DropTarget`]s. The pointer selects
/// it inside `band`; the preview is the line `line`.
/// it inside `band`; `line` is the edge it docks to.
#[derive(Clone, Debug, PartialEq)]
pub(super) struct DockDrop<P> {
pub(super) target: DockTarget<P>,
pub(super) band: Rect,
pub(super) line: Rect,
/// A band along the window edge: a new dock, or a new column outside the outermost one.
/// Its preview is `line`, not the rect the drop takes.
pub(super) outer: bool,
}
#[derive(Clone, Debug, PartialEq)]
+51 -1
View File
@@ -140,6 +140,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> Do
let monitors = self.monitors.clone();
#[cfg(feature = "native-windows")]
let input_hook = self.input_hook.clone();
#[cfg(feature = "native-windows")]
let cursor = self.host.as_ref().and_then(|host| host.borrow().cursor);
let (style_fn, maximized, raise, auto, bounds, locked, native, blocked) = (
&self.style,
self.maximized.clone(),
@@ -170,6 +172,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> Do
dock = dock.float_bounds(bounds);
}
#[cfg(feature = "native-windows")]
{
dock = dock.desktop_cursor(super::render::DesktopCursor::Fixed(cursor));
}
#[cfg(feature = "native-windows")]
if let Some(hook) = input_hook.clone() {
dock = dock.native_input_hook(move |ctx, input| hook(ctx, input));
}
@@ -249,6 +255,26 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static, C: DockContent<P>> Do
self.window_step(window, vec![Event::PointerMoved(to)])
}
/// Move the pointer to the desktop point `point` in the native window of `window`, and
/// step. The window gets the point in its coordinates, measured against its place
/// [`NativeHost::stale`] picks; with [`NativeHost::tells_cursor`] the window system also
/// tells the point as the cursor's.
#[cfg(feature = "native-windows")]
pub fn window_pointer(&mut self, window: u64, point: Pos2) -> DockOutput<P> {
let viewport = self.viewport(window);
{
let mut host = self.host();
let behind = host.pointer_moves.checked_rem(host.stale.len()).and_then(|i| host.stale.get(i)).copied().unwrap_or(0);
host.pointer_moves += 1;
host.cursor = host.tells_cursor.then_some(point);
if let Some(target) = host.windows.get_mut(&viewport) {
let place = target.places.iter().rev().nth(behind).or(target.places.first()).copied().unwrap_or(target.rect.min);
target.events.push(Event::PointerMoved(point - place.to_vec2()));
}
}
self.step(Vec::new())
}
/// Release at `pos` in a native window, in its coordinates.
#[cfg(feature = "native-windows")]
pub fn window_release(&mut self, window: u64, pos: Pos2) -> DockOutput<P> {
@@ -400,11 +426,21 @@ pub struct NativeHost {
/// `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,
/// The window system tells where the cursor is on the desktop, as AppKit does: where
/// [`DockDriver::window_pointer`] last put it.
pub tells_cursor: bool,
/// [`DockDriver::window_pointer`] measures a window's pointer events against its place
/// this many frames back, one entry per call in turn, as AppKit does while the window
/// server catches up with a move it reported at once. Empty: its place now.
pub stale: Vec<usize>,
pub windows: std::collections::HashMap<egui::ViewportId, HostWindow>,
/// Every command sent to a native window or the main 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)>,
cursor: Option<Pos2>,
/// Calls of [`DockDriver::window_pointer`], for [`Self::stale`].
pointer_moves: usize,
frames: usize,
/// The driver's modifiers: the keyboard's, whichever window has focus.
modifiers: Modifiers,
@@ -434,6 +470,8 @@ pub struct HostWindow {
pub dropped_files: Vec<egui::DroppedFileHandle>,
/// Shapes of its last frame.
pub shapes: Vec<ClippedShape>,
/// Desktop positions of its content at the end of recent frames, latest last.
places: Vec<Pos2>,
}
#[cfg(feature = "native-windows")]
@@ -446,9 +484,13 @@ impl NativeHost {
no_position: false,
frame: egui::Vec2::ZERO,
lag: false,
tells_cursor: false,
stale: Vec::new(),
windows: Default::default(),
commands: Vec::new(),
late: Vec::new(),
cursor: None,
pointer_moves: 0,
frames: 0,
modifiers: Modifiers::NONE,
screen: Rect::NOTHING,
@@ -492,6 +534,7 @@ impl NativeHost {
hovered_files: Vec::new(),
dropped_files: Vec::new(),
shapes: Vec::new(),
places: Vec::new(),
});
let mut viewports = host.infos();
let (time, focused, modifiers) = (host.time, host.focus == Some(id), host.modifiers);
@@ -524,8 +567,10 @@ impl NativeHost {
}
}
/// End a frame: moves sent in an earlier one land.
/// End a frame: moves sent in an earlier one land, and each window keeps its place.
fn land(&mut self) {
/// How many frames back [`Self::stale`] can reach.
const PLACES: usize = 8;
let frames = self.frames;
let (landed, late) = std::mem::take(&mut self.late).into_iter().partition::<Vec<_>, _>(|(.., sent)| *sent < frames);
self.late = late;
@@ -534,6 +579,11 @@ impl NativeHost {
window.rect = Rect::from_min_size(pos, window.rect.size());
}
}
for window in self.windows.values_mut() {
window.places.push(window.rect.min);
let excess = window.places.len().saturating_sub(PLACES);
window.places.drain(..excess);
}
}
/// Carry out the commands a frame sent to native windows.
+53 -27
View File
@@ -215,7 +215,9 @@ A flyout fits its content the same way, without the title bar.
- A tab drags one panel. A group's drag bar (the space beside its tabs) or its top border drags
the whole group. A column bar drags a column. A floating window's title bar drags the window;
with `title_bar_docks` a release over a target docks the whole window there, else it only
moves.
moves. While the drag highlights a drop target, the window is drawn at 75% opacity so the
target shows through it; over empty space, and once the drag ends, it is opaque. A native
window moved by its only tab or tab row fades the same way.
- Inside a floating window, tabs and drag bars drag and dock as they do in the docked layout,
into any window. A drag bar or border of a window's only group takes the whole window along,
so, like the title bar, it looks through the window: the groups, docks and windows behind it
@@ -239,10 +241,19 @@ A flyout fits its content the same way, without the title bar.
beside the group on that side. Where two sides meet, at a corner, the side the pointer is
relatively nearer wins.
- The middle third joins the group's tabs.
- A group that takes no tabs, such as a protected canvas, has no middle: every point of its
body docks beside it on the relatively nearest side. Its strip takes nothing.
- A group that takes no tabs, such as a protected canvas, takes nothing on its strip, and its
body has no middle: every point docks beside it on the relatively nearest side.
- In the docking profile (`new_dock_zones` without `side_edge_zones`), the body of a frame or
floating group that takes no tabs takes nothing, whatever its `accepts_splits`: a release
there is a release over no target, so a floating window stays floating and a docked panel
or group floats as `float_on_empty_drop` says. Only the bands along borders (below) dock
beside it, as in Adobe apps, whose document area is no drop zone. A dock column's group
keeps its sides.
- Beside a frame is a split of that frame. Beside a group in a dock column, the top and bottom
stack a new group above or below it, and the left and right make a new column on that side.
A group dragged to an edge of its own column, such as the window edge of the column it heads,
makes a new column there, as one of its tabs would; when it is the column's only group,
nothing changes.
- A frame too narrow or short for two halves of `DockStyle::min_pane` still splits when the
frames beyond it can give the room, as they do for a dragged divider. When they cannot, the
frame keeps its size and the new frame takes the width (or height) the moved panels have now,
@@ -276,11 +287,12 @@ A flyout fits its content the same way, without the title bar.
`window.panel.dock` to an outer edge picks its anchor the same way, from the permissions
`apply` (or `to_actions`) gets and the area's `FloatSpace` (`with_side_edge_zones` before it
has drawn).
- `DockStyle::drop_edges` (a `Sides`, all four by default) lists which sides of a group that
takes no tabs (a canvas), and of the layout's outer edge, take a drop. A canvas's whole body
goes to the nearest listed side; with none listed it takes nothing. In the docking profile
the outer left and right edges are new docks, so only top and bottom apply there. Groups
that take tabs keep all four sides. A dock-column app whose palettes dock only at the left
- `DockStyle::drop_edges` (a `Sides`, all four by default) lists which sides of the layout's
outer edge, and of a group that takes no tabs (a canvas) without `new_dock_zones`, take a
drop. There a canvas's whole body goes to the nearest listed side; with none listed it takes
nothing. In the docking profile the canvas body takes no drop and the outer left and right
edges are new docks, so only the outer top and bottom apply. Groups that take tabs keep all
four sides. A dock-column app whose palettes dock only at the left
and right, as in AutoCAD, sets `Sides::NONE` with `new_dock_zones` on. Not used with
`side_edge_zones`.
- With `side_edge_zones` (the split-tree arrangement), a frame's zones are a picture frame
@@ -315,15 +327,18 @@ A flyout fits its content the same way, without the title bar.
way. Towards the minimums, groups give way in proportion to their room. A group takes the
largest default, compact and fill height of its panels. All three are body heights, without
the tab strip, like `PanelLimits`.
- Drop previews have three shapes, the same for tab and group drags:
- Drop previews have four shapes, the same for tab and group drags:
- A drop beside something shows the rect the dragged panel, group or column will take, at its
real position and size, as a 2 pt accent outline with a light fill. The renderer applies
the drop to a copy of the layout and lays it out with the code that draws it, so the rect
is the one the panel's group gets on release: the new frame for a split (including
`Placement::SplitSized`) or the outer edge band, the new group for a stack above or below
a dock group or on a seam, and the whole column, bar included, for a new column or dock.
A drop between the icon groups of an iconic column shows a `DockStyle::drop_line_width`
(4 pt) accent line there instead.
`Placement::SplitSized`), the new group for a stack above or below a dock group or on a
seam, and the whole column, bar included, for a new column between columns or beside a
dock group's side.
- A drop on a band along the layout's outer edge shows a `DockStyle::drop_line_width` (4 pt)
accent line along that edge: the new-dock band, the window edge of a dock's outermost
column, and the outer edge band (`Placement::Edge`), whose line spans the edge area it
wraps. A drop between the icon groups of an iconic column shows the same line there.
- A drop that joins a group shows the same outline and fill around the whole group, and on
its strip the 2 pt insertion marker.
- A drop that floats shows the floating rect.
@@ -462,15 +477,19 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho
monitor on a second display. A window holding any `Normal` panel is a normal window, whichever
of its tabs shows; the rest are palettes.
- Palettes stay above other windows (`WindowLevel::AlwaysOnTop`), so clicking the document
does not bury them. While none of the app's windows has focus they hide
(`ViewportCommand::Visible(false)`), and they come back when one has it again. The hide
waits 250 ms, so focus moving between the app's own windows does not flicker.
does not bury them. They stay shown while the app is in the background, so files dragged
from another app reach them and an OS sheet attached to one stays open. While none of the
app's windows has focus they take `WindowLevel::Normal`, so they do not cover other apps
(on Windows `WindowLevel::AlwaysOnBottom`: there `Normal` would put them in front of the
app the user switched to), and they go back to `AlwaysOnTop` when one has it again. The app counts as in the
background after 250 ms without focus, so focus moving between its own windows changes
nothing. The level is sent only when it changes.
- Normal windows have `WindowLevel::Normal` and stay shown while the app is in the
background. They stack with the main window by focus, below every palette; a drag over a
normal window that lies behind the main window targets the main window where the two
overlap, and the normal window outside the main window.
- Like palettes, normal windows have no taskbar entry; they come forward with the app
instead. When one of the app's windows gains focus after none had it for the hide delay,
instead. When one of the app's windows gains focus after none had it for 250 ms,
the main window and each normal window get `WindowLevel::AlwaysOnTop` and then
`WindowLevel::Normal`, one window per frame, in their focus order from back to front.
They keep their order, nothing takes focus, and the window the user activated is raised
@@ -560,9 +579,8 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho
answers the OS close request with `ViewportCommand::CancelClose`. On macOS it is not
resizable meanwhile.
- *Stacking.* A palette drops to `WindowLevel::Normal` while blocked, so a click on the main
window brings it in front of a palette that covers the modal. A shown palette stays shown
while the app is in the background, since showing it again on the app's return would raise
it over the modal and, on macOS, give it focus. When the app is activated meanwhile, the
window brings it in front of a palette that covers the modal. It stays shown and at that
level when the app goes to the background and returns. When the app is activated meanwhile, the
main window is raised last, in front of the normal windows and of a palette the user
clicked.
- *Edges.* Turning blocking on cancels a drag in progress, and a widget of a native window
@@ -614,12 +632,17 @@ windows from the first frame. `DockArea::native_windows(bool)` overrides the cho
(the window's top-left is the pointer minus the grab offset, less the bar's height). A
Ctrl/Cmd-drag floats there wherever it is released.
- *Moving a window.* Dragging the bar, the only tab of a window, or the drag area of its only
group moves the window, so the pressed point stays under the pointer. Each move is worked
out from a pointer move of that frame and the window's reported place, which the pointer's
position is relative to; a move commanded earlier may land late. A frame with no pointer
move leaves the window. X11 sends no move whose position in the window repeats the last
one, so while the pointer keeps exact pace with the window the window can trail it by one
move until the next. Released over a target in another window, the whole window docks or
group moves the window, so the pressed point stays under the pointer. On macOS each frame
puts the window at the cursor's desktop position (AppKit's `NSEvent::mouseLocation`) less
the pressed point, and targets are hit at that position. AppKit reports a window's new place
as soon as a move is commanded, but measures the pointer events it sends against where the
window server had the window, which can be a move behind; placing the window from those
events would overshoot and step back. Elsewhere each move is worked out from a pointer move
of that frame and the window's reported place, which the pointer's position is relative to;
a move commanded earlier may land late. A frame with no pointer move leaves the window. X11
sends no move whose position in the window repeats the last one, so while the pointer keeps
exact pace with the window the window can trail it by one move until the next. Released over
a target in another window, the whole window docks or
joins there as its groups (`Action::MoveWindow`). The bar docks only with
`DockStyle::title_bar_docks`; the only tab or group docks either way, since it is the way
back in with that flag off. Targets: in the
@@ -719,7 +742,10 @@ OS windows: the main window's content sits at `origin` on the desktop, and each
runs its own frame with its own input and desktop rect (`host()`, `window_step`,
`window_click`). `NativeHost` can report a monitor size (`monitor_size`), give windows a frame
between their outer position and content (`frame`), land `OuterPosition` a frame late (`lag`),
and report no positions (`no_position`, as on Wayland). A `HostWindow` takes files dragged
and report no positions (`no_position`, as on Wayland). `window_pointer` moves the pointer to a
desktop point in a native window; the host can tell that point as the cursor's (`tells_cursor`,
as AppKit does) and measure the window's pointer events against its place some frames back
(`stale`, a pattern taken one entry per call). A `HostWindow` takes files dragged
over it (`hovered_files`) or dropped on it (`dropped_files`), and `with_native_input_hook`
passes a hook to `DockArea::native_input_hook` every frame.