Naming: use Diagram and Decorative Text for own features (#1981)

* Naming: use Diagram and Decorative Text for own features

* test(naming): guard neutral display labels

* test(ui): handle escaped literals in feature-name guard

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
This commit is contained in:
FredAppsandFredApps authored and GitHub committed 2026-11-14 01:29:01 +01:00
1 parent 7a24b20c8e
commit 69379cf689
21 files changed
+308 -60

No files matched your search

+3 -3
View File
@@ -70,7 +70,7 @@ pub fn specs() -> Vec<CommandSpec> {
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<CommandSpec> {
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<CommandSpec> {
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,
+2 -2
View File
@@ -91,7 +91,7 @@ pub fn specs() -> Vec<CommandSpec> {
),
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<CommandSpec> {
),
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}",
+1 -1
View File
@@ -196,7 +196,7 @@ pub(super) fn flip(s: &mut Session, p: &Value) -> Result<Value> {
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 {
+19 -19
View File
@@ -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<CommandSpec> {
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<CommandSpec> {
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<CommandSpec> {
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<CommandSpec> {
),
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<CommandSpec> {
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<CommandSpec> {
),
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<CommandSpec> {
),
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<CommandSpec> {
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,
+6 -6
View File
@@ -38,7 +38,7 @@ fn target_group(s: &Session, p: &Value, cmd: &str) -> Result<ShapeId> {
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<T>(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<DiagramNode>) -> 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<Value> {
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<Value> {
pub(super) fn to_text(s: &mut Session, p: &Value) -> Result<Value> {
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| {
+4 -4
View File
@@ -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() }));
+5 -5
View File
@@ -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<deckcraft_model::Shape> {
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();
+1 -1
View File
@@ -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 });
}
}
+1 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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}"),
+1 -1
View File
@@ -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"),
("-", ""),
+2 -2
View File
@@ -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<String> = 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();
+2 -2
View File
@@ -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);
}
+23 -5
View File
@@ -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");
}
}
+1 -1
View File
@@ -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!({}));
+3 -3
View File
@@ -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}));
}
}
+1 -1
View File
@@ -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 {
+215
View File
@@ -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<String> {
let chars: Vec<char> = 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<String>) {
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)));
}
}
+15
View File
@@ -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.