egui/crates/epaint/src
Hubert Głuchowski 557bd56e19
Optimize editing long text by caching each paragraph (#5411)
## What
(written by @emilk)
When editing long text (thousands of line), egui would previously
re-layout the entire text on each edit. This could be slow.

With this PR, we instead split the text into paragraphs (split on `\n`)
and then cache each such paragraph. When editing text then, only the
changed paragraph needs to be laid out again.

Still, there is overhead from splitting the text, hashing each
paragraph, and then joining the results, so the runtime complexity is
still O(N).

In our benchmark, editing a 2000 line string goes from ~8ms to ~300 ms,
a speedup of ~25x.

In the future, we could also consider laying out each paragraph in
parallel, to speed up the initial layout of the text.

## Details
This is an ~~almost complete~~ implementation of the approach described
by emilk [in this
comment](<https://github.com/emilk/egui/issues/3086#issuecomment-1724205777>),
excluding CoW semantics for `LayoutJob` (but including them for `Row`).
It supersedes the previous unsuccessful attempt here:
https://github.com/emilk/egui/pull/4000.

Draft because:
- [X] ~~Currently individual rows will have `ends_with_newline` always
set to false.
This breaks selection with Ctrl+A (and probably many other things)~~
- [X] ~~The whole block for doing the splitting and merging should
probably become a function (I'll do that later).~~
- [X] ~~I haven't run the check script, the tests, and haven't made sure
all of the examples build (although I assume they probably don't rely on
Galley internals).~~
- [x] ~~Layout is sometimes incorrect (missing empty lines, wrapping
sometimes makes text overlap).~~
- A lot of text-related code had to be changed so this needs to be
properly tested to ensure no layout issues were introduced, especially
relating to the now row-relative coordinate system of `Row`s. Also this
requires that we're fine making these very breaking changes.

It does significantly improve the performance of rendering large blocks
of text (if they have many newlines), this is the test program I used to
test it (adapted from <https://github.com/emilk/egui/issues/3086>):
<details>
<summary>code</summary>

```rust
use eframe::egui::{self, CentralPanel, TextEdit};
use std::fmt::Write;

fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
        ..Default::default()
    };

    eframe::run_native(
        "editor big file test",
        options,
        Box::new(|_cc| Ok(Box::<MyApp>::new(MyApp::new()))),
    )
}

struct MyApp {
    text: String,
}

impl MyApp {
    fn new() -> Self {
        let mut string = String::new();
        for line_bytes in (0..50000).map(|_| (0u8..50)) {
            for byte in line_bytes {
                write!(string, " {byte:02x}").unwrap();
            }
            write!(string, "\n").unwrap();
        }
        println!("total bytes: {}", string.len());
        MyApp { text: string }
    }
}

impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        CentralPanel::default().show(ctx, |ui| {
            let start = std::time::Instant::now();
            egui::ScrollArea::vertical().show(ui, |ui| {
                let code_editor = TextEdit::multiline(&mut self.text)
                    .code_editor()
                    .desired_width(f32::INFINITY)
                    .desired_rows(40);
                let response = code_editor.show(ui).response;
                if response.changed() {
                    println!("total bytes now: {}", self.text.len());
                }
            });
            let end = std::time::Instant::now();
            let time_to_update = end - start;
            if time_to_update.as_secs_f32() > 0.5 {
                println!("Long update took {:.3}s", time_to_update.as_secs_f32())
            }
        });
    }
}
```
</details>

I think the way to proceed would be to make a new type, something like
`PositionedRow`, that would wrap an `Arc<Row>` but have a separate `pos`
~~and `ends_with_newline`~~ (that would mean `Row` only holds a `size`
instead of a `rect`). This type would of course have getters that would
allow you to easily get a `Rect` from it and probably a `Deref` to the
underlying `Row`.
~~I haven't done this yet because I wanted to get some opinions whether
this would be an acceptable API first.~~ This is now implemented, but of
course I'm still open to discussion about this approach and whether it's
what we want to do.

Breaking changes (currently):
- The `Galley::rows` field has a different type.
- There is now a `PlacedRow` wrapper for `Row`.
- `Row` now uses a coordinate system relative to itself instead of the
`Galley`.

* Closes <https://github.com/emilk/egui/issues/3086>
* [X] I have followed the instructions in the PR template

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2025-04-01 18:55:39 +02:00
..
shapes Optimize editing long text by caching each paragraph (#5411) 2025-04-01 18:55:39 +02:00
text Optimize editing long text by caching each paragraph (#5411) 2025-04-01 18:55:39 +02:00
util Add `emath::OrderedFloat` (moved from `epaint::util::OrderedFloat`) (#4389) 2024-04-21 20:36:32 +02:00
brush.rs Add `epaint::Brush` for controlling `RectShape` texturing (#5565) 2025-01-02 15:34:28 +01:00
color.rs Added ability to define colors at UV coordinates along a path (#4353) 2024-04-22 18:35:09 +02:00
corner_radius.rs ⚠️ Rename `Rounding` to `CornerRadius` (#5673) 2025-02-04 12:53:18 +01:00
corner_radius_f32.rs ⚠️ Rename `Rounding` to `CornerRadius` (#5673) 2025-02-04 12:53:18 +01:00
image.rs Add assert messages and print bad argument values in asserts (#5216) 2025-03-25 09:20:29 +01:00
lib.rs Rename `Marginf` to `MarginF32` for consistency with `CornerRadiusF32` (#5677) 2025-02-11 11:23:59 +01:00
margin.rs Rename `Marginf` to `MarginF32` for consistency with `CornerRadiusF32` (#5677) 2025-02-11 11:23:59 +01:00
margin_f32.rs Rename `Marginf` to `MarginF32` for consistency with `CornerRadiusF32` (#5677) 2025-02-11 11:23:59 +01:00
mesh.rs Add assert messages and print bad argument values in asserts (#5216) 2025-03-25 09:20:29 +01:00
mutex.rs Fix some clippy issues found by 1.84.0 (#5603) 2025-01-13 08:29:13 +01:00
shadow.rs Rename `Marginf` to `MarginF32` for consistency with `CornerRadiusF32` (#5677) 2025-02-11 11:23:59 +01:00
shape_transform.rs Optimize editing long text by caching each paragraph (#5411) 2025-04-01 18:55:39 +02:00
stats.rs Optimize editing long text by caching each paragraph (#5411) 2025-04-01 18:55:39 +02:00
stroke.rs Make text underline and strikethrough pixel perfect crisp (#5857) 2025-03-28 20:37:38 +01:00
tessellator.rs Optimize editing long text by caching each paragraph (#5411) 2025-04-01 18:55:39 +02:00
texture_atlas.rs Add assert messages and print bad argument values in asserts (#5216) 2025-03-25 09:20:29 +01:00
texture_handle.rs Update MSRV to 1.80 (#5457) 2024-12-10 16:09:03 +01:00
textures.rs Add support for mipmap textures. (#5146) 2024-09-22 19:16:16 +02:00
viewport.rs Refactor: put each shape into its own file (#5564) 2025-01-02 14:55:49 +01:00