craft-ui: a tab dragged out of the window floats where it left

On X11 (including XWayland) and Windows, the pointer leaving the window
reaches egui as PointerGone, and egui-winit then drops the button release
because it has no pointer position. finish_drag never saw the drag end:
the panel stayed in its dock with the drop outline following the pointer
until the next click inside the window.

Leaving the window now ends the drag as a drop. interact_pos still holds
the last position on that frame, and the panel floats there (clamped to
the window) instead of docking into whatever target sits at the edge.

Test: tab_dragged_out_of_the_window_floats_where_it_left. It fails without
the fix (no action at all).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit aecdf819ba)
This commit is contained in:
prekabreki authored and aiongg committed 2026-10-19 04:17:34 +00:00
1 parent ae2f796b67
commit 8db84c54f1
2 files changed
+32 -4

No files matched your search

+8 -4
View File
@@ -829,8 +829,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
fn finish_drag(&mut self, ctx: &Context) {
let Some(drag) = ctx.data(|d| d.get_temp::<Drag<P>>(self.id.with("drag"))) else { return };
let (pointer, down, released, focused) =
ctx.input(|i| (i.pointer.interact_pos(), i.pointer.primary_down(), i.pointer.primary_released(), i.focused));
let (pointer, down, released, focused, gone) =
ctx.input(|i| (i.pointer.interact_pos(), i.pointer.primary_down(), i.pointer.primary_released(), i.focused, !i.pointer.has_pointer()));
// Leaving the window is the drop: on X11 and Windows the release outside never reaches
// egui, so the drag would otherwise stay on the pointer. `interact_pos` still holds the
// last position this frame, and the panel floats there rather than docking at the edge.
let released = released || gone;
let escape = ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, Key::Escape));
if escape || !focused || !self.layout.contains(&drag.panel) || (!down && !released) {
cancel_drag::<P>(ctx, self.id);
@@ -854,10 +858,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
if pointer.distance(drag.origin) < 2.0 {
return;
}
let action = if let Some(proposal) = proposal {
let action = if let Some(proposal) = proposal.filter(|_| !gone) {
Some(Action::Move { panel: drag.panel.clone(), anchor: proposal.anchor, placement: proposal.placement })
} else if (self.permissions)(&drag.panel).floatable
&& (!drag.source.contains(pointer) || self.layout.floating.iter().any(|g| g.panels.contains(&drag.panel)))
&& (gone || !drag.source.contains(pointer) || self.layout.floating.iter().any(|g| g.panels.contains(&drag.panel)))
{
let rect = clamp_float(Rect::from_min_size(pointer - drag.grab, drag.source.size()), ctx.content_rect());
Some(Action::Float { panel: drag.panel.clone(), rect: [rect.left(), rect.top(), rect.width(), rect.height()] })
+24
View File
@@ -395,6 +395,30 @@ fn actual_pointer_drag_docks_a_floating_panel_into_the_main_tab_strip() {
assert_eq!(panels.get(*active), Some(&5));
}
/// On X11 and Windows a release outside the window never reaches egui (egui-winit drops a
/// button event once the pointer has left), so a tab dragged out of the window used to stay on
/// the pointer for good. Leaving the window detaches the panel where it left, even over a dock
/// target, and ends the drag.
#[test]
fn tab_dragged_out_of_the_window_floats_where_it_left() {
let ctx = Context::default();
let mut layout = sample();
for _ in 0..3 {
step(&ctx, &mut layout, Vec::new());
}
let tab = ctx.read_response(Id::new("test-area").with(("tab", &3u32))).unwrap().rect.center();
let exit = pos2(4.0, 300.0);
step(&ctx, &mut layout, vec![Event::PointerMoved(tab), pointer_button(tab, true)]);
step(&ctx, &mut layout, vec![Event::PointerMoved(tab + vec2(-8.0, 0.0))]);
step(&ctx, &mut layout, vec![Event::PointerMoved(exit)]);
let actions = step(&ctx, &mut layout, vec![Event::PointerGone]);
assert!(actions.iter().any(|a| matches!(a, Action::Float { panel: 3, .. })), "{actions:?}");
assert!(layout.floating.iter().any(|g| g.panels == [3]), "{layout:?}");
let back = pos2(400.0, 300.0);
assert!(step(&ctx, &mut layout, vec![Event::PointerMoved(back)]).is_empty());
assert!(step(&ctx, &mut layout, vec![pointer_button(back, false)]).is_empty(), "the drag ended when the pointer left");
}
#[test]
fn panel_size_limits_clamp_geometry_without_overwriting_preferred_widths() {
let layout = Layout {