Merge PR #627 from NikitkaRa: SmartArt: Convert to SmartArt gallery on Home; Convert to Text keeps the text size

This commit is contained in:
Brandon Thomas
2026-10-14 13:29:01 -04:00
5 changed files with 127 additions and 0 deletions
+62
View File
@@ -103,6 +103,29 @@ pub const COLORS: [(&str, &str); 17] = [
("accent6Grad", "Gradient Range - Accent 6"),
];
/// The gallery category a layout is shown under in Convert to SmartArt and Change Layout.
pub fn category(kind: &str) -> &'static str {
match kind {
"pictureList" | "pictureCaption" | "pictureProcess" | "pictureHList" => "picture",
"process"
| "chevron"
| "verticalProcess"
| "stepUp"
| "timeline"
| "gear"
| "funnel"
| "accentProcess"
| "continuousBlockProcess"
| "bendingProcess" => "process",
"cycle" | "blockCycle" | "textCycle" | "continuousCycle" | "segmentedCycle" | "multidirectionalCycle" | "pie" => "cycle",
"hierarchy" | "horizontalHierarchy" | "orgChart" | "halfCircleOrgChart" | "nameTitleOrgChart" => "hierarchy",
"radial" | "venn" | "target" | "linearVenn" | "stackedVenn" | "nestedTarget" => "relationship",
"matrix" | "titledMatrix" | "gridMatrix" | "cycleMatrix" => "matrix",
"pyramid" | "invertedPyramid" | "segmentedPyramid" => "pyramid",
_ => "list",
}
}
pub fn is_kind(k: &str) -> bool {
LAYOUTS.iter().any(|(id, _)| *id == k)
}
@@ -1833,6 +1856,13 @@ 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"))?;
// 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"))?;
text_sizes(g.children())
};
let new = s.edit(|doc, sel| {
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(CMD, "no slide"))?;
let at = shapes.iter().position(|x| x.id == gid).ok_or_else(|| bad(CMD, "the graphic isn't at the top of the slide"))?;
@@ -1841,6 +1871,11 @@ fn to_text(s: &mut Session, p: &Value) -> Result<Value> {
for n in &d.nodes {
let mut para = Paragraph::new(&n.text);
para.level = n.level.min(MAX_LEVEL);
let size = if n.level == 0 { title_pt } else { bullet_pt };
for r in &mut para.runs {
r.props.size = Some(size);
}
para.end_props.size = Some(size);
para.props.bullet = Some(deckcraft_model::text::Bullet::Char { char: "•".into() });
para.props.margin_left = Some(18.0 * (para.level as f64 + 1.0));
para.props.indent = Some(-14.0);
@@ -1878,6 +1913,33 @@ fn to_shapes(s: &mut Session, p: &Value) -> Result<Value> {
Ok(Value::Null)
}
/// The most common title and bullet font sizes in a diagram's shapes (20 and 14 pt when none).
fn text_sizes(children: &[Shape]) -> (f64, f64) {
let mut titles: Vec<f64> = vec![];
let mut bullets: Vec<f64> = vec![];
for c in children {
let Some(b) = &c.text else { continue };
for p in &b.paragraphs {
let Some(sz) = p.runs.iter().find_map(|r| r.props.size) else { continue };
if p.props.bullet.is_some() { bullets.push(sz) } else { titles.push(sz) }
}
}
let mode = |v: &mut Vec<f64>, d: f64| {
v.sort_by(|a, b| a.total_cmp(b));
let mut best = (d, 0usize);
let mut k = 0;
while k < v.len() {
let j = v[k..].iter().take_while(|x| (**x - v[k]).abs() < 1e-9).count();
if j > best.1 {
best = (v[k], j);
}
k += j;
}
best.0
};
(mode(&mut titles, 20.0), mode(&mut bullets, 14.0))
}
/// The graphic `gid` with its outline (typed text synced).
fn selected_for(s: &Session, gid: ShapeId) -> Option<(ShapeId, Diagram)> {
let st = s.doc().ok()?;
+2
View File
@@ -1844,6 +1844,8 @@ fn smartart_converts_to_and_from_text_and_shapes() {
let tb = st.doc.slides[1].shapes.iter().find(|x| x.id.0 as u64 == tid).unwrap();
assert!(tb.diagram.is_none());
assert_eq!(tb.text.as_ref().unwrap().paragraphs.len(), 3);
// The text keeps the size it had in the graphic.
assert!(tb.text.as_ref().unwrap().paragraphs.iter().all(|p| p.runs.iter().all(|r| r.props.size.is_some_and(|z| z > 10.0))));
s.execute("edit.undo", &json!({})).unwrap();
s.execute("smartArt.toShapes", &json!({"id": gid})).unwrap();
+37
View File
@@ -551,6 +551,41 @@ pub fn cref_param(c: &deckcraft_model::ColorRef) -> Value {
}
}
/// Convert to SmartArt: the layouts by category, three to a row; a click converts the selected
/// text.
fn smartart_gallery(app: &mut SlideApp, ui: &mut Ui) {
use deckcraft_engine::cmd::smartart::{LAYOUTS, category};
ui.set_min_width(420.0);
for (cat, title) in [
("list", "List"),
("process", "Process"),
("cycle", "Cycle"),
("hierarchy", "Hierarchy"),
("relationship", "Relationship"),
("matrix", "Matrix"),
("pyramid", "Pyramid"),
("picture", "Picture"),
] {
let kinds: Vec<(&str, &str)> = LAYOUTS.iter().copied().filter(|(k, _)| category(k) == cat).collect();
if kinds.is_empty() {
continue;
}
ui.label(egui::RichText::new(title).strong());
egui::Grid::new(("smartart_gallery", cat)).num_columns(3).spacing([6.0, 2.0]).show(ui, |ui| {
for (i, (k, label)) in kinds.iter().enumerate() {
if ui.add_sized([136.0, 22.0], egui::Button::new(*label)).clicked() {
run(app, "smartArt.fromText", json!({"kind": k}));
ui.close();
}
if i % 3 == 2 {
ui.end_row();
}
}
});
ui.separator();
}
}
fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) {
let en = enabled(app, "format.bullets");
group(ui, |ui| {
@@ -661,6 +696,8 @@ 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);
egui::Popup::menu(&r).show(|ui| smartart_gallery(app, ui));
});
});
});
+24
View File
@@ -51,3 +51,27 @@ fn text_pane_types_adds_and_demotes() {
h.run_steps(4);
assert_eq!(levels(&h), [("Plan".into(), 0), ("Build it".into(), 0), ("Test".into(), 1)]);
}
#[test]
fn home_convert_to_smartart_gallery_converts_the_text() {
let mut s = Session::with_new();
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
s.execute("insert.textBox", &json!({"text": "Goals\nRevenue\nGrowth"})).unwrap();
let app = SlideApp::new(s, Services::default());
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
app,
);
h.run_steps(3);
h.get_by_label("Convert to SmartArt").click();
h.run_steps(3);
h.get_by_label("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");
assert_eq!(d.kind, "blockList");
assert_eq!(d.nodes.len(), 3);
}
+2
View File
@@ -155,6 +155,8 @@ pub fn icon_toggle(ui: &mut Ui, icon: Icon, tip: &str, enabled: bool, on: bool)
painter.rect_filled(rect, CornerRadius::same(4), c);
}
icons::paint(&painter, rect.shrink(3.0), icon, t.text, !enabled);
// Named for screen readers (and tests) by its tooltip.
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled, tip));
resp.on_hover_text(tip)
}