Files
vectorcraft/crates/ui-egui/src/dialogs/hyphenation.rs
T
dotCooCoo 0641412cce Dialogs: ui.dialog.set returns an error for a field the open dialog does not read
ui.dialog.set stored any field name it was given, so a misspelled field
(`discrad` for `discard`) was kept without effect and the dialog then
ran with its old value. It now answers such a field with an error that
lists the dialog's fields, and leaves the dialog unchanged.

A dialog accepts the fields it holds, `__` fields, `discard` with a
discard button, `preview` with a live preview, the fields it reads
before a control adds them (each spec's `inputs`, for example Copy and
the object options of the Transform dialogs, Move's `distance` and
`angle`, the reference point, `path` of the export options dialogs,
`searchFolder`, Brush Options' `tips`, the Justification and
Hyphenation values that open blank, the Corners `kind` and `radius` of
corners that differ, the Gradient Stop popover's `color`, `opacity`,
`location` and `spread`, and a new artboard's `color` and display
marks), and the fields it passes to the command it runs: every
parameter of the generic `command` dialog's command, and the
`textureImage` that Load Texture… sets in the Glass and Texturizer
effects. Edit Selection now opens holding `deleted<i>` for each of its
rows, so setting a row number it does not have returns an error, and
Transparency Flattener Presets opens holding the selected preset's name
and options instead of adding them on its first frame.
2026-10-26 09:09:45 +07:00

227 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 (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
//! `hyphenateCapitalized`, as `text.setFormat` takes them, and `__initial` (the values it opened
//! with).
use serde_json::{Map, Value, json};
use super::swatch_options::{grid, label};
use super::{DialogSpec, form};
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of Hyphenation.
pub const KIND: &str = "hyphenation";
/// The field holding the values the dialog opened with.
const INITIAL: &str = "__initial";
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Hyphenation").into(),
body,
confirm,
min_width: 300.0,
// A value the paragraphs don't share opens blank, without its field.
inputs: |_| &["hyphenate", "wordsLongerThan", "afterFirst", "beforeLast", "hyphenLimit", "hyphenationZone", "hyphenateCapitalized"],
..DialogSpec::FORM
};
/// Open Hyphenation on the paragraph attributes the Paragraph panel shows.
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
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();
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 = [
("wordsLongerThan", tl!("Words Longer Than:"), tl!("letters"), (1.0, 25.0)),
("afterFirst", tl!("After First:"), tl!("letters"), (1.0, 15.0)),
("beforeLast", tl!("Before Last:"), tl!("letters"), (1.0, 15.0)),
("hyphenLimit", tl!("Hyphen Limit:"), tl!("hyphens"), (0.0, 25.0)),
];
grid(ui, |ui| {
for (key, text, suffix, (lo, hi)) in counts {
label(ui, text);
ui.horizontal(|ui| {
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);
});
ui.end_row();
}
label(ui, tl!("Hyphenation Zone:"));
form::length(ui, d, "hyphenationZone", unit, 90.0);
ui.end_row();
});
ui.add_space(8.0);
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
}
/// `text.setFormat` parameters: the fields that differ from the values the dialog opened with.
fn params(d: &Dialog) -> Map<String, Value> {
let initial = d.fields.get(INITIAL).and_then(Value::as_object);
let mut p = Map::new();
for key in ["hyphenate", "wordsLongerThan", "afterFirst", "beforeLast", "hyphenLimit", "hyphenationZone", "hyphenateCapitalized"] {
let Some(v) = d.fields.get(key) else { continue };
// Numbers are compared as numbers (a field stores 3 as 3.0), and a length typed with its
// unit ("6 mm") is sent in points.
let v = if v.is_boolean() { v.clone() } else { json!(d.f64(key, 0.0)) };
let same = match (initial.and_then(|m| m.get(key)), &v) {
(Some(Value::Bool(was)), Value::Bool(now)) => was == now,
(Some(was), now) => was.as_f64().zip(now.as_f64()).is_some_and(|(a, b)| a == b),
(None, _) => false,
};
if !same {
p.insert(key.into(), v);
}
}
p
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let p = params(d);
app.ui.dialog = None;
if p.is_empty() {
return Ok(Value::Null);
}
crate::panels::paragraph::run_on_paragraphs(app, "text.setFormat", Value::Object(p))
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
use vectorcraft_engine::doc::{HyphenationOptions, NodeId, NodeKind, ParaStyle};
/// A selected area type object with two paragraphs.
fn app() -> (VectorcraftApp, u64) {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
let id = app.run("text.create", json!({"x": 10, "y": 10, "text": "One\nTwo", "area": {"width": 200, "height": 100}})).unwrap()["id"]
.as_u64()
.unwrap();
app.run("select.set", json!({"ids": [id]})).unwrap();
(app, id)
}
fn paras(app: &VectorcraftApp, id: u64) -> (ParaStyle, ParaStyle) {
match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => (t.para_at(0).clone(), t.para_at(1).clone()),
_ => panic!("not text"),
}
}
/// `ui.hyphenation` opens the dialog on the selected type; OK sets the paragraphs' hyphenation
/// options in one undo step.
#[test]
fn hyphenation_dialog_sets_the_paragraphs_options() {
let (mut app, id) = app();
app.run("ui.hyphenation", json!({})).unwrap();
let d = app.ui.dialog.clone().expect("Hyphenation opened");
assert_eq!(d.kind, KIND);
assert_eq!((d.bool("hyphenate"), d.f64("wordsLongerThan", 0.0), d.f64("hyphenLimit", -1.0)), (false, 5.0, 0.0));
assert!(d.bool("hyphenateCapitalized"));
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("hyphenate".into(), json!(true));
d.fields.insert("wordsLongerThan".into(), json!(8));
d.fields.insert("hyphenLimit".into(), json!(2));
d.fields.insert("hyphenationZone".into(), json!("1 in"));
d.fields.insert("hyphenateCapitalized".into(), json!(false));
let undo = app.session.active().unwrap().history.undo.len();
super::super::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
let want = HyphenationOptions { words_longer_than: 8, hyphen_limit: 2, zone: 72.0, capitalized: false, ..HyphenationOptions::default() };
for p in [paras(&app, id).0, paras(&app, id).1] {
assert!(p.hyphenate);
assert_eq!(p.hyphenation, want);
}
assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1);
}
/// OK sends only the values that changed: paragraphs whose other options differ keep them.
#[test]
fn hyphenation_dialog_leaves_unchanged_values_alone() {
let (mut app, id) = app();
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.as_mut().unwrap();
d.fields.insert("beforeLast".into(), json!(4));
super::super::confirm(&mut app).unwrap();
let (first, second) = paras(&app, id);
assert_eq!((first.hyphenation.after_first, first.hyphenation.before_last), (3, 4));
assert_eq!((second.hyphenation.after_first, second.hyphenation.before_last), (2, 4));
// Nothing changed: no undo step.
let undo = app.session.active().unwrap().history.undo.len();
app.run("ui.hyphenation", json!({})).unwrap();
super::super::confirm(&mut app).unwrap();
assert_eq!(app.session.active().unwrap().history.undo.len(), undo);
// With no type selected the dialog does not open.
app.run("select.set", json!({"ids": []})).unwrap();
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));
}
}