Fields: exact field boxes for rotated, multi-line and shaped text

Layout glyph cells carry their unrotated cell, so a field box in rotated
MTEXT is placed exactly; multi-line attributes box each line of a field
from their own layout cells and single-line attributes split on \P per
line; shaped words (TrueType fonts, right-to-left and complex scripts)
take the shaper's glyph cells for their characters instead of evenly
divided slots. The field background is also drawn for text rendered as
shaped strokes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-06 13:24:52 +03:00
1 parent 383eba14e5
commit 3830a69ca7
4 files changed
+273 -75

No files matched your search

+30 -2
View File
@@ -577,6 +577,30 @@ pub struct ShapedRun {
pub glyphs: Vec<PlacedGlyph>,
/// Total pen advance of the run (9-unit).
pub advance: f32,
/// Every laid-out glyph (blank ones too) as the byte range of the text it
/// shows and its visual [left, right] pen cell, 9-unit.
pub cells: Vec<(usize, usize, f32, f32)>,
}
/// Each character's visual [left, right] cell (9-unit) in logical order:
/// the cell of the glyph that shows it; a glyph showing several characters
/// shares its cell among them in reading order.
pub fn char_cells(run: &ShapedRun, text: &str) -> Vec<(f32, f32)> {
text.char_indices()
.map(|(byte, _)| {
let Some(&(start, end, x0, x1)) =
run.cells.iter().find(|(start, end, _, _)| (*start..(*end).max(start + 1)).contains(&byte))
else {
return (0.0, 0.0);
};
let chars: Vec<usize> = text[start..end.min(text.len())].char_indices().map(|(i, _)| start + i).collect();
let (n, k) = (chars.len().max(1), chars.iter().position(|&b| b == byte).unwrap_or(0));
let rtl = text[start..end.min(text.len())].chars().any(crate::entities::text_support::is_rtl_char);
let slot = (x1 - x0) / n as f32;
let k = if rtl { n - 1 - k } else { k };
(x0 + slot * k as f32, x0 + slot * (k + 1) as f32)
})
.collect()
}
type ShapeCache = HashMap<(String, String), Option<Arc<ShapedRun>>>;
@@ -881,10 +905,12 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
buffer.shape_until_scroll(&mut font_system, false);
let mut glyphs = Vec::new();
let mut cells = Vec::new();
let mut advance = 0.0_f32;
for run in buffer.layout_runs() {
advance = advance.max(run.line_w * px_to_9);
for glyph in run.glyphs.iter() {
cells.push((glyph.start, glyph.end, glyph.x * px_to_9, (glyph.x + glyph.w) * px_to_9));
let face_index = font_system
.db_mut()
.face(glyph.font_id)
@@ -923,7 +949,7 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
if advance <= 0.0 && glyphs.is_empty() {
None
} else {
Some(ShapedRun { glyphs, advance })
Some(ShapedRun { glyphs, advance, cells })
}
}
@@ -986,10 +1012,12 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
let px_to_9 = CAP_UNITS * upem_p / (SHAPE_FS * cap_p);
let mut glyphs: Vec<PlacedGlyph> = Vec::new();
let mut cells = Vec::new();
let mut advance = 0.0_f32;
for run in buf.layout_runs() {
advance = advance.max(run.line_w * px_to_9);
for g in run.glyphs.iter() {
cells.push((g.start, g.end, g.x * px_to_9, (g.x + g.w) * px_to_9));
let face_index = fs.db_mut().face(g.font_id).map(|f| f.index).unwrap_or(0);
let Some(font) = fs.get_font(g.font_id, g.font_weight) else {
continue;
@@ -1028,7 +1056,7 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
if advance <= 0.0 && glyphs.is_empty() {
return None;
}
Some(ShapedRun { glyphs, advance })
Some(ShapedRun { glyphs, advance, cells })
}
#[cfg(test)]