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