diff options
Diffstat (limited to 'library')
| -rw-r--r-- | library/src/layout/par.rs | 59 | ||||
| -rw-r--r-- | library/src/math/fragment.rs | 2 | ||||
| -rw-r--r-- | library/src/text/shaping.rs | 44 |
3 files changed, 91 insertions, 14 deletions
diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs index 8dd81d29..6178c059 100644 --- a/library/src/layout/par.rs +++ b/library/src/layout/par.rs @@ -145,12 +145,12 @@ impl ParNode { let children = par.children(); // Collect all text into one string for BiDi analysis. - let (text, segments) = collect(&children, &styles, consecutive)?; + let (text, segments, spans) = collect(&children, &styles, consecutive)?; // Perform BiDi analysis and then prepare paragraph layout by building a // representation on which we can do line breaking without layouting // each and every line from scratch. - let p = prepare(&mut vt, &children, &text, segments, styles, region)?; + let p = prepare(&mut vt, &children, &text, segments, spans, styles, region)?; // Break the paragraph into lines. let lines = linebreak(&vt, &p, region.x); @@ -264,6 +264,8 @@ struct Preparation<'a> { bidi: BidiInfo<'a>, /// Text runs, spacing and layouted nodes. items: Vec<Item<'a>>, + /// The span mapper. + spans: SpanMapper, /// The styles shared by all children. styles: StyleChain<'a>, /// Whether to hyphenate if it's the same for all children. @@ -388,6 +390,35 @@ impl<'a> Item<'a> { } } +/// Maps byte offsets back to spans. +pub struct SpanMapper(Vec<(usize, Span)>); + +impl SpanMapper { + /// Create a new span mapper. + pub fn new() -> Self { + Self(vec![]) + } + + /// Push a span for a segment with the given length. + pub fn push(&mut self, len: usize, span: Span) { + self.0.push((len, span)); + } + + /// Determine the span at the given byte offset. + /// + /// May return a detached span. + pub fn span_at(&self, offset: usize) -> (Span, u16) { + let mut cursor = 0; + for &(len, span) in &self.0 { + if (cursor..=cursor + len).contains(&offset) { + return (span, u16::try_from(offset - cursor).unwrap_or(0)); + } + cursor += len; + } + (Span::detached(), 0) + } +} + /// A layouted line, consisting of a sequence of layouted paragraph items that /// are mostly borrowed from the preparation phase. This type enables you to /// measure the size of a line in a range before comitting to building the @@ -485,10 +516,11 @@ fn collect<'a>( children: &'a [Content], styles: &'a StyleChain<'a>, consecutive: bool, -) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>)> { +) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> { let mut full = String::new(); let mut quoter = Quoter::new(); let mut segments = vec![]; + let mut spans = SpanMapper::new(); let mut iter = children.iter().peekable(); if consecutive { @@ -578,6 +610,8 @@ fn collect<'a>( quoter.last(last); } + spans.push(segment.len(), child.span()); + if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = (segments.last_mut(), segment) { @@ -590,7 +624,7 @@ fn collect<'a>( segments.push((segment, styles)); } - Ok((full, segments)) + Ok((full, segments, spans)) } /// Prepare paragraph layout by shaping the whole paragraph and layouting all @@ -600,6 +634,7 @@ fn prepare<'a>( children: &'a [Content], text: &'a str, segments: Vec<(Segment<'a>, StyleChain<'a>)>, + spans: SpanMapper, styles: StyleChain<'a>, region: Size, ) -> SourceResult<Preparation<'a>> { @@ -620,7 +655,7 @@ fn prepare<'a>( let end = cursor + segment.len(); match segment { Segment::Text(_) => { - shape_range(&mut items, vt, &bidi, cursor..end, styles); + shape_range(&mut items, vt, &bidi, cursor..end, &spans, styles); } Segment::Spacing(spacing) => match spacing { Spacing::Rel(v) => { @@ -655,6 +690,7 @@ fn prepare<'a>( Ok(Preparation { bidi, items, + spans, styles, hyphenate: shared_get(styles, children, TextNode::hyphenate_in), lang: shared_get(styles, children, TextNode::lang_in), @@ -670,11 +706,12 @@ fn shape_range<'a>( vt: &Vt, bidi: &BidiInfo<'a>, range: Range, + spans: &SpanMapper, styles: StyleChain<'a>, ) { - let mut process = |text, level: BidiLevel| { + let mut process = |range: Range, level: BidiLevel| { let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; - let shaped = shape(vt, text, styles, dir); + let shaped = shape(vt, range.start, &bidi.text[range], spans, styles, dir); items.push(Item::Text(shaped)); }; @@ -694,7 +731,7 @@ fn shape_range<'a>( if level != prev_level || !is_compatible(script, prev_script) { if cursor < i { - process(&bidi.text[cursor..i], prev_level); + process(cursor..i, prev_level); } cursor = i; prev_level = level; @@ -704,7 +741,7 @@ fn shape_range<'a>( } } - process(&bidi.text[cursor..range.end], prev_level); + process(cursor..range.end, prev_level); } /// Whether this is not a specific script. @@ -1073,7 +1110,7 @@ fn line<'a>( if hyphen || start + shaped.text.len() > range.end { if hyphen || start < range.end || before.is_empty() { let shifted = start - base..range.end - base; - let mut reshaped = shaped.reshape(vt, shifted); + let mut reshaped = shaped.reshape(vt, &p.spans, shifted); if hyphen || shy { reshaped.push_hyphen(vt); } @@ -1096,7 +1133,7 @@ fn line<'a>( if range.start + shaped.text.len() > end { if range.start < end { let shifted = range.start - base..end - base; - let reshaped = shaped.reshape(vt, shifted); + let reshaped = shaped.reshape(vt, &p.spans, shifted); width += reshaped.width; first = Some(Item::Text(reshaped)); } diff --git a/library/src/math/fragment.rs b/library/src/math/fragment.rs index 73daa4b2..de456b82 100644 --- a/library/src/math/fragment.rs +++ b/library/src/math/fragment.rs @@ -222,6 +222,8 @@ impl GlyphFragment { c: self.c, x_advance: Em::from_length(self.width, self.font_size), x_offset: Em::zero(), + span: Span::detached(), + offset: 0, }], }; let size = Size::new(self.width, self.ascent + self.descent); diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs index e7ce4027..a96238d9 100644 --- a/library/src/text/shaping.rs +++ b/library/src/text/shaping.rs @@ -6,6 +6,7 @@ use typst::font::{Font, FontVariant}; use typst::util::SliceExt; use super::*; +use crate::layout::SpanMapper; use crate::prelude::*; /// The result of shaping text. @@ -14,6 +15,8 @@ use crate::prelude::*; /// measured, used to reshape substrings more quickly and converted into a /// frame. pub struct ShapedText<'a> { + /// The start of the text in the full paragraph. + pub base: usize, /// The text that was shaped. pub text: &'a str, /// The text direction. @@ -53,6 +56,10 @@ pub struct ShapedGlyph { pub safe_to_break: bool, /// The first char in this glyph's cluster. pub c: char, + /// The source code location of the text. + pub span: Span, + /// The offset within the spanned text. + pub offset: u16, } impl ShapedGlyph { @@ -110,6 +117,8 @@ impl<'a> ShapedText<'a> { }, x_offset: glyph.x_offset, c: glyph.c, + span: glyph.span, + offset: glyph.offset, }) .collect(); @@ -187,9 +196,15 @@ impl<'a> ShapedText<'a> { /// Reshape a range of the shaped text, reusing information from this /// shaping process if possible. - pub fn reshape(&'a self, vt: &Vt, text_range: Range<usize>) -> ShapedText<'a> { + pub fn reshape( + &'a self, + vt: &Vt, + spans: &SpanMapper, + text_range: Range<usize>, + ) -> ShapedText<'a> { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { Self { + base: self.base + text_range.start, text: &self.text[text_range], dir: self.dir, styles: self.styles, @@ -199,7 +214,14 @@ impl<'a> ShapedText<'a> { glyphs: Cow::Borrowed(glyphs), } } else { - shape(vt, &self.text[text_range], self.styles, self.dir) + shape( + vt, + self.base + text_range.start, + &self.text[text_range], + spans, + self.styles, + self.dir, + ) } } @@ -225,6 +247,8 @@ impl<'a> ShapedText<'a> { cluster, safe_to_break: true, c: '-', + span: Span::detached(), + offset: 0, }); Some(()) }); @@ -298,6 +322,8 @@ impl Debug for ShapedText<'_> { /// Holds shaping results and metadata common to all shaped segments. struct ShapingContext<'a> { vt: &'a Vt<'a>, + base: usize, + spans: &'a SpanMapper, glyphs: Vec<ShapedGlyph>, used: Vec<Font>, styles: StyleChain<'a>, @@ -311,13 +337,17 @@ struct ShapingContext<'a> { /// Shape text into [`ShapedText`]. pub fn shape<'a>( vt: &Vt, + base: usize, text: &'a str, + spans: &SpanMapper, styles: StyleChain<'a>, dir: Dir, ) -> ShapedText<'a> { let size = TextNode::size_in(styles); let mut ctx = ShapingContext { vt, + base, + spans, size, glyphs: vec![], used: vec![], @@ -335,6 +365,7 @@ pub fn shape<'a>( track_and_space(&mut ctx); ShapedText { + base, text, dir, styles, @@ -410,6 +441,7 @@ fn shape_segment<'a>( if info.glyph_id != 0 { // Add the glyph to the shaped output. // TODO: Don't ignore y_advance. + let (span, offset) = ctx.spans.span_at(ctx.base + cluster); ctx.glyphs.push(ShapedGlyph { font: font.clone(), glyph_id: info.glyph_id as u16, @@ -419,6 +451,8 @@ fn shape_segment<'a>( cluster: base + cluster, safe_to_break: !info.unsafe_to_break(), c: text[cluster..].chars().next().unwrap(), + span, + offset, }); } else { // Determine the source text range for the tofu sequence. @@ -478,15 +512,19 @@ fn shape_segment<'a>( fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { let x_advance = font.advance(0).unwrap_or_default(); for (cluster, c) in text.char_indices() { + let cluster = base + cluster; + let (span, offset) = ctx.spans.span_at(ctx.base + cluster); ctx.glyphs.push(ShapedGlyph { font: font.clone(), glyph_id: 0, x_advance, x_offset: Em::zero(), y_offset: Em::zero(), - cluster: base + cluster, + cluster, safe_to_break: true, c, + span, + offset, }); } } |
