Merge PR #553 from NikitkaRa: Text: Bullets and Numbering dialog

# Conflicts:
#	crates/engine/src/tests.rs
This commit is contained in:
Brandon Thomas
2026-10-14 13:08:53 -04:00
5 changed files with 145 additions and 3 deletions
+10 -2
View File
@@ -135,7 +135,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
),
@@ -527,7 +527,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 {
@@ -556,9 +556,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 {
+20
View File
@@ -1546,3 +1546,23 @@ fn motion_path_presets_have_paths_ids_and_round_trip() {
assert_eq!((b.effect.as_str(), b.class), ("heart", AnimClass::Path));
assert!(b.path.as_deref().is_some_and(|p| p.contains('L')));
}
/// 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));
}
+102
View File
@@ -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",
}
@@ -355,6 +356,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?");
+12
View File
@@ -565,6 +565,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);
@@ -589,6 +595,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
View File
@@ -244,7 +244,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.