From 2effc8bf3ef1d3e0a777592d6c7368e82d31cd27 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:38:18 +0200 Subject: [PATCH] Double-click on text, mtext or an attribute definition starts TEXTEDIT on it (part of #842) (#868) --- crates/ui-egui/src/canvas.rs | 26 ++++++++ crates/ui-egui/tests/text_double_click.rs | 78 +++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 crates/ui-egui/tests/text_double_click.rs diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index cab997a6..e83c703d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1207,6 +1207,15 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { return; } } + // Double-click text to edit it, as in AutoCAD: TEXTEDIT with that object already picked. + if inside + && dbl_primary + && app.session.running.is_none() + && let Some(hp) = hover_pos + && edit_text_at(app, xf.to_world(hp)) + { + return; + } // Double-click a hatch (no command running) to edit it in the Hatch Edit dialog. The click is // used up: it must not also start a grip edit on the hatch's centre grip below. @@ -1627,6 +1636,23 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) { } /// The model space tiles on screen (none without tiling): each tile's rectangle in `full`. +/// Start TEXTEDIT on the text, mtext or attribute definition at `p` (the objects its shortcut menu +/// offers Edit Text for); false when there is none. +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(_))) + }); + if is_text != Some(true) { + return false; + } + app.start("textedit"); + if let Err(e) = app.session.input(Input::Point(p)) { + app.session.echo(e.to_string()); + } + true +} + fn tile_rects(app: &CadApp, full: Rect) -> Vec<(Rect, cadcraft_engine::ModelTile)> { let x = |f: f64| full.left() + full.width() * f.clamp(0.0, 1.0) as f32; let y = |f: f64| full.bottom() - full.height() * f.clamp(0.0, 1.0) as f32; diff --git a/crates/ui-egui/tests/text_double_click.rs b/crates/ui-egui/tests/text_double_click.rs new file mode 100644 index 00000000..782234d1 --- /dev/null +++ b/crates/ui-egui/tests/text_double_click.rs @@ -0,0 +1,78 @@ +//! Double-clicking text with no command running edits it, as in AutoCAD (#842). + +use cadcraft_engine::doc::EntityKind; +use cadcraft_engine::geom::Vec2; +use cadcraft_engine::{Input, Session}; +use cadcraft_ui_egui::{CadApp, Services}; +use egui::{Event, Modifiers, PointerButton, Pos2, vec2}; +use egui_kittest::Harness; +use serde_json::json; + +fn harness() -> Harness<'static, CadApp> { + let mut app = CadApp::new(Session::new(), Services::default()); + app.run("text", json!({ "at": [0, 0], "text": "HELLO", "height": 2.5 })).unwrap(); + app.run("mtext", json!({ "at": [0, 20], "text": "first\\Psecond", "height": 2.5, "width": 20 })).unwrap(); + app.run("line", json!({ "points": [[30, 0], [30, 20]] })).unwrap(); + app.session.execute("view.set", &json!({ "center": [15, 10], "height": 40 })).unwrap(); + app.canvas.zoom_pending = false; + // 60 frames a second: the default step (0.25 s) spreads two clicks past the double-click delay. + let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_ui_state( + |ui, app: &mut CadApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + h.run_steps(5); + h +} + +fn double_click(h: &mut Harness<'_, CadApp>, at: Vec2) { + let pos: Pos2 = h.state().canvas.xf.unwrap().to_screen(at); + h.event(Event::PointerMoved(pos)); + h.run_steps(2); + for _ in 0..2 { + for pressed in [true, false] { + h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + h.run_steps(1); + } + } + h.run_steps(2); +} + +fn text_at(h: &Harness<'_, CadApp>, i: usize) -> String { + let d = h.state().session.doc().unwrap(); + let e = d.model.iter().nth(i).unwrap(); + match &e.kind { + EntityKind::Text(t) => t.value.clone(), + EntityKind::MText(t) => t.contents.clone(), + k => panic!("not text: {}", k.type_name()), + } +} + +#[test] +fn double_click_on_text_starts_textedit_for_that_object() { + for (i, at, old, new) in [(0, Vec2::new(4.0, 1.2), "HELLO", "HELLO WORLD"), (1, Vec2::new(3.0, 18.8), "first\nsecond", "edited")] { + let mut h = harness(); + double_click(&mut h, at); + let s = &h.state().session; + assert_eq!(s.running.as_ref().map(|r| r.id.as_str()), Some("textedit"), "object {i}"); + let p = s.current_prompt().unwrap(); + assert_eq!(p.message, "Enter new text", "object {i}: the clicked text is already picked"); + assert_eq!(p.default.as_deref(), Some(old), "object {i}"); + h.state_mut().session.input(Input::Text(new.into())).unwrap(); + h.run_steps(2); + assert_eq!(text_at(&h, i), new); + // TEXTEDIT goes on asking for objects (its Multiple mode), as AutoCAD's does. + assert_eq!(h.state().session.current_prompt().unwrap().message, "Select an annotation object"); + } +} + +#[test] +fn double_click_elsewhere_starts_no_command() { + for at in [Vec2::new(30.0, 5.0), Vec2::new(15.0, 30.0)] { + let mut h = harness(); + double_click(&mut h, at); + assert_eq!(h.state().session.running.as_ref().map(|r| r.id.as_str()), None, "{at:?}"); + } +}