* Part of https://github.com/emilk/egui/issues/8180
Drag-to-scroll is a must-have on touch-screens, since there is no other
way to scroll.
However, when you are not on a touch screens, it is more surprising than
useful.
### Related
* Closes#8134.
* Related to #5136.
Possibly fixes:
* #8123
* #5145
### What
We did not properly handle the variants of
[`CurrentSurfaceTexture`](https://docs.rs/wgpu/latest/wgpu/enum.CurrentSurfaceTexture.html)
and always returned `SkipFrame`.
Because of this `egui` could end up in a state where frames are always
skipped after observing `Outdated`, without the chance to recover
(unless an event arrives from the outside).
> [!NOTE]
> This is not Wayland-specific, but could happen on all platforms. It
just happens frequently for Wayland compositors that directly resize a
window after creation (such as tiling/scrolling compositors like
`hyprland` and `niri`).
This PR improves this by separating the code paths for `Outdated` and
`Lost`, to help recover from those events.
## Related
* https://github.com/emilk/egui/issues/8043
* Introduced in #8103
## What
I noticed resizing the native winit window was really choppy and bad on
macOS, and this was the readon
* part of #7264
* based on https://github.com/emilk/egui/pull/8152
The resize fix allows use to really simplify how the Window Titlebar is
rendered. Previously it was using some complex flow to calculate and
allocate the height first and then render it later once we knew the
windows final width.
Since now windows can't shrink past their minimum content widths, I can
just show the titlebar inline with the regular content, just outside of
the `Resize` container so that it is always visible.
This does change what the size of a window means. Before, size was just
the size of the contents, while now size (e.g. via min_height) will
include the Frames margin and outline, title bar and the contents.
Also, the window label now truncates as you shrink the window (meaning
windows can now be smaller than their label allows).
---------
Co-authored-by: lucasmerlin <8009393+lucasmerlin@users.noreply.github.com>
This allows the base egui crate to run on platforms without 64 bit
atomics
* Addresses <https://github.com/emilk/egui/issues/7692>, but does not
address the `egui_extras` crate
* [x] I have followed the instructions in the PR template
* Closes part of <https://github.com/emilk/egui/issues/3284>
Add a class system toward a CSS-like styling.
Widget and Ui can implement the trait `HasClasses` which can be used
later by theme engines to compute a style based on the set of classes
the component has.
---------
Co-authored-by: adrien <221212@umons.ac.be>
Co-authored-by: Adrien Zianne <adrien@iq002.ipa.iqrypto.com>
Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
Fixes some bugs that happen randomly when resizing horizontal_wrapped
texts:
https://github.com/user-attachments/assets/141392d2-0239-465a-ba7b-c864f7823319
Adds regression tests (I enjoy using claude to fix these bugs, first
have it create a minimal repro test case, then fix the bug by iterating
until it figures out a fix).
* [x] I have followed the instructions in the PR template
```
+ echo 'All checks passed.'
All checks passed.
```
I had to update `.png`'s for tests to pass, but I guess it's specific to
my laptop.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This lets you start up the test app from within the test itself, which
can be very useful when you have a specific test scenario set up that
you need to debug.
### Related
* Previous attempt: https://github.com/emilk/egui/pull/5418
### macOS
On macOS, you may only run UIs on the main loop, so you need a few
additional steps. Not ideal, but works!
```diff
diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml
index f9a153268..4e0cc14ee 100644
--- a/crates/egui_demo_app/Cargo.toml
+++ b/crates/egui_demo_app/Cargo.toml
@@ -84,3 +84,7 @@ web-sys.workspace = true
[dev-dependencies]
egui_kittest = { workspace = true, features = ["eframe", "snapshot", "wgpu"] }
+
+[[test]]
+name = "test_demo_app"
+harness = false
diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs
index e083c8455..7ad9ed516 100644
--- a/crates/egui_demo_app/tests/test_demo_app.rs
+++ b/crates/egui_demo_app/tests/test_demo_app.rs
@@ -4,7 +4,10 @@ use egui_demo_app::{Anchor, WrapApp};
use egui_kittest::SnapshotResults;
use egui_kittest::kittest::Queryable as _;
-#[test]
+fn main() {
+ test_demo_app();
+}
+
fn test_demo_app() {
let mut harness = egui_kittest::Harness::builder()
.with_size(Vec2::new(900.0, 600.0))
@@ -73,5 +76,8 @@ fn test_demo_app() {
harness.run_steps(4);
results.add(harness.try_snapshot(anchor.to_string()));
+
+ harness.spawn_eframe_app();
+ break;
}
}
```
This is a breaking public API change, but is otherwise trivial due to it
not changing any actual runtime behaviour.
This renames eframe's NativeOptions `vsync` option to `glow_vsync` to
make clear without even looking at docs fully that this is specific to
the `glow` backend.
While I think a better option would actually be to change the wgpu
creation options to match the vsync option if not specified (either to
`AutoVsync` or `AutoNoVsync` depending on setting) this would require
this be made an `Option<PresentMode>`, which would be confusing - and
the `WgpuConfiguration` should probably take priority over other options
here, as there's more than 2 present modes that are relevant. So I think
this is a suitable way to go.
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
-->
* This does not close an issue - this was a trivial amount of code to
change, so I might as well just make it a PR on the spot.
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Let apps change present_mode and desired_maximum_frame_latency at
runtime instead of only at startup.
API changes (egui-wgpu):
- New SurfaceConfig { present_mode, desired_maximum_frame_latency }.
- WgpuConfiguration now nests these as pub surface: SurfaceConfig (was
two top-level fields).
- RenderState gains pub surface_config: SurfaceConfig — the
currently-requested value.
API additions (eframe):
- Frame::wgpu_surface_config() / Frame::set_wgpu_surface_config(...) for
get/set.
- SurfaceConfig re-exported as eframe::SurfaceConfig.
How it works:
The wgpu painter compares render_state.surface_config to its
currently-applied values each paint. If they differ it updates its
config and flips
needs_reconfigure on every surface, piggybacking on the existing
deferred-reconfigure pathway.
Demo:
The backend panel (egui_demo_app) gets dropdowns for present mode and
desired max frame latency, wired through the new Frame accessors.
<img width="282" height="172" alt="image"
src="https://github.com/user-attachments/assets/0b1274b2-7e4e-4413-969b-0a014c415f79"
/>
When enabled, trailing whitespace is included in the row width used for
horizontal alignment. This is useful for text editors where hiding
trailing spaces feels wrong when typing.
By default this is `false`, preserving existing behavior where trailing
whitespace is stripped for alignment (so "Hello " centers the same as
"Hello").
* follow up to https://github.com/emilk/egui/pull/8082
May close#8087, but cannot test macOS builtin Japanese IME.
## Summary
PR #8031 (harfrust text shaping) introduced a regression: when harfrust
shapes multi-codepoint clusters (flag emojis, ligatures, combining
marks) into fewer glyphs than input characters, the invariant
`glyphs.len() == char_count` breaks. This causes IME composition to
duplicate characters and text selection to behave incorrectly.
## Fix
In `layout_shaped_run()`, after emitting shaped glyphs for a cluster, we
now check if the cluster had more characters than glyphs. If so,
zero-width "continuation" glyphs are emitted for the extra characters,
restoring the 1:1 glyph-to-character mapping.
Continuation glyphs have `UvRect::default()` (`is_nothing() == true`),
so `tessellate_glyphs` skips them entirely. Background, underline, and
strikethrough rendering handle zero-width glyphs naturally.
Only `crates/epaint/src/text/text_layout.rs` is modified. No changes to
cursor logic, selection code, or public API.
## Test plan
- [x] `cargo fmt --all -- --check`
- [x] `cargo clippy -p epaint --tests`
- [x] `cargo test -p epaint -p egui` (all pass)
- [x] New test `test_grapheme_cluster_glyph_count`: verifies glyph count
== char count for flag emojis, combining marks, and plain ASCII
- [x] New test `test_grapheme_cluster_cursor_roundtrip`: verifies cursor
position stability through `pos_from_cursor` -> `cursor_from_pos`
round-trips on text containing flag emojis
- [x] Manual testing with demo app: selection and cursor navigation work
correctly on `A🇯🇵B`
- [ ] IME testing (macOS Japanese IME) needs to be validated by someone
on macOS
---
**This PR was developed with the assistance of Claude Code.**
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
## Summary
`next_word_boundary_char_index` calls `char_index_from_byte_index` on
every word-boundary segment. Since `char_index_from_byte_index` scans
from the start of the string each time, this makes the function O(n·m)
where m is the number of segments — effectively O(n²) for large texts.
The fix replaces those repeated scans with a running char counter
maintained as we iterate segments. Same results, O(n) instead of O(n²).
## The problem
Any pointer interaction (click, double-click, drag) inside a `TextEdit`
with a large text buffer triggers `pointer_interaction` →
`select_word_at` → `ccursor_previous_word` →
`next_word_boundary_char_index`. On a 500 KB buffer this takes ~30
seconds, freezing the application.
## Benchmark results
Measured with cursor near end of text (worst case):
| Text size | Before | After | Speedup |
|-----------|--------|-------|---------|
| 1 KB | 147 µs | 38 µs | 3.9x |
| 100 KB | 1.2 s | 3.8 ms | 313x |
| 500 KB | 30 s | 18 ms | 1,636x |
Benchmark source:
[`bench/word-boundary-perf`](https://github.com/hallyhaa/egui/tree/bench/word-boundary-perf)
(separate branch, not part of this PR)
## Changes
- `next_word_boundary_char_index`: replace `char_index_from_byte_index`
calls with a `running_ci` counter (the fix — 6 changed lines)
- New tests for `ccursor_previous_word`, `ccursor_next_word`,
`select_word_at`, and a large-text performance test
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
**Perf: Optimize text selection and navigation performance for large
documents**
#### **Summary**
This PR significantly improves the performance of text selection
(double-clicking) and cursor navigation within `TextEdit` and `Label`
widgets, particularly when handling large documents (e.g., 1MB+ or
logs). It eliminates several $O(N^2)$ bottlenecks and unnecessary memory
allocations in `text_cursor_state.rs`.
#### **Problems Identified**
1. **$O(N^2)$ Word Boundary Scanning:** In
`next_word_boundary_char_index`, `char_index_from_byte_index` was called
repeatedly inside a loop. This caused the entire document to be scanned
from the beginning for every word found, leading to quadratic time
complexity.
2. **Heavy String Allocations:** `ccursor_previous_word` used
`collect::<String>()` and `rev()` to search backwards, causing a full
copy and memory allocation of the text (or line) every time the user
moved the cursor or double-clicked.
3. **Inefficient Line Start Finding:** `find_line_start` performed
global character counts (`text.chars().count()`) and global skips, which
is very slow for large files.
4. **Global Search Scope:** `select_word_at` was performing word
boundary searches across the entire document even for simple
double-click actions.
#### **Key Changes & Optimizations**
1. **Line-Scoped Selection:** Updated `select_word_at` to first identify
the current line and then perform word boundary searches within that
local scope. This reduces the search space from millions of characters
to hundreds.
2. **Linear Time ($O(N)$) Boundary Search:** Refactored
`next_word_boundary_char_index` to use a running cumulative character
counter. This ensures the text is scanned only once.
3. **Zero-Allocation Backwards Search:** Optimized
`ccursor_previous_word` to use `next_back()` on the
`DoubleEndedIterator` provided by `unicode-segmentation`. This removes
all temporary `String` allocations.
4. **Byte-Based Line Search:** Optimized `find_line_start` to use
byte-based reverse scanning (`rfind('\n')`), which is significantly
faster than counting characters from the start of the document.
#### **Performance Impact**
In my tests with large text files (over 10,000 lines / 1MB+):
- **Before:** Double-clicking a word caused a UI freeze for 2–5 seconds.
- **After:** Word selection and navigation are near-instantaneous
(0–1ms), providing a smooth "native-like" experience even in WASM
environments.
A couple improvements to centered and right-aligned text edits:
- Fix text selection in centered and right aligned text edits
(ironically, this broke in #8076)
- Fix cursor movement in centered and right aligned text edits
(horizontal cursor position will be retained on vertical movement)
- Multiline text edit exceeding available width if there are atoms
- Added atoms & alignment options to text edit demo
- Improve how vertical_align and horizontal_align are applied
- Textedit atom is grow now, removing the need for the extra seperate
grow atom
- This allows us to apply the `align` on the text edit atom instead of
the whole AtomLayout
- Fixes https://github.com/emilk/egui/pull/8022
- Fixes https://github.com/emilk/egui/issues/7999
* Closes N/A
* Partially replaces #7983
* Related: #8045
* [x] I have followed the instructions in the PR template
## Details
In #7983, I modified `Memory::request_focus` to interrupt any ongoing
IME composition. This fixed a bug where clicking inside an already
focused `TextEdit` failed to cancel the active composition, resulting in
duplicated text:
https://github.com/emilk/egui/pull/8045#issuecomment-4193310616
To avoid introducing API changes in that PR, I ensured the IME state was
reset by forcing `PlatformOutput::ime` to `None` for at least one frame.
While this works well on desktop platforms, it causes virtual keyboard
flickering on the web:
https://github.com/emilk/egui/pull/8045#issuecomment-4193035008
In this PR, I delegate the responsibility for handling IME composition
interruptions to integrations, allowing each integration to decide how
to interrupt compositions in a flexible manner.
### The new field `should_interrupt_composition` on `IMEOutput`.
Instead of introducing a new `OutputCommand` variant, this PR adds a new
field `should_interrupt_composition` to `IMEOutput`.
Interrupting an active composition is only meaningful when IME remains
allowed. If IME should be disabled altogether, `PlatformOutput::ime` can
simply be set to `None`.
Given this, IMO, it is more appropriate to attach the interrupt signal
to `IMEOutput` (i.e., the type of `PlatformOutput::ime`).