From 7bbc5b0319c326aa9716d593c914cfd455eeb66d Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sat, 10 Oct 2026 22:57:52 +0100 Subject: [PATCH] fix(ui): explain silent anno-scale toggles, theme xref empty state AnnoObjectScaleToggle had three silent no-op paths (no target, locked layer, unknown scale); each now pushes command-line info mirroring the open-path guards. Xref empty-state hardcoded grey now uses muted_style. DD_BOUNDS eviction and attr-clamp changes investigated and deliberately dropped (no reachable defect in either). Tests: anno_scale_toggle_tests cover all three paths (RED: silence). Full suite: 2032 passed / 0 failed / 25 ignored, zero warnings. --- src/app/update/mod.rs | 129 ++++++++++++++++++++++++++++------ src/ui/window/xref_manager.rs | 7 +- 2 files changed, 109 insertions(+), 27 deletions(-) diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index d11a087ed..334b0b49b 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -4588,28 +4588,39 @@ impl OpenCADStudio { } Message::AnnoObjectScaleToggle(name) => { let i = self.active_tab; - if let Some(entity) = self.anno_object_scale_target { - if self.tabs[i].scene.is_layer_locked(entity) { - return Task::none(); - } - if let Some(sh) = self.tabs[i].scene.scale_handle_ensuring(&name) { - self.push_undo_snapshot(i, "OBJECTSCALE"); - let doc = &mut self.tabs[i].scene.document; - let is_member = - crate::scene::annotative::object_scale_memberships(doc, entity) - .iter() - .any(|(_, h)| *h == sh); - if is_member { - crate::scene::annotative::remove_annotation_context_for_scale( - doc, entity, sh, - ); - } else { - crate::scene::annotative::create_annotation_context(doc, entity, sh); - } - self.tabs[i].dirty = true; - self.invalidate_property_targets(i, &[entity]); - self.refresh_properties(); + let Some(entity) = self.anno_object_scale_target else { + self.command_line.push_info( + crate::t!("Select one object first, then run OBJECTSCALE.").as_ref(), + ); + return Task::none(); + }; + if self.tabs[i].scene.is_layer_locked(entity) { + self.command_line.push_info( + crate::t!("The object is on a locked layer.").as_ref(), + ); + return Task::none(); + } + if let Some(sh) = self.tabs[i].scene.scale_handle_ensuring(&name) { + self.push_undo_snapshot(i, "OBJECTSCALE"); + let doc = &mut self.tabs[i].scene.document; + let is_member = + crate::scene::annotative::object_scale_memberships(doc, entity) + .iter() + .any(|(_, h)| *h == sh); + if is_member { + crate::scene::annotative::remove_annotation_context_for_scale( + doc, entity, sh, + ); + } else { + crate::scene::annotative::create_annotation_context(doc, entity, sh); } + self.tabs[i].dirty = true; + self.invalidate_property_targets(i, &[entity]); + self.refresh_properties(); + } else { + self.command_line.push_info( + crate::tf!("Unknown annotation scale '{name}'.").as_ref(), + ); } Task::none() } @@ -11139,6 +11150,82 @@ mod prop_pointer_tests { } } +#[cfg(test)] +mod anno_scale_toggle_tests { + // #59: silent no-op toggles must explain themselves on the command line. + use super::Message; + use crate::app::OpenCADStudio; + + fn app_with_target() -> (OpenCADStudio, usize) { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let i = app.active_tab; + let mut line = codec::entities::Line::new(); + line.start = codec::types::Vector3::ZERO; + line.end = codec::types::Vector3::new(1.0, 0.0, 0.0); + let h = app + .commit_entity_handle(codec::EntityType::Line(line)) + .expect("line commits"); + app.anno_object_scale_target = Some(h); + (app, i) + } + + #[test] + fn locked_layer_toggle_explains_itself() { + let (mut app, i) = app_with_target(); + app.tabs[i] + .scene + .document + .layers + .get_mut("0") + .expect("layer 0 exists") + .flags + .locked = true; + let before = app.command_line.history.len(); + let _ = app.update(Message::AnnoObjectScaleToggle("1:50".to_string())); + let new: Vec = app.command_line.history[before..] + .iter() + .map(|e| e.text.clone()) + .collect(); + assert!( + new.iter().any(|t| t.to_lowercase().contains("locked")), + "locked-layer toggle must say so, got {new:?}" + ); + } + + #[test] + fn unknown_scale_toggle_explains_itself() { + let (mut app, _i) = app_with_target(); + let before = app.command_line.history.len(); + let _ = app.update(Message::AnnoObjectScaleToggle("NOPE".to_string())); + let new: Vec = app.command_line.history[before..] + .iter() + .map(|e| e.text.clone()) + .collect(); + assert!( + !new.is_empty(), + "unknown-scale toggle must say something, got silence" + ); + } + + #[test] + fn missing_target_toggle_explains_itself() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.anno_object_scale_target = None; + let before = app.command_line.history.len(); + let _ = app.update(Message::AnnoObjectScaleToggle("1:50".to_string())); + let new: Vec = app.command_line.history[before..] + .iter() + .map(|e| e.text.clone()) + .collect(); + assert!( + !new.is_empty(), + "target-less toggle must say something, got silence" + ); + } +} + #[cfg(test)] mod free_text_entry_tests { //! The free-form text prompts (table cell content, TEXT / MTEXT bodies) diff --git a/src/ui/window/xref_manager.rs b/src/ui/window/xref_manager.rs index a79f5328c..af9d20634 100644 --- a/src/ui/window/xref_manager.rs +++ b/src/ui/window/xref_manager.rs @@ -879,12 +879,7 @@ impl XrefManagerPanel { container( text(crate::t!("XREF No external references in this drawing.")) .size(12) - .color(iced::Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, - }), + .style(muted_style), ) .center_x(Fill) .center_y(Fill)