From 329916c2b82622c123cdaaa1ef854591f9a92c70 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 22 Oct 2026 23:46:14 +0700 Subject: [PATCH] G4: Mixed Hyphenation dialog fields Squashed from #3649 onto main. --- crates/ui-egui/src/dialogs/hyphenation.rs | 76 +++++++++++++++++------ crates/ui-egui/src/panels/paragraph.rs | 12 +++- 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/crates/ui-egui/src/dialogs/hyphenation.rs b/crates/ui-egui/src/dialogs/hyphenation.rs index 47e1c5863..d982f37c0 100644 --- a/crates/ui-egui/src/dialogs/hyphenation.rs +++ b/crates/ui-egui/src/dialogs/hyphenation.rs @@ -1,8 +1,9 @@ //! Paragraph panel menu › Hyphenation… (or `ui.hyphenation`): the Hyphenation checkbox and the //! limits automatic hyphenation keeps to in the paragraphs the Paragraph panel acts on: Words //! Longer Than, After First, Before Last, Hyphen Limit, Hyphenation Zone and Hyphenate Capitalized -//! Words. The dialog opens on the values the Paragraph panel shows. OK runs `text.setFormat` with -//! the values that changed, as one undo step, and paragraphs that differ keep the values left alone. +//! Words. The dialog opens on the values the Paragraph panel shows (blank fields and dashed +//! checkboxes where paragraphs in scope disagree). OK runs `text.setFormat` with the values that +//! changed, as one undo step, and paragraphs that differ keep the values left alone. //! //! Fields: `hyphenate`, `wordsLongerThan` (letters, 1..25), `afterFirst` and `beforeLast` //! (letters, 1..15), `hyphenLimit` (hyphens, 0..25, 0: no limit), `hyphenationZone` (pt) and @@ -26,26 +27,41 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Hyphenation") /// Open Hyphenation on the paragraph attributes the Paragraph panel shows. pub fn open(app: &mut VectorcraftApp) -> Result { - let (_, para) = crate::panels::character::text_style(app).ok_or("select some type first")?; - let h = ¶.hyphenation; - let values = json!({ - "hyphenate": para.hyphenate, - "wordsLongerThan": h.words_longer_than, - "afterFirst": h.after_first, - "beforeLast": h.before_last, - "hyphenLimit": h.hyphen_limit, - "hyphenationZone": h.zone, - "hyphenateCapitalized": h.capitalized, - }); - let mut fields = values.clone(); - fields[INITIAL] = values; + crate::panels::character::text_style(app).ok_or("select some type first")?; + let mut values = Map::new(); + if let Some(v) = crate::panels::paragraph::shared_hyphenate(app) { + values.insert("hyphenate".into(), json!(v)); + } + if let Some(v) = crate::panels::paragraph::shared_hyphenation_field(app, |h| h.words_longer_than) { + values.insert("wordsLongerThan".into(), json!(v)); + } + if let Some(v) = crate::panels::paragraph::shared_hyphenation_field(app, |h| h.after_first) { + values.insert("afterFirst".into(), json!(v)); + } + if let Some(v) = crate::panels::paragraph::shared_hyphenation_field(app, |h| h.before_last) { + values.insert("beforeLast".into(), json!(v)); + } + if let Some(v) = crate::panels::paragraph::shared_hyphenation_field(app, |h| h.hyphen_limit) { + values.insert("hyphenLimit".into(), json!(v)); + } + if let Some(v) = crate::panels::paragraph::shared_hyphenation_zone(app) { + values.insert("hyphenationZone".into(), json!(v)); + } + if let Some(v) = crate::panels::paragraph::shared_hyphenation_field(app, |h| h.capitalized) { + values.insert("hyphenateCapitalized".into(), json!(v)); + } + let mut fields = Value::Object(values.clone()); + fields[INITIAL] = Value::Object(values); app.ui.dialog = Some(Dialog::new(KIND, fields)); Ok(Value::Null) } fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let unit = app.session.general_unit(); - form::check(ui, d, "hyphenate", tl!("Hyphenation")); + let hyphenate = d.fields.get("hyphenate").and_then(|v| v.as_bool()); + if widgets::check3(ui, tl!("Hyphenation"), hyphenate, true) { + d.fields.insert("hyphenate".into(), json!(hyphenate != Some(true))); + } ui.add_space(8.0); // The whole-number fields: (field, label, unit, range). let counts = [ @@ -58,7 +74,8 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { for (key, text, suffix, (lo, hi)) in counts { label(ui, text); ui.horizontal(|ui| { - if let Some(v) = widgets::range_field(ui, ("hyphenation", key), d.f64(key, lo), lo..=hi, "", 0, 60.0) { + let value = d.fields.get(key).and_then(|v| v.as_f64()); + if let Some(v) = widgets::range_field(ui, ("hyphenation", key), value, lo..=hi, "", 0, 60.0) { d.fields.insert(key.into(), json!(v)); } ui.label(suffix); @@ -70,7 +87,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.end_row(); }); ui.add_space(8.0); - form::check(ui, d, "hyphenateCapitalized", tl!("Hyphenate Capitalized Words")); + let capitalized = d.fields.get("hyphenateCapitalized").and_then(|v| v.as_bool()); + if widgets::check3(ui, tl!("Hyphenate Capitalized Words"), capitalized, true) { + d.fields.insert("hyphenateCapitalized".into(), json!(capitalized != Some(true))); + } false } @@ -163,8 +183,6 @@ mod tests { app.run("ui.hyphenation", json!({})).unwrap(); let d = app.ui.dialog.as_mut().unwrap(); d.fields.insert("beforeLast".into(), json!(4)); - // Edited and set back to the value it opened with, as a field stores it. - d.fields.insert("afterFirst".into(), json!(3.0)); super::super::confirm(&mut app).unwrap(); let (first, second) = paras(&app, id); assert_eq!((first.hyphenation.after_first, first.hyphenation.before_last), (3, 4)); @@ -179,4 +197,22 @@ mod tests { assert!(app.run("ui.hyphenation", json!({})).is_err()); assert!(app.ui.dialog.is_none()); } + + /// When paragraphs in scope disagree, only shared hyphenation values are filled in. + #[test] + fn hyphenation_dialog_mixed_paragraph_fields_show_blank() { + let (mut app, id) = app(); + app.run("text.setFormat", json!({"ids": [id], "afterFirst": 3})).unwrap(); + app.run("text.setFormat", json!({"ids": [id], "afterFirst": 2, "start": 5, "end": 5})).unwrap(); + app.run("ui.hyphenation", json!({})).unwrap(); + let d = app.ui.dialog.clone().unwrap(); + assert!(!d.fields.contains_key("afterFirst")); + assert_eq!(d.f64("beforeLast", 0.0), 3.0); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("afterFirst".into(), json!(4)); + super::super::confirm(&mut app).unwrap(); + let (first, second) = paras(&app, id); + assert_eq!((first.hyphenation.after_first, second.hyphenation.after_first), (4, 4)); + assert_eq!((first.hyphenation.before_last, second.hyphenation.before_last), (3, 3)); + } } diff --git a/crates/ui-egui/src/panels/paragraph.rs b/crates/ui-egui/src/panels/paragraph.rs index 56e2eda2f..9c391d65c 100644 --- a/crates/ui-egui/src/panels/paragraph.rs +++ b/crates/ui-egui/src/panels/paragraph.rs @@ -220,7 +220,7 @@ fn shared_composer(app: &VectorcraftApp) -> Option { } /// Hyphenation shared by every paragraph in scope, or `None` when they differ (checkbox off). -fn shared_hyphenate(app: &VectorcraftApp) -> Option { +pub(crate) fn shared_hyphenate(app: &VectorcraftApp) -> Option { shared_para(app, |p| p.hyphenate) } @@ -287,6 +287,16 @@ fn selected_paragraph_direction(app: &VectorcraftApp, fallback: &ParaStyle) -> O shared.or(default) } +/// A hyphenation option shared by every paragraph in scope, or `None` when they differ. +pub(crate) fn shared_hyphenation_field(app: &VectorcraftApp, f: impl Fn(&HyphenationOptions) -> T) -> Option { + shared_para(app, |p| f(&p.hyphenation)) +} + +/// The hyphenation zone shared by every paragraph in scope, or `None` when they differ. +pub(crate) fn shared_hyphenation_zone(app: &VectorcraftApp) -> Option { + shared_para_field(app, |p| p.hyphenation.zone) +} + /// None means differing displayed alignments, following the other mixed-value controls. fn selected_alignment(app: &VectorcraftApp, fallback: &ParaStyle) -> Option { let default = Some(shown_alignment(fallback.justify, fallback.direction == Some(ParaDirection::RightToLeft)));