Skip to main content

incular_text/
geometry.rs

1//! Line geometry shared by whole-text layouts and per-paragraph document
2//! layouts, so editing code reads either through one interface.
3
4use crate::{TextCaretPosition, TextClusterSpan, TextLayout, TextLine, TextMetrics};
5use incular_core::Offset;
6use incular_rendering::{GlyphPosition, GlyphRun};
7use std::sync::Arc;
8
9/// Line geometry of laid-out text: lines in layout coordinates, their caret
10/// stops and the overall metrics.
11///
12/// [`TextLayout`] implements it for one shaped text, and
13/// [`TextDocumentLayout`](crate::TextDocumentLayout) for a document laid out
14/// paragraph by paragraph.
15pub trait TextGeometry {
16    fn metrics(&self) -> TextMetrics;
17
18    fn line_count(&self) -> usize;
19
20    fn get_line(&self, index: usize) -> Option<TextLineRef<'_>>;
21
22    fn lines(&self) -> TextLines<'_>;
23
24    /// Lines that may meet the vertical span `top..bottom`, plus one line on
25    /// each side; pad the span for glyphs that overhang their line.
26    fn lines_between(&self, top: f32, bottom: f32) -> TextLines<'_>;
27
28    /// Index of the line whose `[start, end)` holds `byte`, or the last line
29    /// when none does, like Flutter's `Paragraph.getLineBoundary`.
30    fn line_for_offset(&self, byte: usize) -> usize;
31
32    /// The visual insertion points of one line in left-to-right screen order.
33    fn line_caret_positions(&self, line: usize) -> &[TextCaretPosition];
34
35    /// The line at `index`.
36    ///
37    /// # Panics
38    ///
39    /// Panics when `index` is not below [`TextGeometry::line_count`].
40    fn line(&self, index: usize) -> TextLineRef<'_> {
41        self.get_line(index).expect("text line index out of range")
42    }
43}
44
45/// Layouts are shared as `Arc`s; geometry reads through them unchanged.
46impl<T: TextGeometry + ?Sized> TextGeometry for Arc<T> {
47    fn metrics(&self) -> TextMetrics {
48        (**self).metrics()
49    }
50
51    fn line_count(&self) -> usize {
52        (**self).line_count()
53    }
54
55    fn get_line(&self, index: usize) -> Option<TextLineRef<'_>> {
56        (**self).get_line(index)
57    }
58
59    fn lines(&self) -> TextLines<'_> {
60        (**self).lines()
61    }
62
63    fn lines_between(&self, top: f32, bottom: f32) -> TextLines<'_> {
64        (**self).lines_between(top, bottom)
65    }
66
67    fn line_for_offset(&self, byte: usize) -> usize {
68        (**self).line_for_offset(byte)
69    }
70
71    fn line_caret_positions(&self, line: usize) -> &[TextCaretPosition] {
72        (**self).line_caret_positions(line)
73    }
74}
75
76impl TextGeometry for TextLayout {
77    fn metrics(&self) -> TextMetrics {
78        self.metrics
79    }
80
81    fn line_count(&self) -> usize {
82        self.lines.len()
83    }
84
85    fn get_line(&self, index: usize) -> Option<TextLineRef<'_>> {
86        let line = self.lines.get(index)?;
87        Some(TextLineRef::new(line, index, 0, 0.))
88    }
89
90    fn lines(&self) -> TextLines<'_> {
91        TextLines::flat(&self.lines, 0, self.lines.len())
92    }
93
94    fn lines_between(&self, top: f32, bottom: f32) -> TextLines<'_> {
95        let first = self
96            .lines
97            .partition_point(|line| line.baseline < top)
98            .saturating_sub(1);
99        let end = (self.lines.partition_point(|line| line.baseline <= bottom) + 1)
100            .min(self.lines.len())
101            .max(first);
102        TextLines::flat(&self.lines, first, end)
103    }
104
105    fn line_for_offset(&self, byte: usize) -> usize {
106        let last = self.lines.len().saturating_sub(1);
107        let line = self.lines.partition_point(|line| line.start <= byte);
108        match line.checked_sub(1) {
109            Some(line) if byte < self.lines[line].end => line,
110            _ => last,
111        }
112    }
113
114    fn line_caret_positions(&self, line: usize) -> &[TextCaretPosition] {
115        TextLayout::line_caret_positions(self, line)
116    }
117}
118
119/// A line of laid-out text in layout coordinates: byte offsets index the
120/// laid-out text and vertical positions are measured from its top.
121#[derive(Clone, Copy, Debug)]
122pub struct TextLineRef<'a> {
123    line: &'a TextLine,
124    index: usize,
125    byte: usize,
126    y: f32,
127}
128
129impl<'a> TextLineRef<'a> {
130    pub(crate) const fn new(line: &'a TextLine, index: usize, byte: usize, y: f32) -> Self {
131        Self {
132            line,
133            index,
134            byte,
135            y,
136        }
137    }
138
139    /// The line's index in its layout.
140    #[must_use]
141    pub const fn index(&self) -> usize {
142        self.index
143    }
144
145    #[must_use]
146    pub const fn start(&self) -> usize {
147        self.line.start + self.byte
148    }
149
150    /// End of selectable/caret text on this visual line. This excludes a
151    /// trailing hard line-break which has no shaped glyph of its own.
152    #[must_use]
153    pub const fn caret_end(&self) -> usize {
154        self.line.caret_end + self.byte
155    }
156
157    #[must_use]
158    pub const fn end(&self) -> usize {
159        self.line.end + self.byte
160    }
161
162    #[must_use]
163    pub fn baseline(&self) -> f32 {
164        self.line.baseline + self.y
165    }
166
167    /// The unshifted advance; see [`TextLineRef::offset`].
168    #[must_use]
169    pub const fn width(&self) -> f32 {
170        self.line.width
171    }
172
173    /// Horizontal alignment offset in text-area coordinates. Glyph and caret
174    /// x positions already include it; consumers mapping absolute x
175    /// positions add it to `width` instead of re-deriving the alignment.
176    #[must_use]
177    pub const fn offset(&self) -> f32 {
178        self.line.offset
179    }
180
181    /// Glyph runs positioned relative to [`TextLineRef::paint_origin`].
182    #[must_use]
183    pub fn runs(&self) -> &'a [Arc<GlyphRun>] {
184        &self.line.runs
185    }
186
187    /// Where the line's glyph runs are drawn: the top of its paragraph, or
188    /// the layout's origin for a whole-text layout.
189    #[must_use]
190    pub const fn paint_origin(&self) -> Offset {
191        Offset::new(0., self.y)
192    }
193
194    /// Positioned glyphs with clusters indexing the laid-out text and y in
195    /// the renderer's negative space below the layout's top.
196    pub fn glyphs(&self) -> impl ExactSizeIterator<Item = GlyphPosition> + 'a {
197        let (byte, y) = (self.byte as u32, self.y);
198        self.line.glyphs.iter().map(move |glyph| GlyphPosition {
199            offset: Offset::new(glyph.offset.x, glyph.offset.y - y),
200            cluster: glyph.cluster + byte,
201            ..*glyph
202        })
203    }
204
205    /// Shaped cluster coverage in visual order; see [`TextClusterSpan`].
206    pub fn clusters(&self) -> impl ExactSizeIterator<Item = TextClusterSpan> + 'a {
207        let byte = self.byte as u32;
208        self.line.clusters.iter().map(move |span| TextClusterSpan {
209            start: span.start + byte,
210            end: span.end + byte,
211            ..*span
212        })
213    }
214
215    /// Visual x-intervals covered by a half-open byte range of the laid-out
216    /// text; see [`TextLine::selection_spans`].
217    #[must_use]
218    pub fn selection_spans(&self, start: usize, end: usize) -> Vec<(f32, f32)> {
219        if end < self.byte {
220            return Vec::new();
221        }
222        self.line.selection_spans(
223            start.saturating_sub(self.byte),
224            end.saturating_sub(self.byte),
225        )
226    }
227
228    /// The shaped line in the coordinates of the text it was shaped from.
229    #[must_use]
230    pub const fn shaped(&self) -> &'a TextLine {
231        self.line
232    }
233}
234
235/// One paragraph of a document layout and where it sits in the document.
236#[derive(Clone, Debug)]
237pub(crate) struct ParagraphSlot {
238    /// Byte offset of the paragraph in the document's text.
239    pub(crate) byte: usize,
240    /// Vertical offset of the paragraph's top in the document.
241    pub(crate) y: f32,
242    /// Index of the paragraph's first line in the document.
243    pub(crate) first_line: usize,
244    /// The paragraph shaped on its own, in its own coordinates.
245    pub(crate) layout: Arc<TextLayout>,
246}
247
248/// Lines sharing one origin: a paragraph, or every line of a whole layout.
249#[derive(Clone, Copy, Debug)]
250struct LineSegment<'a> {
251    lines: &'a [TextLine],
252    first_line: usize,
253    byte: usize,
254    y: f32,
255}
256
257impl<'a> LineSegment<'a> {
258    fn of(slot: &'a ParagraphSlot) -> Self {
259        Self {
260            lines: &slot.layout.lines,
261            first_line: slot.first_line,
262            byte: slot.byte,
263            y: slot.y,
264        }
265    }
266}
267
268/// Iterator over the lines of a [`TextGeometry`].
269#[derive(Clone, Debug)]
270pub struct TextLines<'a> {
271    current: LineSegment<'a>,
272    next: usize,
273    rest: &'a [ParagraphSlot],
274    remaining: usize,
275}
276
277impl<'a> TextLines<'a> {
278    /// Lines `first..end` of a whole layout.
279    fn flat(lines: &'a [TextLine], first: usize, end: usize) -> Self {
280        Self {
281            current: LineSegment {
282                lines,
283                first_line: 0,
284                byte: 0,
285                y: 0.,
286            },
287            next: first,
288            rest: &[],
289            remaining: end.saturating_sub(first),
290        }
291    }
292
293    /// Every line of `slots`.
294    pub(crate) fn paragraphs(slots: &'a [ParagraphSlot]) -> Self {
295        let remaining = slots.iter().map(|slot| slot.layout.lines.len()).sum();
296        match slots.split_first() {
297            Some((first, rest)) => Self {
298                current: LineSegment::of(first),
299                next: 0,
300                rest,
301                remaining,
302            },
303            None => Self::flat(&[], 0, 0),
304        }
305    }
306}
307
308impl<'a> Iterator for TextLines<'a> {
309    type Item = TextLineRef<'a>;
310
311    fn next(&mut self) -> Option<Self::Item> {
312        if self.remaining == 0 {
313            return None;
314        }
315        while self.next >= self.current.lines.len() {
316            let (slot, rest) = self.rest.split_first()?;
317            self.current = LineSegment::of(slot);
318            self.rest = rest;
319            self.next = 0;
320        }
321        let segment = self.current;
322        let index = self.next;
323        self.next += 1;
324        self.remaining -= 1;
325        Some(TextLineRef::new(
326            &segment.lines[index],
327            segment.first_line + index,
328            segment.byte,
329            segment.y,
330        ))
331    }
332
333    fn size_hint(&self) -> (usize, Option<usize>) {
334        (self.remaining, Some(self.remaining))
335    }
336}
337
338impl ExactSizeIterator for TextLines<'_> {}