Merge pull request #1127 from storytold/fix/1111-paragraph-align-shortcuts

#1111: Ctrl+Shift+L, C, R, J and F align paragraphs
This commit is contained in:
BFlat
2026-10-11 12:11:21 +07:00
committed by GitHub
4 changed files with 71 additions and 1 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+14
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"
+18
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);
+38
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.