Shape a text once per width, not once per ask
A container measures a child by drawing it in a box it may not keep, so one layout asks a text for a dozen widths and comes back to widths it has already had -- the hottest text in the depth-8 tree draws 32 times. Each ask re-ran the shaper, because the two caches in front of it held one entry each and a trial width alternating with a final width evicts the answer about to be wanted again. `perf record` put 63% of a resize frame in text and 0.9% in `draw_inner`. So keep more than one: a bounded store of shapings on `TextData`, keyed by the text, the attrs and the width, holding the parley layout and the glyphs placed from it. Bounding the store rather than each buffer is what keeps it a fixed cost -- +4 MB on a tree of 4,000 texts, which is 19 MB less than the code before #16 holds after the same resizes. `TextBuffer` now holds the glyphs of the shaping it is drawn as, which is where `TextView::tex` was. That leaves one place to invalidate rather than two, so the `MutDetect` flags on a view's text and attrs have no reader and go, along with the `buf.changed = true` after every edit. On a 40-row tree of distinct random paragraphs, 500 resize frames: 124.2M instructions per frame before, 17.7M after, and 45.9M when the width never repeats. The five reference renders and the resize render are byte-identical, and the 100-seed sweep passes. `tests/revision_cost.rs` is that tree, written in the API subset `43ce8c7` shares so the same source measures the code this replaced. Report the worst frame and p99 beside the median, since a stutter is what somebody sees. Count glyph placements, and count a text render per ask rather than per shaping, so the store cannot hide how many times a layout drew the same text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1a47e9b7b
commit
e5f8b6b244
7 files changed
+321
-43
No files matched your search
@@ -130,7 +130,6 @@ impl<'a> TextEditCtx<'a> {
|
||||
pub fn set(&mut self, text: &str) {
|
||||
let text = self.string(text);
|
||||
self.text.view.buf.set_text(text);
|
||||
self.text.view.buf.changed = true;
|
||||
self.text.selection = None;
|
||||
}
|
||||
|
||||
@@ -177,7 +176,6 @@ impl<'a> TextEditCtx<'a> {
|
||||
};
|
||||
let at = at.min(self.text.view.buf.text().len());
|
||||
self.text.view.buf.edit().insert_str(at, text);
|
||||
self.text.view.buf.changed = true;
|
||||
self.set_caret(at + text.len());
|
||||
}
|
||||
|
||||
@@ -190,7 +188,6 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
let range = sel.text_range();
|
||||
self.text.view.buf.edit().replace_range(range.clone(), "");
|
||||
self.text.view.buf.changed = true;
|
||||
self.set_caret(range.start);
|
||||
true
|
||||
}
|
||||
@@ -268,7 +265,6 @@ impl<'a> TextEditCtx<'a> {
|
||||
|
||||
fn delete_range(&mut self, start: usize, end: usize) {
|
||||
self.text.view.buf.edit().replace_range(start..end, "");
|
||||
self.text.view.buf.changed = true;
|
||||
self.set_caret(start);
|
||||
}
|
||||
|
||||
|
||||
+10
-21
@@ -14,11 +14,8 @@ pub struct Text {
|
||||
}
|
||||
|
||||
pub struct TextView {
|
||||
pub attrs: MutDetect<TextAttrs>,
|
||||
pub buf: MutDetect<TextBuffer>,
|
||||
// cache
|
||||
tex: Option<RenderedText>,
|
||||
width: Option<f32>,
|
||||
pub attrs: TextAttrs,
|
||||
pub buf: TextBuffer,
|
||||
pub hint: Option<StrongWidget>,
|
||||
}
|
||||
|
||||
@@ -28,19 +25,13 @@ impl TextView {
|
||||
}
|
||||
|
||||
pub fn wrap_width(&self) -> Option<f32> {
|
||||
self.width
|
||||
self.buf.wrap_width()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextView {
|
||||
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
||||
Self {
|
||||
attrs: attrs.into(),
|
||||
buf: buf.into(),
|
||||
tex: None,
|
||||
width: None,
|
||||
hint,
|
||||
}
|
||||
Self { attrs, buf, hint }
|
||||
}
|
||||
|
||||
/// region where the text should be draw
|
||||
@@ -52,22 +43,20 @@ impl TextView {
|
||||
.align(self.align)
|
||||
}
|
||||
|
||||
/// The text shaped for the width it is drawn in. The buffer keeps its
|
||||
/// answers under the attrs too, so changing those asks a new question
|
||||
/// rather than invalidating anything.
|
||||
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
||||
let width = if self.attrs.wrap {
|
||||
Some(painter.px_len(Axis::X))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
|
||||
self.width = width;
|
||||
self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width));
|
||||
self.attrs.changed = false;
|
||||
self.buf.changed = false;
|
||||
}
|
||||
self.tex.as_ref().unwrap()
|
||||
painter.render_text(&mut self.buf, &self.attrs, width)
|
||||
}
|
||||
|
||||
pub fn tex(&self) -> Option<&RenderedText> {
|
||||
self.tex.as_ref()
|
||||
self.buf.rendered()
|
||||
}
|
||||
/// Draws the text, and says where the glyphs went and what they use.
|
||||
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
|
||||
|
||||
Reference in new issue
Block a user