pub struct VisualMap {
pub rows: Vec<VRow>,
pub content_start: usize,
pub tables: Vec<TableInfo>,
pub code_blocks: Vec<CodeBlockInfo>,
pub media: Vec<MediaInfo>,
pub directives: Vec<DirectiveInfo>,
/* private fields */
}Expand description
The rendered document plus the offset⇄position mapping the caret rides on.
Fields§
§rows: Vec<VRow>The document’s default monospace rendering — one VRow of glyphs
per visual line, tables spelled with box-drawing borders (│ ─ ┌┬┐…) and
cells padded to whole character-cell columns. Any monospace surface can
draw these verbatim, so a consumer gets a working view for free: the TUI
paints them as-is, and a five-line plain-text dump would too.
It’s a default, not the only truth. A frontend with its own geometry —
a proportional GUI — lays text out in its own units, and for a table
skips the box-drawn rows named by TableInfo::rows_span and draws from
the structural TableInfo instead. The box glyphs live here rather than
in a frontend precisely because they are a renderable default: unlike a
colour (a role each surface must map to its own palette — see
crate::style), ┌─┐ is finished text that needs no interpretation.
content_start: usizeThe first source offset that is actually rendered — the caret floor for
the WYSIWYG view. Non-zero when a leading metadata block (YAML/TOML
frontmatter) is skipped: the frontmatter is preserved in the source and
editable in the source view, but hidden and unreachable here, so the
caret and selection can’t wander into it (and copy won’t grab it).
tables: Vec<TableInfo>Every table in the document, in order, described structurally rather than
drawn — see TableInfo for why both exist.
code_blocks: Vec<CodeBlockInfo>Every fenced/indented code block, in order, as the range of rows it
occupies — a frontend draws one bordered, tinted box around each and
scrolls it horizontally rather than wrapping. Derived from the per-row
VRow::code flag once the rows are final (so it survives incremental
row reuse), the same way [collect_stops] derives the stop table.
media: Vec<MediaInfo>Every block-level image in the document, in order — one per placeholder
row a frontend replaces with a real picture. Derived from the per-row
[VRow::image] mark once the rows are final (so it survives incremental
row reuse), the same way code_blocks is
derived from VRow::code.
directives: Vec<DirectiveInfo>Every leaf directive in the document, in order — one per placeholder
row a frontend may replace with whatever the host app’s vocabulary makes
of it. Derived from the per-row VRow::leaf_directive mark once the
rows are final, exactly as images is.
Implementations§
Source§impl VisualMap
impl VisualMap
pub fn num_rows(&self) -> usize
Sourcepub fn row_width(&self, row: usize) -> usize
pub fn row_width(&self, row: usize) -> usize
The width of row in display columns — the rightmost column its caret
can occupy, and so what a goal column is clamped to on the way in.
Sourcepub fn pos_of_offset(&self, off: usize) -> (usize, usize)
pub fn pos_of_offset(&self, off: usize) -> (usize, usize)
The screen (row, col) for a source offset — where to draw the caret:
the nearest stop at or past off. Snaps a hidden offset (inside a
delimiter) to the next visible glyph, and never resolves onto decoration
(a table border, a cell’s padding), which is drawn but holds no caret.
“Nearest” rather than “the first one found” because a table’s wrapped cells put rows slightly out of offset order: scanning top to bottom, the second line of column 1 comes after the first line of column 2 but holds smaller offsets. Where rows are in order the two rules agree.
A soft wrap is the one place two rows want the same offset: the row above ends where the row below opens, the space the wrap ate being drawn on the row above and the offset past it being the row below’s first character. It resolves downstream, to the row that character is on — the row above’s last column is a phantom, a place the caret can be drawn but never sent, and resolving upstream into it is what pinned Down at the first wrap of a paragraph: it aimed at the row below’s column 0, landed on the offset it already had, and read that back as the row above’s end.
Sourcepub fn row_range_for(&self, range: Range<usize>) -> (usize, usize)
pub fn row_range_for(&self, range: Range<usize>) -> (usize, usize)
The rows a source range occupies, inclusive: (first, last).
A different question from pos_of_offset, which
is why it can’t be spelled with two calls to it. That one answers “where
does the caret go”, and for a caret its forward snap is right — an offset
inside a hidden delimiter has no column of its own, so the caret belongs
at the next visible glyph, wherever that turns out to be. This one asks
“which rows does this block cover”, and there the snap is a trap: a
footnote whose body ends in a link ([^2]: [title](url)) has a last
byte inside the hidden destination, so pos_of_offset(end - 1) walked
clean off the note’s row and landed on the next note’s — and a peek
slicing first..=last out of the frame drew two notes where the reader
asked for one. Every block ending in a link, an image, or any trailing
hidden markup had the same fault; only a block ending in visible text
(which is what the tests happened to use) did not.
row.end_src is no help either: it is where the rendered text of a row
ends, not how far into the source the block reaches, and redefining it
would move every end-of-line caret.
So the last row is found by asking which rows open before the range
does, rather than by mapping its last byte: a row belongs to the range
when its first caret stop lies before range.end. Decoration is skipped
(a drawn gap between blocks is not part of either), and the answer is
never shorter than one row — a range whose every byte is hidden still
covers the row it started on.
Sourcepub fn task_box_at(&self, row: usize, col: usize) -> Option<usize>
pub fn task_box_at(&self, row: usize, col: usize) -> Option<usize>
The source offset of the task checkbox drawn at (row, col), or None
when that cell holds no box — the hit-test a frontend runs on a click
before treating it as a tick rather than a caret placement.
Only the box’s own cells answer. Clicking an item’s text places the
caret like any other click, so the box is a target aimed at rather than
something tripped over while editing — which is also why this is a
separate question from offset_of_pos instead of
a flag on the offset it returns.
Sourcepub fn task_box_at_glyph(&self, row: usize, glyph: usize) -> Option<usize>
pub fn task_box_at_glyph(&self, row: usize, glyph: usize) -> Option<usize>
task_box_at keyed by glyph index rather than
display column — for a frontend that shapes its own rows (the GUI) and so
resolves a click to a glyph before it ever has a column.
Sourcepub fn offset_of_pos(&self, row: usize, col: usize) -> usize
pub fn offset_of_pos(&self, row: usize, col: usize) -> usize
The source offset for a screen (row, col) — where a click or a
visual-space move lands the caret. Clicking decoration maps through its
src, which points at the text it decorates, so a click on a border or
on a cell’s padding lands in that cell.
The inverse of pos_of_offset, which it has to
agree with: col is a display column, and the one it names may be the
far cell of a wide glyph — VRow::glyph_at_col is where that lands.
Sourcepub fn block_media_stop(&self, off: usize) -> Option<(MediaStop, Range<usize>)>
pub fn block_media_stop(&self, off: usize) -> Option<(MediaStop, Range<usize>)>
Which of a block media’s two caret homes off is, or None for every
other offset in the document.
block_media gives a block-level image, video, or
audio exactly two stops — one in front of it and one just past it — and
nothing inside the markup. Both are ordinary offsets to everything else in
core, but they are the two places where inserting text would dissolve the
picture:  with anything typed against it is no longer a block
image but a paragraph with an inline one, and the frontend that was
painting a photo there paints a text run instead. A caller that is about to
insert asks this so it can open a paragraph first — see
Doc::insert.
An inline image reports None: it has no placeholder row and no stops of
its own, and typing beside one is ordinary editing.
Answers with the media’s own source span as well, since a caller that has
to keep the picture whole usually has to address it — Doc::backspace
takes the picture out in one piece rather than nibbling a byte off its
markup, which is the same dissolution from the other side.
Sourcepub fn snap_to_stop(&self, off: usize) -> usize
pub fn snap_to_stop(&self, off: usize) -> usize
Snap off to the nearest caret stop — the funnel a frontend that
hit-tests pixels straight to a source offset must run its result through.
A click or drag can land in the blank gap a paragraph break is drawn with,
or inside a hidden delimiter; both are offsets the caret can’t rest at, so
resting there would draw the caret in one place and type in another. This
settles it on a real caret home instead. Idempotent on an offset that is
already a stop — the (row, col) click path already snaps this way inside
offset_of_pos, and this gives the pixel path the
same guarantee. Returns off unchanged only for an empty document (no
stops at all).
Whether the caret can occupy row at all: decoration rows (a table’s
border rules) are stepped over by vertical motion.
Sourcepub fn row_start(&self, row: usize) -> Option<usize>
pub fn row_start(&self, row: usize) -> Option<usize>
The first offset the caret can rest at on row — its first stop, or the
row’s own end when it holds no text (an empty paragraph). None for a
decoration row, which holds no caret at all.
Not offset_of_pos(row, 0): column 0 of a quoted or listed row is the
gutter, and a gutter’s src points at the block it opens, so the stop
nearest it is the one on the block’s first row rather than on this one.
Which is right for a click — the gutter decorates the whole block — and
wrong for Home, whose whole question is where this row starts.
The nearest row above row the caret can occupy, skipping decoration.
The nearest row below row the caret can occupy, skipping decoration.
Sourcepub fn stop_before(&self, off: usize) -> Option<usize>
pub fn stop_before(&self, off: usize) -> Option<usize>
The caret stop just before off — one press of Left. None at the
first stop in the document.
Runs of decoration (a table border, a cell’s alignment padding) are stepped over in a single press: they hold no stop, so they aren’t in the table to land on.
Sourcepub fn stop_after(&self, off: usize) -> Option<usize>
pub fn stop_after(&self, off: usize) -> Option<usize>
The caret stop just after off — one press of Right. None at the last
stop in the document.
Sourcepub fn stop_at_or_after(&self, off: usize) -> Option<usize>
pub fn stop_at_or_after(&self, off: usize) -> Option<usize>
The first caret stop at or past off — where the caret at a hidden
offset is drawn, and so where a rightward walk over the rendered text
starts from.
Sourcepub fn stop_at_or_before(&self, off: usize) -> Option<usize>
pub fn stop_at_or_before(&self, off: usize) -> Option<usize>
The last caret stop at or before off — where a leftward walk starts
from. Snapping the way the walk is headed, rather than always forward,
is what keeps a leftward motion from ever moving the caret right.
Sourcepub fn is_stop(&self, off: usize) -> bool
pub fn is_stop(&self, off: usize) -> bool
Whether the caret may rest at off — the invariant every motion in this
view has to leave standing.
Sourcepub fn visible_text(&self, from: usize, to: usize) -> String
pub fn visible_text(&self, from: usize, to: usize) -> String
The visible text a caret crosses walking rightward from from up to
(but not including) to — UITextInput.text(in:)’s [from, to) in
this view. A hidden inline-mark delimiter (**, `, _, an
escape backslash) never got a glyph in the first place — see
[push_text]/[synth] — so it contributes nothing; what’s left is
exactly what’s drawn on screen for that span.
Built from the same stop glyphs stop_after steps
across (every glyph with Glyph::stop set, i.e. one per grapheme
cluster, decoration excluded) — plus one inserted '\n' for every
genuine block boundary strictly inside [from, to): a run of whole
decoration rows sitting between two content rows — a paragraph
gap, a table rule, an image’s reserved filler rows — never an ordinary
soft wrap, which puts no decoration row between the two halves of
its one paragraph (only inline decoration glyphs, e.g. a table’s │,
live inside a single content row, and never split one).
Without that inserted break, two blocks abutting in this string were
indistinguishable from one run of text: [collect_stops] gives a
block boundary zero stops of its own (crossing one is a single,
free hop — see the_caret_skips_the_gap_between_two_paragraphs in
doc.rs’s tests, which pins that as intentional caret behaviour, a
paragraph gap costing no extra Right presses, not a bug to fix here).
So the last word of one paragraph and the first word of the next used
to land directly adjacent with nothing between them in this string
("...edb\n\nhello\n" read back as "edbhello"), and UITextInput’s
default word tokenizer then saw one unbroken run of letters and
selected across the boundary — reported as double-tapping the last
word on a line expanding the selection into the following
paragraph(s).
This means the once-strict equality with distance_offset/
step_offset (leaf-ffi) no longer always holds: those intentionally
keep costing a block boundary zero stops, while this text now
spends one character on it that is never itself a stop. So the
relationship is visible_text(a, b).chars().count() >= distance_offset(a, b), equality holding whenever (a, b) spans no
block boundary (the common case, and the only case the previous
equality was ever tested against). It can only ever be greater,
never less: every character this function omits relative to a plain
stop count is a stop with no glyph of its own (a hidden delimiter, or
a block’s own trailing “end of row” stop), and every such omission at
a block’s end is exactly paired with the one inserted separator that
follows it, so nothing this function returns is ever short of what a
consumer walking stops one at a time would need. That inequality is
still exactly what UITextInput‘s tokenizer needs: it only ever reads
this string to find a boundary and converts the character index it
finds back to a position with position(from:offset:), which walks
stops — an inserted separator is never handed back as one, it only
keeps two paragraphs’ words apart for the tokenizer’s letter-run scan.
from is snapped to its nearest stop first, exactly as a caret asked
to stand at a hidden offset is drawn at the next stop instead; to is
left as given, so a stop landing exactly on it is still the walk’s
last step — the same asymmetry distance_offset’s own loop has.