Skip to main content

markdown/render/
layouts.rs

1//! Hit-testing and caret geometry over the rows a frame painted.
2
3use super::*;
4
5/// Where each block's text landed, recorded as it painted.
6///
7/// A caret has to be placeable by pointer, and only paint knows where a glyph
8/// ended up. An editor hands one of these in, the renderer fills it, and the
9/// next click resolves against it. Read-only callers pass nothing and pay
10/// nothing.
11#[derive(Clone, Default)]
12pub struct BlockLayouts(Rc<RefCell<Frames>>);
13
14#[derive(Default)]
15pub(super) struct Frames {
16    texts: Vec<Painted>,
17    rows: Vec<PaintedRow>,
18    /// Each block's whole box, which a text layout does not give: a rule and
19    /// an image hold no text at all, and a gutter handle still has to find them.
20    blocks: Vec<(usize, Bounds<Pixels>)>,
21    /// A fenced block's language label, which a host may want to hang a
22    /// picker on.
23    languages: Vec<(usize, Bounds<Pixels>)>,
24    /// An image block's picture, which is not its block: the block runs the
25    /// full column and carries the caption, and a resize handle belongs on the
26    /// edge of the picture itself.
27    pictures: Vec<(usize, Bounds<Pixels>)>,
28    /// A task block's checkbox, which is not its marker column: the column is
29    /// gutter either side of the box, and a click there places a caret.
30    checkboxes: Vec<(usize, Bounds<Pixels>)>,
31    /// Kept across frames: [`Self::clear`] leaves it.
32    heights: Heights,
33}
34
35/// Block heights kept across frames, so a block off-screen is placed without
36/// being built.
37#[derive(Default)]
38pub(super) struct Heights {
39    /// A block's height by [`block_key`], as last measured at whatever width
40    /// the column had then.
41    by_key: HashMap<u64, Pixels>,
42}
43
44/// One shaped run and the slice of its part it covers.
45///
46/// A paragraph is one entry over all of its text; a code block is one entry per
47/// line. The range is what lets both resolve a click the same way — the layout
48/// answers in its own coordinates and the base puts the answer back into the
49/// part's.
50pub(super) struct Painted {
51    block: usize,
52    part: Part,
53    range: Range<usize>,
54    layout: TextLayout,
55}
56
57pub(super) struct PaintedRow {
58    painted: usize,
59    block: usize,
60    part: Part,
61    range: Range<usize>,
62    bounds: Bounds<Pixels>,
63    line_start: usize,
64    wrapped_row: usize,
65}
66
67impl BlockLayouts {
68    /// The position under `point`.
69    ///
70    /// Falls back to the nearest text vertically, so clicking the margin
71    /// beside a line — or below the last one — still lands somewhere useful
72    /// rather than doing nothing.
73    pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
74        let frames = self.0.borrow();
75        if let Some(row) = frames.rows.iter().find(|row| row.bounds.contains(&point)) {
76            return cursor_in_row(&frames, row, point.x);
77        }
78        frames
79            .rows
80            .iter()
81            .min_by_key(|row| {
82                let bounds = row.bounds;
83                let above = (bounds.origin.y - point.y).abs();
84                let below = (bounds.origin.y + bounds.size.height - point.y).abs();
85                f32::from(above.min(below)) as i64
86            })
87            .and_then(|row| cursor_in_row(&frames, row, point.x))
88    }
89
90    /// Where a position painted last frame, and how tall its line is.
91    ///
92    /// Vertical motion is geometry rather than arithmetic on line numbers, so
93    /// a wrapped row and a hard newline are the same case and neither needs
94    /// counting — the rule `ui::TextField` arrived at.
95    pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
96        let frames = self.0.borrow();
97        let painted = frames.texts.iter().find(|painted| {
98            painted.block == at.block
99                && painted.part == at.part
100                && painted.range.start <= at.offset
101                && at.offset <= painted.range.end
102        })?;
103        let point = painted
104            .layout
105            .position_for_index(at.offset - painted.range.start)?;
106        Some((point, painted.layout.line_height()))
107    }
108
109    /// The painted rows of a range, in document order — what a bar centred over
110    /// a selection is placed against, and what a highlight of a caller's own is
111    /// drawn from.
112    ///
113    /// One rect per visual row rather than one box: a selection that wraps or
114    /// crosses blocks has no single box, and a caller given one would paint
115    /// over the gaps.
116    pub fn rects(&self, selection: Selection) -> Vec<Bounds<Pixels>> {
117        let (start, end) = selection.ordered();
118        self.0
119            .borrow()
120            .texts
121            .iter()
122            .filter_map(|painted| {
123                let here = Cursor::new(painted.block, painted.part, 0);
124                let (from, to) = (
125                    Cursor::new(start.block, start.part, 0),
126                    Cursor::new(end.block, end.part, 0),
127                );
128                if here < from || here > to {
129                    return None;
130                }
131                // The offsets only matter at the two ends: in between, the
132                // whole of the text is covered.
133                let len = painted.range.len();
134                let first = if here == from { start.offset } else { 0 };
135                let last = if here == to { end.offset } else { usize::MAX };
136                let range = first.saturating_sub(painted.range.start).min(len)
137                    ..last.saturating_sub(painted.range.start).min(len);
138                (range.start < range.end).then(|| range_rects(&painted.layout, &range, 0.0, 0.0))
139            })
140            .flatten()
141            .collect()
142    }
143
144    /// The position one painted row above or below `at`, and the row it landed
145    /// on. Walks the recorded runs in paint order — which is document order.
146    ///
147    /// Two things make this refuse to be a hit test. The gap between blocks
148    /// belongs to no run, so a probe there answers with whichever run is
149    /// nearest — and at a boundary that is the block being *left*, whose bottom
150    /// edge is zero pixels away while the next block's top is a whole gap. And
151    /// `from` is passed in rather than derived from `at`, because an offset at
152    /// a soft wrap belongs to two rows and `position_for_index` always answers
153    /// with the first: derive it and every step down recomputes the same row.
154    pub fn step_row(
155        &self,
156        at: Cursor,
157        from: Point<Pixels>,
158        down: bool,
159    ) -> Option<(Cursor, Pixels)> {
160        let frames = self.0.borrow();
161        let ix = frames
162            .rows
163            .iter()
164            .position(|row| {
165                row.block == at.block
166                    && row.part == at.part
167                    && row_contains(row, at.offset)
168                    && row.bounds.origin.y <= from.y
169                    && from.y < row.bounds.origin.y + row.bounds.size.height
170            })
171            .or_else(|| {
172                frames.rows.iter().position(|row| {
173                    row.block == at.block && row.part == at.part && row_contains(row, at.offset)
174                })
175            })?;
176        let next = match down {
177            true => frames.rows.get(ix + 1)?,
178            false => frames.rows.get(ix.checked_sub(1)?)?,
179        };
180        Some((cursor_in_row(&frames, next, from.x)?, next.bounds.origin.y))
181    }
182
183    /// Whether `point` is inside painted text.
184    ///
185    /// [`Self::hit`] answers with the nearest run wherever it is asked, which
186    /// is what a click wants and what a *pointer* must not have: an I-beam
187    /// belongs where a caret would land, not everywhere an editor's box
188    /// reaches.
189    pub fn over_text(&self, point: Point<Pixels>) -> bool {
190        self.0
191            .borrow()
192            .texts
193            .iter()
194            .any(|painted| painted.layout.bounds().contains(&point))
195    }
196
197    /// The block under `point`, for a gutter handle and a drop target.
198    pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
199        let blocks = &self.0.borrow().blocks;
200        blocks
201            .iter()
202            .find(|(_, bounds)| bounds.contains(&point))
203            .or_else(|| {
204                blocks.iter().min_by_key(|(_, bounds)| {
205                    let above = (bounds.origin.y - point.y).abs();
206                    let below = (bounds.origin.y + bounds.size.height - point.y).abs();
207                    f32::from(above.min(below)) as i64
208                })
209            })
210            .map(|(ix, _)| *ix)
211    }
212
213    /// Where a block's first painted row sits, and how tall that row is — what
214    /// a mark in the gutter has to line up with.
215    ///
216    /// [`Self::block_bounds`] is not that: it spans every row the block holds,
217    /// and a heading's single row is taller than a paragraph's, so anything
218    /// placed from the top of the box rides above the text it points at.
219    /// `None` for a block that paints no text at all, a rule being the one
220    /// that does.
221    pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
222        let texts = &self.0.borrow().texts;
223        let painted = texts.iter().find(|painted| painted.block == ix)?;
224        Some((
225            painted.layout.bounds().origin.y,
226            painted.layout.line_height(),
227        ))
228    }
229
230    /// Where a block painted last frame, in window coordinates.
231    pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
232        self.0
233            .borrow()
234            .blocks
235            .iter()
236            .find(|(block, _)| *block == ix)
237            .map(|(_, bounds)| *bounds)
238    }
239
240    /// Where a fenced block's language label painted — the box a host hangs its
241    /// picker on. Recorded rather than derived: the word's width is the text
242    /// system's answer, and padding arithmetic would be wrong the first time
243    /// any of it changed.
244    pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
245        self.0
246            .borrow()
247            .languages
248            .iter()
249            .find(|(block, _)| *block == ix)
250            .map(|(_, bounds)| *bounds)
251    }
252
253    /// Where an image block's picture painted, which
254    /// [`BlockLayouts::block_bounds`] does not give: that box spans the column
255    /// and takes in the caption, so a handle placed from it sits off the edge
256    /// of any picture narrower than the page.
257    pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
258        self.0
259            .borrow()
260            .pictures
261            .iter()
262            .find(|(block, _)| *block == ix)
263            .map(|(_, bounds)| *bounds)
264    }
265
266    /// Where a task block's checkbox painted, which is the box itself and not
267    /// the marker column it sits in — a press outside it is a press on the
268    /// gutter, and belongs to whatever handles one.
269    pub fn checkbox_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
270        self.0
271            .borrow()
272            .checkboxes
273            .iter()
274            .find(|(block, _)| *block == ix)
275            .map(|(_, bounds)| *bounds)
276    }
277
278    pub(super) fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
279        let mut frames = self.0.borrow_mut();
280        let painted = frames.texts.len();
281        record_rows(&mut frames.rows, painted, block, part, &range, &layout);
282        frames.texts.push(Painted {
283            block,
284            part,
285            range,
286            layout,
287        });
288    }
289
290    pub(super) fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
291        self.0.borrow_mut().blocks.push((ix, bounds));
292    }
293
294    pub(super) fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
295        self.0.borrow_mut().languages.push((ix, bounds));
296    }
297
298    pub(super) fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
299        self.0.borrow_mut().pictures.push((ix, bounds));
300    }
301
302    pub(super) fn record_checkbox(&self, ix: usize, bounds: Bounds<Pixels>) {
303        self.0.borrow_mut().checkboxes.push((ix, bounds));
304    }
305
306    pub(super) fn height(&self, key: u64) -> Option<Pixels> {
307        self.0.borrow().heights.by_key.get(&key).copied()
308    }
309
310    pub(super) fn record_height(&self, key: u64, height: Pixels) {
311        self.0.borrow_mut().heights.by_key.insert(key, height);
312    }
313
314    /// Drops the heights of blocks no longer in the document once they
315    /// outnumber the ones that are.
316    pub(super) fn prune(&self, keys: &[u64]) {
317        let by_key = &mut self.0.borrow_mut().heights.by_key;
318        if by_key.len() > 2 * keys.len() {
319            let keep: std::collections::HashSet<u64> = keys.iter().copied().collect();
320            by_key.retain(|key, _| keep.contains(key));
321        }
322    }
323
324    pub(super) fn clear(&self) {
325        let mut frames = self.0.borrow_mut();
326        frames.texts.clear();
327        frames.rows.clear();
328        frames.blocks.clear();
329        frames.languages.clear();
330        frames.pictures.clear();
331        frames.checkboxes.clear();
332    }
333}
334
335pub(super) fn row_contains(row: &PaintedRow, offset: usize) -> bool {
336    row.range.start <= offset && offset <= row.range.end
337}
338
339pub(super) fn cursor_in_row(frames: &Frames, row: &PaintedRow, x: Pixels) -> Option<Cursor> {
340    let painted = &frames.texts[row.painted];
341    let line = painted
342        .layout
343        .line_layout_for_index(row.line_start - painted.range.start)?;
344    let height = row.bounds.size.height;
345    let local = point(
346        x - row.bounds.origin.x,
347        height * (row.wrapped_row as f32 + 0.5),
348    );
349    let (Ok(offset) | Err(offset)) = line.closest_index_for_position(local, height);
350    Some(Cursor::new(
351        row.block,
352        row.part,
353        (row.line_start + offset).min(row.range.end),
354    ))
355}
356
357pub(super) fn record_rows(
358    rows: &mut Vec<PaintedRow>,
359    painted: usize,
360    block: usize,
361    part: Part,
362    range: &Range<usize>,
363    layout: &TextLayout,
364) {
365    let line_height = layout.line_height();
366    let bounds = layout.bounds();
367    let mut origin = bounds.origin;
368    let mut line_start = range.start;
369    for line in layout.line_layouts() {
370        let shaped = &line.unwrapped_layout;
371        let row_ends = line
372            .wrap_boundaries()
373            .iter()
374            .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
375            .chain([line.len()]);
376        let mut row_start = 0;
377        for (row, row_end) in row_ends.enumerate() {
378            rows.push(PaintedRow {
379                painted,
380                block,
381                part,
382                range: line_start + row_start..line_start + row_end,
383                bounds: Bounds::new(
384                    origin + point(px(0.0), line_height * row as f32),
385                    size(bounds.size.width, line_height),
386                ),
387                line_start,
388                wrapped_row: row,
389            });
390            row_start = row_end;
391        }
392        origin.y += line.size(line_height).height;
393        line_start += line.len() + 1;
394    }
395}