Drop caps: one-line caps keep a smaller size and set their tabs

A one-line drop cap keeps its characters' own point size when it is smaller than the text (verse numbers), and tabs inside a left-to-right drop cap advance to the paragraph's tab stops.
This commit is contained in:
aiongg
2026-10-10 15:50:05 +00:00
parent 21863563f3
commit bcdbb68eee
3 changed files with 43 additions and 7 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Appearance of Black complete: Printing/Exporting rich black for RGB output (PNG), Overprint [Black] Swatch at 100% (document, IDML OverprintBlack).
- Hyperlinks and Bookmarks panels (Window ▸ Interactive): new from the selection, edit (`hyperlink.edit`), go to source (`hyperlink.goToSource`), rename bookmarks (`bookmark.rename`), go to page, delete.
- Nested line styles (`nestedLineStyles` paragraph attribute, Paragraph Style Options › Drop Caps and Nested Styles, IDML AllNestedLineStyles): the first lines restyle until the line breaks settle.
- Drop caps (`type.para {dropCapLines, dropCapChars, dropCapStyle, dropCapAlignLeft, dropCapScaleDescenders}`, Paragraph panel, Paragraph Style Options › Drop Caps and Nested Styles; IDML DropCapLines/DropCapCharacters/DropcapDetail, DropCapStyle on import): the first characters (code points, as InDesign counts them, never splitting a letter from its combining marks; up to 150) are enlarged from the first line's cap height down N − 1 leadings of the text after them (up to 25 lines; grid steps on a baseline grid), the lines beside them are indented by their advance (on the right in right-to-left paragraphs), spaces right after them are set at their size without widening the indent, baselines don't move; Align Left Edge (on by default) moves the drop cap left by its first letter's side bearing so its ink starts at the indent; Scale for Descenders is kept but doesn't change the layout yet; a drop cap character style, and nested styles through the drop cap (Dropcap delimiter). Drawn on screen and in PDF (text in order), with caret, hit testing and selection over the enlarged letters. Vertical text sets no drop caps.
- Drop caps (`type.para {dropCapLines, dropCapChars, dropCapStyle, dropCapAlignLeft, dropCapScaleDescenders}`, Paragraph panel, Paragraph Style Options › Drop Caps and Nested Styles; IDML DropCapLines/DropCapCharacters/DropcapDetail, DropCapStyle on import): the first characters (code points, as InDesign counts them, never splitting a letter from its combining marks; up to 150) are enlarged from the first line's cap height down N − 1 leadings of the text after them (up to 25 lines; grid steps on a baseline grid), the lines beside them are indented by their advance (on the right in right-to-left paragraphs), spaces right after them are set at their size without widening the indent, baselines don't move; a one-line drop cap keeps a smaller size of its own and sets its tabs at the paragraph's tab stops; Align Left Edge (on by default) moves the drop cap left by its first letter's side bearing so its ink starts at the indent; Scale for Descenders is kept but doesn't change the layout yet; a drop cap character style, and nested styles through the drop cap (Dropcap delimiter). Drawn on screen and in PDF (text in order), with caret, hit testing and selection over the enlarged letters. Vertical text sets no drop caps.
- User dictionary hyphenation exceptions (`hyphenation.addException/removeException/list`, Edit ▸ Spelling ▸ User Dictionary): `ex~am~ple` breaks only at `~`, a plain word never hyphenates.
- Frame Fitting Options (`object.fittingOptions`, Object ▸ Fitting ▸ Frame Fitting Options…): Auto-Fit refits on resize, fitting, Align From reference point, crop amounts; IDML FrameFittingOption.
- Paragraph borders (per-side weights, colour, tint, offsets) and shading offsets, drawn per column part of split paragraphs (also when overset); Paragraph panel controls; IDML ParagraphBorder*/ParagraphShading*Offset.
+24 -6
View File
@@ -695,6 +695,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
let drop_grid = cur_frame.and_then(|f| f.grid).filter(|_| pp.grid_align == GridAlign::AllLines).map(|g| g.1);
// Align Left Edge (left-to-right paragraphs).
let drop_align = pp.drop_cap_align_left && pp.direction == designcraft_doc::TextDirection::LeftToRight;
let drop_tabs = (pp.direction == designcraft_doc::TextDirection::LeftToRight).then_some((&pp.tabs[..], pp.left_indent));
if !pp.nested_line_styles.is_empty() && cur.fi < frames.len() {
// Nested line styles: find where the first lines end in this column, restyle them, and
// look again (the style changes the widths) until the lines settle.
@@ -706,7 +707,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
for _ in 0..3 {
// The same drop cap, spacing, hyphenation and breaker as the layout below.
let mut gl = sp.glyphs.clone();
let dc = split_drop_cap(db, &mut gl, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align);
let dc = split_drop_cap(db, &mut gl, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align, drop_tabs);
let width = |j: usize| {
let ind = match &dc {
Some(dc) if j < dc.lines => dc.indent(),
@@ -774,7 +775,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
let rule_color = |r: &designcraft_doc::Rule, text: &(String, f32)| {
if r.color == designcraft_doc::TEXT_COLOR { text.clone() } else { (r.color.clone(), r.tint) }
};
let drop_cap = split_drop_cap(db, &mut sp.glyphs, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align);
let drop_cap = split_drop_cap(db, &mut sp.glyphs, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align, drop_tabs);
// List labels take the default super/subscript settings.
let label_env = shape::TypeEnv { adv: Default::default(), ..env };
let list_label = match pp.list_type {
@@ -2866,10 +2867,11 @@ struct DropCap {
/// Take the drop cap (the glyphs before byte `end`) off the front of a paragraph's glyphs and scale
/// it so its cap height reaches from the first line's cap height down to line `lines`' baseline,
/// measured in the leading and cap height of the text after it.
/// On a baseline grid of increment `grid`, lines are that many grid steps apart. With
/// `align_left` its ink starts at the indent (see [`DropCap::lsb`]). Spaces right after it join it
/// at its size without widening it.
/// measured in the leading and cap height of the text after it. A one-line drop cap keeps its own
/// size when that is smaller. On a baseline grid of increment `grid`, lines are that many grid
/// steps apart. With `align_left` its ink starts at the indent (see [`DropCap::lsb`]). `tabs`
/// (stops and left indent, left-to-right paragraphs) sets the tabs inside it. Spaces right after
/// it join it at its size without widening it.
#[allow(clippy::too_many_arguments)]
fn split_drop_cap(
db: &ScopedFonts<'_>,
@@ -2880,6 +2882,7 @@ fn split_drop_cap(
base_cap: f64,
grid: Option<f64>,
align_left: bool,
tabs: Option<(&[designcraft_doc::TabStop], f64)>,
) -> Option<DropCap> {
let n = glyphs.iter().take_while(|g| g.byte < end).count();
if n == 0 {
@@ -2899,6 +2902,7 @@ fn split_drop_cap(
let drop = (lines - 1) as f64 * lead;
let own = dc.iter().map(|g| g.cap).fold(0.0, f64::max);
let k = if own > 0.0 { (drop + cap) / own } else { 1.0 };
let k = if lines == 1 { k.min(1.0) } else { k };
let k = if k.is_finite() && k > 0.0 { k.min(1000.0) } else { 1.0 };
for g in dc.iter_mut().chain(after.iter_mut()) {
g.adv *= k;
@@ -2922,6 +2926,20 @@ fn split_drop_cap(
}
_ => 0.0,
};
// Tabs advance to the paragraph's stops, measured from the column edge as in its lines.
if let Some((stops, left_indent)) = tabs {
let mut x = left_indent - lsb;
for i in 0..dc.len() {
let tab = match dc.get(i) {
Some(g) if g.ch == '\t' => Some(tab_advance(stops, left_indent, x, dc.get(i + 1..).unwrap_or_default()).0),
_ => None,
};
if let (Some(w), Some(g)) = (tab, dc.get_mut(i)) {
g.adv = w;
}
x += dc.get(i).map_or(0.0, |g| g.adv);
}
}
let width = dc.iter().map(|g| g.adv).sum();
dc.append(&mut after);
Some(DropCap { glyphs: dc, end, lines, width, drop, lsb })
+18
View File
@@ -2182,6 +2182,24 @@ fn space_after_a_drop_cap_does_not_widen_the_indent() {
assert_eq!(lines[0].drop_cap.unwrap().end, 3);
}
/// A one-line drop cap keeps its characters' own size (a smaller verse number stays smaller), and
/// the tabs in it advance to the paragraph's tab stops.
#[test]
fn one_line_drop_cap_keeps_its_size_and_sets_its_tabs() {
let stop = |position, align| designcraft_doc::TabStop { position, align, leader: String::new(), align_on: String::new() };
let para = ParaAttrs { tabs: Some(vec![stop(30.0, TabAlign::Right), stop(40.0, TabAlign::Left)]), ..drop_cap(1, 3) };
let (mut d, sid, _) = drop_doc(&format!("\t7\t{LOREM}"), para);
d.story_mut(sid).unwrap().format_chars(0..3, |f| f.over.size = Some(8.0));
let cs = compose_story(&d, sid, &ComposeOptions::default());
let l = &cs.frames[0].lines[0];
let at = |b: usize| l.glyphs.iter().find(|g| g.byte == b).unwrap();
let (seven, body) = (at(1), at(3));
assert!((seven.sy / body.sy - 0.8).abs() < 1e-9, "{} vs {}", seven.sy, body.sy);
assert!(seven.y.abs() < 1e-9);
assert!((seven.x + seven.adv - 30.0).abs() < 1e-6, "the number ends at the right tab: {}", seven.x + seven.adv);
assert!((text_x(l, 3) - 40.0).abs() < 1e-6, "the text starts at the left tab: {}", text_x(l, 3));
}
/// A rule in Text Color takes the colour of the paragraph's text: the first character's for the
/// rule above, the last character's for the rule below. A swatch colour is used as is.
#[test]