From e91b19f2a54f840d5f727b114af1480109947796 Mon Sep 17 00:00:00 2001 From: pravdomil <2387356+pravdomil@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:39:27 +0200 Subject: [PATCH] Floating panels: no widget ID clash when dragged past the window edge [skip ci] The floating group's title strip used the same id as its Area's own background widget, and the Area was shifted back into the window while the group's rect kept moving, so the two no longer overlapped and egui painted "Double use of widget ID" over the panel. The strip has its own id and the group is drawn where its rect says. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dock_ui.rs | 7 +++++-- crates/ui-egui/tests/shared_docking_ui.rs | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dock_ui.rs b/crates/ui-egui/src/dock_ui.rs index de971bdc..a92d376e 100644 --- a/crates/ui-egui/src/dock_ui.rs +++ b/crates/ui-egui/src/dock_ui.rs @@ -330,12 +330,15 @@ impl EffectcraftApp { )); let id = egui::Id::new(("floating-panel", f.panels.first().map(|p| p.id()).unwrap_or_default())); let mut group = None; - egui::Area::new(id).order(egui::Order::Middle).fixed_pos(rect.min).interactable(true).show(ctx, |ui| { + // Not constrained to the window: the group is drawn where its rect says (the clamp above + // keeps its strip reachable), so it follows the pointer past the window's edges. + egui::Area::new(id).order(egui::Order::Middle).fixed_pos(rect.min).constrain(false).interactable(true).show(ctx, |ui| { let (r, _) = ui.allocate_exact_size(rect.size(), Sense::hover()); ui.painter().rect_filled(r.expand(1.0), t.radius, Color32::from_black_alpha(90)); let strip = Rect::from_min_size(r.min, vec2(r.width(), t.tab_h)); // Move by the strip (tabs, registered later, take precedence over it). - let mv = ui.interact(strip, id.with("move"), Sense::drag()); + // (`id.with("move")` is the id of the `Area`'s own background widget.) + let mv = ui.interact(strip, id.with("strip"), Sense::drag()); self.auto.add(&format!("panel.float.{i}.move"), strip, "Move floating panel"); let mut nr = r; if mv.dragged() { diff --git a/crates/ui-egui/tests/shared_docking_ui.rs b/crates/ui-egui/tests/shared_docking_ui.rs index a1bc9280..ad56e68d 100644 --- a/crates/ui-egui/tests/shared_docking_ui.rs +++ b/crates/ui-egui/tests/shared_docking_ui.rs @@ -273,6 +273,29 @@ fn float_command_rejects_malformed_rectangles_atomically() { assert!(!app.edit_layout(|layout| layout.apply(Action::Float { panel: PanelKind::Project, rect: [1.0, 2.0, 300.0, 200.0] }).is_ok())); } +/// A floating panel dragged past the window's bottom edge is drawn where its rect says: it was +/// shifted back into the window while its rect kept moving, and its strip then clashed with the +/// floating area's own widget ("Double use of widget ID"). +#[test] +fn floating_panel_dragged_past_the_window_edge_is_drawn_at_its_rect() { + let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(Session::default())); + h.run_steps(3); + let ctx = h.ctx.clone(); + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "window.floatPanel", json!({"panel": "Preview", "rect": [600.0, 400.0, 360.0, 300.0]})).unwrap(); + h.run_steps(3); + let strip = rect(&h, "panel.float.0.move"); + let from = pos2(strip.right() - 24.0, strip.center().y); + drag(&mut h, from, from + vec2(40.0, 420.0), Modifiers::NONE); + let moved = h.state().ui.floating[0].rect; + assert!(moved[1] + moved[3] > 1000.0, "the panel hangs past the bottom edge: {moved:?}"); + let strip = rect(&h, "panel.float.0.move"); + assert_eq!([strip.min.x, strip.min.y], [moved[0], moved[1]]); + // The strip still moves it from there. + let from = pos2(strip.right() - 24.0, strip.center().y); + drag(&mut h, from, from - vec2(30.0, 200.0), Modifiers::NONE); + assert_eq!(h.state().ui.floating[0].rect[1], moved[1] - 200.0); +} + fn action_state(h: &Harness<'_, EffectcraftApp>, action: &str) -> serde_json::Value { json!({"action": action, "dock": h.state().ui.dock, "floating": h.state().ui.floating, "focused": h.state().ui.focused}) }