Merge pull request #933 from SiadeLucas/feat/pages-scroll-area

ui: keep the current page scrolled into view in the pages list (#930)
This commit is contained in:
BFlat
2026-10-21 16:21:38 +07:00
committed by GitHub
4 changed files with 75 additions and 0 deletions
+4
View File
@@ -24,6 +24,10 @@ display_name = "VanTan"
real_name = "Tyler Jennings"
display_name = "tylerjenningsw"
[people.SiadeLucas]
real_name = "Siade Lucas"
sync_github_name = true
[people.Silvertree2010]
display_name = "Netter"
sync_github_name = false
+12
View File
@@ -229,6 +229,9 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
let mut move_to: Option<(NodeId, String)> = None;
// Pages are dragged up and down the list to reorder them.
let mut reorder = crate::list_drag::ListDrag::in_order(ui, "pages");
let page_key = (uid, cur);
let scroll_to_cur = app.ui.pages_seen_page != Some(page_key);
let mut current_row_rendered = false;
// A long list scrolls in its own area, so the layers keep their room below it.
let max_h = pages_list_height(app.ui.pages_height, ui.available_height());
let mut moved = egui::ScrollArea::vertical().id_salt("pages_list").max_height(max_h).show(ui, |ui| {
@@ -238,6 +241,12 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
}
let sense = if filtering { Sense::click() } else { Sense::click_and_drag() };
let (r, resp) = ui.allocate_exact_size(vec2(w, 32.0), sense);
if id == cur {
current_row_rendered = true;
if scroll_to_cur {
ui.scroll_to_rect(r, None);
}
}
if !filtering {
reorder.row_by(&resp, k, r, || if divider { tr("Divider").to_string() } else { name.clone() });
}
@@ -301,6 +310,9 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
}
reorder.finish(ui)
});
if current_row_rendered {
app.ui.pages_seen_page = Some(page_key);
}
// 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)) {
@@ -507,3 +507,59 @@ fn the_pages_list_keeps_three_rows_and_leaves_the_layers_room() {
// A panel too short for both keeps the three rows.
assert_eq!(pages_list_height(Some(500.0), 100.0), 3.0 * ROW);
}
/// Selecting a page that is far down the list automatically scrolls it into view.
#[test]
fn selecting_a_distant_page_scrolls_it_into_view() {
let mut s = Session::new();
let rect = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap());
for _ in 0..40 {
s.execute("page.add", &json!({})).unwrap();
}
let pages = s.doc().unwrap().pages.clone();
let first = pages[0];
let last = *pages.last().unwrap();
s.execute("page.select", &json!({"id": first})).unwrap();
s.execute("select.none", &json!({})).unwrap();
let mut u = Ui::new(s);
let layer = u.row_at(rect);
u.frame(vec![]);
// Select the last page.
u.app.run("page.select", json!({"id": last}));
for _ in 0..15 {
u.frame(vec![]);
}
assert_eq!(u.app.ui.pages_seen_page, Some((u.app.session.ds().unwrap().uid, last)));
let opened = pages_down_to(&mut u, layer.y - 60.0);
let top = pages.iter().position(|p| Some(p) == opened.first()).unwrap();
assert!(top > 20, "the list scrolled past its first pages to reveal the selection (top row: page {top})");
}
/// If the active page is filtered out by search, it is not marked as seen
/// until the filter is cleared and its row actually renders.
#[test]
fn filtered_out_active_page_is_seen_only_after_rendering() {
let mut s = Session::new();
let p1 = s.doc().unwrap().pages[0];
let p2 = id(s.execute("page.add", &json!({})).unwrap());
s.execute("page.rename", &json!({"id": p1, "name": "Alpha"})).unwrap();
s.execute("page.rename", &json!({"id": p2, "name": "Beta"})).unwrap();
s.execute("page.select", &json!({"id": p1})).unwrap();
let uid = s.ds().unwrap().uid;
let mut u = Ui::new(s);
assert_eq!(u.app.ui.pages_seen_page, Some((uid, p1)));
// Filter to only "Alpha", then select "Beta" (p2).
u.app.ui.page_search = true;
u.app.ui.page_filter = "Alpha".into();
u.app.run("page.select", json!({"id": p2}));
u.frame(vec![]);
// Beta was filtered out and not rendered, so pages_seen_page has not recorded it.
assert_ne!(u.app.ui.pages_seen_page, Some((uid, p2)));
// Clear filter: Beta renders now and is recorded.
u.app.ui.page_filter.clear();
u.frame(vec![]);
assert_eq!(u.app.ui.pages_seen_page, Some((uid, p2)));
}
+3
View File
@@ -497,6 +497,8 @@ pub struct UiState {
pub page_filter: String,
/// The height asked for the pages list (`None`: a share of the panel).
pub pages_height: Option<f32>,
/// The document and active page last drawn in the pages list.
pub pages_seen_page: Option<(u64, NodeId)>,
pub expanded: BTreeSet<NodeId>,
/// The layer being renamed in the layers panel.
pub rename: Option<layers::Rename>,
@@ -669,6 +671,7 @@ impl Default for UiState {
page_search: false,
page_filter: String::new(),
pages_height: None,
pages_seen_page: None,
expanded: BTreeSet::new(),
rename: None,
rename_page: None,