mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
SmartArt: Convert to SmartArt gallery on Home, Convert to Text keeps the text size
Home > Paragraph gets a Convert to SmartArt button whose menu lists every layout by category (List, Process, Cycle, Hierarchy, Relationship, Matrix, Pyramid, Picture); a click converts the selected text. Convert to Text sets the text box's titles and bullets to the sizes the graphic showed them at. Icon toggles carry their tooltip as their accessible name. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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()?;
|
||||
|
||||
@@ -1000,6 +1000,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();
|
||||
|
||||
@@ -539,6 +539,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| {
|
||||
@@ -637,6 +672,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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user