pub struct LeafDoc { /* private fields */ }Expand description
A live leaf document bound for a native Apple frontend: leaf_core::Doc plus
the wrap width the current viewport implies, behind a mutex. Constructed from
an in-memory string and driven entirely through method calls — there is no
filesystem behind it.
Implementations§
Source§impl LeafDoc
impl LeafDoc
Sourcepub fn new(source: String, format: String) -> Result<Arc<Self>, LeafError>
pub fn new(source: String, format: String) -> Result<Arc<Self>, LeafError>
Parse source as format ("markdown"/"md", "djot"/"dj",
"html", "xml") into a live, untitled document.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn view(&self) -> DocView
pub fn view(&self) -> DocView
Resolve the current document to a whole frame — the first paint, and
the frame a frontend taking changes (set_incremental_frames) is
brought back into step by: every change after this is against it.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn paper_view(&self) -> DocView
pub fn paper_view(&self) -> DocView
A whole frame of the document as a page shows it: no line revealed,
where view reveals the caret’s — its delimiters under
MarkupMode::Full, and in every mode a formula on it as its TeX. What
the sheet a PDF or a printout is laid out from asks for in place of
view, since paper has no caret. See leaf_core::Doc::set_unrevealed.
Kept apart from the screen: the screen’s map is set aside and put back, and neither the frame count nor the rows kept for the next change move, so the frame after this one is still a change from the screen’s last.
Sourcepub fn set_incremental_frames(&self, on: bool)
pub fn set_incremental_frames(&self, on: bool)
Whether the frame every method answers with is the change since the
frame before rather than the whole document — see DocView for the
shape, and for what a frontend does with one. Off by default, so a
frontend that reads rows as the document goes on getting it; a
frontend that keeps its own copy of the rows turns it on once and
splices each frame into that copy, which makes a caret move lift no
row across the binding and a keystroke lift the row it changed.
The first frame after turning it on is whole (there is no frame before
to be a change from), and view is whole at any time.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn rows(&self, from: u32, to: u32) -> Vec<Row>
pub fn rows(&self, from: u32, to: u32) -> Vec<Row>
The document’s rows from..to, as the last frame had them — for a
frontend that wants a window of rows back without a whole frame,
having applied every change so far or not. Clamped to the document;
empty when from >= to. Costs the rows of the document to build and
the window to lift, so it is the occasional resynchronisation, not the
per-gesture path.
Sourcepub fn set_width(&self, cols: u32) -> DocView
pub fn set_width(&self, cols: u32) -> DocView
Set the wrap width (in columns) the viewport implies and repaint. For a
fixed-cell frontend (a terminal); a proportional GUI uses [set_unwrapped].
Sourcepub fn set_unwrapped(&self) -> DocView
pub fn set_unwrapped(&self) -> DocView
Switch to unwrapped layout — one visual row per block, no column wrapping — and repaint. A proportional GUI calls this once at start-up, then wraps each row at its own pixel width (the caret/hit/selection geometry it derives from the pixel wrap; core still owns the caret model, in byte offsets). Idempotent and cheap to leave in place across edits.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn set_dark_appearance(&self, dark: bool) -> DocView
pub fn set_dark_appearance(&self, dark: bool) -> DocView
Tell core whether the host is in a dark appearance, so a <picture>’s
prefers-color-scheme <source>s resolve to the right banner. Call it
from viewDidChangeEffectiveAppearance (AppKit) or
traitCollectionDidChange (UIKit).
Cheap to call repeatedly: resolving at the same appearance yields the same
URLs, and a renderer keying its views by src tears nothing down.
Sourcepub fn set_media_rows(&self, heights: Vec<MediaHeight>) -> DocView
pub fn set_media_rows(&self, heights: Vec<MediaHeight>) -> DocView
Report how many visual rows each block media actually needs, measured from
the views the renderer laid out, keyed by the media’s src.
Core does no I/O and can’t know how tall a picture or a player is, so this is the only way a placeholder grows past its default single row. The loop is: lay out at the current reservation → measure → call this → repaint if it changed. Handing over the same measurements again is a no-op, so a renderer can report its current state each frame without diffing first.
A frontend that lays media out in its own units and simply reserves the vertical space itself (the way the gpui GUI does with images) never needs to call this at all.
Sourcepub fn set_math_rows(&self, heights: Vec<MathHeight>) -> DocView
pub fn set_math_rows(&self, heights: Vec<MathHeight>) -> DocView
Report how many visual rows each display formula needs, keyed by its
TeX as MathView handed it over — set_media_rows’s peer for a
renderer that reserves rows. One that lays a formula out in its own
units, as the Swift views do, never calls this.
Sourcepub fn set_inline_pictures(&self, on: bool) -> DocView
pub fn set_inline_pictures(&self, on: bool) -> DocView
Say whether the renderer can paint a picture inside a line of text.
When it can, an inline formula arrives as one math run and a
MathView with inline set, for the renderer to draw its typeset
picture over; when it cannot, as the code-styled TeX it always was.
Off until called, so a host that has not caught up sees what it saw.
Sourcepub fn insert_media(
&self,
kind: MediaKind,
destination: String,
alt: String,
) -> DocView
pub fn insert_media( &self, kind: MediaKind, destination: String, alt: String, ) -> DocView
Insert a block-level image, video, or audio at the caret. Any selection
becomes the alt / fallback text. See leaf_core::Doc::insert_media for
the markup each kind spells.
Sourcepub fn append_media(
&self,
kind: MediaKind,
destination: String,
alt: String,
) -> DocView
pub fn append_media( &self, kind: MediaKind, destination: String, alt: String, ) -> DocView
Append an image, video, or audio at the end of the document, as a
block of its own — for media that arrives rather than media the
writer places at the caret. See leaf_core::Doc::append_media.
Sourcepub fn move_block_up(&self) -> DocView
pub fn move_block_up(&self) -> DocView
Move the caret’s block one place up — Alt+↑ and the Format menu’s Move
Block Up: above the block before it, and out of its container to just
above it when it is the first block there. A list item goes with its
children. The caret rides the block. Gate on
Capabilities::move_block; see leaf_core::Doc::move_block_up.
Sourcepub fn move_block_down(&self) -> DocView
pub fn move_block_down(&self) -> DocView
Move the caret’s block one place down — the mirror of
move_block_up.
Sourcepub fn move_block(&self, from: u32, to: u32) -> DocView
pub fn move_block(&self, from: u32, to: u32) -> DocView
Move the block at source offset from to the boundary to — the drop
half of a drag, with to from drop_target_at
and from any offset inside the block being carried. One undo step;
the caret rides the block; a drop back onto the block’s own boundary
is a quiet no-op. See leaf_core::Doc::move_block.
Sourcepub fn block_range_at(&self, row: u32, ch: u32) -> Option<LandingView>
pub fn block_range_at(&self, row: u32, ch: u32) -> Option<LandingView>
The source range of the block a drag starting at visual (row, ch)
would pick up — the whole paragraph, picture, table or fence, or the
whole list item with its children — for the outline drawn under the
pointer. None on a blank line. Does not move the caret; map the pair
through row_range_for for the rows.
Sourcepub fn drop_target_at(&self, row: u32) -> Option<DropTargetView>
pub fn drop_target_at(&self, row: u32) -> Option<DropTargetView>
Where a block dragged over visual row would land: the boundary
before the row’s block when the row is in its upper half, after it
otherwise, the document’s end for a row below everything. None for
a row with no block under it. See leaf_core::Doc::drop_target_at.
Sourcepub fn insert_thematic_break(&self) -> DocView
pub fn insert_thematic_break(&self) -> DocView
Insert a thematic break (---) at the caret — the toolbar’s Horizontal
Rule button. See leaf_core::Doc::insert_thematic_break for how it
handles a selection, a blank line, and the caret sitting mid-paragraph,
mid-list, or inside a quote.
Sourcepub fn source(&self) -> String
pub fn source(&self) -> String
The current source text — for a save (write to disk / iCloud / a document wrapper) or a source-view display.
Sourcepub fn selected_text(&self) -> Option<String>
pub fn selected_text(&self) -> Option<String>
The selected text, if any — for a clipboard copy/cut.
Sourcepub fn selection_quote(&self, context: u32) -> Option<SelectionQuote>
pub fn selection_quote(&self, context: u32) -> Option<SelectionQuote>
The selection as a quote with up to context characters of what
surrounded it, cut from the source — the shape a host that cites or
annotates a passage wants, findable in the document again by plain
string search. None when nothing is selected. See
leaf_core::Doc::selection_quote.
Sourcepub fn counts(&self) -> TextCounts
pub fn counts(&self) -> TextCounts
Words, characters, and paragraphs over the whole document — the numbers a status bar or an inspector puts next to a piece of writing.
Counted over the text a reader sees rather than the markup that spells
it: **bold** is one word and four characters, a link is its label and
not its destination, a picture counts nothing, and frontmatter is not
writing. The same in both views — the count reads neither the view nor
the map the host last built. See leaf_core::Doc::counts.
It is O(document) and not free (about 4 ms on a 45 KB file), so ask
when the typing settles rather than on every keystroke; there is no
DocView in it, because nothing about the document changes by being
counted.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn selection_counts(&self) -> Option<TextCounts>
pub fn selection_counts(&self) -> Option<TextCounts>
The same statistics over the selection alone — None when nothing is
selected. See leaf_core::Doc::selection_counts.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn set_read_only(&self, on: bool) -> DocView
pub fn set_read_only(&self, on: bool) -> DocView
Turn the read-only gate on or off — a reading surface over the same rendering, selection and navigation the editor has. Enforced in core at the three doors every mutation goes through, so a host that also quiets its input chrome is polishing, not protecting.
Sourcepub fn set_highlights(&self, highlights: Vec<Highlight>) -> DocView
pub fn set_highlights(&self, highlights: Vec<Highlight>) -> DocView
Replace the host-painted source ranges wholesale and repaint — see
leaf_core::Doc::set_highlights for why it is a replace, and
Highlight for what one is.
Sourcepub fn highlight_at(&self, offset: u32) -> Option<String>
pub fn highlight_at(&self, offset: u32) -> Option<String>
The id of the highlight covering source offset, if one does — what a
frontend asks when the reader activates a spot on the page.
Sourcepub fn highlights(&self) -> Vec<Highlight>
pub fn highlights(&self) -> Vec<Highlight>
The host-painted ranges as last set, sorted by start — what a frontend walks to lay out margin markers.
Sourcepub fn mark_saved(&self) -> DocView
pub fn mark_saved(&self) -> DocView
Mark the buffer saved after the host persisted LeafDoc::source its own
way — clears the dirty flag without touching a filesystem.
Sourcepub fn insert(&self, text: String) -> DocView
pub fn insert(&self, text: String) -> DocView
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}pub fn paste(&self, text: String) -> DocView
Sourcepub fn newline(&self) -> DocView
pub fn newline(&self) -> DocView
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn indent(&self) -> DocView
pub fn indent(&self) -> DocView
Tab away from a table: indent the caret’s line (or the selected lines) one
level, nesting a list item under its sibling. The frontend calls this when
LeafDoc::cell_tab declined because the caret isn’t in a table.
Sourcepub fn outdent(&self) -> DocView
pub fn outdent(&self) -> DocView
Shift+Tab away from a table: take one indent level back off the caret’s
line (or the selected lines), unnesting a list item. The mirror of
LeafDoc::indent.
Sourcepub fn cell_tab(&self, forward: bool) -> Option<DocView>
pub fn cell_tab(&self, forward: bool) -> Option<DocView>
Tab (forward) / Shift+Tab hops to the next/previous cell; Tab past the
last cell appends a fresh row and enters it.
Sourcepub fn cell_return(&self) -> Option<DocView>
pub fn cell_return(&self) -> Option<DocView>
Return drops to the cell below in the same column, appending a row at the table’s bottom.
Sourcepub fn cell_line_break(&self) -> Option<DocView>
pub fn cell_line_break(&self) -> Option<DocView>
Shift+Return inserts a hard line break within the current cell.
pub fn backspace(&self) -> DocView
pub fn delete_forward(&self) -> DocView
pub fn delete_word_back(&self) -> DocView
pub fn delete_word_forward(&self) -> DocView
pub fn move_left(&self, extend: bool) -> DocView
pub fn move_right(&self, extend: bool) -> DocView
pub fn move_up(&self, extend: bool) -> DocView
pub fn move_down(&self, extend: bool) -> DocView
pub fn move_word_left(&self, extend: bool) -> DocView
pub fn move_word_right(&self, extend: bool) -> DocView
pub fn move_home(&self, extend: bool) -> DocView
pub fn move_end(&self, extend: bool) -> DocView
pub fn move_doc_start(&self, extend: bool) -> DocView
pub fn move_doc_end(&self, extend: bool) -> DocView
pub fn select_all(&self) -> DocView
Sourcepub fn click(&self, row: u32, col: u32, extend: bool) -> DocView
pub fn click(&self, row: u32, col: u32, extend: bool) -> DocView
Place the caret from a click, in core’s column grid: row indexes the
visual Rows and col is the glyph column within it. Core clamps both
to real caret stops. Prefer LeafDoc::click_ch from a proportional
renderer.
Sourcepub fn click_ch(&self, row: u32, ch: u32, extend: bool) -> DocView
pub fn click_ch(&self, row: u32, ch: u32, extend: bool) -> DocView
Place the caret from a click whose horizontal position is a UTF-16
offset into the visual row’s text — what characterIndex(for:) hands
back. Converted to core’s display column before clicking, so a proportional
renderer never reasons about column widths itself.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn select_word_ch(&self, row: u32, ch: u32) -> DocView
pub fn select_word_ch(&self, row: u32, ch: u32) -> DocView
Select the word under a click (row, ch) — the double-click gesture.
Sourcepub fn select_block_ch(&self, row: u32, ch: u32) -> DocView
pub fn select_block_ch(&self, row: u32, ch: u32) -> DocView
Select the whole logical text block under a click (row, ch) — the
triple-click gesture. Grabs the entire block even where it soft-wraps.
Sourcepub fn click_past_end(&self) -> DocView
pub fn click_past_end(&self) -> DocView
A click in the blank space under the last row — the caret goes onto an
empty paragraph under the last block, opening one if the document does
not end with one, wherever the pointer was horizontally. See
leaf_core::Doc::click_past_end. The frontend decides “under”: the
point is below every line box it laid out.
Sourcepub fn set_selection(
&self,
anchor_row: u32,
anchor_ch: u32,
focus_row: u32,
focus_ch: u32,
) -> DocView
pub fn set_selection( &self, anchor_row: u32, anchor_ch: u32, focus_row: u32, focus_ch: u32, ) -> DocView
Mirror a native selection into the model: [anchor, focus] given as
row + UTF-16 offset pairs. Each is resolved to a source offset the way a
click is, then set as the selection’s fixed and moving ends. A collapsed
range (anchor == focus) just places the caret.
Sourcepub fn selection_html(&self) -> Option<String>
pub fn selection_html(&self) -> Option<String>
The current selection rendered to HTML by twig — the rich flavor a copy
writes alongside the plain LeafDoc::selected_text. None when nothing
is selected.
Sourcepub fn paste_rich(&self, html: Option<String>, text: String) -> DocView
pub fn paste_rich(&self, html: Option<String>, text: String) -> DocView
Paste, preferring the clipboard’s rich (text/html) flavor: twig parses
html into the document’s own markup and inserts it. Falls back to the
plain text when there’s no HTML or it doesn’t parse.
pub fn toggle_bold(&self) -> DocView
pub fn toggle_italic(&self) -> DocView
pub fn toggle_code(&self) -> DocView
pub fn toggle_mark(&self) -> DocView
Sourcepub fn caret_in_mark(&self) -> bool
pub fn caret_in_mark(&self) -> bool
Whether the caret stands in a highlight — what a colour palette enables
itself by, since a colour is a property of a highlight that already
exists. The caret-side half of Capabilities::mark_color.
Sourcepub fn set_mark_color(&self, color: Option<MarkColor>) -> DocView
pub fn set_mark_color(&self, color: Option<MarkColor>) -> DocView
Colour the highlight at the caret, or clear its colour with None.
Markdown only — ==🔴 text== is its spelling and djot has none — and
only where there is a highlight to colour: this does not make one, so a
coloured highlight from bare text is LeafDoc::toggle_mark and then
this, which is the order the button and its palette already sit in. Both
refusals leave the document alone and say so in the status line.
Sourcepub fn highlight(&self, color: Option<MarkColor>) -> DocView
pub fn highlight(&self, color: Option<MarkColor>) -> DocView
One press of a colour swatch: colour the highlight at the caret, or — over a selection that isn’t highlighted yet — highlight it and colour it, as one undo step.
LeafDoc::set_mark_color is the exact gesture; this is the compound a
toolbar presses, and it lives in core so that every frontend answers
“what does a swatch mean over plain text” the same way. None clears the
colour, and over an unhighlighted selection means simply “highlight
this”. A bare caret in no highlight is left alone.
pub fn toggle_underline(&self) -> DocView
pub fn toggle_strike(&self) -> DocView
pub fn set_paragraph(&self) -> DocView
Sourcepub fn set_heading(&self, level: u32) -> DocView
pub fn set_heading(&self, level: u32) -> DocView
Toggle the current block to a heading of level (1–6); toggling the
active level off returns it to a paragraph, per core.
pub fn toggle_blockquote(&self) -> DocView
pub fn toggle_list(&self, ordered: bool) -> DocView
Sourcepub fn toggle_code_block(&self) -> DocView
pub fn toggle_code_block(&self) -> DocView
Toggle a fenced code block over the selection or the block at the caret;
on a blank line, open an empty one with the caret inside. See
leaf_core::Doc::toggle_code_block. Gate on
Capabilities::code_block; light from DocView::code_block.
Sourcepub fn toggle_task_checked(&self) -> DocView
pub fn toggle_task_checked(&self) -> DocView
Tick or untick the task item at the caret. See
leaf_core::Doc::toggle_task_checked.
Sourcepub fn toggle_task_at(&self, offset: u64) -> DocView
pub fn toggle_task_at(&self, offset: u64) -> DocView
Tick or untick the task item covering offset — a tap on a rendered
checkbox, which must not drag the caret across the document to get there.
Sourcepub fn toggle_task_item(&self) -> DocView
pub fn toggle_task_item(&self) -> DocView
Give the list item at the caret a checkbox, or take its checkbox away.
Sourcepub fn task_checked_at_caret(&self) -> Option<bool>
pub fn task_checked_at_caret(&self) -> Option<bool>
Whether the item at the caret has a box and which way it faces — None
for a plain list item or no item at all. Drives a toolbar’s checked state.
Sourcepub fn set_alignment(&self, align: Option<Align>) -> DocView
pub fn set_alignment(&self, align: Option<Align>) -> DocView
Align the caret’s block, or return it to the theme’s default with nil.
A block property, so it is the caret’s block whatever is selected: a
line belongs to a block, and “centre this” with three words selected
means the paragraph, not the words. Other class tokens on the block are
kept. Gate on Capabilities::alignment.
Sourcepub fn set_line_spacing(&self, spacing: Option<LineHeight>) -> DocView
pub fn set_line_spacing(&self, spacing: Option<LineHeight>) -> DocView
Set the line spacing of the caret’s block, or return it to the theme’s
with nil. set_alignment’s peer in every
respect but the key. Gate on Capabilities::line_spacing.
.step(.oneHalf) from the menu’s three, or .ratio(1.3) from an
Other… field. A ratio that spells one of the three is that name, and
a ratio of 1 is single spacing, which is absence and clears the key. A
ratio the vocabulary cannot carry at all — 0, 700, a NaN — writes
nothing, and the block’s own spacing stands.
Sourcepub fn set_font_size(&self, size: Option<FontSize>) -> DocView
pub fn set_font_size(&self, size: Option<FontSize>) -> DocView
Set the size of the selected run, or of the caret’s whole block when
nothing is selected; nil returns it to the theme’s own size.
Size, face and colour are the run’s, and the block’s when no run is
chosen — so “make this paragraph larger” is a press with the caret in it
rather than a select-all first. With a selection the range is wrapped in
an attributed span, or the span it already lies in is re-styled, never
nested. Gate on Capabilities::font_size.
.step(.large) from the menu’s seven, or .points(14) from an
Other… field — a size in points, which is what the paper will show. A
number of points the vocabulary cannot carry (0.01 to 655.35 is the
range) writes nothing at all, and the run’s own size stands: validate
the field before calling, because from here the refusal is silent.
Sourcepub fn set_font_family(&self, font: Option<FontFace>) -> DocView
pub fn set_font_family(&self, font: Option<FontFace>) -> DocView
Set the face of the selected run, or of the caret’s whole block.
set_font_size’s peer. Gate on
Capabilities::font_family.
.generic(.serif) from the menu’s four, or .named("Garamond") from
the platform’s font picker — which draws in that family where it is
installed and in the theme’s body face where it is not. A name that
names nothing (" ") writes nothing, and the run’s own face stands.
Sourcepub fn set_text_color(&self, color: Option<TextColor>) -> DocView
pub fn set_text_color(&self, color: Option<TextColor>) -> DocView
Set the text colour of the selected run, or of the caret’s whole block.
set_font_size’s peer, and not
set_mark_color: that one colours a highlight’s
background and needs a highlight to colour, this one paints the letters
and needs nothing. They share the seven names on purpose. Gate on
Capabilities::text_color.
.named(.red) from the seven swatches, or .rgb(r:g:b:) from the
system colour picker — painted as written in both appearances, which is
what “exact” costs.
Sourcepub fn insert_page_break(&self) -> DocView
pub fn insert_page_break(&self) -> DocView
Insert a page break at the caret — a leaf directive with no label, placed
exactly as insert_thematic_break places a
rule, a selection replaced by it and a bare paragraph parted at the caret
first.
A frontend that paginates opens a page at the row’s directive mark and
gives the row no height; one that does not draws the ⧉ page-break
placeholder every leaf directive gets. Gate on
Capabilities::page_break.
Sourcepub fn alignment_at_caret(&self) -> Option<Align>
pub fn alignment_at_caret(&self) -> Option<Align>
The alignment in force at the caret, or nil for the theme’s default —
which segment of an alignment control is lit.
Read off the nearest node that names one: the caret’s block, and the
divs around it after that, so the control follows the caret into a
centred <div>.
Sourcepub fn line_spacing_at_caret(&self) -> Option<LineHeight>
pub fn line_spacing_at_caret(&self) -> Option<LineHeight>
The line spacing in force at the caret, or nil for the theme’s own —
which entry a spacing menu shows ticked.
alignment_at_caret’s peer.
A .ratio is a spacing no row of the menu’s three can tick, and is the
author’s own: the menu shows it as a row of its own above Other….
Sourcepub fn font_size_at_caret(&self) -> Option<FontSize>
pub fn font_size_at_caret(&self) -> Option<FontSize>
The size in force at the caret, or nil for the theme’s own — which
entry a size menu shows ticked. Run-level, so the chain starts one node
deeper: the attributed span the caret stands in, then its block, then the
divs around it, the nearest winning.
A .points is a size no row of the menu’s seven can tick, and the menu
shows it as a row of its own — “14 pt” — above Other….
Sourcepub fn font_family_at_caret(&self) -> Option<FontFace>
pub fn font_family_at_caret(&self) -> Option<FontFace>
The face in force at the caret, or nil for the theme’s body face.
font_size_at_caret’s peer, and a .named
is the family the author picked, shown as its own ticked row.
Sourcepub fn text_color_at_caret(&self) -> Option<TextColor>
pub fn text_color_at_caret(&self) -> Option<TextColor>
The text colour in force at the caret, or nil for the theme’s — which
swatch a text-colour control marks as the current one.
font_size_at_caret’s peer, and not
DocView::mark_color, which reads a highlight’s background off a
mark node the caret is standing in. A .rgb is the author’s own
triple, which the palette shows as a swatch of its own.
Sourcepub fn capabilities(&self) -> Capabilities
pub fn capabilities(&self) -> Capabilities
Which of the formatting commands above this document’s format can actually spell — one flag per control, for building the toolbar.
Read once when a document opens: the answer depends only on the format, so it cannot change under an edit. Every command refuses on its own regardless — the model is the authority, not the toolbar — so a frontend that ignores this stays correct, it just offers buttons whose only effect is a line in the status bar.
Don’t collapse it to one flag. An HTML document takes ⌘B, ⌘I and inline
code (its marks are a tag pair) while refusing every heading, list, quote
and link, and Markdown refuses the underline djot spells — so a toolbar
driven by Self::authorable alone would be wrong in both directions.
Whether this document’s format offers any door in — false only for a
wholly parse-only one (XML), where an app may as well open the file
read-only and hide the formatting section outright. For anything finer,
including whether to dim an individual button, use Self::capabilities.
Sourcepub fn caret_in_table(&self) -> bool
pub fn caret_in_table(&self) -> bool
Whether the caret is inside a table — for enabling the table controls.
Pair it with Capabilities::table: the caret is genuinely inside an
HTML <table>, and the grid controls still cannot edit one.
Sourcepub fn table_insert_row(&self, below: bool) -> DocView
pub fn table_insert_row(&self, below: bool) -> DocView
Insert an empty row below (below) or above the caret’s row.
Sourcepub fn table_delete_row(&self) -> DocView
pub fn table_delete_row(&self) -> DocView
Delete the caret’s row (not the header or the last body row).
Sourcepub fn table_insert_column(&self, right: bool) -> DocView
pub fn table_insert_column(&self, right: bool) -> DocView
Insert an empty column right (right) or left of the caret’s column.
Sourcepub fn table_delete_column(&self) -> DocView
pub fn table_delete_column(&self) -> DocView
Delete the caret’s column (unless it is the only one).
Sourcepub fn table_set_alignment(&self, alignment: TableAlignment) -> DocView
pub fn table_set_alignment(&self, alignment: TableAlignment) -> DocView
Set the caret’s column to alignment.
Sourcepub fn table_move_row(&self, down: bool) -> DocView
pub fn table_move_row(&self, down: bool) -> DocView
Move the caret’s row one place down (down) or up.
Sourcepub fn table_move_column(&self, right: bool) -> DocView
pub fn table_move_column(&self, right: bool) -> DocView
Move the caret’s column one place right (right) or left.
Sourcepub fn insert_table(&self, rows: u32, cols: u32) -> DocView
pub fn insert_table(&self, rows: u32, cols: u32) -> DocView
Insert a fresh table at the caret — one header row, rows empty body
rows, cols columns — and leave the caret in its first header cell.
The one table verb that needs no table under the caret; gate it on
Capabilities::table alone. See leaf_core::Doc::insert_table for
the placement (a paragraph is parted around the caret, as for the rule)
and for what a zero shape does.
pub fn insert_link(&self, destination: String) -> DocView
Sourcepub fn link_destination_at_caret(&self) -> Option<String>
pub fn link_destination_at_caret(&self) -> Option<String>
The destination of the link under the caret, if the caret is inside one —
so a frontend can open it (⌘-click / “Open Link”) or show it. None when the
caret isn’t on a link.
Sourcepub fn image_destination_at_caret(&self) -> Option<String>
pub fn image_destination_at_caret(&self) -> Option<String>
The source of the image the caret stands in — the src of an
 or a <img>, exactly as the document spells it. None
