Merge pull request #5025 from dotCooCoo/fix/paragraph-reset-panel-mojikumi-tabs-r

Paragraph: Reset Panel also resets the Mojikumi Set and tab stops
This commit is contained in:
BFlat
2026-11-09 01:15:22 +08:00
committed by GitHub
3 changed files with 160 additions and 4 deletions
+18 -4
View File
@@ -828,11 +828,19 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
/// Reset Panel: the paragraphs the panel acts on ([`format`]) take Auto alignment, no list, no
/// indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification
/// options, Auto direction, Bottom-to-Bottom Leading, Regular burasagari, the Hard kinsoku set, the
/// Every-line Composer, Roman Hanging Punctuation off and no kashidas.
/// options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi
/// Set, Regular burasagari, the Hard kinsoku set, the Every-line Composer, Roman Hanging
/// Punctuation off, no kashidas and no tab stops, as one undo step named Reset Panel (inside an
/// undo group that is already open, as part of its step).
fn reset_panel(app: &mut VectorcraftApp) {
let h = HyphenationOptions::default();
let j = vectorcraft_doc::JustificationOptions::default();
// The Type tool's typing session stays an undo step of its own.
super::character::end_typing(app);
let open = app.session.active().is_some_and(|st| st.undo_group.is_some());
if !open {
app.session.begin_named_undo_group(Some("Reset Panel".into()));
}
para_cmd(app, "text.setStyle", json!({"justify": "auto"}));
format(
app,
@@ -842,10 +850,16 @@ fn reset_panel(app: &mut VectorcraftApp) {
"hyphenLimit": h.hyphen_limit, "hyphenationZone": h.zone, "hyphenateCapitalized": h.capitalized, "kashida": "none",
"wordSpacing": j.word_spacing, "letterSpacing": j.letter_spacing, "glyphScaling": j.glyph_scaling,
"autoLeading": j.auto_leading, "singleWordJustification": "fullJustify",
"direction": "auto", "leadingModel": "romanBaseline", "burasagari": "standard", "kinsoku": "hard", "composer": "everyLine",
"romanHangingPunctuation": false
"direction": "auto", "leadingModel": "romanBaseline", "mojikumi": "lineEndHalf", "burasagari": "standard", "kinsoku": "hard",
"composer": "everyLine", "romanHangingPunctuation": false
}),
);
para_cmd(app, "text.tabs.clear", json!({}));
// Closes the active document's undo group only, so an undo group open in another document
// stays open.
if !open && let Some(st) = app.session.active_mut() {
st.undo_group = None;
}
}
#[cfg(test)]
@@ -1,4 +1,5 @@
use super::*;
use vectorcraft_doc::Mojikumi;
use vectorcraft_doc::NodeId;
use vectorcraft_engine::Session;
@@ -691,3 +692,143 @@ fn the_control_bar_trims_its_paragraph_section_after_the_controls_to_its_right()
assert!(seen.iter().all(|s| *s), "{case}: a wide bar shows every group: {seen:?}");
}
}
/// Reset Panel gives the paragraphs it acts on new type's Mojikumi Set (Line-end Punctuation
/// Half Width) and no tab stops, as one undo step named Reset Panel: while the Type tool edits,
/// the paragraph at its caret (the typing before it stays a step of its own), and then the
/// selected type's paragraphs. In an undo group that is already open, the reset is part of
/// that group's step.
#[test]
fn reset_panel_resets_the_mojikumi_set_and_tab_stops_in_one_step() {
let (mut app, id) = editing_text("First\nSecond");
app.run("text.setFormat", json!({"id": id, "mojikumi": "none"})).unwrap();
app.run("text.tabs.set", json!({"id": id, "stops": [{"position": 36}, {"position": 90, "align": "right"}]})).unwrap();
let text = |app: &VectorcraftApp| {
let node = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap();
let NodeKind::Text(text) = &node.kind else { panic!("text") };
text.as_ref().clone()
};
let paras = |app: &VectorcraftApp| {
let text = text(app);
[text.para_at(0), text.para_at(1)].map(|p| (p.mojikumi, p.tabs.len()))
};
let custom = (Mojikumi::None, 2);
let reset = (Mojikumi::LineEndHalf, 0);
assert_eq!(paras(&app), [custom, custom]);
let steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
let last_step = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.last().map(|e| e.label.clone());
// Type over the second paragraph's text: the typing session is still open.
select_range(&mut app, 6, 12);
let before = steps(&app);
let ctx = egui::Context::default();
let raw = egui::RawInput { events: vec![egui::Event::Text("X".into())], ..Default::default() };
ctx.run_ui(raw, |ui| crate::shortcuts::handle(&mut app, ui.ctx())).textures_delta.clear();
assert_eq!(app.session.tool_options()["typing"], json!(true));
reset_panel(&mut app);
assert_eq!(paras(&app), [custom, reset], "only the paragraph at the caret");
assert_eq!((steps(&app), last_step(&app)), (before + 2, Some("Reset Panel".to_string())));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(paras(&app), [custom, custom]);
assert_eq!(text(&app).plain_text(), "First\nX", "the typing stays");
app.run("edit.redo", json!({})).unwrap();
app.session.set_tool_option("stopEditing", &json!(true));
let doc = app.session.active().unwrap().doc.clone();
let before = steps(&app);
reset_panel(&mut app);
assert_eq!(paras(&app), [reset, reset]);
assert_eq!((steps(&app), last_step(&app)), (before + 1, Some("Reset Panel".to_string())));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc, doc, "one Undo takes the whole reset back");
// In an undo group that is already open (MCP `command_batch` with `atomic`), the reset is
// part of that group's step.
app.run("edit.undoGroup.begin", json!({"label": "Batch"})).unwrap();
let before = steps(&app);
reset_panel(&mut app);
assert_eq!(paras(&app), [reset, reset]);
app.run("text.setFormat", json!({"id": id, "mojikumi": "none"})).unwrap();
app.run("edit.undoGroup.end", json!({})).unwrap();
assert_eq!((steps(&app), last_step(&app)), (before + 1, Some("Batch".to_string())));
}
/// Reset Panel gives paragraphs that start with different Mojikumi Sets and tab stops the same
/// Mojikumi Set and no tab stops, as one undo step, and one Undo gives each paragraph its own
/// values back: while the Type tool edits, the paragraphs the selection touches, and then the
/// paragraphs of every selected type object.
#[test]
fn reset_panel_resets_paragraphs_with_different_values_in_one_step() {
let (mut app, id) = editing_text("First\nSecond\nThird");
app.run("text.setFormat", json!({"id": id, "start": 0, "end": 1, "mojikumi": "none"})).unwrap();
app.run("text.setFormat", json!({"id": id, "start": 6, "end": 7, "mojikumi": "lineEndHalf"})).unwrap();
app.run("text.setFormat", json!({"id": id, "start": 13, "end": 14, "mojikumi": "none"})).unwrap();
app.run("text.tabs.set", json!({"id": id, "start": 0, "end": 1, "stops": [{"position": 36}, {"position": 90, "align": "right"}]})).unwrap();
app.run("text.tabs.set", json!({"id": id, "start": 6, "end": 7, "stops": [{"position": 120, "align": "decimal", "leader": "."}]})).unwrap();
let paras = |app: &VectorcraftApp, id: &Value| {
let node = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap();
let NodeKind::Text(text) = &node.kind else { panic!("text") };
text.paragraph_styles().into_iter().map(|p| (p.mojikumi, p.tabs.iter().map(|t| t.position).collect::<Vec<_>>())).collect::<Vec<_>>()
};
let first = (Mojikumi::None, vec![36.0, 90.0]);
let second = (Mojikumi::LineEndHalf, vec![120.0]);
let third = (Mojikumi::None, vec![]);
let reset = (Mojikumi::LineEndHalf, vec![]);
assert_eq!(paras(&app, &id), [first.clone(), second.clone(), third.clone()]);
let steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
let last_step = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.last().map(|e| e.label.clone());
// A selection from the first paragraph into the second.
select_range(&mut app, 2, 8);
let doc = app.session.active().unwrap().doc.clone();
let before = steps(&app);
reset_panel(&mut app);
assert_eq!(paras(&app, &id), [reset.clone(), reset.clone(), third.clone()], "only the paragraphs the selection touches");
assert_eq!((steps(&app), last_step(&app)), (before + 1, Some("Reset Panel".to_string())));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc, doc, "one Undo takes the whole reset back");
assert_eq!(paras(&app, &id), [first.clone(), second.clone(), third.clone()]);
// Two selected type objects.
app.session.set_tool_option("stopEditing", &json!(true));
let other = app.run("text.create", json!({"x": 40, "y": 120, "text": "Other"})).unwrap()["id"].clone();
app.run("text.setFormat", json!({"id": other, "mojikumi": "none"})).unwrap();
app.run("text.tabs.set", json!({"id": other, "stops": [{"position": 48, "align": "center"}]})).unwrap();
let other_values = (Mojikumi::None, vec![48.0]);
app.run("select.set", json!({"ids": [id, other]})).unwrap();
let doc = app.session.active().unwrap().doc.clone();
let before = steps(&app);
reset_panel(&mut app);
assert_eq!(paras(&app, &id), [reset.clone(), reset.clone(), reset.clone()]);
assert_eq!(paras(&app, &other), [reset]);
assert_eq!((steps(&app), last_step(&app)), (before + 1, Some("Reset Panel".to_string())));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc, doc, "one Undo takes the whole reset back");
assert_eq!(paras(&app, &id), [first, second, third]);
assert_eq!(paras(&app, &other), [other_values]);
}
/// Reset Panel closes only the undo group it opens: an undo group open in another document
/// stays open, and that document's later edits stay part of its step.
#[test]
fn reset_panel_leaves_an_undo_group_open_in_another_document() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 200})).unwrap();
let a = app.run("text.create", json!({"x": 20, "y": 50, "text": "A"})).unwrap()["id"].clone();
// The second document is open before the group begins, so switching to it opens no group in it.
app.run("file.new", json!({"width": 400, "height": 200})).unwrap();
assert_eq!(app.session.active_index(), Some(1));
app.run("text.create", json!({"x": 20, "y": 50, "text": "B"})).unwrap();
app.session.set_active(0);
app.run("edit.undoGroup.begin", json!({"label": "Batch"})).unwrap();
app.run("text.setFormat", json!({"id": a, "mojikumi": "none"})).unwrap();
let steps = |st: &vectorcraft_engine::DocState| (st.history.undo.len(), st.history.undo.last().map(|e| e.label.clone()));
let a_steps = steps(&app.session.documents()[0]);
assert_eq!(a_steps.1, Some("Batch".to_string()));
app.session.set_active(1);
assert!(app.session.active().unwrap().undo_group.is_none());
let b_steps = steps(app.session.active().unwrap()).0;
reset_panel(&mut app);
assert_eq!(steps(app.session.active().unwrap()), (b_steps + 1, Some("Reset Panel".to_string())));
assert!(app.session.active().unwrap().undo_group.is_none());
assert!(app.session.documents()[0].undo_group.is_some(), "the other document's undo group stays open");
app.session.set_active(0);
app.run("text.setFormat", json!({"id": a, "mojikumi": "lineEndHalf"})).unwrap();
app.run("edit.undoGroup.end", json!({})).unwrap();
assert_eq!(steps(app.session.active().unwrap()), a_steps, "the later edit is part of the Batch step");
}
+1
View File
@@ -1037,6 +1037,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
The frames of threaded text share one Optical Margin Alignment setting: turning it on or off in one frame sets it in every frame, threading frames gives the thread its first frame's setting, and frames taken out of a thread keep it.
Type › Optical Margin Alignment (`type.opticalMarginAlignment`) hangs the punctuation and letter edges at the ends of a type object's lines outside its margins: at the left of left-aligned lines, at the right of right-aligned lines, and at both ends of centered and justified lines, in point and area type of left-to-right paragraphs. Quotation marks, hyphens, full stops and commas hang wholly; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their width. It is off by default and saved with the type object. Justification's Glyph Scaling leaves the glyphs that hang at their width, so they hang exactly their share, and Standard burasagari counts what hangs at a line's start as room before hanging a comma; `crates/text/src/tests/hang.rs` checks whole paragraphs with both composers, ragged, centred and justified, with Line-end Punctuation Half Width and bidirectional lines.
Optical Margin Alignment and Roman Hanging Punctuation hang right-to-left paragraphs of point and area type, and type on a path whose first paragraph runs right to left, too. Auto aligns a right-to-left line at its right, or at the end bracket of a path, so it hangs there. A right-to-left line is read from right to left: where it hangs at its left margin, `."` ending it hangs both glyphs.
The Paragraph panel menu's Reset Panel (`text.setStyle`, `text.setFormat` and `text.tabs.clear`) gives the selected type's paragraphs, or the paragraphs the Type tool's selection touches, Auto alignment, no indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi Set, Regular burasagari, the Hard kinsoku set, the Every-line Composer, Roman Hanging Punctuation off, no kashidas and no tab stops, as one undo step.
The Paragraph panel menu's Hyphenation… (`ui.hyphenation`, `text.setFormat {wordsLongerThan, afterFirst, beforeLast, hyphenLimit, hyphenationZone, hyphenateCapitalized}`) sets each paragraph's hyphenation limits: the shortest word that breaks, the fewest letters on each side of the hyphen, the most hyphenated lines in a row, a zone at the right edge where the Single-line Composer breaks before a word instead of hyphenating it, and whether capitalized words break. The defaults are words longer than 5 letters, 3 letters on each side, no limit or zone, and capitalized words hyphenated; paragraph styles and the native file keep the options.
PDF export with type as text writes the hyphen that ends a line broken inside a word as a hyphen character where the line draws it, and the file opens with the hyphen in the line's text; kashidas are drawn as paths.
The Paragraph panel's Insert Kashidas (with the Indic options; `text.setFormat {kashida}`) lengthens each Arabic word of a justified horizontal area type line with a kashida, a tatweel between its last pair of joined letters, before the word spaces widen: Short, Medium and Long kashidas are at most a quarter, half and a whole em long, Stylistic ones are whole tatweels up to an em, and None (the default) leaves the line as before; paragraph styles and the native file keep the setting.