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"); +}