when the caret is in no image.
Two hosts ask. An image prompt seeds from it, so editing an existing
picture starts from its current URL rather than blank; and a host that
gives an attachment a place of its own — a node, a page, a file
inspector — asks it to answer “show me this one” from a menu raised
over the body. See LeafEditorModel.onShowMedia in the Swift package.
A caret resting just after a block image (its trailing stop) is already
past it and gets None, which is the same half-open rule
link_destination_at_caret follows.
Sourcepub fn link_destination_at(&self, off: u32) -> Option<String>
pub fn link_destination_at(&self, off: u32) -> Option<String>
The destination of the link at byte offset off —
link_destination_at_caret for a place
the caret isn’t.
What a frontend drawing part of the document outside the document asks:
a footnote’s text in a popover has link runs in it, and this is how those
runs learn where they point, since a Run carries how a span looks and
not what it means.
Sourcepub fn heading_at(&self, off: u32) -> Option<HeadingView>
pub fn heading_at(&self, off: u32) -> Option<HeadingView>
The heading byte offset off is under — the nearest heading at or
above it — or None above the first. What a host writing a link to
a place names it by: the #slug comes from its text. See
leaf_core::Doc::heading_at.
Sourcepub fn heading_at_caret(&self) -> Option<HeadingView>
pub fn heading_at_caret(&self) -> Option<HeadingView>
The heading the caret is under — heading_at at
the caret.
Sourcepub fn locate(&self, id: String) -> Option<LandingView>
pub fn locate(&self, id: String) -> Option<LandingView>
Where the locator id lands in this document — the #v2 half of a
chapter.dj#v2, resolved to the block it names. None when nothing here
answers to it, which is a host’s cue to open the document at its top
rather than refuse to go.
The query that gives a link finer granularity than the file. It reads an
explicit {#v1}, a djot heading’s minted id, or (for Markdown, which
mints none) a heading’s own words slugged — see leaf_core::Doc::locate.
Asked of any document, not only the open one: a host peeking at a
citation builds a LeafDoc over the other file’s bytes and asks this,
which is what lets a hover show the verse instead of the filename.
Sourcepub fn insert_footnote(&self) -> DocView
pub fn insert_footnote(&self) -> DocView
Write a footnote at the caret — the toolbar’s Footnote button. Both the
[^1] and the definition it needs go in as one edit (one undo takes both
back), the label is the lowest number the document has free, and the caret
is left in the empty note ready to type it. Gate the button on
Capabilities::footnote; see leaf_core::Doc::insert_footnote.
Sourcepub fn footnote_at_caret(&self) -> Option<FootnoteView>
pub fn footnote_at_caret(&self) -> Option<FootnoteView>
The footnote reference under the caret, resolved to the note it names —
so a frontend can show the note when a reader activates a [1], instead
of the nothing a reference click used to do. None when the caret isn’t
on a reference; see FootnoteView for the reference that resolved to
no definition.
Sourcepub fn footnote_at(&self, off: u32) -> Option<FootnoteView>
pub fn footnote_at(&self, off: u32) -> Option<FootnoteView>
The footnote reference at byte offset off, resolved to the note it
names — footnote_at_caret for a place the
caret isn’t.
This is what a hover asks: a pointer resting on a [1] wants the note’s
text in a popover, and moving the caret to find out would yank the reader
out of wherever they were typing.
Sourcepub fn footnote_definition_at_caret(&self) -> Option<FootnoteDefView>
pub fn footnote_definition_at_caret(&self) -> Option<FootnoteDefView>
The footnote definition the caret stands in, and where the reference that
names it is — the return leg of footnote_at_caret,
so following a footnote is a round trip rather than a fall.
None when the caret isn’t in a definition, which is also how a frontend
tells the two directions apart: the reference query answers up top, this
one answers down in the notes, and never both at once.
pub fn undo(&self) -> DocView
pub fn redo(&self) -> DocView
Sourcepub fn begin_undo_group(&self)
pub fn begin_undo_group(&self)
Open an undo group: every edit until the matching
end_undo_group undoes and redoes as one step —
a Replace All, or a Writing Tools session. Groups nest; an undo or redo
closes any that is open. See Doc::begin_undo_group.
Sourcepub fn end_undo_group(&self)
pub fn end_undo_group(&self)
Close the group begin_undo_group opened; a
no-op when none is open.
Sourcepub fn toggle_view(&self) -> DocView
pub fn toggle_view(&self) -> DocView
Switch between the rendered WYSIWYG surface and the raw source.
Sourcepub fn markup_mode(&self) -> MarkupMode
pub fn markup_mode(&self) -> MarkupMode
The current markup-exposure preference (see MarkupMode).
Sourcepub fn set_markup_mode(&self, mode: MarkupMode) -> DocView
pub fn set_markup_mode(&self, mode: MarkupMode) -> DocView
Set the markup-exposure preference. Returns a fresh view so a frontend
can repaint — and under Full it must, because the returned view is the
first one showing the caret’s line raw. Diaryx leaves it at the None
default.
Sourcepub fn set_line_flow(&self, mode: LineFlow) -> DocView
pub fn set_line_flow(&self, mode: LineFlow) -> DocView
Set the soft-break flow preference. Returns a fresh view so a frontend can repaint: like the markup-exposure preference this one changes rendering immediately, laying preserved soft breaks out as their own rows.
Source§impl LeafDoc
impl LeafDoc
Sourcepub fn caret_offset(&self) -> u32
pub fn caret_offset(&self) -> u32
The caret’s source offset (the selection’s moving end).
Sourcepub fn anchor_offset(&self) -> u32
pub fn anchor_offset(&self) -> u32
The selection’s fixed end (equals the caret when there’s no selection).
Sourcepub fn doc_end_offset(&self) -> u32
pub fn doc_end_offset(&self) -> u32
The last caret stop in the document — UITextInput.endOfDocument.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn snap_offset(&self, off: u32) -> u32
pub fn snap_offset(&self, off: u32) -> u32
Snap an arbitrary offset to the nearest valid caret stop.
Sourcepub fn pos_for_offset(&self, off: u32) -> RowCol
pub fn pos_for_offset(&self, off: u32) -> RowCol
Where a source offset sits on screen: its visual (row, ch).
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn row_range_for(&self, start: u32, end: u32) -> RowRange
pub fn row_range_for(&self, start: u32, end: u32) -> RowRange
The rows a source range covers, inclusive — for drawing a block away from where it sits (a footnote peek, a link peek, a landing flash).
Ask this rather than mapping start and end - 1 through
Self::pos_for_offset. That pair reads correctly and is wrong: a
block’s last byte is often hidden — a note or a paragraph ending in a
link ends inside the link’s destination — and pos_for_offset snaps a
hidden offset forward to the next visible glyph, which for a trailing
one is on the next block’s row. A peek slicing that span drew the block
after it too. pos_for_offset’s snap is right for a caret and wrong for
a span; this is the question spans should be asking.
Sourcepub fn offset_for_pos(&self, row: u32, ch: u32) -> u32
pub fn offset_for_pos(&self, row: u32, ch: u32) -> u32
The source offset at visual (row, ch) — the inverse of
Self::pos_for_offset, for hit-testing a point to a position.
Sourcepub fn step_offset(&self, off: u32, delta: i32) -> u32
pub fn step_offset(&self, off: u32, delta: i32) -> u32
Move off by delta caret stops (negative = left) — position(from:offset:).
Sourcepub fn distance_offset(&self, from: u32, to: u32) -> i32
pub fn distance_offset(&self, from: u32, to: u32) -> i32
The count of caret stops between two offsets (signed) — offset(from:to:).
Sourcepub fn utf16_index_for_offset(&self, off: u32) -> u32
pub fn utf16_index_for_offset(&self, off: u32) -> u32
The UTF-16 index at which source offset off sits in the visible text —
the string text_in_range(0, doc_end_offset()) returns — which is the
character space AppKit’s NSTextInputClient and NSAccessibility speak.
leaf’s own handle is the source byte offset, and the two are not one
scale apart: WYSIWYG hides delimiters, a block gap is spelled as one
\n, and a character outside the BMP is two UTF-16 units. A frontend
hands the system an NSRange converted with this and turns the ranges
it gets back through offset_for_utf16_index, so Look Up, dictation, and
VoiceOver all index the same text the frontend drew.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn utf16_indices_for_offsets(&self, offs: Vec<u32>) -> Vec<u32>
pub fn utf16_indices_for_offsets(&self, offs: Vec<u32>) -> Vec<u32>
utf16_index_for_offset over many offsets in one crossing — the index
of each, in the order given. For a caller that converts every run of
the frame at once, as a spell checker masking the visible text does:
thousands of runs, and a call across the binding for each was the cost.
Sourcepub fn offset_for_utf16_index(&self, index: u32) -> u32
pub fn offset_for_utf16_index(&self, index: u32) -> u32
The inverse of utf16_index_for_offset: the source offset of the
visible character at UTF-16 index, or the document’s end stop at or
past the end of the text. An index inside a surrogate pair resolves to
the character that owns it, and one on the \n a block gap is spelled
with to the stop at the end of the block before it. Always a caret stop.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn vertical_offset(&self, off: u32, down: bool) -> Option<u32>
pub fn vertical_offset(&self, off: u32, down: bool) -> Option<u32>
The offset one navigable row up/down from off, keeping its column —
position(from:in: .up/.down). None at the top/bottom edge.
Sourcepub fn text_in_range(&self, from: u32, to: u32) -> String
pub fn text_in_range(&self, from: u32, to: u32) -> String
The visible text between two offsets — text(in:). In the WYSIWYG
view this is not the raw source slice: a hidden inline-mark
delimiter (**, `, _) contributes nothing, and a stop that
draws no glyph — a row’s end, a table cell’s end — is spelled '\n'.
Exactly one character per caret stop, so that for any two stops
text_in_range(a, b).chars().count() == distance_offset(a, b). That
equality is what UITextInput’s word tokenizer relies on: it reads a
window of this text, indexes into it by offset(from:to:), and hands
a character delta back through position(from:offset:) — see
leaf_core::wysiwyg::VisualMap::visible_text for the rule and what
a one-character drift did to a double-tapped word. The source view has
nothing hidden to begin with, so there this is exactly the raw slice.
Examples found in repository?
27fn main() {
28 let mut args = std::env::args().skip(1);
29 let source = match args.next().as_deref() {
30 // The generated document, written out — to open in the app, or to
31 // concatenate with itself and see which rows grow with it.
32 Some("--dump") => {
33 let path = args.next().expect("--dump takes a path");
34 std::fs::write(&path, generated()).unwrap_or_else(|e| panic!("{path}: {e}"));
35 return;
36 }
37 Some(path) => std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}")),
38 None => generated(),
39 };
40 let words = source.split_whitespace().count();
41 let profile = if cfg!(debug_assertions) {
42 "dev"
43 } else {
44 "release"
45 };
46 println!(
47 "document: {} bytes, {words} words · profile: {profile}\n",
48 source.len()
49 );
50
51 let open = Instant::now();
52 let doc = LeafDoc::new(source.clone(), "markdown".into()).expect("parse");
53 let opened = open.elapsed();
54 let first = Instant::now();
55 let view = doc.set_unwrapped();
56 let first_view = first.elapsed();
57 let runs: usize = view.rows.iter().map(|r| r.runs.len()).sum();
58 println!("rows: {} · runs: {runs}", view.rows.len());
59 println!("{:<52} {:>10}", "open (twig parse)", fmt(opened));
60 println!(
61 "{:<52} {:>10}",
62 "first view (build the map + the frame)",
63 fmt(first_view)
64 );
65
66 let len = doc.doc_end_offset();
67 let mid = len / 2;
68 let mid_utf16 = doc.utf16_index_for_offset(mid);
69 let mid_row = doc.pos_for_offset(mid).row;
70
71 // Reads a frontend makes per frame or per gesture.
72 row("view() — the whole frame, unchanged", || {
73 std::hint::black_box(doc.view());
74 });
75 row("pos_for_offset (mid-document)", || {
76 std::hint::black_box(doc.pos_for_offset(mid));
77 });
78 row("utf16_index_for_offset (mid-document)", || {
79 doc.utf16_index_for_offset(mid);
80 });
81 row("offset_for_utf16_index (mid-document)", || {
82 doc.offset_for_utf16_index(mid_utf16);
83 });
84 row("text_in_range (whole document)", || {
85 std::hint::black_box(doc.text_in_range(0, len));
86 });
87 row("counts", || {
88 std::hint::black_box(doc.counts());
89 });
90
91 // Gestures: each answers with a whole frame.
92 row("click_ch (place the caret)", || {
93 std::hint::black_box(doc.click_ch(mid_row, 3, false));
94 });
95 row("click_ch extend (grow a selection)", || {
96 doc.click_ch(mid_row, 0, false);
97 std::hint::black_box(doc.click_ch(mid_row + 4, 0, true));
98 });
99 doc.click_ch(mid_row, 3, false);
100 row("selection_counts (rows selected)", || {
101 doc.click_ch(mid_row, 0, false);
102 doc.click_ch(mid_row + 4, 0, true);
103 std::hint::black_box(doc.selection_counts());
104 });
105 doc.click_ch(mid_row, 3, false);
106 row("insert one character", || {
107 std::hint::black_box(doc.insert("x".into()));
108 });
109 // The UTF-16 table is derived on the first lookup after a map rebuild,
110 // so the first conversion after a keystroke pays for it; every later one
111 // is the lookup alone.
112 row("utf16_index_for_offset (first after an edit)", || {
113 doc.insert("x".into());
114 doc.utf16_index_for_offset(mid);
115 });
116 row("newline", || {
117 std::hint::black_box(doc.newline());
118 });
119
120 // The same gestures on a document answering with changes rather than
121 // whole frames (`set_incremental_frames`): what a frontend that keeps its
122 // own copy of the rows pays. The rows each lifts are in the label, since
123 // that is the number that must not grow with the document.
124 let inc = LeafDoc::new(source, "markdown".into()).expect("parse");
125 inc.set_incremental_frames(true);
126 let _ = inc.set_unwrapped();
127 let lifted = inc.click_ch(mid_row, 3, false).rows.len();
128 row(
129 &format!("click_ch, as a change ({lifted} rows lifted)"),
130 || {
131 std::hint::black_box(inc.click_ch(mid_row, 3, false));
132 },
133 );
134 let lifted = inc.insert("x".into()).rows.len();
135 row(
136 &format!("insert one character, as a change ({lifted} rows lifted)"),
137 || {
138 std::hint::black_box(inc.insert("x".into()));
139 },
140 );
141 inc.click_ch(mid_row, 0, false);
142 let lifted = inc.click_ch(mid_row + 4, 0, true).rows.len();
143 row(
144 &format!("click_ch extend, as a change ({lifted} rows lifted)"),
145 || {
146 inc.click_ch(mid_row, 0, false);
147 std::hint::black_box(inc.click_ch(mid_row + 4, 0, true));
148 },
149 );
150}Sourcepub fn set_selection_offsets(&self, anchor: u32, focus: u32) -> DocView
pub fn set_selection_offsets(&self, anchor: u32, focus: u32) -> DocView
Set the selection to [anchor, focus] by source offsets — the setter behind
UITextInput.selectedTextRange and handle dragging.
Sourcepub fn select_range(&self, start: u32, end: u32) -> DocView
pub fn select_range(&self, start: u32, end: u32) -> DocView
Select the exact source range [start, end), snapping neither end to a
visible caret stop — for a host painting a range it already knows the
bytes of (a search hit, an annotation) rather than hit-testing a touch.
set_selection_offsets above is the other verb: it goes through
place_caret, which snaps, and is what a drag handle wants. This one
takes the range as given, so a selection over **needle**’s inner word
is the word and not one byte short of it.
Sourcepub fn substitute(&self, from: u32, to: u32, text: String) -> DocView
pub fn substitute(&self, from: u32, to: u32, text: String) -> DocView
Replace the source range [from, to) with text behind the caret — an
automatic substitution as the user types (a correction, a text
replacement, a smart quote or dash). The caret and selection stay where
they were, and the substitution is an undo step of its own. See
Doc::substitute.