mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 23:59:16 +00:00
Reconcile native docking fixes with egui 0.37 input routing
This commit is contained in:
commit
42e30cd5b3
18 files changed
+1114
-282
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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 } }]);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user