Browse / github.com/storytold/cadcraft / patch
Multileaders: make their text editable like other annotations (fixes #1081)
[Bug Report] [CadCraft] Can't edit multileader text ### Description Create Multileader with text "example text" Double clicking the leader text does not enable editing. There is no text field in the Multileader Properties. There doesn't appear to be a way to change the multileader text. ### Ticket details | | | |---|---| | **App** | CadCraft | | **Type** | Bug Report | | **Reported by** | Bob (Discord username: bobbeats) — Discord user ID `983433270479970334` | | **Submitted** | 2026-10-13 23:13:32 UTC | | **Discord server** | ArtCraft (`1359579021108842617`) | | **Submitted from channel** | `1558917176453374102` | | **Submission ID** | `T
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- SusieeTheLinuxUser (imported from pull request #2033, open)
- Base
7bf5905ac97316a7896de8cbc12d243f57b09bd8- Size
- +97 −3 in 6 files
- Review
- pending
- Shared
- 2026-10-28
- Id
7c06baffdce01cd03e670212
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/modify2.rs crates/engine/src/cmd/modify2_tests.rs crates/engine/src/cmd/props.rs crates/ui-egui/src/canvas.rs crates/ui-egui/src/palettes.rs crates/ui-egui/tests/it/text_double_click.rs
@@ -2173,6 +2173,7 @@ fn text_of(k: &EntityKind) -> Option<String> { EntityKind::Text(t) => Some(t.value.clone()), EntityKind::AttDef(a) => Some(a.text.value.clone()), EntityKind::MText(t) => Some(t.contents.replace("\\P", "\n")),+ EntityKind::MLeader(m) => m.text.as_ref().map(|t| t.contents.replace("\\P", "\n")), EntityKind::Dimension(d) => Some(if d.text.is_empty() { "<>".into() } else { d.text.clone() }), _ => None, }@@ -2187,11 +2188,16 @@ pub(crate) fn set_text(s: &mut Session, h: Handle, text: &str) -> Result<()> { EntityKind::Text(t) => t.value = text.to_string(), EntityKind::AttDef(a) => a.text.value = text.to_string(), EntityKind::MText(t) => t.contents = text.replace('\n', "\\P"),+ EntityKind::MLeader(m) => match &mut m.text {+ Some(t) => t.contents = text.replace('\n', "\\P"),+ // A multileader carrying a block has no text to change (MLEADEREDIT moves that).+ None => return Err(other("This multileader carries a block, not text.")),+ }, EntityKind::Dimension(d) => { d.text = if text == "<>" { String::new() } else { text.to_string() }; d.block = None; }- _ => return Err(other("Select a text, mtext, attribute definition or dimension.")),+ _ => return Err(other("Select a text, mtext, attribute definition, dimension or multileader.")), } set_kind(s, h, k) }@@ -2245,7 +2251,8 @@ impl Interactive for TextEditM { match (&self.h, i) { (None, Input::Point(p)) => { let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;- let old = text_of(&curves::entity(s, h)?.kind).ok_or_else(|| other("Select a text, mtext, attribute definition or dimension."))?;+ let old = text_of(&curves::entity(s, h)?.kind)+ .ok_or_else(|| other("Select a text, mtext, attribute definition, dimension or multileader."))?; self.h = Some((h, old)); Ok(Step::Continue) }@@ -659,6 +659,24 @@ fn textedit_json_and_command_line() { assert!(matches!(kind(&s, &t), EntityKind::Text(x) if x.value == "hello world")); } +#[test]+fn textedit_edits_a_multileader() {+ let mut s = Session::new();+ let m = h(&s.execute("mleader", &json!({"points": [[10, 5], [0, 0]], "text": "example text"})).unwrap());+ s.execute("textedit", &json!({"handle": m, "text": "changed"})).unwrap();+ assert!(matches!(kind(&s, &m), EntityKind::MLeader(x) if x.text.as_ref().unwrap().contents == "changed"));+ // The Properties palette's Contents and Text height go through properties.set.+ s.execute("properties.set", &json!({"handles": [m], "text": "via the palette", "height": 0.5})).unwrap();+ assert!(+ matches!(kind(&s, &m), EntityKind::MLeader(x) if x.text.as_ref().map(|t| (t.contents.as_str(), t.height)) == Some(("via the palette", 0.5)))+ );+ // A block multileader has no text to edit.+ let sq = h(&s.execute("pline", &json!({"vertices": [[-1, -1], [1, -1], [1, 1], [-1, 1]], "closed": true})).unwrap());+ s.execute("block", &json!({"name": "TAG", "base": [-1, -1], "handles": [sq], "keep": "delete"})).unwrap();+ let b = h(&s.execute("mleader", &json!({"points": [[10, 5], [0, 0]], "block": "TAG"})).unwrap());+ assert!(s.execute("textedit", &json!({"handle": b, "text": "x"})).is_err());+}+ #[test] fn explode_polyline3d_keeps_elevations() { let mut s = Session::new();@@ -320,6 +320,17 @@ fn run_set(s: &mut Session, p: &Value) -> Result<Value> { t.rotation = x.to_radians(); } }+ // The multileader's own text; a block multileader has none to set.+ EntityKind::MLeader(m) => {+ if let Some(t) = &mut m.text {+ if let Some(v) = str_param(p, "text") {+ t.contents = v.replace('\n', "\\P");+ }+ if let Some(x) = num("height").filter(|x| *x > 0.0) {+ t.height = x;+ }+ }+ } EntityKind::LwPolyline(pl) => { if let Some(x) = num("width").filter(|x| *x >= 0.0) { pl.const_width = x;@@ -2039,7 +2039,12 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { fn edit_text_at(app: &mut CadApp, p: Vec2) -> bool { use cadcraft_engine::doc::EntityKind; let is_text = cadcraft_engine::cmd::curves::pick_at(&app.session, p).and_then(|h| {- app.session.doc().ok()?.entity(h).map(|e| matches!(e.kind, EntityKind::Text(_) | EntityKind::MText(_) | EntityKind::AttDef(_)))+ app.session.doc().ok()?.entity(h).map(|e| match &e.kind {+ EntityKind::Text(_) | EntityKind::MText(_) | EntityKind::AttDef(_) => true,+ // A multileader carrying a block has no text to edit.+ EntityKind::MLeader(m) => m.text.is_some(),+ _ => false,+ }) }); if is_text != Some(true) { return false;@@ -1055,6 +1055,17 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { num(ui, "Text height", "height", len(tx.height), &mut set, &mut bad, &ids); num(ui, "Defined width", "width", len(tx.width), &mut set, &mut bad, &ids); }+ // The multileader's own text; a block multileader has none, so it shows no fields.+ EntityKind::MLeader(m) => {+ if let Some(tx) = &m.text {+ prop_row(ui, "Contents", |ui| {+ if let Some(s) = edit_field(ui, ui.id().with(("mldr", &ids)), tx.contents.clone()) {+ set = Some(json!({ "handles": ids, "text": s }));+ }+ });+ num(ui, "Text height", "height", len(tx.height), &mut set, &mut bad, &ids);+ }+ } EntityKind::LwPolyline(pl) => { num(ui, "Global width", "width", len(pl.const_width), &mut set, &mut bad, &ids); let g = cadcraft_geom::Polyline { vertices: pl.vertices.clone(), closed: pl.closed };@@ -76,3 +76,45 @@ fn double_click_elsewhere_starts_no_command() { assert_eq!(h.state().session.running.as_ref().map(|r| r.id.as_str()), None, "{at:?}"); } }++#[test]+fn double_click_on_a_multileader_edits_its_text() {+ let mut h = harness();+ h.state_mut().session.execute("mleader", &json!({"points": [[10, 5], [0, 0]], "text": "example text"})).unwrap();+ h.run_steps(2);+ // The multileader's text sits at the end of its landing.+ double_click(&mut h, Vec2::new(-0.45, 0.05));+ let s = &h.state().session;+ assert_eq!(s.running.as_ref().map(|r| r.id.as_str()), Some("textedit"), "the clicked multileader is already picked");+ let p = s.current_prompt().unwrap();+ assert_eq!(p.message, "Enter new text");+ assert_eq!(p.default.as_deref(), Some("example text"));+ h.state_mut().session.input(Input::Text("changed text".into())).unwrap();+ h.run_steps(2);+ let got = h+ .state()+ .session+ .doc()+ .unwrap()+ .model+ .iter()+ .find_map(|e| match &e.kind {+ EntityKind::MLeader(m) => m.text.as_ref().map(|t| t.contents.clone()),+ _ => None,+ })+ .unwrap();+ assert_eq!(got, "changed text");+}++#[test]+fn double_click_on_a_block_multileader_starts_no_text_edit() {+ let mut h = harness();+ let s = &mut h.state_mut().session;+ let sq = s.execute("pline", &json!({"vertices": [[-1, -1], [1, -1], [1, 1], [-1, 1]], "closed": true})).unwrap();+ s.execute("block", &json!({"name": "TAG", "base": [-1, -1], "handles": [sq["handle"].as_str().or_else(|| sq["handles"][0].as_str()).unwrap()], "keep": "delete"})).unwrap();+ s.execute("mleader", &json!({"points": [[10, 5], [0, 0]], "block": "TAG"})).unwrap();+ h.run_steps(2);+ // On the leader line, halfway along.+ double_click(&mut h, Vec2::new(5.0, 2.5));+ assert_ne!(h.state().session.running.as_ref().map(|r| r.id.as_str()), Some("textedit"), "a block multileader has no text");+}