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}) }