mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Type: Alt+arrow spacing steps turn with vertical type
In vertical type, Alt+Down/Up (with or without Shift) steps kerning at a caret or tracking of selected text, Alt+Left/Right steps leading, and Alt+Shift+Right/Left steps baseline shift; Cmd/Ctrl makes five steps. Horizontal type keeps its mapping. Selected type objects follow the same rule when all of them are vertical, and a selection that mixes horizontal and vertical type takes the horizontal mapping in one undo step.
This commit is contained in:
@@ -84,7 +84,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Step Type",
|
||||
[],
|
||||
None,
|
||||
"{attribute: \"size\"|\"leading\"|\"tracking\"|\"kerning\"|\"baselineShift\", by?: steps (1; negative steps down), id?, start?: byte, end?: byte, ids?: [id…] (instead of id)} step type by the Preferences › Type increments (size and leading: typeSizeIncrement, pt; tracking and kerning: trackingIncrement, 1/1000 em; baseline shift: baselineShiftIncrement, pt), what the Type tool's Alt+arrows do (Cmd/Ctrl too: five steps; ←/→ kerning at the caret or tracking of the selection, ↑/↓ leading, Shift+↑/↓ baseline shift). The range of `id`, else the type objects in `ids`, else the text the Type tool has selected (at a caret: kerning steps the character before it, leading its paragraph), else every selected type object → {ids}",
|
||||
"{attribute: \"size\"|\"leading\"|\"tracking\"|\"kerning\"|\"baselineShift\", by?: steps (1; negative steps down), id?, start?: byte, end?: byte, ids?: [id…] (instead of id)} step type by the Preferences › Type increments (size and leading: typeSizeIncrement, pt; tracking and kerning: trackingIncrement, 1/1000 em; baseline shift: baselineShiftIncrement, pt), what the Type tool's Alt+arrows do (Cmd/Ctrl too: five steps; horizontal: ←/→ kerning at the caret or tracking of the selection, ↑/↓ leading, Shift+↑/↓ baseline shift; vertical: ↓/↑ with or without Shift kerning or tracking, ←/→ leading, Shift+→/← baseline shift; mixed selected text objects use the horizontal mapping). The range of `id`, else the type objects in `ids`, else the text the Type tool has selected (at a caret: kerning steps the character before it, leading its paragraph), else every selected type object → {ids}",
|
||||
has_doc,
|
||||
type_step
|
||||
),
|
||||
@@ -1132,7 +1132,7 @@ fn type_step(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Cmd/Ctrl+Alt+Q: tracking 0 and kerning Auto (0 in the Character panel) in the type objects in
|
||||
/// `ids`, else in the Type tool's selected text, else in every selected type object. At a caret,
|
||||
/// only the kerning of the character before it changes, as Alt+←/→ kerns there.
|
||||
/// only the kerning of the character before it changes, as Alt+←/→ in horizontal type or Alt+↓/↑ in vertical type kerns there.
|
||||
fn reset_tracking(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ranges = type_ranges(s, p, "type.resetTracking", |text, a| Some((edit::prev_char(text, a), a)))?;
|
||||
if !ranges.is_empty() {
|
||||
|
||||
@@ -202,12 +202,12 @@ pub struct TypeTool {
|
||||
}
|
||||
|
||||
impl TypeTool {
|
||||
pub(super) fn step_attribute(&mut self, key: ToolKey, mods: Mods, a: usize, b: usize) -> Option<Vec<Action>> {
|
||||
pub(super) fn step_attribute(&mut self, key: ToolKey, mods: Mods, a: usize, b: usize, vertical: bool) -> Option<Vec<Action>> {
|
||||
if !mods.alt {
|
||||
return None;
|
||||
}
|
||||
let id = self.editing?;
|
||||
let (attribute, by) = plain::step_key(key, mods.shift, a == b)?;
|
||||
let (attribute, by) = plain::step_key(key, mods.shift, a == b, vertical)?;
|
||||
let mut out = self.commit();
|
||||
let by = if mods.cmd { by * 5.0 } else { by };
|
||||
out.push(Action::Exec("type.step".into(), json!({"id": id.0, "start": a, "end": b, "attribute": attribute, "by": by})));
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
use super::{ImeSurrounding, Range, ToolKey};
|
||||
|
||||
/// Byte range in `s` of the characters `r` (IME ranges count characters; carets count bytes).
|
||||
/// `None` when `r` runs past the end of `s` or backwards.
|
||||
/// The `type.step` an Alt+arrow asks for, by one step: ←/→ kerning at a `caret`, else tracking;
|
||||
/// ↑/↓ leading (down opens it up); Shift+↑/↓ baseline shift.
|
||||
pub fn step_key(key: ToolKey, shift: bool, caret: bool) -> Option<(&'static str, f64)> {
|
||||
/// Map Alt+arrows to one type step. Horizontal ←/→ changes kerning at a caret or
|
||||
/// tracking on a range, ↑/↓ changes leading, and Shift+↑/↓ changes baseline shift.
|
||||
/// Vertical ↑/↓ (with or without Shift) changes kerning or tracking, ←/→ changes
|
||||
/// leading, and Shift+→/← changes baseline shift.
|
||||
pub fn step_key(key: ToolKey, shift: bool, caret: bool, vertical: bool) -> Option<(&'static str, f64)> {
|
||||
let shift = shift && !(vertical && matches!(key, ToolKey::Up | ToolKey::Down));
|
||||
let key = if vertical {
|
||||
match key {
|
||||
ToolKey::Up => ToolKey::Left,
|
||||
ToolKey::Down => ToolKey::Right,
|
||||
ToolKey::Right => ToolKey::Up,
|
||||
ToolKey::Left => ToolKey::Down,
|
||||
other => other,
|
||||
}
|
||||
} else {
|
||||
key
|
||||
};
|
||||
let sign = |up: bool| if up { 1.0 } else { -1.0 };
|
||||
Some(match (key, shift) {
|
||||
(ToolKey::Left | ToolKey::Right, false) => (if caret { "kerning" } else { "tracking" }, sign(key == ToolKey::Right)),
|
||||
@@ -14,6 +26,8 @@ pub fn step_key(key: ToolKey, shift: bool, caret: bool) -> Option<(&'static str,
|
||||
})
|
||||
}
|
||||
|
||||
/// Byte range in `s` of the characters `r` (IME ranges count characters; carets count bytes).
|
||||
/// `None` when `r` runs past the end of `s` or backwards.
|
||||
pub(super) fn char_range_to_bytes(s: &str, r: Range<usize>) -> Option<Range<usize>> {
|
||||
let byte = |c: usize| if c == s.chars().count() { Some(s.len()) } else { s.char_indices().nth(c).map(|(i, _)| i) };
|
||||
let (a, b) = (byte(r.start)?, byte(r.end)?);
|
||||
|
||||
@@ -898,6 +898,65 @@ fn marked_text_is_underlined_with_the_converting_clause_thick() {
|
||||
assert!(!tool.composing());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spacing_arrows_follow_each_text_objects_orientation() {
|
||||
let cases = [
|
||||
(ToolKey::Down, ALT, "kerning", 1.0),
|
||||
(ToolKey::Up, ALT, "kerning", -1.0),
|
||||
(ToolKey::Left, ALT, "leading", 1.0),
|
||||
(ToolKey::Right, ALT, "leading", -1.0),
|
||||
(ToolKey::Right, SHIFT_ALT, "baselineShift", 1.0),
|
||||
(ToolKey::Left, SHIFT_ALT, "baselineShift", -1.0),
|
||||
(ToolKey::Down, SHIFT_ALT, "kerning", 1.0),
|
||||
(ToolKey::Up, SHIFT_ALT, "kerning", -1.0),
|
||||
(ToolKey::Down, Mods { cmd: true, ..ALT }, "kerning", 5.0),
|
||||
(ToolKey::Up, Mods { cmd: true, ..ALT }, "kerning", -5.0),
|
||||
];
|
||||
for kind in 0..3 {
|
||||
for selected in [false, true] {
|
||||
let (mut d, id, mut tool) = editing("abcd");
|
||||
let NodeKind::Text(t) = &mut d.node_mut(id).unwrap().kind else { panic!("text") };
|
||||
t.vertical = true;
|
||||
let path = vectorcraft_geom::PathData::from_bezpath(&Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1));
|
||||
t.kind = match kind {
|
||||
1 => TextKind::Area { frame: path },
|
||||
2 => TextKind::OnPath { path, start: 0.0, end: None },
|
||||
_ => TextKind::Point,
|
||||
};
|
||||
tool.caret = 2;
|
||||
tool.anchor = if selected { 1 } else { 2 };
|
||||
for (key_code, mods, caret_attribute, by) in cases {
|
||||
let before = (tool.anchor, tool.caret);
|
||||
let actions = key(&mut tool, &d, key_code, mods);
|
||||
let step = actions
|
||||
.iter()
|
||||
.find_map(|action| match action {
|
||||
Action::Exec(command, params) if command == "type.step" => Some(params),
|
||||
_ => None,
|
||||
})
|
||||
.expect("spacing command");
|
||||
assert_eq!(
|
||||
step["attribute"],
|
||||
if selected && caret_attribute == "kerning" { "tracking" } else { caret_attribute },
|
||||
"kind {kind}, selected {selected}"
|
||||
);
|
||||
assert_eq!(step["by"], by);
|
||||
assert_eq!((tool.anchor, tool.caret), before);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (mut d, id, mut tool) = editing("abcd");
|
||||
let NodeKind::Text(t) = &mut d.node_mut(id).unwrap().kind else { panic!("text") };
|
||||
t.vertical = true;
|
||||
key(&mut tool, &d, ToolKey::Down, SHIFT);
|
||||
assert_eq!((tool.anchor, tool.caret), (0, 1));
|
||||
let (d, _, mut tool) = editing("abcd");
|
||||
let actions = key(&mut tool, &d, ToolKey::Right, ALT);
|
||||
assert!(actions.iter().any(
|
||||
|action| matches!(action, Action::Exec(command, params) if command == "type.step" && params["attribute"] == "kerning" && params["by"] == 1.0)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrows_follow_the_columns_of_vertical_type() {
|
||||
let (mut d, id) = doc_with_text("§§§\n§§§");
|
||||
|
||||
@@ -175,7 +175,7 @@ impl Tool for TypeTool {
|
||||
self.anchor = self.anchor.min(len);
|
||||
let (a, b) = self.sel();
|
||||
// Alt+arrows step the type by the Preferences › Type increments (Cmd/Ctrl too: five steps).
|
||||
if let Some(out) = self.step_attribute(key, mods, a, b) {
|
||||
if let Some(out) = self.step_attribute(key, mods, a, b, t.vertical) {
|
||||
return out;
|
||||
}
|
||||
let word = mods.cmd || mods.alt;
|
||||
|
||||
@@ -361,19 +361,28 @@ fn single_key_shortcuts(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
/// Route Alt+arrows to spacing controls when text objects are selected.
|
||||
fn alt_type_arrows(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
|
||||
let m = ctx.input(|i| i.modifiers);
|
||||
if !m.alt
|
||||
|| !app.session.active().is_some_and(|st| {
|
||||
st.selection.guides.is_empty()
|
||||
&& !st.selection.objects.is_empty()
|
||||
&& st.selection.objects.iter().all(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(vectorcraft_doc::NodeKind::Text(_))))
|
||||
})
|
||||
{
|
||||
if !m.alt {
|
||||
return false;
|
||||
}
|
||||
let orientation = app.session.active().and_then(|st| {
|
||||
if !st.selection.guides.is_empty() || st.selection.objects.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut all_vertical = true;
|
||||
for id in &st.selection.objects {
|
||||
let node = st.doc.node(*id)?;
|
||||
let vectorcraft_doc::NodeKind::Text(t) = &node.kind else { return None };
|
||||
all_vertical &= t.vertical;
|
||||
}
|
||||
Some(all_vertical)
|
||||
});
|
||||
let Some(vertical) = orientation else {
|
||||
return false;
|
||||
};
|
||||
let keys = [(Key::ArrowLeft, ToolKey::Left), (Key::ArrowRight, ToolKey::Right), (Key::ArrowUp, ToolKey::Up), (Key::ArrowDown, ToolKey::Down)];
|
||||
for (key, tool_key) in keys {
|
||||
if ctx.input_mut(|i| i.consume_key(m, key)) {
|
||||
if let Some((attribute, by)) = vectorcraft_tools::text::step_key(tool_key, m.shift, false) {
|
||||
if let Some((attribute, by)) = vectorcraft_tools::text::step_key(tool_key, m.shift, false, vertical) {
|
||||
let by = if m.command { by * 5.0 } else { by };
|
||||
let _ = app.run("type.step", json!({"attribute": attribute, "by": by}));
|
||||
}
|
||||
|
||||
@@ -1094,6 +1094,104 @@ fn cmd_alt_shift_period_and_comma_step_the_font_size_by_five() {
|
||||
assert_eq!(sizes(&mut app), [(json!("One "), json!(14.0)), (json!("Two"), json!(24.0))]);
|
||||
}
|
||||
|
||||
/// Spacing arrow keys change tracking, leading, and baseline shift for a selected vertical text object.
|
||||
#[test]
|
||||
fn alt_arrows_rotate_for_selected_vertical_type() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let vertical = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "One Two", "size": 20, "vertical": true})).unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
app.select_tool("selection");
|
||||
app.session.execute("select.set", &json!({"ids": [vertical]})).unwrap();
|
||||
let press = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
let cases = [
|
||||
(Key::ArrowDown, Modifiers::ALT, "tracking", 20.0),
|
||||
(Key::ArrowUp, Modifiers::ALT, "tracking", 0.0),
|
||||
(Key::ArrowLeft, Modifiers::ALT, "leading", 26.0),
|
||||
(Key::ArrowRight, Modifiers::ALT, "leading", 24.0),
|
||||
(Key::ArrowRight, Modifiers::ALT | Modifiers::SHIFT, "baseline_shift", 2.0),
|
||||
(Key::ArrowLeft, Modifiers::ALT | Modifiers::SHIFT, "baseline_shift", 0.0),
|
||||
(Key::ArrowDown, Modifiers::ALT | Modifiers::COMMAND, "tracking", 100.0),
|
||||
(Key::ArrowUp, Modifiers::ALT | Modifiers::COMMAND, "tracking", 0.0),
|
||||
];
|
||||
for (key, modifiers, field, value) in cases {
|
||||
frame(&mut app, vec![egui::Event::ModifiersChanged(modifiers), press(key, modifiers)]);
|
||||
let range = app.session.execute("text.getRange", &json!({"id": vertical})).unwrap();
|
||||
assert_eq!(range["runs"][0]["style"][field], json!(value), "{key:?}");
|
||||
assert_eq!(app.session.active().unwrap().selection.objects, [vectorcraft_doc::NodeId(vertical)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_type_objects_take_horizontal_spacing_chords_in_both_orders() {
|
||||
let press = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
for vertical_first in [true, false] {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let vertical = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "One", "size": 20, "vertical": true})).unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let horizontal = app.session.execute("text.create", &json!({"x": 80, "y": 50, "text": "Two", "size": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
let ids = if vertical_first { [vertical, horizontal] } else { [horizontal, vertical] };
|
||||
app.select_tool("type");
|
||||
app.session.execute("select.set", &json!({"ids": ids})).unwrap();
|
||||
let before = app.session.active().unwrap().history.undo.len();
|
||||
let original = ids.map(|id| app.session.execute("text.getRange", &json!({"id": id})).unwrap());
|
||||
frame(&mut app, vec![egui::Event::ModifiersChanged(Modifiers::ALT), press(Key::ArrowDown, Modifiers::ALT)]);
|
||||
for id in ids {
|
||||
let range = app.session.execute("text.getRange", &json!({"id": id})).unwrap();
|
||||
assert_eq!(range["runs"][0]["style"]["leading"], json!(26.0));
|
||||
}
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1);
|
||||
assert_eq!(app.session.active().unwrap().selection.objects.len(), 2);
|
||||
app.session.execute("edit.undo", &json!({})).unwrap();
|
||||
for (id, expected) in ids.into_iter().zip(original) {
|
||||
let range = app.session.execute("text.getRange", &json!({"id": id})).unwrap();
|
||||
assert_eq!(range, expected);
|
||||
}
|
||||
app.session.execute("edit.redo", &json!({})).unwrap();
|
||||
for id in ids {
|
||||
let range = app.session.execute("text.getRange", &json!({"id": id})).unwrap();
|
||||
assert_eq!(range["runs"][0]["style"]["leading"], json!(26.0));
|
||||
}
|
||||
frame(&mut app, vec![egui::Event::ModifiersChanged(Modifiers::ALT), press(Key::ArrowRight, Modifiers::ALT)]);
|
||||
for id in ids {
|
||||
let range = app.session.execute("text.getRange", &json!({"id": id})).unwrap();
|
||||
assert_eq!(range["runs"][0]["style"]["tracking"], json!(20.0));
|
||||
}
|
||||
assert_eq!(app.session.active().unwrap().selection.objects.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_vertical_area_and_path_type_use_vertical_spacing_chords() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
|
||||
let area_frame = app.session.execute("path.create", &json!({"d": "M10 10 L100 10 L100 100 L10 100 Z"})).unwrap()["id"].as_u64().unwrap();
|
||||
let path = app.session.execute("path.create", &json!({"d": "M150 10 L250 10"})).unwrap()["id"].as_u64().unwrap();
|
||||
let area =
|
||||
app.session.execute("text.createInPath", &json!({"path": area_frame, "mode": "area", "text": "Area", "vertical": true, "size": 20})).unwrap()
|
||||
["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let on_path = app
|
||||
.session
|
||||
.execute("text.createInPath", &json!({"path": path, "mode": "onPath", "text": "Path", "vertical": true, "size": 20}))
|
||||
.unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
app.select_tool("selection");
|
||||
app.session.execute("select.set", &json!({"ids": [area, on_path]})).unwrap();
|
||||
let modifiers = Modifiers::ALT;
|
||||
let press = egui::Event::Key { key: Key::ArrowDown, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
frame(&mut app, vec![egui::Event::ModifiersChanged(modifiers), press]);
|
||||
for id in [area, on_path] {
|
||||
let range = app.session.execute("text.getRange", &json!({"id": id})).unwrap();
|
||||
assert_eq!(range["runs"][0]["style"]["tracking"], json!(20.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// #3786: Alt+arrows step a selected text object's spacing instead of copying and nudging it.
|
||||
#[test]
|
||||
fn alt_arrows_step_selected_type_object() {
|
||||
|
||||
+4
-2
@@ -3173,8 +3173,10 @@ Pen's second anchor at (150, 431), in line with the square's centre.
|
||||
Cmd+Shift+,) step the size. `type.size.increaseByFive` (Cmd+Alt+Shift+., `press_key {key: ".", mods: {cmd: true,
|
||||
alt: true, shift: true}}`) and `type.size.decreaseByFive` (Cmd+Alt+Shift+,) step it by five times `typeSizeIncrement`.
|
||||
The Type tool's Alt+arrows (`press_key {key: "Right", mods: {alt: true}}`) step kerning at
|
||||
a caret or tracking of the selection (←/→), leading (↑/↓) and baseline shift (Shift+↑/↓), five steps with Cmd/Ctrl
|
||||
too. `type.resetTracking` (Cmd+Alt+Q, `press_key {key: "Q", mods: {cmd: true, alt: true}}`) sets tracking to 0 and
|
||||
a caret or tracking of the selection (←/→ in horizontal type, ↓/↑ with or without Shift in vertical type),
|
||||
leading (↑/↓ in horizontal type, ←/→ in vertical type), and baseline shift (Shift+↑/↓ in horizontal type,
|
||||
Shift+→/← in vertical type). Mixed selected text objects use the horizontal mapping; Cmd/Ctrl makes five steps.
|
||||
`type.resetTracking` (Cmd+Alt+Q, `press_key {key: "Q", mods: {cmd: true, alt: true}}`) sets tracking to 0 and
|
||||
kerning to Auto in the type objects in `ids`, the Type tool's selected text or the selected type objects; at a caret
|
||||
it resets only the kerning of the character before it. `type.resetHorizontalScale` (Cmd+Shift+X,
|
||||
`press_key {key: "X", mods: {cmd: true, shift: true}}`) sets the horizontal scale to 100%, as the Character panel
|
||||
|
||||
@@ -814,6 +814,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
With anchors or segments of a path selected with Direct Selection, Simplify acts on just that part of the path, as the [Manually simplify paths](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/manually-simplify-paths.html) page puts it: the slider, Auto-Simplify and the dialog refit only each run of selected segments, between its end anchors, which keep their place and their handles on the side away from the run. The anchors and segments outside the runs stay exactly as they were, a closed path's run may cross its start point, separate runs on one path are refitted independently in one undo step, and the runs stay selected. A path with only lone anchors selected is still simplified whole, and `object.path.simplify {wholePaths: true}` simplifies partly selected paths whole. Tests: `crates/engine/src/tests/simplify_part.rs` and `simplify_slider_and_dialog_simplify_only_the_direct_selected_part_of_a_path`.
|
||||
Object › Path › Smooth… (#4115) opens a slider under the selection from Minimum to Maximum smoothing with an Auto-Smooth button, as the [Smooth slider](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/refine-path-segments-with-the-smooth-slider.html) page describes; the canvas previews it and Enter or a click outside keeps it as one undo step (`object.path.smooth {level, auto, ids}`). The page names only the ends, so the rest is VectorCraft's own rule: each selected path, compound path members included, is refitted with curves within 0.1% (level 0) to 2.5% (level 100) of its longer side, corners kept only where it turns back and never with more anchors (a rectangle stays as it is); Auto-Smooth is level 50. Closed paths stay closed and open ones keep their end points. Tests: `crates/engine/src/tests/smooth_path.rs` and `smooth_slider_previews_auto_smooth_and_keeps_the_result`.
|
||||
- **Devanagari shaping:** with an OpenType Devanagari font, Ka, virama and Ssa (क्ष) form one conjunct glyph, Ra and virama before a consonant (र्क) become a reph drawn after it, the i matra (कि) is drawn before its consonant, and Ka with a nukta is drawn like the precomposed क़ (U+0958), as [Microsoft's Devanagari shaping specification](https://learn.microsoft.com/en-us/typography/script-development/devanagari) describes. Each syllable is one cluster of its source bytes, and the arrow keys stop at its ends. A click inside a syllable drawn with several glyphs puts the caret at the syllable's nearer end, in point, area and path type. Tests: `crates/text/src/tests/devanagari.rs`; they print SKIPPED when no such font is installed.
|
||||
- **Vertical type spacing shortcuts:** The [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page gives Alt+Down/Up for kerning at a caret or tracking of selected text, Alt+Left/Right for leading, and Alt+Shift+Right/Left for baseline shift. Alt+Shift+Down/Up also steps kerning or tracking. Down increases kerning or tracking, Left increases leading, and Shift+Right increases baseline shift; the opposite arrows decrease them. Cmd/Ctrl makes five steps. Horizontal type keeps its existing mapping; mixed horizontal and vertical text-object selections take the horizontal mapping for both objects in one undo step. Selected vertical point, area and path type use the same directions. Tests: `spacing_arrows_follow_each_text_objects_orientation`, `alt_arrows_rotate_for_selected_vertical_type`, `mixed_type_objects_take_horizontal_spacing_chords_in_both_orders`, and `selected_vertical_area_and_path_type_use_vertical_spacing_chords`.
|
||||
- **Vertical punctuation:** fullwidth `!` (U+FF01) and `?` (U+FF1F) use their nominal glyph upright when no vertical alternate is present, following the Tu fallback in [Unicode UAX #50 §3.1](https://www.unicode.org/reports/tr50/#vo). Code-built test fonts cover point and area type with and without `vert` alternates, retain the rotated fallback for a bracket and long vowel mark, and leave horizontal type unchanged. Tests: `crates/text/src/tests/vertical.rs`.
|
||||
- Free Distort's dialog (Effect › Distort & Transform › Free Distort…) shows a box in the proportions of the first selected object's bounds with a handle on each corner. Dragging a handle moves that corner. A handle focused by a click or Tab moves 1% of the box with each arrow key, and 10% with Shift. Reset puts the four corners back on the box. The preview and OK run `effect.apply`, or `effect.setParams` when the dialog edits an applied effect, and the Appearance panel's inline editor opens the dialog with Edit…. Behavior reference: Illustrator CC Help (2014, [PDF](https://help.adobe.com/archive/en/illustrator/cc/2014/illustrator_reference.pdf)), page 285 (PDF page 291).
|
||||
- **Kerning across style runs:** characters that differ only in fill, stroke, underline, strikethrough, overprint, character style name, leading, baseline shift, rotation, Character Alignment, kerning, or tracking that keeps or drops ligatures alike are shaped together, so restyling one character keeps the font's kerning pairs with its neighbors. A character with manual kerning that follows one with automatic kerning keeps the font's kerning with it (unless All Caps or Small Caps sets it as two letters, as ß becomes SS), and its value replaces the font's kerning after it. A ligature does not form across a change of style, and a change of font, size or OpenType features still shapes the characters apart. Behavior reference: [Line and character spacing](https://web.archive.org/web/20260916050333/https://helpx.adobe.com/illustrator/using/line-character-spacing.html) (public documentation). Tests: `crates/text/src/tests/typo.rs`, `crates/text/src/tests/bidi.rs`.
|
||||
|
||||
Reference in New Issue
Block a user