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 painted block that can be resized: a painted fence, or a card
29    /// [`crate::Form::Embed`] spells.
30    painted: Vec<(usize, Bounds<Pixels>)>,
31    /// A task block's checkbox, which is not its marker column: the column is
32    /// gutter either side of the box, and a click there places a caret.
33    checkboxes: Vec<(usize, Bounds<Pixels>)>,
34    /// A table cell's box, padding included.
35    cells: Vec<(usize, Part, Bounds<Pixels>)>,
36    /// Kept across frames: [`Self::clear`] leaves it.
37    heights: Heights,
38    /// Kept across frames until the column has shown it.
39    reveal: Option<Reveal>,
40}
41
42/// Where [`BlockLayouts::reveal_with`] puts a range that is not showing. A
43/// range already showing does not move either way.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub enum RevealMode {
46    /// In the middle of the view.
47    #[default]
48    Center,
49    /// The least scroll that shows it, 16px in from the edge it came in
50    /// over. Taller than the view, its top shows.
51    Nearest,
52}
53
54/// The room [`RevealMode::Nearest`] leaves between a range and the view's
55/// edge, in pixels.
56pub(super) const REVEAL_MARGIN: f32 = 16.0;
57
58/// A range waiting to be scrolled into view.
59#[derive(Clone, Copy)]
60pub(super) struct Reveal {
61    pub(super) range: Selection,
62    pub(super) mode: RevealMode,
63    /// Frames it has scrolled in.
64    pub(super) tries: u8,
65    /// Whether any of them moved the document.
66    pub(super) scrolled: bool,
67    /// Where the column item holding it was placed last frame, in window
68    /// coordinates. `None` before it has been built.
69    pub(super) top: Option<Pixels>,
70}
71
72/// Block heights kept across frames, so a block off-screen is placed without
73/// being built.
74#[derive(Default)]
75pub(super) struct Heights {
76    /// A block's height by [`block_key`], as last measured at whatever width
77    /// the column had then.
78    by_key: HashMap<u64, Pixels>,
79    /// The height last measured at each column index, whatever block was
80    /// there.
81    by_index: Vec<Option<Pixels>>,
82}
83
84/// One shaped run and the slice of its part it covers.
85///
86/// A paragraph is one entry over all of its text; a code block is one entry per
87/// line. The range is what lets both resolve a click the same way — the layout
88/// answers in its own coordinates and the base puts the answer back into the
89/// part's.
90pub(super) struct Painted {
91    block: usize,
92    part: Part,
93    range: Range<usize>,
94    layout: TextLayout,
95    shown: Shown,
96}
97
98pub(super) struct PaintedRow {
99    painted: usize,
100    block: usize,
101    part: Part,
102    range: Range<usize>,
103    bounds: Bounds<Pixels>,
104    /// Where its line starts in the painted text's [`Shown`] offsets.
105    line_start: usize,
106    wrapped_row: usize,
107    /// Whether it ends at a soft wrap rather than at the end of its line.
108    wraps: bool,
109}
110
111impl BlockLayouts {
112    /// The position under `point`.
113    ///
114    /// Falls back to the nearest text vertically, so clicking the margin
115    /// beside a line — or below the last one — still lands somewhere useful
116    /// rather than doing nothing.
117    pub fn hit(&self, point: Point<Pixels>) -> Option<(Cursor, Affinity)> {
118        let frames = self.0.borrow();
119        if let Some(row) = frames.rows.iter().find(|row| row.bounds.contains(&point)) {
120            return cursor_in_row(&frames, row, point.x);
121        }
122        frames
123            .rows
124            .iter()
125            .min_by_key(|row| {
126                let bounds = row.bounds;
127                let above = (bounds.origin.y - point.y).abs();
128                let below = (bounds.origin.y + bounds.size.height - point.y).abs();
129                f32::from(above.min(below)) as i64
130            })
131            .and_then(|row| cursor_in_row(&frames, row, point.x))
132    }
133
134    /// Where a position painted last frame, on the row `affinity` names at a
135    /// soft wrap, and how tall its line is.
136    ///
137    /// Vertical motion is geometry rather than arithmetic on line numbers, so
138    /// a wrapped row and a hard newline are the same case and neither needs
139    /// counting — the rule `ui::TextField` arrived at.
140    pub fn position(&self, at: Cursor, affinity: Affinity) -> Option<(Point<Pixels>, Pixels)> {
141        let frames = self.0.borrow();
142        let painted = frames.texts.iter().find(|painted| {
143            painted.block == at.block
144                && painted.part == at.part
145                && painted.range.start <= at.offset
146                && at.offset <= painted.range.end
147        })?;
148        let point = caret_position(
149            &painted.layout,
150            painted.shown.at(at.offset - painted.range.start),
151            affinity,
152        )?;
153        Some((point, painted.layout.line_height()))
154    }
155
156    /// The start of the painted row holding `at`.
157    pub fn row_start(&self, at: Cursor, affinity: Affinity) -> Option<Cursor> {
158        let frames = self.0.borrow();
159        let row = row_of(&frames, at, affinity)?;
160        Some(Cursor::new(row.block, row.part, row.range.start))
161    }
162
163    /// The end of the painted row holding `at`, and the affinity that keeps a
164    /// caret there on that row.
165    pub fn row_end(&self, at: Cursor, affinity: Affinity) -> Option<(Cursor, Affinity)> {
166        let frames = self.0.borrow();
167        let row = row_of(&frames, at, affinity)?;
168        Some((
169            Cursor::new(row.block, row.part, row.range.end),
170            end_affinity(row),
171        ))
172    }
173
174    /// Whether `at` is a soft wrap last frame: the end of one painted row and
175    /// the start of the next.
176    pub fn wraps_at(&self, at: Cursor) -> bool {
177        self.0.borrow().rows.iter().any(|row| {
178            row.wraps && row.block == at.block && row.part == at.part && row.range.end == at.offset
179        })
180    }
181
182    /// The painted rows of a range, in document order — what a bar centred over
183    /// a selection is placed against, and what a highlight of a caller's own is
184    /// drawn from.
185    ///
186    /// One rect per visual row rather than one box: a selection that wraps or
187    /// crosses blocks has no single box, and a caller given one would paint
188    /// over the gaps.
189    pub fn rects(&self, selection: Selection) -> Vec<Bounds<Pixels>> {
190        let (start, end) = selection.ordered();
191        self.0
192            .borrow()
193            .texts
194            .iter()
195            .filter_map(|painted| {
196                let here = Cursor::new(painted.block, painted.part, 0);
197                let (from, to) = (
198                    Cursor::new(start.block, start.part, 0),
199                    Cursor::new(end.block, end.part, 0),
200                );
201                if here < from || here > to {
202                    return None;
203                }
204                // The offsets only matter at the two ends: in between, the
205                // whole of the text is covered.
206                let len = painted.range.len();
207                let first = if here == from { start.offset } else { 0 };
208                let last = if here == to { end.offset } else { usize::MAX };
209                let range = first.saturating_sub(painted.range.start).min(len)
210                    ..last.saturating_sub(painted.range.start).min(len);
211                let range = painted.shown.range(&range);
212                (range.start < range.end).then(|| range_rects(&painted.layout, &range, 0.0, 0.0))
213            })
214            .flatten()
215            .collect()
216    }
217
218    /// The position one painted row above or below `at` nearest `x`. Walks
219    /// the recorded runs in paint order — which is document order.
220    ///
221    /// Not a hit test: the gap between blocks belongs to no run, so a probe
222    /// there answers with whichever run is nearest — and at a boundary that is
223    /// the block being *left*, whose bottom edge is zero pixels away while the
224    /// next block's top is a whole gap.
225    pub fn step_row(
226        &self,
227        at: Cursor,
228        affinity: Affinity,
229        x: Pixels,
230        down: bool,
231    ) -> Option<(Cursor, Affinity)> {
232        let frames = self.0.borrow();
233        let row = row_of(&frames, at, affinity)?;
234        let ix = frames.rows.iter().position(|r| std::ptr::eq(r, row))?;
235        let next = match down {
236            true => frames.rows.get(ix + 1)?,
237            false => frames.rows.get(ix.checked_sub(1)?)?,
238        };
239        cursor_in_row(&frames, next, x)
240    }
241
242    /// Whether `point` is inside painted text.
243    ///
244    /// [`Self::hit`] answers with the nearest run wherever it is asked, which
245    /// is what a click wants and what a *pointer* must not have: an I-beam
246    /// belongs where a caret would land, not everywhere an editor's box
247    /// reaches.
248    pub fn over_text(&self, point: Point<Pixels>) -> bool {
249        self.0
250            .borrow()
251            .texts
252            .iter()
253            .any(|painted| painted.layout.bounds().contains(&point))
254    }
255
256    /// The block under `point`, for a gutter handle and a drop target.
257    pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
258        let blocks = &self.0.borrow().blocks;
259        blocks
260            .iter()
261            .find(|(_, bounds)| bounds.contains(&point))
262            .or_else(|| {
263                blocks.iter().min_by_key(|(_, bounds)| {
264                    let above = (bounds.origin.y - point.y).abs();
265                    let below = (bounds.origin.y + bounds.size.height - point.y).abs();
266                    f32::from(above.min(below)) as i64
267                })
268            })
269            .map(|(ix, _)| *ix)
270    }
271
272    /// Where a block's first painted row sits, and how tall that row is — what
273    /// a mark in the gutter has to line up with.
274    ///
275    /// [`Self::block_bounds`] is not that: it spans every row the block holds,
276    /// and a heading's single row is taller than a paragraph's, so anything
277    /// placed from the top of the box rides above the text it points at.
278    /// `None` for a block that paints no text at all, a rule being the one
279    /// that does.
280    pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
281        let texts = &self.0.borrow().texts;
282        let painted = texts.iter().find(|painted| painted.block == ix)?;
283        Some((
284            painted.layout.bounds().origin.y,
285            painted.layout.line_height(),
286        ))
287    }
288
289    /// Where a block painted last frame, in window coordinates.
290    pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
291        self.0
292            .borrow()
293            .blocks
294            .iter()
295            .find(|(block, _)| *block == ix)
296            .map(|(_, bounds)| *bounds)
297    }
298
299    /// Where a fenced block's language label painted — the box a host hangs its
300    /// picker on. Recorded rather than derived: the word's width is the text
301    /// system's answer, and padding arithmetic would be wrong the first time
302    /// any of it changed.
303    pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
304        self.0
305            .borrow()
306            .languages
307            .iter()
308            .find(|(block, _)| *block == ix)
309            .map(|(_, bounds)| *bounds)
310    }
311
312    /// Where a resizable painted block painted last frame — a painted fence,
313    /// or an embed card — and `None` for any other block.
314    pub fn painted_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
315        self.0
316            .borrow()
317            .painted
318            .iter()
319            .find(|(at, _)| *at == ix)
320            .map(|(_, bounds)| *bounds)
321    }
322
323    /// Where an image block's picture painted, which
324    /// [`BlockLayouts::block_bounds`] does not give: that box spans the column
325    /// and takes in the caption, so a handle placed from it sits off the edge
326    /// of any picture narrower than the page.
327    pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
328        self.0
329            .borrow()
330            .pictures
331            .iter()
332            .find(|(block, _)| *block == ix)
333            .map(|(_, bounds)| *bounds)
334    }
335
336    /// Where a task block's checkbox painted, which is the box itself and not
337    /// the marker column it sits in — a press outside it is a press on the
338    /// gutter, and belongs to whatever handles one.
339    pub fn checkbox_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
340        self.0
341            .borrow()
342            .checkboxes
343            .iter()
344            .find(|(block, _)| *block == ix)
345            .map(|(_, bounds)| *bounds)
346    }
347
348    /// Where a table cell painted, padding included.
349    pub fn cell_bounds(&self, ix: usize, part: Part) -> Option<Bounds<Pixels>> {
350        self.0
351            .borrow()
352            .cells
353            .iter()
354            .find(|(block, at, _)| *block == ix && *at == part)
355            .map(|(_, _, bounds)| *bounds)
356    }
357
358    /// The table cell under `point`: its block and part.
359    pub fn cell_at(&self, point: Point<Pixels>) -> Option<(usize, Part)> {
360        self.0
361            .borrow()
362            .cells
363            .iter()
364            .find(|(_, _, bounds)| bounds.contains(&point))
365            .map(|(block, part, _)| (*block, *part))
366    }
367
368    /// Scrolls the document until the start of `range` shows, centring it
369    /// when it does not.
370    ///
371    /// Answered over the next few frames by a document rendered with these
372    /// layouts and an [`Editing::scroll`]; without one the request is dropped
373    /// at the next frame. A later call replaces an earlier one.
374    pub fn reveal(&self, range: Selection) {
375        self.reveal_with(range, RevealMode::Center);
376    }
377
378    /// [`Self::reveal`], placing the start of `range` by `mode`.
379    pub fn reveal_with(&self, range: Selection, mode: RevealMode) {
380        self.0.borrow_mut().reveal = Some(Reveal {
381            range,
382            mode,
383            tries: 0,
384            scrolled: false,
385            top: None,
386        });
387    }
388
389    pub(super) fn revealing(&self) -> Option<Reveal> {
390        self.0.borrow().reveal
391    }
392
393    pub(super) fn set_revealing(&self, reveal: Option<Reveal>) {
394        self.0.borrow_mut().reveal = reveal;
395    }
396
397    pub(super) fn record(
398        &self,
399        block: usize,
400        part: Part,
401        range: Range<usize>,
402        layout: TextLayout,
403        shown: Shown,
404    ) {
405        let mut frames = self.0.borrow_mut();
406        let painted = frames.texts.len();
407        record_rows(
408            &mut frames.rows,
409            painted,
410            block,
411            part,
412            &range,
413            &layout,
414            &shown,
415        );
416        frames.texts.push(Painted {
417            block,
418            part,
419            range,
420            layout,
421            shown,
422        });
423    }
424
425    pub(super) fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
426        self.0.borrow_mut().blocks.push((ix, bounds));
427    }
428
429    pub(super) fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
430        self.0.borrow_mut().languages.push((ix, bounds));
431    }
432
433    pub(super) fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
434        self.0.borrow_mut().pictures.push((ix, bounds));
435    }
436
437    pub(super) fn record_painted(&self, ix: usize, bounds: Bounds<Pixels>) {
438        self.0.borrow_mut().painted.push((ix, bounds));
439    }
440
441    pub(super) fn record_checkbox(&self, ix: usize, bounds: Bounds<Pixels>) {
442        self.0.borrow_mut().checkboxes.push((ix, bounds));
443    }
444
445    pub(super) fn record_cell(&self, ix: usize, part: Part, bounds: Bounds<Pixels>) {
446        self.0.borrow_mut().cells.push((ix, part, bounds));
447    }
448
449    /// The height last measured for `key`, or else for whatever block was
450    /// last at `ix`.
451    pub(super) fn height(&self, ix: usize, key: u64) -> Option<Pixels> {
452        let heights = &self.0.borrow().heights;
453        heights
454            .by_key
455            .get(&key)
456            .copied()
457            .or_else(|| heights.by_index.get(ix).copied().flatten())
458    }
459
460    pub(super) fn record_height(&self, ix: usize, key: u64, height: Pixels) {
461        let heights = &mut self.0.borrow_mut().heights;
462        heights.by_key.insert(key, height);
463        if heights.by_index.len() <= ix {
464            heights.by_index.resize(ix + 1, None);
465        }
466        heights.by_index[ix] = Some(height);
467    }
468
469    /// Drops the heights of blocks no longer in the document once they
470    /// outnumber the ones that are.
471    pub(super) fn prune(&self, keys: &[u64]) {
472        let heights = &mut self.0.borrow_mut().heights;
473        heights.by_index.truncate(keys.len());
474        if heights.by_key.len() > 2 * keys.len() {
475            let keep: std::collections::HashSet<u64> = keys.iter().copied().collect();
476            heights.by_key.retain(|key, _| keep.contains(key));
477        }
478    }
479
480    pub(super) fn clear(&self) {
481        let mut frames = self.0.borrow_mut();
482        frames.texts.clear();
483        frames.rows.clear();
484        frames.blocks.clear();
485        frames.languages.clear();
486        frames.pictures.clear();
487        frames.painted.clear();
488        frames.checkboxes.clear();
489        frames.cells.clear();
490    }
491}
492
493pub(super) fn row_contains(row: &PaintedRow, offset: usize) -> bool {
494    row.range.start <= offset && offset <= row.range.end
495}
496
497/// The row a caret at `at` paints on: at a soft wrap, the row before it when
498/// `affinity` is upstream and the row after it otherwise.
499fn row_of(frames: &Frames, at: Cursor, affinity: Affinity) -> Option<&PaintedRow> {
500    let mut rows = frames
501        .rows
502        .iter()
503        .filter(|row| row.block == at.block && row.part == at.part && row_contains(row, at.offset));
504    let first = rows.next()?;
505    let wrapped = first.wraps && first.range.end == at.offset;
506    match (wrapped, affinity) {
507        (true, Affinity::Downstream) => rows.next().or(Some(first)),
508        _ => Some(first),
509    }
510}
511
512/// The affinity of a caret at the end of `row`.
513fn end_affinity(row: &PaintedRow) -> Affinity {
514    match row.wraps {
515        true => Affinity::Upstream,
516        false => Affinity::Downstream,
517    }
518}
519
520pub(super) fn cursor_in_row(
521    frames: &Frames,
522    row: &PaintedRow,
523    x: Pixels,
524) -> Option<(Cursor, Affinity)> {
525    let painted = &frames.texts[row.painted];
526    let line = painted.layout.line_layout_for_index(row.line_start)?;
527    let height = row.bounds.size.height;
528    let local = point(
529        x - row.bounds.origin.x,
530        height * (row.wrapped_row as f32 + 0.5),
531    );
532    let (Ok(offset) | Err(offset)) = line.closest_index_for_position(local, height);
533    let offset =
534        (painted.range.start + painted.shown.offset(row.line_start + offset)).min(row.range.end);
535    let affinity = match offset == row.range.end {
536        true => end_affinity(row),
537        false => Affinity::Downstream,
538    };
539    Some((Cursor::new(row.block, row.part, offset), affinity))
540}
541
542pub(super) fn record_rows(
543    rows: &mut Vec<PaintedRow>,
544    painted: usize,
545    block: usize,
546    part: Part,
547    range: &Range<usize>,
548    layout: &TextLayout,
549    shown: &Shown,
550) {
551    let line_height = layout.line_height();
552    let bounds = layout.bounds();
553    let mut origin = bounds.origin;
554    let mut line_start = 0;
555    for line in layout.line_layouts() {
556        let shaped = &line.unwrapped_layout;
557        let row_ends = line
558            .wrap_boundaries()
559            .iter()
560            .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
561            .chain([line.len()]);
562        let rows_in_line = line.wrap_boundaries().len() + 1;
563        let mut row_start = 0;
564        for (row, row_end) in row_ends.enumerate() {
565            rows.push(PaintedRow {
566                painted,
567                block,
568                part,
569                range: range.start + shown.offset(line_start + row_start)
570                    ..range.start + shown.offset(line_start + row_end),
571                bounds: Bounds::new(
572                    origin + point(px(0.0), line_height * row as f32),
573                    size(bounds.size.width, line_height),
574                ),
575                line_start,
576                wrapped_row: row,
577                wraps: row + 1 < rows_in_line,
578            });
579            row_start = row_end;
580        }
581        origin.y += line.size(line_height).height;
582        line_start += line.len() + 1;
583    }
584}