diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 4e273dc1c..4eafad20d 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -70,7 +70,7 @@ pub fn specs() -> Vec { cmd!( "format.textTransparency", "Text Transparency", - ["Shape Format", "WordArt Styles"], + ["Shape Format", "Decorative Text Styles"], None, "{value: 0..1} — 0 opaque, 1 invisible; keeps the text's colour (inherited colours included)", has_text_or_shapes, @@ -128,7 +128,7 @@ pub fn specs() -> Vec { cmd!( "format.textOutline", "Text Outline", - ["Shape Format", "WordArt Styles"], + ["Shape Format", "Decorative Text Styles"], None, "{color?, width?: pt} (no color = none)", has_text_or_shapes, @@ -137,7 +137,7 @@ pub fn specs() -> Vec { cmd!( "format.textWarp", "Text Transform", - ["Shape Format", "WordArt Styles"], + ["Shape Format", "Decorative Text Styles"], None, "{preset: a prstTxWarp name or none, adj?: [finite raw guide values, at most 8]} — none clears the transform; omitted adj uses the preset's defaults", has_text_or_shapes, diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index f7bbc0e0b..1fd6e258e 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -91,7 +91,7 @@ pub fn specs() -> Vec { ), cmd!("insert.audio", "Audio from File…", ["Insert", "Media", "Audio"], None, "{path? | data: base64, name?, rect?}", has_slide, audio), cmd!("insert.video", "Video from File…", ["Insert", "Media", "Video"], None, "{path? | data: base64, name?, rect?}", has_slide, video), - cmd!("insert.wordArt", "WordArt", ["Insert", "Text"], None, "{text?, style?: 0..}", has_slide, word_art), + cmd!("insert.wordArt", "Decorative Text", ["Insert", "Text"], None, "{text?, style?: 0..}", has_slide, word_art), cmd!("insert.slideNumber", "Slide Number", ["Insert", "Text"], None, "{}", has_slide, slide_number), cmd!( "insert.dateTime", @@ -142,7 +142,7 @@ pub fn specs() -> Vec { ), cmd!( "insert.smartArt", - "SmartArt", + "Diagram", ["Insert", "Illustrations"], None, "{kind: list|process|cycle|hierarchy|pyramid|matrix, items?: [text] (leading tabs = sub-points), nodes?: [{text, level}]} → {id}", diff --git a/crates/engine/src/cmd/shape/size.rs b/crates/engine/src/cmd/shape/size.rs index 7568a0632..44be79ecd 100644 --- a/crates/engine/src/cmd/shape/size.rs +++ b/crates/engine/src/cmd/shape/size.rs @@ -196,7 +196,7 @@ pub(super) fn flip(s: &mut Session, p: &Value) -> Result { let mut graphic = false; deckcraft_model::walk(std::slice::from_ref(&*sh), &mut |x, _| graphic |= x.diagram.is_some()); if graphic { - return Err(bad("shape.flip", "Flip is not available for a SmartArt graphic; use `smartArt.layout` to change how it is laid out")); + return Err(bad("shape.flip", "Flip is not available for a Diagram graphic; use `smartArt.layout` to change how it is laid out")); } if let Some(x) = sh.xfrm.as_mut() { if v { diff --git a/crates/engine/src/cmd/smartart.rs b/crates/engine/src/cmd/smartart.rs index 3dbcf40f9..703251c5a 100644 --- a/crates/engine/src/cmd/smartart.rs +++ b/crates/engine/src/cmd/smartart.rs @@ -162,7 +162,7 @@ pub fn preview(kind: &str, style: &str, colors: &str, nodes: Option<&[DiagramNod } Shape { id: ShapeId(9_999), - name: "SmartArt preview".into(), + name: "Diagram preview".into(), xfrm: Some(area), kind: ShapeKind::Group { children, child: area }, ..Default::default() @@ -178,16 +178,16 @@ pub fn specs() -> Vec { cmd!( query "smartArt.nodes", "Text Pane", - ["SmartArt Design", "Create Graphic"], + ["Diagram Design", "Create Graphic"], None, - "{id?} → {id, kind, nodes: [{text, level}]} (id: the SmartArt group or one of its shapes; default the selection)", + "{id?} → {id, kind, nodes: [{text, level}]} (id: the Diagram group or one of its shapes; default the selection)", has_selection, nodes ), cmd!( "smartArt.setNodes", - "Edit SmartArt Text", - ["SmartArt Design", "Create Graphic"], + "Edit Diagram Text", + ["Diagram Design", "Create Graphic"], None, "{id?, nodes?: [{text, level}], lines?: [string] (leading tabs set the level), index?, text?, title?} (index + text edits one node; title: its Name and Title box)", has_selection, @@ -196,7 +196,7 @@ pub fn specs() -> Vec { cmd!( "smartArt.addNode", "Add Shape", - ["SmartArt Design", "Create Graphic"], + ["Diagram Design", "Create Graphic"], None, "{id?, index?, position?: after|before|below|above|assistant, text?} → {index} (default: after the last node)", has_selection, @@ -205,18 +205,18 @@ pub fn specs() -> Vec { cmd!( "smartArt.removeNode", "Remove Shape", - ["SmartArt Design", "Create Graphic"], + ["Diagram Design", "Create Graphic"], None, "{id?, index} (its sub-points move up one level)", has_selection, remove_node ), - cmd!("smartArt.promote", "Promote", ["SmartArt Design", "Create Graphic"], None, "{id?, index}", has_selection, promote), - cmd!("smartArt.demote", "Demote", ["SmartArt Design", "Create Graphic"], None, "{id?, index}", has_selection, demote), + cmd!("smartArt.promote", "Promote", ["Diagram Design", "Create Graphic"], None, "{id?, index}", has_selection, promote), + cmd!("smartArt.demote", "Demote", ["Diagram Design", "Create Graphic"], None, "{id?, index}", has_selection, demote), cmd!( "smartArt.layout", "Change Layout", - ["SmartArt Design", "Layouts"], + ["Diagram Design", "Layouts"], None, "{id?, kind: list|blockList|hBulletList|process|chevron|verticalProcess|stepUp|cycle|blockCycle|radial|hierarchy|pyramid|invertedPyramid|matrix|titledMatrix|pictureList|pictureCaption|pictureProcess|pictureHList|timeline|venn|target|textCycle|continuousCycle|horizontalHierarchy|gridMatrix|gear|funnel|segmentedCycle|multidirectionalCycle|orgChart|halfCircleOrgChart|cycleMatrix|segmentedPyramid|pie|nameTitleOrgChart|verticalBoxList|trapezoidList|tableList|accentProcess|continuousBlockProcess|bendingProcess|linearVenn|stackedVenn|nestedTarget}", has_selection, @@ -224,17 +224,17 @@ pub fn specs() -> Vec { ), cmd!( "smartArt.fromText", - "Convert to SmartArt", + "Convert to Diagram", ["Home", "Paragraph"], None, - "{id?, kind?} → {id} (a text box or placeholder becomes a SmartArt graphic in its place: one node per paragraph, indent levels kept)", + "{id?, kind?} → {id} (a text box or placeholder becomes a Diagram graphic in its place: one node per paragraph, indent levels kept)", has_selection, from_text ), cmd!( "smartArt.toText", "Convert to Text", - ["SmartArt Design", "Reset"], + ["Diagram Design", "Reset"], None, "{id?} → {id} (the graphic becomes a text box of its outline, bulleted by level)", has_selection, @@ -243,7 +243,7 @@ pub fn specs() -> Vec { cmd!( "smartArt.toShapes", "Convert to Shapes", - ["SmartArt Design", "Reset"], + ["Diagram Design", "Reset"], None, "{id?} (the graphic becomes a plain group of its shapes; the outline is dropped)", has_selection, @@ -251,8 +251,8 @@ pub fn specs() -> Vec { ), cmd!( "smartArt.picture", - "Insert Picture in SmartArt", - ["SmartArt Design", "Create Graphic"], + "Insert Picture in Diagram", + ["Diagram Design", "Create Graphic"], None, "{id?, index, data?: base64 | path?: string | reset?: bool} (the node's picture placeholder in picture layouts)", has_selection, @@ -260,8 +260,8 @@ pub fn specs() -> Vec { ), cmd!( "smartArt.style", - "SmartArt Styles", - ["SmartArt Design", "SmartArt Styles"], + "Diagram Styles", + ["Diagram Design", "Diagram Styles"], None, "{id?, style: flat|subtle|intense|outline|polished|inset|cartoon|powder|brick}", has_selection, @@ -270,7 +270,7 @@ pub fn specs() -> Vec { cmd!( "smartArt.colors", "Change Colors", - ["SmartArt Design", "SmartArt Styles"], + ["Diagram Design", "Diagram Styles"], None, "{id?, colors: colorful|range23|range34|range45|range56|accent1|…|accent6|accent1Grad|…|accent6Grad}", has_selection, diff --git a/crates/engine/src/cmd/smartart/edit.rs b/crates/engine/src/cmd/smartart/edit.rs index 77662f71a..7c6b0b934 100644 --- a/crates/engine/src/cmd/smartart/edit.rs +++ b/crates/engine/src/cmd/smartart/edit.rs @@ -38,7 +38,7 @@ fn target_group(s: &Session, p: &Value, cmd: &str) -> Result { let st = s.doc()?; let shapes = current_shapes(&st.doc, &st.selection).ok_or_else(|| bad(cmd, "no slide"))?; let ids = targets(s, p)?; - ids.iter().find_map(|id| diagram_group(shapes, *id)).map(|g| g.id).ok_or_else(|| bad(cmd, "select a SmartArt graphic")) + ids.iter().find_map(|id| diagram_group(shapes, *id)).map(|g| g.id).ok_or_else(|| bad(cmd, "select a Diagram graphic")) } /// Children with fresh ids for diagram `d` inside a group whose child space is `area`. @@ -55,7 +55,7 @@ fn edit_diagram(s: &mut Session, p: &Value, cmd: &str, f: impl FnOnce(&mut Di let gid = target_group(s, p, cmd)?; s.edit(|doc, sel| { let g = current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, gid)).cloned().ok_or_else(|| bad(cmd, "no such shape"))?; - let (ShapeKind::Group { children, child }, Some(mut d)) = (g.kind, g.diagram) else { return Err(bad(cmd, "not a SmartArt graphic")) }; + let (ShapeKind::Group { children, child }, Some(mut d)) = (g.kind, g.diagram) else { return Err(bad(cmd, "not a Diagram graphic")) }; sync_text(&mut d, &children); let out = f(&mut d)?; normalize(&mut d.nodes); @@ -90,7 +90,7 @@ pub fn insert(s: &mut Session, kind: &str, mut nodes: Vec) -> Resul let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("insert.smartArt", NO_ID_SPACE))?); let g = Shape { id, - name: format!("SmartArt ({kind})"), + name: format!("Diagram ({kind})"), xfrm: Some(area), kind: ShapeKind::Group { children, child: area }, diagram: Some(d), @@ -201,7 +201,7 @@ pub(super) fn from_text(s: &mut Session, p: &Value) -> Result { let gid = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?); let g = Shape { id: gid, - name: format!("SmartArt ({kind})"), + name: format!("Diagram ({kind})"), xfrm: Some(area), kind: ShapeKind::Group { children, child: area }, diagram: Some(d), @@ -222,12 +222,12 @@ pub(super) fn from_text(s: &mut Session, p: &Value) -> Result { pub(super) fn to_text(s: &mut Session, p: &Value) -> Result { const CMD: &str = "smartArt.toText"; let gid = target_group(s, p, CMD)?; - let (_, d) = selected_for(s, gid).ok_or_else(|| bad(CMD, "select a SmartArt graphic"))?; + let (_, d) = selected_for(s, gid).ok_or_else(|| bad(CMD, "select a Diagram graphic"))?; // The sizes the graphic's text shows at: its titles' and its bullets' (shrunk to fit included). let (title_pt, bullet_pt) = { let st = s.doc()?; let shapes = current_shapes(&st.doc, &st.selection).ok_or_else(|| bad(CMD, "no slide"))?; - let g = diagram_group(shapes, gid).ok_or_else(|| bad(CMD, "select a SmartArt graphic"))?; + let g = diagram_group(shapes, gid).ok_or_else(|| bad(CMD, "select a Diagram graphic"))?; text_sizes(g.children()) }; let new = s.edit(|doc, sel| { diff --git a/crates/engine/src/cmd/smartart/paint.rs b/crates/engine/src/cmd/smartart/paint.rs index 88192cad3..7da5a1576 100644 --- a/crates/engine/src/cmd/smartart/paint.rs +++ b/crates/engine/src/cmd/smartart/paint.rs @@ -166,21 +166,21 @@ pub(super) fn text_shape(preset: &str, x: Xfrm, lines: &[&DiagramNode], title: b if !title { body.body.anchor = Some(deckcraft_model::text::Anchor::Top); } - Shape { name: "SmartArt shape".into(), xfrm: Some(x), geom: Geom::preset(preset), text: Some(body), ..Default::default() } + Shape { name: "Diagram shape".into(), xfrm: Some(x), geom: Geom::preset(preset), text: Some(body), ..Default::default() } } /// A plain shape with no text (arrows between steps). pub(super) fn deco(preset: &str, x: Xfrm, rot: f64) -> Shape { let mut x = x; x.rot = rot; - Shape { name: "SmartArt arrow".into(), xfrm: Some(x), geom: Geom::preset(preset), ..Default::default() } + Shape { name: "Diagram arrow".into(), xfrm: Some(x), geom: Geom::preset(preset), ..Default::default() } } pub(super) fn line_shape(a: (f64, f64), b: (f64, f64)) -> Shape { let mut x = Xfrm::new(a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()); x.flip_h = (b.0 < a.0) != (b.1 < a.1); Shape { - name: "SmartArt connector".into(), + name: "Diagram connector".into(), xfrm: Some(x), geom: Geom::preset("line"), fill: Some(Fill::None), @@ -257,7 +257,7 @@ fn fit_words(sh: &mut Shape) { /// A picture placeholder for node `node`: its picture when it has one, else a pale box. pub(super) fn picture_shape(d: &Diagram, preset: &str, x: Xfrm, i: usize, node: usize) -> Shape { - let mut sh = Shape { name: "SmartArt picture".into(), xfrm: Some(x), geom: Geom::preset(preset), ..Default::default() }; + let mut sh = Shape { name: "Diagram picture".into(), xfrm: Some(x), geom: Geom::preset(preset), ..Default::default() }; paint(d, &mut sh, i, true); if let Some(m) = d.nodes.get(node).and_then(|n| n.picture) { sh.fill = Some(Fill::Picture(deckcraft_model::style::PictureFill { media: m, ..Default::default() })); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index a6dc75f8d..21b67310a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -4400,10 +4400,10 @@ fn smartart_round_trips_through_pptx_as_a_diagram() { } let back = deckcraft_pptx::import(&bytes).unwrap(); assert!(back.validate().is_empty()); - let g = back.slides[1].shapes.iter().find(|x| x.diagram.is_some()).expect("a SmartArt group"); + let g = back.slides[1].shapes.iter().find(|x| x.diagram.is_some()).expect("a Diagram group"); let d = g.diagram.as_ref().unwrap(); assert_eq!(d.kind, "hierarchy"); - assert_eq!((d.style.as_str(), d.colors.as_str()), ("intense", "accent3"), "SmartArt style and colours come back"); + assert_eq!((d.style.as_str(), d.colors.as_str()), ("intense", "accent3"), "Diagram style and colours come back"); assert!(d.imported); let outline: Vec<(&str, u8)> = d.nodes.iter().map(|n| (n.text.as_str(), n.level)).collect(); assert_eq!(outline, [("CEO", 0), ("CTO", 1), ("Dev", 2), ("CFO", 1)]); @@ -4431,7 +4431,7 @@ fn smartart_pictures_3d_styles_and_colour_variants() { let pics = |s: &Session| -> Vec { let st = s.doc().unwrap(); let g = st.doc.slides[1].shapes.iter().find(|x| x.diagram.is_some()).unwrap().clone(); - g.children().iter().filter(|c| c.name == "SmartArt picture").cloned().collect() + g.children().iter().filter(|c| c.name == "Diagram picture").cloned().collect() }; assert_eq!(pics(&s).len(), 2, "one picture placeholder per top-level node"); assert!(pics(&s).iter().all(|p| !matches!(p.fill, Some(deckcraft_model::Fill::Picture(_))))); @@ -7385,13 +7385,13 @@ fn flip_is_refused_on_a_smartart_graphic_and_leaves_it_a_graphic() { let gid = s.execute("smartArt.fromText", &json!({"kind": "blockList"})).unwrap()["id"].as_u64().unwrap(); for cmd in ["arrange.flipHorizontal", "arrange.flipVertical", "shape.flip"] { let e = s.execute(cmd, &json!({"ids": [gid]})).unwrap_err().to_string(); - assert!(e.contains("SmartArt") && e.contains("smartArt.layout"), "{cmd}: {e}"); + assert!(e.contains("Diagram") && e.contains("smartArt.layout"), "{cmd}: {e}"); } // Grouped with another shape it cannot be flipped either: ungrouping would hand it the flip. let other = s.execute("insert.textBox", &json!({"text": "x"})).unwrap()["id"].as_u64().unwrap(); let group = s.execute("arrange.group", &json!({"ids": [gid, other]})).unwrap()["id"].as_u64().unwrap(); let e = s.execute("shape.flip", &json!({"ids": [group]})).unwrap_err().to_string(); - assert!(e.contains("SmartArt"), "{e}"); + assert!(e.contains("Diagram"), "{e}"); s.execute("arrange.ungroup", &json!({"ids": [group]})).unwrap(); // Still a SmartArt graphic: smartArt commands keep working. s.execute("smartArt.layout", &json!({"id": gid, "kind": "cycle"})).unwrap(); diff --git a/crates/engine/src/tools/pointer.rs b/crates/engine/src/tools/pointer.rs index e289e5c70..bf195b9f8 100644 --- a/crates/engine/src/tools/pointer.rs +++ b/crates/engine/src/tools/pointer.rs @@ -353,7 +353,7 @@ impl Session { bp.warp.map(|n| (n, bp.warp_adj)) }); if let Some((name, base)) = found { - self.begin("Adjust WordArt"); + self.begin("Adjust Decorative Text"); self.tool.drag = Some(Drag::Warp { id, index, name, base, orig }); } } diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index 718b95ace..75eba4b22 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -238,7 +238,7 @@ impl SlideApp { "app.insertPictureDialog" => self.pick_and_insert("picture", "insert.picture"), "app.smartArtPictureDialog" => { if self.services.pick_open.is_none() { - return Err("picking a SmartArt picture needs the desktop app".into()); + return Err("picking a Diagram picture needs the desktop app".into()); } self.pick_and_run("picture", "smartArt.picture", p); } diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index ed33b9cd6..c7575dfd9 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -67,7 +67,7 @@ fn title(id: &str) -> &'static str { "customShow" => "Custom Shows", "chartData" => "Chart Data", "trim" => "Trim Media", - "smartart" => "Choose a SmartArt Graphic", + "smartart" => "Choose a Diagram Graphic", "bullets" => "Bullets and Numbering", "action" => "Action Settings", "quit" => "DeckCraft", diff --git a/crates/ui-egui/src/dialogs/body.rs b/crates/ui-egui/src/dialogs/body.rs index 932cd469a..935823d3f 100644 --- a/crates/ui-egui/src/dialogs/body.rs +++ b/crates/ui-egui/src/dialogs/body.rs @@ -1260,7 +1260,7 @@ fn dlg_smartart(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } }); let mut v = d.get("text", "Plan\nBuild\nLaunch"); - ui.add(egui::TextEdit::multiline(&mut v).hint_text("SmartArt items").desired_rows(5).desired_width(320.0)); + ui.add(egui::TextEdit::multiline(&mut v).hint_text("Diagram items").desired_rows(5).desired_width(320.0)); d.fields.insert("text".into(), v.clone()); let (ok, cancel) = buttons(ui, "Insert"); if ok { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 08fcc2391..b92d088ab 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -143,7 +143,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("app.preferences", "Preferences…", Some("Cmd+,"), "{}"), ("app.openDialog", "Open…", None, "{purpose?}"), ("app.insertPictureDialog", "Picture from File…", None, "{}"), - ("app.smartArtPictureDialog", "Insert Picture in SmartArt…", None, "{id?, index}"), + ("app.smartArtPictureDialog", "Insert Picture in Diagram…", None, "{id?, index}"), ("app.changePictureDialog", "Change Picture…", None, "{}"), ("app.insertAudioDialog", "Audio from File…", None, "{}"), ("app.reuseSlidesDialog", "Reuse Slides…", None, "{keepFormatting?: bool}"), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index fb4c50fc4..b1b44200e 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -248,7 +248,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)> { ("-", ""), ("Section", "section.add"), ("Text Box", "insert.textBox"), - ("WordArt", "insert.wordArt"), + ("Decorative Text", "insert.wordArt"), ("Date and Time", "insert.dateTime"), ("Slide Number", "insert.slideNumber"), ("-", ""), diff --git a/crates/ui-egui/src/panels/smartart.rs b/crates/ui-egui/src/panels/smartart.rs index aa872d1f0..a50f95070 100644 --- a/crates/ui-egui/src/panels/smartart.rs +++ b/crates/ui-egui/src/panels/smartart.rs @@ -4,7 +4,7 @@ use super::{SlideApp, Tokens, Ui, Value, json, run}; /// Shift+Tab demote and promote, Backspace in an empty line removes it. pub(super) fn smartart(app: &mut SlideApp, ui: &mut Ui) { let Some((gid, d)) = app.session.active().and_then(deckcraft_engine::cmd::smartart::selected) else { - ui.label("Select a SmartArt graphic to edit its text."); + ui.label("Select a Diagram graphic to edit its text."); return; }; let texts: Vec = d.nodes.iter().map(|n| n.text.clone()).collect(); @@ -24,7 +24,7 @@ pub(super) fn smartart(app: &mut SlideApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.add_space(16.0 * node.level as f32); ui.label("•"); - let te = egui::TextEdit::singleline(draft).id(id).hint_text("SmartArt text").desired_width(f32::INFINITY); + let te = egui::TextEdit::singleline(draft).id(id).hint_text("Diagram text").desired_width(f32::INFINITY); let r = ui.add(te); if app.ui.smartart_focus == Some(i) { r.request_focus(); diff --git a/crates/ui-egui/src/panels/smartart_tests.rs b/crates/ui-egui/src/panels/smartart_tests.rs index 01510197d..dfba4e6c8 100644 --- a/crates/ui-egui/src/panels/smartart_tests.rs +++ b/crates/ui-egui/src/panels/smartart_tests.rs @@ -66,12 +66,12 @@ fn home_convert_to_smartart_gallery_converts_the_text() { app, ); h.run_steps(3); - h.get_by_label("Convert to SmartArt").click(); + h.get_by_label("Convert to Diagram").click(); h.run_steps(3); h.get_by_role_and_label(egui::accesskit::Role::Button, "Basic Block List").click(); h.run_steps(3); let st = h.state().session.active().unwrap(); - let (_, d) = deckcraft_engine::cmd::smartart::selected(st).expect("the text became a SmartArt graphic"); + let (_, d) = deckcraft_engine::cmd::smartart::selected(st).expect("the text became a Diagram graphic"); assert_eq!(d.kind, "blockList"); assert_eq!(d.nodes.len(), 3); } diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 7d2ab7a23..90a3869ed 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -106,7 +106,7 @@ fn contextual(app: &SlideApp) -> Vec<&'static str> { v.push("Layout"); } if deckcraft_engine::cmd::smartart::selected(st).is_some() { - v.push("SmartArt Design"); + v.push(DIAGRAM_TAB_ID); } if shapes.iter().any(|s| matches!(s.kind, ShapeKind::Chart(_))) { v.push("Chart Design"); @@ -117,6 +117,13 @@ fn contextual(app: &SlideApp) -> Vec<&'static str> { v } +// This is an existing saved/control-channel key, not the displayed label. +const DIAGRAM_TAB_ID: &str = "SmartArt Design"; + +fn tab_label(id: &str) -> &str { + if id == DIAGRAM_TAB_ID { "Diagram Design" } else { id } +} + const TABS: [&str; 10] = ["Home", "Insert", "Draw", "Design", "Transitions", "Animations", "Slide Show", "Record", "Review", "View"]; /// Tab row and the ribbon card. @@ -149,18 +156,19 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { } let all: Vec<(&str, bool)> = TABS.iter().map(|s| (*s, false)).chain(ctx_tabs.iter().map(|s| (*s, true))).collect(); for (name, contextual) in all { + let label = tab_label(name); let font = theme::font(13.0); - let w = ui.fonts_mut(|f| f.layout_no_wrap(name.to_string(), font.clone(), t.text).size().x) + 18.0; + let w = ui.fonts_mut(|f| f.layout_no_wrap(label.to_string(), font.clone(), t.text).size().x) + 18.0; let r = Rect::from_min_size(pos2(x, y), vec2(w, 24.0)); let resp = ui.interact(r, ui.scope_id().with(("tab", name)), Sense::click()); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, name)); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, label)); let active = app.ui.tab == name; if resp.hovered() && !active { ui.painter().rect_filled(r.shrink2(vec2(2.0, 2.0)), CornerRadius::same(4), t.hover); } let col = if contextual { t.contextual } else { t.text }; let f = if active { theme::bold(13.0) } else { font }; - ui.painter().text(r.center(), Align2::CENTER_CENTER, name, f, col); + ui.painter().text(r.center(), Align2::CENTER_CENTER, label, f, col); if active { let uw = (w - 20.0).max(16.0); ui.painter().rect_filled(Rect::from_center_size(pos2(r.center().x, r.max.y - 1.0), vec2(uw, 3.0)), CornerRadius::same(1), t.accent); @@ -227,7 +235,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { "Table Design" => table_design(app, ui), "Layout" => table_layout(app, ui), "Chart Design" => chart_design(app, ui), - "SmartArt Design" => smartart_design(app, ui), + DIAGRAM_TAB_ID => smartart_design(app, ui), "Playback" => playback(app, ui), "Slide Master" => slide_master(app, ui), "Handout Master" => handout_master(app, ui), @@ -462,3 +470,13 @@ mod inktoshape_tests; #[cfg(test)] mod picture_styles_tests; + +#[cfg(test)] +mod feature_name_tests { + #[test] + fn diagram_tab_keeps_its_saved_identity() { + assert_eq!(super::DIAGRAM_TAB_ID, "SmartArt Design"); + assert_eq!(super::tab_label(super::DIAGRAM_TAB_ID), "Diagram Design"); + assert_eq!(super::tab_label("Home"), "Home"); + } +} diff --git a/crates/ui-egui/src/ribbon/home.rs b/crates/ui-egui/src/ribbon/home.rs index 78dc637f7..73a917567 100644 --- a/crates/ui-egui/src/ribbon/home.rs +++ b/crates/ui-egui/src/ribbon/home.rs @@ -384,7 +384,7 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { } } }); - let r = icon_toggle(ui, Icon::SmartArt, "Convert to SmartArt", enabled(app, "smartArt.fromText"), false); + let r = icon_toggle(ui, Icon::SmartArt, "Convert to Diagram", enabled(app, "smartArt.fromText"), false); egui::Popup::menu(&r).show(|ui| smartart_gallery(app, ui, "smartArt.fromText", json!({}), "", ("", ""), None)); if ui.add_enabled(en, egui::Button::new("Paragraph…").min_size(vec2(88.0, 22.0))).on_hover_text("Paragraph").clicked() { run(app, "format.paragraphDialog", json!({})); diff --git a/crates/ui-egui/src/ribbon/insert.rs b/crates/ui-egui/src/ribbon/insert.rs index fab6e3174..ac54e2310 100644 --- a/crates/ui-egui/src/ribbon/insert.rs +++ b/crates/ui-egui/src/ribbon/insert.rs @@ -72,7 +72,7 @@ pub(super) fn insert(app: &mut SlideApp, ui: &mut Ui) { }); let r = big_button(ui, Icon::Icons, "Icons", enabled(app, "insert.icon")); egui::Popup::menu(&r).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).width(340.0).show(|ui| icon_library(app, ui)); - big(app, ui, Icon::SmartArt, "SmartArt", "app.dialog", json!({"id": "smartart"})); + big(app, ui, Icon::SmartArt, "Diagram", "app.dialog", json!({"id": "smartart"})); let r = big_button(ui, Icon::Chart, "Chart", enabled(app, "insert.chart")); egui::Popup::menu(&r).show(|ui| { for k in chart_menu_types() { @@ -111,10 +111,10 @@ pub(super) fn insert(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { big(app, ui, Icon::TextBox, "Text\nBox", "insert.textBox", json!({})); big(app, ui, Icon::HeaderFooter, "Header &\nFooter", "app.dialog", json!({"id": "headerFooter"})); - let r = big_button(ui, Icon::WordArt, "WordArt", enabled(app, "insert.wordArt")); + let r = big_button(ui, Icon::WordArt, "Decorative Text", enabled(app, "insert.wordArt")); egui::Popup::menu(&r).show(|ui| { for i in 0..6 { - if ui.button(format!("WordArt style {}", i + 1)).clicked() { + if ui.button(format!("Decorative Text style {}", i + 1)).clicked() { run(app, "insert.wordArt", json!({"style": i})); } } diff --git a/crates/ui-egui/src/ribbon/smartart.rs b/crates/ui-egui/src/ribbon/smartart.rs index 969cc6146..d1726217a 100644 --- a/crates/ui-egui/src/ribbon/smartart.rs +++ b/crates/ui-egui/src/ribbon/smartart.rs @@ -83,7 +83,7 @@ pub(super) fn smartart_design(app: &mut SlideApp, ui: &mut Ui) { } }); } - let (main, arrow) = small_button_ex(ui, Icon::SmartArt, "SmartArt Styles", gid.is_some(), false, true); + let (main, arrow) = small_button_ex(ui, Icon::SmartArt, "Diagram Styles", gid.is_some(), false, true); if let Some(arrow) = arrow { widgets::split_menu(&main, &arrow, |ui| { for (k, label) in deckcraft_engine::cmd::smartart::STYLES { diff --git a/crates/ui-egui/tests/display_names.rs b/crates/ui-egui/tests/display_names.rs new file mode 100644 index 000000000..5eedc9a34 --- /dev/null +++ b/crates/ui-egui/tests/display_names.rs @@ -0,0 +1,215 @@ +//! Guard the production display surfaces without banning compatibility identifiers or notices. +use std::path::{Path, PathBuf}; + +const BANNED: &[&str] = &["SmartArt", "WordArt"]; +const ALLOWED: &[&str] = &["SmartArt Design"]; +const ROOTS: &[&str] = &["src", "../engine/src/cmd"]; + +fn is_word(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +fn banned_display(text: &str) -> bool { + if ALLOWED.contains(&text) { + return false; + } + BANNED.iter().any(|banned| { + text.match_indices(banned) + .any(|(at, _)| !text[..at].chars().next_back().is_some_and(is_word) && !text[at + banned.len()..].chars().next().is_some_and(is_word)) + }) +} + +// Read ordinary and raw Rust string literals, ignoring comments. No compiler dependency is +// needed, so this guard can also run by itself when a full application build is unavailable. +#[allow(clippy::cognitive_complexity)] // This small test-only lexer keeps raw-string/comment state together. +fn literals(source: &str) -> Vec { + let chars: Vec = source.chars().collect(); + let mut out = Vec::new(); + let mut at = 0; + let mut test_module = false; + let mut test_depth = 0_usize; + while at < chars.len() { + if chars.get(at..at + 12) == Some(&['#', '[', 'c', 'f', 'g', '(', 't', 'e', 's', 't', ')', ']']) { + at += 12; + let rest: String = chars[at..].iter().collect(); + if rest.trim_start().starts_with("mod ") { + test_module = true; + } + } else if chars.get(at..at + 2) == Some(&['/', '/']) { + while at < chars.len() && chars[at] != '\n' { + at += 1; + } + } else if chars.get(at..at + 2) == Some(&['/', '*']) { + at += 2; + let mut depth = 1; + while at < chars.len() && depth > 0 { + if chars.get(at..at + 2) == Some(&['/', '*']) { + depth += 1; + at += 2; + } else if chars.get(at..at + 2) == Some(&['*', '/']) { + depth -= 1; + at += 2; + } else { + at += 1; + } + } + } else if chars[at] == 'r' && matches!(chars.get(at + 1), Some('"' | '#')) { + let start = at; + at += 1; + while chars.get(at) == Some(&'#') { + at += 1; + } + let hashes = at - start - 1; + if chars.get(at) != Some(&'"') { + continue; + } + at += 1; + let begin = at; + while at < chars.len() { + if chars[at] == '"' && (1..=hashes).all(|n| chars.get(at + n) == Some(&'#')) { + break; + } + at += 1; + } + if !test_module { + out.push(chars[begin..at].iter().collect()); + } + at += 1 + hashes; + } else if chars[at] == '"' { + at += 1; + let mut text = String::new(); + while at < chars.len() { + let c = chars[at]; + at += 1; + if c == '"' { + break; + } + if c == '\\' && at < chars.len() { + let escaped = chars[at]; + at += 1; + match escaped { + 'n' => text.push('\n'), + 't' => text.push('\t'), + 'r' => text.push('\r'), + '0' => text.push('\0'), + '\n' | '\r' => { + while chars.get(at).is_some_and(|c| c.is_whitespace()) { + at += 1; + } + } + other => text.push(other), + } + } else { + text.push(c); + } + } + if !test_module { + out.push(text); + } + } else if chars[at] == '\'' && chars.get(at + 1) == Some(&'\\') { + // Skip the escape and its payload, including hexadecimal/Unicode escapes. + at += 3; + while at < chars.len() && chars[at] != '\'' { + at += 1; + } + at += 1; + } else if chars[at] == '\'' && chars.get(at + 2) == Some(&'\'') { + at += 3; // A character literal such as '"', not the start of a string. + } else { + if test_module { + match chars[at] { + '{' => test_depth += 1, + '}' => { + test_depth = test_depth.saturating_sub(1); + if test_depth == 0 { + test_module = false; + } + } + ';' if test_depth == 0 => test_module = false, + _ => {} + } + } + at += 1; + } + } + out +} + +fn check_tree(path: &Path, failures: &mut Vec) { + if path.is_dir() { + for entry in std::fs::read_dir(path).expect("source directory") { + let path = entry.expect("source entry").path(); + if path.file_name().is_some_and(|name| name == "tests") { + continue; + } + check_tree(&path, failures); + } + } else if path.extension().is_some_and(|ext| ext == "rs") { + let name = path.file_name().expect("source filename").to_string_lossy(); + if name == "tests.rs" || name.ends_with("_tests.rs") { + return; + } + let source = std::fs::read_to_string(path).expect("UTF-8 Rust source"); + // Skip only inline test modules, not later production following test-only imports. + for text in literals(&source) { + if banned_display(&text) { + failures.push(format!("{}: {text:?}", path.display())); + } + } + } +} + +#[test] +fn production_display_names_stay_neutral() { + let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let mut failures = Vec::new(); + for root in ROOTS { + let root = base.join(root); + assert!(root.exists(), "missing display source: {}", root.display()); + check_tree(&root, &mut failures); + } + assert!(failures.is_empty(), "Banned display names:\n{}", failures.join("\n")); +} + +#[test] +fn guard_distinguishes_labels_from_identifiers_and_comments() { + for name in BANNED { + assert!(banned_display(name), "{name}"); + } + for name in ALLOWED { + assert!(!banned_display(name), "{name}"); + } + assert!(!banned_display("filter.cameraRaw")); + assert!(!banned_display("tool.guidedUpright")); + assert!(!banned_display("insert.smartArt")); + assert_eq!(literals("// \"ignored\"\n/* \"ignored\" */ \"label\""), ["label"]); + assert_eq!(literals("r#\"raw label\"#"), ["raw label"]); +} + +#[test] +fn guard_scans_production_after_test_only_items() { + let source = r###"#[cfg(test)] mod tests { let _ = "ignored"; } + #[cfg(test)] use fixtures::Helper; + fn production() { let _ = r#"live label"#; } + "###; + assert_eq!(literals(source), ["live label"]); + assert_eq!(literals("#[cfg(test)] mod tests; fn live() { let _ = \"live label\"; }"), ["live label"]); +} + +#[test] +fn guard_skips_escaped_character_literals() { + assert_eq!(literals(r#"let q = '\"'; "label""#), ["label"]); + assert_eq!(literals(r#"let q = '\''; "label""#), ["label"]); + assert_eq!(literals(r#"let q = '\\'; "label""#), ["label"]); + assert_eq!(literals(r#"let q = '\u{22}'; "label""#), ["label"]); +} + +#[test] +fn guard_decodes_whitespace_escapes() { + assert_eq!(literals(r#""one\ntwo\tthree\rfour\0""#), ["one\ntwo\tthree\rfour\0"]); + assert_eq!(literals("\"joined\\\n label\""), ["joinedlabel"]); + for name in BANNED { + let source = format!("\"\\t{name}\\n\""); + assert!(literals(&source).iter().any(|label| banned_display(label))); + } +} diff --git a/docs/feature-names.md b/docs/feature-names.md new file mode 100644 index 000000000..b26cfbda2 --- /dev/null +++ b/docs/feature-names.md @@ -0,0 +1,15 @@ +# Feature naming + +DeckCraft uses **Diagram**, **Diagram Design**, and **Diagram Styles** for its diagram feature, and **Decorative Text** / **Decorative Text Styles** for styled text. Menus, dialogs, tooltips, command labels, errors and newly generated object names use these names. + +## Compatibility boundary + +The existing `smartArt.*`, `insert.smartArt`, `insert.wordArt`, dialog and icon IDs remain stable. The persisted/control-channel tab key `SmartArt Design` still opens the same tab; only its displayed and accessible label is Diagram Design. Dispatch does not depend on the new display text. + +Existing document-owned object names are not rewritten. PPTX diagram parts, DrawingML names, public data types, import/export code and attribution remain unchanged. + +DeckCraft currently authors these interface strings directly in English. Shared DeckCraft catalogs exist in craft-i18n, but this change does not add a catalog loader. Before enabling that integration, catalog keys must include the new Diagram and Decorative Text labels rather than only their retired English names. Diagram/format round-trip tests, UI tests and visual checks remain part of application verification. + +## Regression coverage + +The standard-library-only `display_names` integration tests scan production Rust string literals for the retired display names and preserve explicit compatibility exceptions. They also test identifier, comment and raw-string handling. These focused checks do not replace application or visual testing.