mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Text: Bullets and Numbering dialog
The Bullets and Numbering menus only offered a fixed list of styles. "Bullets and Numbering…" at the foot of both menus opens PowerPoint's dialog: Bulleted and Numbered tabs with their styles, a custom bullet character, Start at, Size (% of text, 25-400) and Color (automatic, text or an accent), and None. format.numbering now takes color and size like format.bullets, and both clamp the size to PowerPoint's 25-400 %. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -115,7 +115,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Numbering",
|
||||
["Home", "Paragraph"],
|
||||
None,
|
||||
"{on?: bool, scheme?: arabicPeriod|arabicParenR|romanUcPeriod|romanLcPeriod|alphaUcPeriod|alphaLcParenR|alphaLcPeriod, start?: n}",
|
||||
"{on?: bool, scheme?: arabicPeriod|arabicParenR|romanUcPeriod|romanLcPeriod|alphaUcPeriod|alphaLcParenR|alphaLcPeriod, start?: n, color?, size?: % of text}",
|
||||
has_text_or_shapes,
|
||||
numbering
|
||||
),
|
||||
@@ -442,7 +442,7 @@ fn bullets(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let on = on_param(p, Some(cur));
|
||||
let ch = str_param(p, "char").unwrap_or("•").to_string();
|
||||
let col = color_param(p, "color");
|
||||
let size = f64_param(p, "size").map(|v| v / 100.0);
|
||||
let size = f64_param(p, "size").map(|v| (v / 100.0).clamp(0.25, 4.0));
|
||||
let explicit_char = p.get("char").is_some();
|
||||
format_para(s, &move |para| {
|
||||
if on {
|
||||
@@ -471,9 +471,17 @@ fn numbering(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let on = on_param(p, Some(cur));
|
||||
let scheme = str_param(p, "scheme").unwrap_or("arabicPeriod").to_string();
|
||||
let start = p.get("start").and_then(Value::as_u64).map(|v| v.clamp(1, 32767) as u32).unwrap_or(1);
|
||||
let col = color_param(p, "color");
|
||||
let size = f64_param(p, "size").map(|v| (v / 100.0).clamp(0.25, 4.0));
|
||||
format_para(s, &move |para| {
|
||||
if on {
|
||||
para.props.bullet = Some(Bullet::AutoNum { scheme: scheme.clone(), start_at: start });
|
||||
if col.is_some() {
|
||||
para.props.bullet_color = col.clone();
|
||||
}
|
||||
if size.is_some() {
|
||||
para.props.bullet_size = size;
|
||||
}
|
||||
para.props.margin_left = Some(27.0 + 36.0 * para.level as f64);
|
||||
para.props.indent = Some(-27.0);
|
||||
} else {
|
||||
|
||||
@@ -835,3 +835,23 @@ fn slide_ranges_parse_like_powerpoints_print_box() {
|
||||
let opts = cmd::file::pdf_options(&json!({"slides": [0, 7, 1]}), 2).unwrap();
|
||||
assert_eq!(opts.slides, Some(vec![0, 1]), "indices past the end are dropped");
|
||||
}
|
||||
|
||||
/// Bullets and Numbering dialog: numbering takes a colour and a size like bullets do, and sizes
|
||||
/// stay within PowerPoint's 25–400 %.
|
||||
#[test]
|
||||
fn numbering_takes_color_and_size() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("insert.textBox", &json!({"rect": [10, 10, 300, 100], "text": "One\nTwo"})).unwrap();
|
||||
s.execute("format.numbering", &json!({"on": true, "scheme": "romanUcPeriod", "start": 3, "color": "accent2", "size": 150})).unwrap();
|
||||
let para = |s: &Session| {
|
||||
let st = s.doc().unwrap();
|
||||
let id = st.selection.shapes[0];
|
||||
st.shape(id).unwrap().text.as_ref().unwrap().paragraphs[0].props.clone()
|
||||
};
|
||||
let pp = para(&s);
|
||||
assert!(matches!(&pp.bullet, Some(deckcraft_model::text::Bullet::AutoNum { scheme, start_at: 3 }) if scheme == "romanUcPeriod"));
|
||||
assert_eq!(pp.bullet_size, Some(1.5));
|
||||
assert_eq!(pp.bullet_color, Some(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Accent2)));
|
||||
s.execute("format.bullets", &json!({"on": true, "char": "★", "size": 9000})).unwrap();
|
||||
assert_eq!(para(&s).bullet_size, Some(4.0));
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ fn title(id: &str) -> &'static str {
|
||||
"chartData" => "Chart Data",
|
||||
"trim" => "Trim Media",
|
||||
"smartart" => "Choose a SmartArt Graphic",
|
||||
"bullets" => "Bullets and Numbering",
|
||||
"quit" => "DeckCraft",
|
||||
_ => "DeckCraft",
|
||||
}
|
||||
@@ -344,6 +345,107 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
}
|
||||
ok || cancel
|
||||
}
|
||||
"bullets" => {
|
||||
// The current paragraph's list, as the dialog's starting point.
|
||||
let cur = app.session.execute("format.state", &json!({})).ok().unwrap_or(Value::Null);
|
||||
let numbered = cur.get("numbering").and_then(Value::as_bool).unwrap_or(false);
|
||||
let tab = d.get("tab", if numbered { "numbered" } else { "bulleted" });
|
||||
ui.horizontal(|ui| {
|
||||
for (k, l) in [("bulleted", "Bulleted"), ("numbered", "Numbered")] {
|
||||
if ui.selectable_label(tab == k, l).clicked() {
|
||||
d.fields.insert("tab".into(), k.into());
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
let style_key = if tab == "numbered" { "scheme" } else { "char" };
|
||||
let current = d.get(style_key, if tab == "numbered" { "arabicPeriod" } else { "•" });
|
||||
let styles: &[(&str, &str)] = if tab == "numbered" {
|
||||
&[
|
||||
("arabicPeriod", "1. 2. 3."),
|
||||
("arabicParenR", "1) 2) 3)"),
|
||||
("romanUcPeriod", "I. II. III."),
|
||||
("romanLcPeriod", "i. ii. iii."),
|
||||
("alphaUcPeriod", "A. B. C."),
|
||||
("alphaLcParenR", "a) b) c)"),
|
||||
("alphaLcPeriod", "a. b. c."),
|
||||
]
|
||||
} else {
|
||||
&[("•", "•"), ("○", "○"), ("■", "■"), ("□", "□"), ("◆", "◆"), ("➢", "➢"), ("✓", "✓"), ("–", "–"), ("★", "★")]
|
||||
};
|
||||
egui::Grid::new("bullet-styles").show(ui, |ui| {
|
||||
for (i, (k, l)) in styles.iter().enumerate() {
|
||||
let r = ui.add_sized(
|
||||
vec2(if tab == "numbered" { 88.0 } else { 44.0 }, 40.0),
|
||||
egui::Button::selectable(current == *k, egui::RichText::new(*l).size(16.0)),
|
||||
);
|
||||
if r.clicked() {
|
||||
d.fields.insert(style_key.into(), (*k).into());
|
||||
}
|
||||
if (i + 1) % if tab == "numbered" { 4 } else { 5 } == 0 {
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
});
|
||||
if tab == "bulleted" {
|
||||
field(ui, d, "Custom character:", "custom", "");
|
||||
} else {
|
||||
field(ui, d, "Start at:", "start", "1");
|
||||
}
|
||||
field(ui, d, "Size (% of text):", "size", "100");
|
||||
let colors = [
|
||||
("", "Automatic"),
|
||||
("tx1", "Text"),
|
||||
("accent1", "Accent 1"),
|
||||
("accent2", "Accent 2"),
|
||||
("accent3", "Accent 3"),
|
||||
("accent4", "Accent 4"),
|
||||
("accent5", "Accent 5"),
|
||||
("accent6", "Accent 6"),
|
||||
];
|
||||
let color = d.get("color", "");
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_sized(vec2(120.0, 20.0), egui::Label::new("Color:"));
|
||||
egui::ComboBox::from_id_salt("bullet-color")
|
||||
.selected_text(colors.iter().find(|c| c.0 == color).map_or("Automatic", |c| c.1))
|
||||
.show_ui(ui, |ui| {
|
||||
for (k, l) in colors {
|
||||
if ui.selectable_label(color == k, l).clicked() {
|
||||
d.fields.insert("color".into(), k.into());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("None").on_hover_text("Remove bullets and numbering").clicked() {
|
||||
d.fields.insert("none".into(), "1".into());
|
||||
}
|
||||
});
|
||||
let (ok, cancel) = buttons(ui, "OK");
|
||||
let none = d.fields.contains_key("none");
|
||||
if none {
|
||||
run(app, if tab == "numbered" { "format.numbering" } else { "format.bullets" }, json!({"on": false}));
|
||||
} else if ok {
|
||||
let mut p = json!({"on": true});
|
||||
if let Some(size) = d.get("size", "100").trim().trim_end_matches('%').parse::<f64>().ok().filter(|v| v.is_finite()) {
|
||||
p["size"] = json!(size.clamp(25.0, 400.0));
|
||||
}
|
||||
if !color.is_empty() {
|
||||
p["color"] = json!(color);
|
||||
}
|
||||
if tab == "numbered" {
|
||||
p["scheme"] = json!(d.get("scheme", "arabicPeriod"));
|
||||
p["start"] = json!(d.get("start", "1").trim().parse::<u64>().unwrap_or(1).clamp(1, 32767));
|
||||
run(app, "format.numbering", p);
|
||||
} else {
|
||||
let custom = d.get("custom", "");
|
||||
let ch = custom.chars().find(|c| !c.is_whitespace()).map(String::from).unwrap_or_else(|| d.get("char", "•"));
|
||||
p["char"] = json!(ch);
|
||||
run(app, "format.bullets", p);
|
||||
}
|
||||
}
|
||||
ok || cancel || none
|
||||
}
|
||||
"altText" => {
|
||||
let cur = app.session.active().and_then(|s| s.selected_shapes().first().map(|x| x.descr.clone())).unwrap_or_default();
|
||||
ui.label("How would you describe this object and its context to someone who is blind or has low vision?");
|
||||
|
||||
@@ -556,6 +556,12 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) {
|
||||
run(app, "format.bullets", json!({"on": true, "char": ch}));
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Bullets and Numbering…").clicked() {
|
||||
let mut dlg = crate::dialogs::Dialog::new("bullets");
|
||||
dlg.fields.insert("tab".into(), "bulleted".into());
|
||||
app.dialog = Some(dlg);
|
||||
}
|
||||
});
|
||||
}
|
||||
let (m, a) = small_button_ex(ui, Icon::Numbering, "", en, b("numbering"), true);
|
||||
@@ -580,6 +586,12 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) {
|
||||
run(app, "format.numbering", json!({"on": true, "scheme": sch}));
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Bullets and Numbering…").clicked() {
|
||||
let mut dlg = crate::dialogs::Dialog::new("bullets");
|
||||
dlg.fields.insert("tab".into(), "numbered".into());
|
||||
app.dialog = Some(dlg);
|
||||
}
|
||||
});
|
||||
}
|
||||
toggle(app, ui, Icon::DecreaseIndent, "Decrease List Level", "format.outdent", false);
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ Detail in [ui-parity.md](ui-parity.md).
|
||||
- **Window chrome** (#71 macOS controls misaligned, #26 GNOME title bar, #41 system light/dark).
|
||||
2–4 h.
|
||||
- **Icon consistency across Craft apps** (#73, #56). 2–4 h; coordinate in craftrules.
|
||||
- **Dialogs**: 27 dialogs vs PowerPoint's ~60 (Font, Bullets and Numbering, Find/Replace dialog,
|
||||
- **Dialogs**: 28 dialogs vs PowerPoint's ~60 (Font, Find/Replace dialog,
|
||||
Replace Fonts, Action Settings, Print, Insert Object…). 8–14 h.
|
||||
- **Command palette defaults to an Export entry that fails** (#79). 0.5 h.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user