From 50f12376179a34ba91c4fb3c01bcb65a265ddac8 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 15 Oct 2026 18:52:54 +0700 Subject: [PATCH] #272: refuse page drops released outside the visible pages list A page drag released over the Layers below could still reorder against a row scrolled out of view, and a layer dropped there could move to that page. The drop now counts only inside the list's visible area. The guard is ported from #366 by Skyfuller1303 (@Skyfuller1303). --- crates/ui-egui/src/layers.rs | 8 +++++++- crates/ui-egui/src/layers_behaviour_tests.rs | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/layers.rs b/crates/ui-egui/src/layers.rs index fd51c67e..97203f94 100644 --- a/crates/ui-egui/src/layers.rs +++ b/crates/ui-egui/src/layers.rs @@ -217,7 +217,7 @@ fn pages(app: &mut FigApp, ui: &mut Ui) { let mut reorder = crate::list_drag::ListDrag::in_order(ui, "pages"); // A long list scrolls in its own area, so the layers keep their room below it. let max_h = (ui.available_height() * 0.4).max(3.0 * ROW); - let moved = egui::ScrollArea::vertical().id_salt("pages_list").max_height(max_h).show(ui, |ui| { + let mut moved = egui::ScrollArea::vertical().id_salt("pages_list").max_height(max_h).show(ui, |ui| { for (k, (id, name, divider)) in pages.iter().cloned().enumerate() { let (r, resp) = ui.allocate_exact_size(vec2(w, 32.0), Sense::click_and_drag()); reorder.row_by(&resp, k, r, || if divider { tr("Divider").to_string() } else { name.clone() }); @@ -279,6 +279,12 @@ fn pages(app: &mut FigApp, ui: &mut Ui) { } reorder.finish(ui) }); + // A drop outside the list's visible area (and the row gap at its edges) isn't on a page row, + // even one scrolled out of view. + if !ui.input(|i| i.pointer.latest_pos()).is_some_and(|p| moved.inner_rect.expand2(vec2(0.0, 3.0)).contains(p)) { + moved.inner = None; + move_to = None; + } if let Some((from, to)) = moved.inner && let Some((id, ..)) = pages.get(from) { diff --git a/crates/ui-egui/src/layers_behaviour_tests.rs b/crates/ui-egui/src/layers_behaviour_tests.rs index b4bb1c13..43a8b76e 100644 --- a/crates/ui-egui/src/layers_behaviour_tests.rs +++ b/crates/ui-egui/src/layers_behaviour_tests.rs @@ -271,6 +271,26 @@ fn a_long_pages_list_scrolls_and_leaves_room_for_the_layers() { assert!(top > 30, "the list scrolled past its first pages (top row: page {top})"); } +#[test] +fn a_page_dropped_below_the_pages_list_stays_where_it_was() { + let mut s = Session::new(); + let first = s.page().unwrap(); + let rect = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()); + let second = id(s.execute("page.add", &json!({})).unwrap()); + for _ in 0..58 { + s.execute("page.add", &json!({})).unwrap(); + } + let pages = s.doc().unwrap().pages.clone(); + s.execute("page.select", &json!({"id": first})).unwrap(); + s.execute("select.none", &json!({})).unwrap(); + let mut u = Ui::new(s); + let (row, layer) = (page_row(&mut u, second, first), u.row_at(rect)); + // Released over the layers, where pages scrolled out of view would be: no page row is there. + u.drag_to(row, layer); + u.button(layer, false); + assert_eq!(u.app.session.doc().unwrap().pages, pages, "the pages keep their order"); +} + /// A double click at `p`, with the screen reader tree checked on every frame: the platform adapter /// (UI Automation on Windows) aborts the app when the focused node isn't in the tree. fn double_click_checking_focus(u: &mut Ui, p: Pos2) {