Fix OK lost while a joint animates; Browser Joints folder inside the root component
Clicking OK during the Joint dialog's motion animation was lost about half the time: the preview's busy spinner came and went in the dialog footer between frames, shifting the automatic ids of OK and Cancel, so a click pressed in one frame and released in the next landed on two different ids. The spinner's place is now always taken. Reproduced through the control channel (6 of 14 OK clicks at different animation phases lost before, 0 of 14 after) and covered by a test that presses OK with the preview busy and releases it once idle. The Joints folder is drawn by component_rows for the root, after its folders (as agreed with solvecraft-shell). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
bdc15d2204
commit
62dbf60eb2
4 files changed
+53
-5
No files matched your search
@@ -433,7 +433,6 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
component_rows(app, ui, 0, 0, &mut acts);
|
||||
crate::dialogs_assembly::browser_joints(app, ui);
|
||||
});
|
||||
});
|
||||
app.viewport.hover_bodies = acts.hover_bodies;
|
||||
@@ -535,6 +534,9 @@ fn component_rows(app: &mut SolveApp, ui: &mut egui::Ui, id: u64, depth: usize,
|
||||
for folder in ["bodies", "sketches", "construction"] {
|
||||
folder_rows(app, ui, id, folder, d, acts);
|
||||
}
|
||||
if id == 0 {
|
||||
crate::dialogs_assembly::browser_joints(app, ui, d);
|
||||
}
|
||||
let children: Vec<u64> = app.session.doc.components.iter().filter(|c| c.parent == id).map(|c| c.id).collect();
|
||||
for c in children {
|
||||
component_rows(app, ui, c, d, acts);
|
||||
|
||||
@@ -1386,8 +1386,13 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.painter().circle_stroke(r.center(), 7.0, Stroke::new(1.2, t.text_dim));
|
||||
ui.painter().text(r.center(), egui::Align2::CENTER_CENTER, "i", egui::FontId::proportional(11.0), t.text_dim);
|
||||
resp.on_hover_text(heading.as_str());
|
||||
// The spinner's place is always taken: a widget that comes and goes would shift
|
||||
// the automatic ids of OK and Cancel, and a click pressed in one frame and
|
||||
// released in the next would be lost (an animating preview toggles it often).
|
||||
let (sr, sresp) = ui.allocate_exact_size(vec2(14.0, 14.0), egui::Sense::hover());
|
||||
if app.preview.busy {
|
||||
ui.add(egui::Spinner::new().size(14.0)).on_hover_text("Updating the preview");
|
||||
egui::Spinner::new().size(14.0).paint_at(ui, sr);
|
||||
sresp.on_hover_text("Updating the preview");
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences) { "Close" } else { "Cancel" };
|
||||
|
||||
@@ -918,7 +918,7 @@ pub fn timeline_joints(app: &mut SolveApp, ui: &mut egui::Ui, p: &egui::Painter,
|
||||
|
||||
/// The Joints folder at the end of the Browser: a row per joint (double-click edits, the menu
|
||||
/// edits, drives, suppresses or deletes).
|
||||
pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
|
||||
let t = Tokens::get();
|
||||
let joints: Vec<(u64, String, JointKind, bool)> =
|
||||
app.session.doc.assembly.joints.iter().map(|j| (j.id, j.name.clone(), j.kind, j.suppressed)).collect();
|
||||
@@ -954,7 +954,8 @@ pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
);
|
||||
resp
|
||||
};
|
||||
if row(ui, 1.0, "folder", "Joints", false, Some(open)).clicked() {
|
||||
let depth = depth as f32;
|
||||
if row(ui, depth, "folder", "Joints", false, Some(open)).clicked() {
|
||||
open = !open;
|
||||
ui.data_mut(|d| d.insert_temp(fold_id, open));
|
||||
}
|
||||
@@ -964,7 +965,7 @@ pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let mut dialog: Option<Dialog> = None;
|
||||
let mut run: Option<(&'static str, Value)> = None;
|
||||
for (id, name, kind, suppressed) in joints {
|
||||
let resp = row(ui, 2.0, "joint", &name, suppressed, None).on_hover_text(format!("{kind:?} joint"));
|
||||
let resp = row(ui, depth + 1.0, "joint", &name, suppressed, None).on_hover_text(format!("{kind:?} joint"));
|
||||
if resp.double_clicked() {
|
||||
dialog = edit_joint(app, id);
|
||||
}
|
||||
|
||||
@@ -163,3 +163,43 @@ fn circles_from_edge_polylines() {
|
||||
let line: Vec<Vec3> = (0..10).map(|i| Vec3::new(f64::from(i), 0.0, 0.0)).collect();
|
||||
assert!(circle_of(&line).is_none());
|
||||
}
|
||||
|
||||
/// The centre of the first text shape reading `text` in a frame's output.
|
||||
fn text_at(out: &egui::FullOutput, text: &str) -> Option<egui::Pos2> {
|
||||
out.shapes.iter().find_map(|c| match &c.shape {
|
||||
egui::Shape::Text(t) if t.galley.text() == text => Some(t.pos + t.galley.rect.center().to_vec2()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// OK pressed while the animated preview is computing and released once it is not (the busy
|
||||
/// spinner coming and going between the two frames) still applies the joint.
|
||||
#[test]
|
||||
fn ok_click_survives_a_busy_preview() {
|
||||
let mut app = two_boxes();
|
||||
app.start("JointAssembleCmdNew");
|
||||
let mut d = app.dialog.take().unwrap();
|
||||
d.pick(&app.session, face_sel(&app.session, Vec3::new(10.0, 10.0, 10.0)).unwrap());
|
||||
d.pick(&app.session, face_sel(&app.session, Vec3::new(40.0, 10.0, 0.0)).unwrap());
|
||||
if let Kind::Assembly(Asm::Joint(f)) = &mut d.kind {
|
||||
f.kind = 1;
|
||||
f.animate = true;
|
||||
}
|
||||
app.dialog = Some(d);
|
||||
let ctx = egui::Context::default();
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
|
||||
let frame = |app: &mut SolveApp, busy: bool, events: Vec<egui::Event>| {
|
||||
app.preview.busy = busy;
|
||||
let input = egui::RawInput { screen_rect: Some(screen), events, ..Default::default() };
|
||||
ctx.run_ui(input, |ui| crate::dialogs::show(app, ui.ctx()))
|
||||
};
|
||||
frame(&mut app, false, vec![]);
|
||||
let out = frame(&mut app, false, vec![]);
|
||||
let ok = text_at(&out, "OK").expect("an OK button");
|
||||
let button = |pressed| egui::Event::PointerButton { pos: ok, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, true, vec![egui::Event::PointerMoved(ok)]);
|
||||
frame(&mut app, true, vec![button(true)]);
|
||||
frame(&mut app, false, vec![button(false)]);
|
||||
assert_eq!(app.session.doc.assembly.joints.len(), 1, "the click applied the joint");
|
||||
assert!(app.dialog.is_none());
|
||||
}
|
||||
Reference in new issue
Block a user