#1111: Ctrl+Shift+L, C, R, J and F align paragraphs

The paragraph alignment shortcuts were missing. Cmd/Ctrl+Shift+L, C and
R align the selected type's paragraphs left, centred and right, J
justifies them with the last line aligned left and F justifies all
lines; while the Type tool edits text they act on the paragraphs its
selection touches, through the Paragraph panel's own path. They are UI
commands (type.alignLeft, …), listed in Keyboard Shortcuts, and enabled
while type is selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-11 11:58:51 +07:00
parent 6ca54579cd
commit 525b79a51a
4 changed files with 71 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@ -189,6 +189,16 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
("view.textThreads", "Hide Text Threads", "Cmd+Shift+Y", "{} toggle the thread lines between threaded text frames"),
("view.gradientAnnotator", "Hide Gradient Annotator", "Cmd+Alt+G", "{} toggle the Gradient tool's annotator"),
("type.hiddenCharacters", "Show Hidden Characters", "Cmd+Alt+I", "{} toggle markers for spaces, paragraph ends and story ends"),
("type.alignLeft", "Align left", "Cmd+Shift+L", "{} align the selected type's paragraphs (or those the Type tool's selection touches) left"),
(
"type.alignCenter",
"Align center",
"Cmd+Shift+C",
"{} align the selected type's paragraphs (or those the Type tool's selection touches) centred",
),
("type.alignRight", "Align right", "Cmd+Shift+R", "{} align the selected type's paragraphs (or those the Type tool's selection touches) right"),
("type.justifyLeft", "Justify with last line aligned left", "Cmd+Shift+J", "{} justify the selected type's paragraphs, last line aligned left"),
("type.justifyAll", "Justify all lines", "Cmd+Shift+F", "{} justify every line of the selected type's paragraphs"),
(
"type.bold",
"Bold",
@ -994,6 +1004,9 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
"type.hiddenCharacters" => flag(&mut app.ui.view.hidden_chars),
"type.bold" => crate::panels::character::toggle_face(app, Face::Bold),
"type.italic" => crate::panels::character::toggle_face(app, Face::Italic),
id if let Some((_, justify)) = crate::panels::paragraph::ALIGN_COMMANDS.iter().find(|c| c.0 == id) => {
crate::panels::paragraph::align(app, justify)
}
"view.gradientAnnotator" => flag(&mut app.ui.view.gradient_annotator),
"effect.last" => match app.last_effect.clone() {
Some((e, params)) => {
@ -1876,6 +1889,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
// Save is off for a clean document that already has its own file.
"file.save" => app.session.active().is_some_and(|d| d.path.is_none() || d.converted || d.is_dirty()),
"type.bold" | "type.italic" => crate::panels::character::text_style(app).is_some(),
id if crate::panels::paragraph::ALIGN_COMMANDS.iter().any(|c| c.0 == id) => crate::panels::character::text_style(app).is_some(),
"file.reveal" => app.services.reveal.is_some() && app.session.active().is_some_and(|d| d.path.is_some()),
"file.place"
| "file.export.svg"

View File

@ -28,6 +28,24 @@ fn format(app: &mut VectorcraftApp, p: Value) {
para_cmd(app, "text.setFormat", p);
}
/// The alignment shortcuts' commands (Cmd+Shift+L, C, R, J and F, as in page layout apps) and the
/// `justify` each sets ([`align`]).
pub const ALIGN_COMMANDS: [(&str, &str); 5] = [
("type.alignLeft", "left"),
("type.alignCenter", "center"),
("type.alignRight", "right"),
("type.justifyLeft", "justifyLeft"),
("type.justifyAll", "justifyAll"),
];
/// Align the paragraphs of the selected type, or those the Type tool's selection touches, as
/// `justify` (an [`ALIGN_COMMANDS`] alignment, #1111).
pub(crate) fn align(app: &mut VectorcraftApp, justify: &str) -> Result<Value, String> {
text_style(app).ok_or("select some type first")?;
para_cmd(app, "text.setStyle", json!({ "justify": justify }));
Ok(Value::Null)
}
fn para_cmd(app: &mut VectorcraftApp, cmd: &str, mut p: Value) {
if let Some((id, a, b)) = super::character::text_editing(app) {
super::character::end_typing(app);

View File

@ -511,6 +511,44 @@ mod tests {
assert!(!app.ui.view.bounding_box);
}
/// Cmd+Shift+L, C, R, J and F align paragraphs (#1111): every paragraph of the selected type,
/// or while the Type tool edits text, those its selection touches.
#[test]
fn cmd_shift_letters_align_paragraphs() {
use vectorcraft_doc::Justify;
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "One\nTwo"})).unwrap()["id"].as_u64().unwrap();
let chord =
|key| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND | Modifiers::SHIFT };
let paras = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
vectorcraft_doc::NodeKind::Text(t) => (0..t.paragraph_count()).map(|i| t.para_at(i).justify).collect::<Vec<_>>(),
_ => panic!("text"),
};
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
for (key, want) in [
(Key::C, Justify::Center),
(Key::R, Justify::Right),
(Key::J, Justify::JustifyLeft),
(Key::F, Justify::JustifyAll),
(Key::L, Justify::Left),
] {
frame(&mut app, vec![chord(key)]);
assert_eq!(paras(&app), [want, want], "{key:?}");
}
// With the Type tool, only the paragraph its selection touches.
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 5, "end": 6}));
assert!(app.session.tool_wants_text());
frame(&mut app, vec![chord(Key::R)]);
assert_eq!(paras(&app), [Justify::Left, Justify::Right]);
// Nothing selected: nothing to align.
app.select_tool("selection");
app.session.execute("select.none", &json!({})).unwrap();
assert!(!crate::menus::enabled(&app, "type.alignCenter"));
}
/// Tab shows and hides the panels, and in the Type tool it types a tab, without moving the
/// keyboard focus on to a field: the keys after it (a tool letter, the next letter typed) still
/// work.