mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #627 from NikitkaRa: SmartArt: Convert to SmartArt gallery on Home; Convert to Text keeps the text size
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()?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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