From 6b24e3cd6ab4ae513caef2cf443f55505cec8d1e Mon Sep 17 00:00:00 2001 From: sungjea kim Date: Tue, 20 Oct 2026 08:25:19 +0900 Subject: [PATCH] Export for Screens: the Assets tab scrolls to the assets it opens on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export Selection… and the Asset Export panel open Export for Screens on the Assets tab with the new assets checked, but new assets land at the end of the list, so in a document with many assets the checked one sat below the list's visible part and had to be scrolled to by hand. The list now scrolls to the first checked asset when the dialog opens on them. It keeps asking until the row is in view (the dialog's first frame only measures it), then stops, so the list scrolls freely afterwards. --- .../ui-egui/src/dialogs/export_for_screens.rs | 19 +++++++++- .../src/dialogs/tests_screen_assets.rs | 35 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index f71847f03..e8fc11484 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -133,6 +133,10 @@ pub(crate) fn open_assets(app: &mut VectorcraftApp, checked: Option<&[u64]>) { d.fields.insert("tab".into(), json!("assets")); if let Some(c) = checked { d.fields.insert("assets".into(), json!(c)); + // The list scrolls to the first of them once: new assets land at its end. + if let Some(first) = c.first() { + d.fields.insert("__scrollTo".into(), json!(first)); + } } } } @@ -544,9 +548,12 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { on.clear(); } }); + // Asked for until the row is in view: the dialog's first frame only measures it. + let scroll_to = d.fields.get("__scrollTo").and_then(Value::as_u64).filter(|id| st.doc.assets.iter().any(|a| a.id == *id)); + let mut in_view = scroll_to.is_none(); egui::ScrollArea::vertical().max_height(330.0).show(ui, |ui| { for a in &st.doc.assets { - ui.horizontal(|ui| { + let row = ui.horizontal(|ui| { let (r, _) = ui.allocate_exact_size(egui::vec2(46.0, 46.0), egui::Sense::hover()); crate::panels::asset_export::paint_thumb(ui, st, a, r); let checked = on.contains(&a.id); @@ -557,8 +564,18 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { } } }); + if scroll_to == Some(a.id) { + if ui.clip_rect().contains_rect(row.response.rect) { + in_view = true; + } else { + row.response.scroll_to_me(Some(egui::Align::Center)); + } + } } }); + if in_view { + d.fields.remove("__scrollTo"); + } if on != before { d.fields.insert("assets".into(), json!(on)); } diff --git a/crates/ui-egui/src/dialogs/tests_screen_assets.rs b/crates/ui-egui/src/dialogs/tests_screen_assets.rs index c48362ea0..18111e552 100644 --- a/crates/ui-egui/src/dialogs/tests_screen_assets.rs +++ b/crates/ui-egui/src/dialogs/tests_screen_assets.rs @@ -145,3 +145,38 @@ fn the_menus_reach_asset_export() { app.run("assets.add", json!({"multiple": false})).unwrap(); assert_eq!(assets(&mut app).len(), 1); } + +/// The Assets tab opened on new assets scrolls its list to the first checked one, once: they land +/// at the end of a long list. +#[test] +fn the_assets_tab_scrolls_to_the_new_asset() { + let (mut app, _, ids) = app(true); + for i in 0..30 { + let id = app.run("shape.rectangle", json!({"x": 5 * i, "y": 50, "width": 4, "height": 4})).unwrap()["id"].clone(); + app.run("assets.add", json!({"ids": [id]})).unwrap(); + } + app.run("select.set", json!({"ids": ids})).unwrap(); + let r = app.run("file.exportSelection", json!({"multiple": false})).unwrap(); + let new = r["assets"][0].clone(); + assert_eq!(assets(&mut app).len(), 31); + assert_eq!(assets(&mut app).last().unwrap()["id"], new); + assert_eq!(field(&app, "__scrollTo"), new); + let name = assets(&mut app).last().unwrap()["name"].as_str().unwrap().to_string(); + // The rows below the list's visible part are painted but clipped away: the new one is inside + // its clip once the frame after the opening one has scrolled. + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut shown = false; + // Scrolling is animated: a second of frames lets it land. + for i in 0..60 { + let input = egui::RawInput { time: Some(f64::from(i) / 60.0), ..egui::RawInput::default() }; + let mut out = ctx.run_ui(input, |ui| super::show(&mut app, ui.ctx())); + out.textures_delta.clear(); + shown = out.shapes.iter().any(|c| match &c.shape { + egui::Shape::Text(t) => t.galley.text() == name && c.clip_rect.contains_rect(t.visual_bounding_rect()), + _ => false, + }); + } + assert!(shown, "{name} is clipped out of the list"); + assert_eq!(field(&app, "__scrollTo"), Value::Null, "scrolled once"); +}