Type: right-aligned area type sets the text after a line's last tab against the edge

In right-aligned area type, the text before a line's last tab sits at its
stops, measured from the left edge, and the text after it (none when the
tab ends the line) against the right edge. The line was laid out from the
left and aligned as a whole, which moved its start off the left edge and
its tabs' text off their stops. Illustrator's own layout in a user's .ai
sets its price lists so. Centred and justified lines are aligned as a
whole as before.
This commit is contained in:
M Balazs PC
2026-11-07 18:49:19 +01:00
parent f2c1df1ec3
commit a4e315796b
3 changed files with 89 additions and 9 deletions
+41 -6
View File
@@ -383,6 +383,9 @@ struct LineJustification {
start_x: f64,
after: Vec<Option<usize>>,
kashidas: Vec<(usize, f64)>,
/// Right-aligned area type: the line's last tab, which takes the text after it to the right
/// edge (the text before it sits at its stops, from the left edge).
last_tab: Option<usize>,
}
fn run_paragraph_lines(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, t: &TextObject, mut paragraph: ParagraphData<'_, '_>) -> bool {
@@ -667,7 +670,14 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
}
}
let position = &line.position;
let start_x = if justify {
// Right-aligned area type with a tab: the text before the last tab sits at its stops, from the
// left edge, and the text after it against the right edge, as Illustrator's own layout in a
// user's `.ai` sets a price list. Centred and justified lines are aligned as a whole.
let last_tab = (para.justify == Justify::Right && !justify && !paragraph.rtl && paragraph.regions.is_some())
// A tab ending the line counts: its text sits at the left edge, the tab up to the right one.
.then(|| (line.index..line.position.end).rev().find(|&j| sg.get(j).is_some_and(|g| g.ch == '\t' && g.len > 0)))
.flatten();
let start_x = if justify || last_tab.is_some() {
line.ax0 - line.opt_l
} else if paragraph.regions.is_none() {
match align {
@@ -682,7 +692,35 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
_ => line.ax1 + line.opt_r - width,
}
};
LineJustification { per_space, per_gap, per_cjk, fit, start_x, after, kashidas }
LineJustification { per_space, per_gap, per_cjk, fit, start_x, after, kashidas, last_tab }
}
/// The advance and leader of tab `glyph_index` of `line`, at pen position `x`: up to its stop
/// ([`tab_advance`], measured from `tab_origin` less `tab_shift`), or, for the last tab of
/// right-aligned area type ([`LineJustification::last_tab`]), up to where the rest of the line
/// ends on the right edge. The tab after a list marker keeps the width `prepend_marker` gave it
/// (`advance`).
fn tab_glyph<'p>(
paragraph: &'p ParagraphData<'_, '_>,
line: &LineGeometry,
justification: &LineJustification,
glyph_index: usize,
x: f64,
(tab_origin, tab_shift): (f64, f64),
advance: f64,
) -> (f64, &'p str) {
let sg = &paragraph.sg;
if !sg.get(glyph_index).is_some_and(|g| g.len > 0) {
return (advance, "");
}
let (advance, leader) = tab_advance(&paragraph.para.tabs, tab_origin, x - tab_shift, sg.get(glyph_index + 1..line.trimmed).unwrap_or_default());
if justification.last_tab != Some(glyph_index) {
return (advance, leader);
}
// The width of what follows up to the line's end (nothing, when the tab ends the line).
let after = sg.get(glyph_index + 1..line.trimmed).unwrap_or_default();
let rest = if after.is_empty() { 0.0 } else { after.iter().map(|g| g.adv).sum::<f64>() - line.end_trim };
((line.ax1 + line.opt_r - rest - x).max(0.0), leader)
}
fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData<'_, '_>, line: &LineGeometry, justification: &LineJustification) {
@@ -724,10 +762,7 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData
let mut scale = 1.0;
let mut leader = "";
if glyph.ch == '\t' {
// The tab after a list marker keeps the width `prepend_marker` gave it.
if glyph.len > 0 {
(advance, leader) = tab_advance(&para.tabs, tab_origin, x - tab_shift, &sg[glyph_index + 1..trimmed.max(glyph_index + 1)]);
}
(advance, leader) = tab_glyph(paragraph, line, justification, glyph_index, x, (tab_origin, tab_shift), advance);
} else if glyph_index < body {
let flex = flow_line_flex(glyph_index, &paragraph.flex, &line.held, &line.hung);
if is_word_space(glyph) {
+45 -1
View File
@@ -664,12 +664,56 @@ fn a_leading_tab_takes_its_width_in_the_line_and_the_justification() {
}
}
/// Right-aligned area type with tabs: the text before the last tab sits at its stops, measured from
/// the frame's left edge, and the text after the last tab against the right edge, as Illustrator's
/// own layout in a user's `.ai` (used locally, not committed) sets a price list: "Name<tab>950
/// Ft<tab>1 750 Ft" with a right stop at 269.29 and a left one at 272.13 puts the name at 0, the
/// first price ending at 269.29 and the second ending on the edge.
#[test]
fn right_aligned_area_type_sets_the_text_after_its_last_tab_against_the_edge() {
use vectorcraft_doc::{TabAlign, TabStop};
let stop = |position: f64, align: TabAlign| TabStop { position, align, leader: String::new(), align_on: '.' };
let frame = Rect::new(0.0, 0.0, 336.0, 100.0);
let mut t = area("Name\t950 Ft\t1 750 Ft", style(13.0), frame, Justify::Right);
t.para.tabs = vec![stop(269.29, TabAlign::Right), stop(272.13, TabAlign::Left)];
let lay = layout(db(), &t);
let glyph = |byte: usize| lay.glyphs.iter().find(|g| g.byte == byte).unwrap();
let end = |byte: usize| glyph(byte).origin.x + glyph(byte).advance;
assert!(glyph(0).origin.x.abs() < 1e-6, "the name starts at the left edge: {}", glyph(0).origin.x);
assert!((end(10) - 269.29).abs() < 1e-6, "the first price ends on its right stop: {}", end(10));
let last = t.plain_text().len() - 1;
assert!((end(last) - 336.0).abs() < 1e-6, "the last price ends on the edge: {}", end(last));
// One tab: the text after it is against the edge, whatever its stop.
let mut one = area("Cseles Spritz\t2 350 Ft", style(13.0), frame, Justify::Right);
one.para.tabs = t.para.tabs.clone();
let lay = layout(db(), &one);
let first = lay.glyphs.first().unwrap();
let last = lay.glyphs.last().unwrap();
assert!(
first.origin.x.abs() < 1e-6 && (last.origin.x + last.advance - 336.0).abs() < 1e-6,
"{} {}",
first.origin.x,
last.origin.x + last.advance
);
// A tab ending the line (a heading "Sör<tab>" in the user's file): the text before it at the
// left edge, the tab to the right edge.
let lay = layout(db(), &area("Heading\t\nNext", style(13.0), frame, Justify::Right));
assert!(lay.glyphs.first().unwrap().origin.x.abs() < 1e-6, "{}", lay.glyphs.first().unwrap().origin.x);
let tab = lay.glyphs.iter().find(|g| g.byte == 7).unwrap();
assert!((tab.origin.x + tab.advance - 336.0).abs() < 1e-6, "the tab ends at {}", tab.origin.x + tab.advance);
// Without a tab, the line is right aligned as a whole.
let lay = layout(db(), &area("Name", style(13.0), frame, Justify::Right));
assert!(lay.glyphs.first().unwrap().origin.x > 200.0);
}
#[test]
fn a_centred_or_right_aligned_line_sets_its_tabs_as_if_it_started_at_the_left() {
use vectorcraft_doc::{TabAlign, TabStop};
// "A<tab>B" with a stop at 100 pt is 100 pt plus B wide whatever the alignment: the whole line
// moves, the gap between A and B stays 100 pt, and a right aligned line ends on the edge (#2457).
for justify in [Justify::Center, Justify::Right, Justify::JustifyCenter, Justify::JustifyRight] {
// (Right aligned area type sets the text after its last tab against the edge instead: see
// `right_aligned_area_type_sets_the_text_after_its_last_tab_against_the_edge`.)
for justify in [Justify::Center, Justify::JustifyCenter, Justify::JustifyRight] {
let mut t = area("A\tB", style(12.0), Rect::new(0.0, 0.0, 200.0, 100.0), justify);
t.para.tabs = vec![TabStop { position: 100.0, align: TabAlign::Left, leader: String::new(), align_on: '.' }];
let lay = layout(db(), &t);
+3 -2
View File
@@ -1,6 +1,6 @@
# Type parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (a soft return in justified type no longer keeps its line from being justified; Indian-language hyphenation review and license allowlist, #4947; Bulgarian among the languages, with its quote marks and the fonts' Bulgarian forms, #5945; previously minor: Show Snap to Glyph Options scrolls to the options, with a Snap to Glyph checkbox, #5273; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Indian-language hyphenation review and license allowlist, #4947; Bulgarian among the languages, with its quote marks and the fonts' Bulgarian forms, #5945; previously minor: Show Snap to Glyph Options scrolls to the options, with a Snap to Glyph checkbox, #5273; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
[target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the
@@ -45,7 +45,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Hyphenation… and Justification… options | done | Eleven Indian languages use attributed patterns and preserve syllables in both composers (#4947). Hyphenation… (`ui.hyphenation`, `text.setFormat`) sets each paragraph's Words Longer Than, After First, Before Last, Hyphen Limit, Hyphenation Zone (Single-line Composer only) and Hyphenate Capitalized Words, and paragraph styles carry them (#2310). Justification… (`ui.justification`, `text.setFormat`, #2755) sets Word Spacing, Letter Spacing and Glyph Scaling (Minimum, Desired, Maximum), Auto Leading and Single Word Justification; Desired applies to every line, justified lines and the Every-line Composer use the ranges. Vertical type isn't glyph scaled; type on a path doesn't use them. Evidence: `crates/text/src/tests/justify.rs`, `dialogs::justification::tests` |
| Roman Hanging Punctuation | done | Paragraph panel menu › Roman Hanging Punctuation, `text.setFormat {romanHangingPunctuation}` (#2756): per paragraph, Optical Margin Alignment's punctuation and amounts without the letters; paragraph styles carry it. Evidence: `tests::roman_hanging_punctuation_hangs_punctuation_but_not_letters`, `typecmd::area_tests::roman_hanging_punctuation_is_set_per_paragraph_and_saved_only_when_on` |
| Hyphenation… and Justification… options | done | Eleven Indian languages use attributed patterns and preserve syllables in both composers (#4947). Hyphenation… (`ui.hyphenation`, `text.setFormat`) sets each paragraph's Words Longer Than, After First, Before Last, Hyphen Limit, Hyphenation Zone (Single-line Composer only) and Hyphenate Capitalized Words, and paragraph styles carry them (#2310). Justification… (`ui.justification`, `text.setFormat`, #2755) sets Word Spacing, Letter Spacing and Glyph Scaling (Minimum, Desired, Maximum), Auto Leading and Single Word Justification; Desired applies to every line, justified lines and the Every-line Composer use the ranges. Vertical type isn't glyph scaled; type on a path doesn't use them. Auto Leading also sets the Leading the Character panel shows for Auto type, the leading Alt+↑/↓ (`type.step`) steps from, per paragraph, and the lines Fill with Placeholder Text counts. Evidence: `crates/text/src/tests/justify.rs`, `dialogs::justification::tests`, `tests_prefs::auto_leading_steps_and_fills_from_the_paragraph_auto_leading`, `panels::character` test `leading_field_shows_the_paragraph_auto_leading` |
| Tab stops and leaders; Tabs panel | partial | Tabs panel and stops with leaders in the model (`text.tabs.set`); a stop drags from where the press landed (#1458); leaders fill the tab's gap, repeated on a grid so they line up from line to line (#1877); Ctrl-dragging a stop (Command on macOS) moves all the stops together |
| Tab stops and leaders; Tabs panel | partial | Tabs panel and stops with leaders in the model (`text.tabs.set`); a stop drags from where the press landed (#1458); leaders fill the tab's gap, repeated on a grid so they line up from line to line (#1877); Ctrl-dragging a stop (Command on macOS) moves all the stops together; in right-aligned area type the text before a line's last tab sits at its stops, from the left edge, and the text after it against the right edge, as Illustrator's own layout in a user's `.ai` sets a price list (evidence: `tests::right_aligned_area_type_sets_the_text_after_its_last_tab_against_the_edge`; centred and justified lines are still aligned as a whole) |
| Hidden characters (Show Hidden Characters) | done | `type.hiddenCharacters` |
| Spell check (Check Spelling, Auto Spell Check, custom dictionary) | missing | menu stubs; needs an openly licensed dictionary |
| Smart Punctuation, Change Case, Find & Replace | done | |
@@ -79,6 +79,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
| Date | Change | Summary |
|---|---|---|
| 2026-11-07 | minor | Tabs: in right-aligned area type, the text before a line's last tab sits at its stops, measured from the left edge, and the text after it (none, when the tab ends the line) against the right edge; the line was aligned as a whole, which moved its start off the left edge. Illustrator's own layout in a user's `.ai` (used locally, not committed) sets its price lists so. Centred and justified lines keep their alignment as a whole. Scores and estimates unchanged. |
| 2026-11-07 | minor | Soft return: in Justify Left, Center or Right, the line a soft return ends is justified as a wrapped line is; only the paragraph's last line keeps its alignment. Illustrator's own layout in a user's `.ai` (used locally, not committed) justifies those lines. Scores and estimates unchanged. |
| 2026-11-07 | minor | Indian-language hyphenation (#4947): eleven MIT-licensed pattern sets, with grapheme and shaping-cluster protection in both composers and existing paragraph limits. Assamese and Sanskrit are selectable; Sanskrit automatic patterns remain pending an allowlisted source. Scores and estimates unchanged. |
| 2026-11-06 | minor | Alt + numeric-keypad codes (`Alt+0176` → °) type their character at the caret on Windows, and `type.insert` inserts a character by `code` or `unicode` (#5108); scores and estimates unchanged |