Review: the Language dialog marks the selected text with the language you pick (fixes #339)

This commit is contained in:
Marco Arnulfo
2026-10-13 06:04:28 +02:00
parent 1660982e58
commit e6a41d1ada
4 changed files with 85 additions and 13 deletions
+18
View File
@@ -59,6 +59,15 @@ pub fn specs() -> Vec<CommandSpec> {
case
),
cmd!("format.caps", "Caps", [], None, "{caps: none|small|all}", has_text_or_shapes, caps),
cmd!(
"format.language",
"Language",
["Review", "Language"],
None,
"{lang: BCP 47 tag (en-US, fr-FR…)} — the proofing language of the selected text",
has_text_or_shapes,
language
),
cmd!(
"format.spacing",
"Character Spacing",
@@ -297,6 +306,14 @@ fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
let c = color_param(p, "color");
format_run(s, &move |r| r.highlight = c.clone())
}
fn language(s: &mut Session, p: &Value) -> Result<Value> {
let l = str_param(p, "lang")
.map(str::trim)
.filter(|l| !l.is_empty() && l.len() <= 35)
.ok_or_else(|| bad("format.language", "missing or invalid `lang`"))?
.to_string();
format_run(s, &move |r| r.lang = Some(l.clone()))
}
fn caps(s: &mut Session, p: &Value) -> Result<Value> {
let c = match str_param(p, "caps").unwrap_or("none") {
"small" => Caps::Small,
@@ -678,6 +695,7 @@ fn state(s: &mut Session, _p: &Value) -> Result<Value> {
"align": pp.align.map(|a| a.xml()),
"bullets": matches!(pp.bullet, Some(Bullet::Char { .. })),
"numbering": matches!(pp.bullet, Some(Bullet::AutoNum { .. })),
"lang": r.lang,
"lineSpacing": serde_json::to_value(pp.line_spacing).unwrap_or_default(),
"painter": s.painter.is_some(),
}))
+14
View File
@@ -835,3 +835,17 @@ 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");
}
#[test]
fn format_language_marks_the_selected_text() {
let mut s = session();
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
s.execute("text.edit", &json!({"id": title})).unwrap();
s.execute("text.insert", &json!({"text": "Hola mundo"})).unwrap();
s.execute("text.move", &json!({"to": "wordLeft", "extend": true})).unwrap();
s.execute("format.language", &json!({"lang": "es-ES"})).unwrap();
assert_eq!(s.execute("format.state", &json!({})).unwrap()["lang"], "es-ES");
let body = s.doc().unwrap().current_slide().unwrap().shapes[0].text.clone().unwrap();
let langs: Vec<_> = body.paragraphs[0].runs.iter().map(|r| (r.text.as_str(), r.props.lang.as_deref())).collect();
assert_eq!(langs, vec![("Hola ", None), ("mundo", Some("es-ES"))]);
}
+24 -13
View File
@@ -556,21 +556,32 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
"language" => {
ui.label("Mark selected text as:");
for l in [
"English (United States)",
"English (United Kingdom)",
"Español",
"Français",
"Deutsch",
"Italiano",
"Português",
"日本語",
"中文",
"العربية",
] {
let _ = ui.selectable_label(l.starts_with("English (United States)"), l);
if !d.fields.contains_key("lang") {
let cur = app.session.execute("format.state", &json!({})).ok().and_then(|v| v.get("lang").and_then(Value::as_str).map(String::from));
d.fields.insert("lang".into(), cur.unwrap_or_else(|| "en-US".into()));
}
let mut lang = d.get("lang", "en-US");
for (tag, name) in [
("en-US", "English (United States)"),
("en-GB", "English (United Kingdom)"),
("es-ES", "Español"),
("fr-FR", "Français"),
("de-DE", "Deutsch"),
("it-IT", "Italiano"),
("pt-PT", "Português"),
("ja-JP", "日本語"),
("zh-CN", "中文"),
("ar-SA", "العربية"),
] {
if ui.selectable_label(lang.eq_ignore_ascii_case(tag), name).clicked() {
lang = tag.to_string();
}
}
d.fields.insert("lang".into(), lang.clone());
let (ok, cancel) = buttons(ui, "OK");
if ok {
run(app, "format.language", json!({"lang": lang}));
}
ok || cancel
}
"customShow" => {
+29
View File
@@ -1408,4 +1408,33 @@ mod tests {
harness.run_steps(3);
assert_eq!(harness.state().textures.canvas_renders, n, "an unchanged slide isn't rendered again");
}
/// #339: clicking a language in Review > Language did nothing, and OK didn't mark the text.
#[test]
fn the_language_dialog_marks_the_selected_text() {
use egui_kittest::kittest::Queryable;
let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
SlideApp::new(Session::with_new(), Services::default()),
);
harness.run_steps(2);
let title = harness.state().session.active().unwrap().doc.slides[0].shapes[0].id.0;
harness.state_mut().session.execute("text.edit", &json!({"id": title})).unwrap();
harness.state_mut().session.execute("text.insert", &json!({"text": "Bonjour"})).unwrap();
harness.state_mut().session.execute("text.selectAll", &json!({})).unwrap();
harness.state_mut().run("app.dialog", json!({"id": "language"})).unwrap();
// The anchored window settles over its first frames; click once it stays put.
harness.run_steps(4);
harness.get_by_label("Français").click();
harness.run_steps(2);
harness.get_by_label("OK").click();
harness.run_steps(2);
assert!(harness.state().dialog.is_none());
let d = harness.state().session.active().unwrap();
let body = d.doc.slides[0].shapes[0].text.as_ref().unwrap();
assert_eq!(body.paragraphs[0].runs[0].props.lang.as_deref(), Some("fr-FR"));
}
}