diff --git a/ROADMAP.md b/ROADMAP.md index ad930f54..01c0079a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -292,6 +292,9 @@ headline counts in-scope tabs only and lists the deferred ones separately. continuous collision detection; local thin walls and small details remain limitations. See [contact-set limits and recovery](docs/contact-sets.md). +Feature dialog header chevrons collapse and restore the form without discarding selections +or edited values, including the Appearance panel. + ## Document-opening performance - Cold rebuild profiling separates parsing, geometry, placement, display meshing and diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index a83be268..0cbe7c62 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1326,7 +1326,17 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { d.collapsed = !d.collapsed; } ui.label(RichText::new(heading.as_str()).strong().size(12.5)); - crate::frame::right_aligned(ui, total, 8.0, |ui| ui.label(RichText::new("»").color(t.text_dim))); + let chevron_width = ui.spacing().interact_size.x; + crate::frame::right_aligned(ui, total, chevron_width, |ui| { + let chevron = if d.collapsed { "«" } else { "»" }; + if ui + .add(egui::Button::new(RichText::new(chevron).color(t.text_dim)).frame(false).min_size(vec2(chevron_width, 0.0))) + .on_hover_text(if d.collapsed { "Expand" } else { "Collapse" }) + .clicked() + { + d.collapsed = !d.collapsed; + } + }); }); if d.collapsed { return; diff --git a/crates/ui-egui/src/dialogs_appearance.rs b/crates/ui-egui/src/dialogs_appearance.rs index 470ecf9d..b52d501a 100644 --- a/crates/ui-egui/src/dialogs_appearance.rs +++ b/crates/ui-egui/src/dialogs_appearance.rs @@ -594,6 +594,27 @@ mod tests { assert_eq!(a.session.doc.appearances.bodies.get("Body1"), a.session.doc.appearances.bodies.get("Body2")); } + #[test] + fn header_chevron_folds_and_restores_appearance_inputs() { + let mut h = crate::scenario::Harness::new(); + h.app.run("solid.box", json!({"length":10,"width":10,"height":10})).unwrap(); + h.app.session.selection = vec![Sel::Body { name: "Body1".into() }]; + h.app.start("appearance.assign"); + h.frames(3); + let before = h.app.dialog.clone().unwrap(); + h.step(&json!({"widget":"»"})).unwrap(); + assert!(h.app.dialog.as_ref().unwrap().collapsed); + assert!(h.widget("Appearance name").is_none()); + h.step(&json!({"widget":"«"})).unwrap(); + let after = h.app.dialog.as_ref().unwrap(); + assert!(!after.collapsed); + assert_eq!(format!("{:?}", after.inputs), format!("{:?}", before.inputs)); + let (Kind::Appearance(before), Kind::Appearance(after)) = (&before.kind, &after.kind) else { panic!("appearance dialog") }; + assert_eq!(before.color, after.color); + assert_eq!(before.opacity, after.opacity); + assert_eq!(before.search, after.search); + } + #[test] fn real_frames_and_unchanged_apply_keep_precise_opacity() { let mut h = crate::scenario::Harness::new();