* Closes <https://github.com/emilk/egui/issues/8339>
* [x] I have followed the instructions in the PR template
## The bug
While a `DragValue` has focus it is rendered as a `TextEdit`, and the
text being
edited is stored in `Memory::data` between frames. That is needed so
that
half-finished input such as `"1."` or `"-"` isn't thrown away just
because it
doesn't parse to the current value.
The stored text was only discarded when the widget *gained* focus or
when the
widget itself changed the value. If something else changed the value
while the
`DragValue` was focused, the stored text was kept, shown to the user,
and
written back to the value when focus was lost — silently undoing the
external
change:
```rust
ui.add(egui::DragValue::new(&mut self.value));
if ui.button("increment").clicked() {
self.value += 1;
}
```
Click into the `DragValue` so it has focus, then press "increment": the
value
goes up for one frame and then snaps back. `Slider` shows the same
behaviour,
since it uses a `DragValue` for its value field.
## The fix
Store the value the text belongs to next to the text, and discard the
text when
the value no longer matches it. The remembered value is read back from
the
get/set closure *after* the widget has applied its own edits, so a
change the
widget made itself never looks like an external one — this matters for
values
that can't represent what was typed, e.g. `"12.5"` in a
`DragValue<i32>`.
This keeps the reason the text is stored in the first place intact: as
long as
nothing else touches the value, the text the user is typing is preserved
verbatim.
## Tests
Three tests in `crates/egui_kittest/tests/regression_tests.rs`:
* `drag_value_should_not_revert_external_changes_while_focused` — the
actual
regression. Fails on `main`:
```
---- drag_value_should_not_revert_external_changes_while_focused stdout
----
assertion `left == right` failed
left: Some("0")
right: Some("42")
```
and, with the display assertion removed so the test reaches the blur, on
the
value itself:
```
assertion `left == right` failed
left: 0
right: 42
```
* `drag_value_should_keep_text_while_typing` and
`drag_value_should_keep_text_the_value_cannot_represent` — guards for
the
behaviour the stored text exists for. Both pass on `main` and after the
fix,
and both fail if the text is re-read from the value too eagerly.
`cargo test -p egui_kittest` and `cargo test -p egui` pass, as do
`cargo fmt --all --check`, `scripts/lint.py` and
`cargo clippy -p egui -p egui_kittest --all-targets --all-features -- -D
warnings`.
## Not changed
`DragValue` still ignores the stored text when <kbd>Escape</kbd> is
pressed, and
`update_while_editing` still decides when typed text is applied —
neither is
touched here.
* [x] I have followed the instructions in the PR template
## Summary
- Add an opt-in `Popup::sizing_pass(bool)` builder for remeasuring a
popup whose contents change while it remains open.
- Preserve the automatic first-open/reopen sizing pass and all existing
default behavior.
- Add a headless regression covering growth to a capped scroll viewport
with overflowing content remaining scrollable.
## Why this is necessary
A continuously open popup can first shrink around a short result set and
later receive more content, such as an autocomplete after its query
changes or a “show more” action. The cached `Area` height constrains the
`ScrollArea` input size, so `ScrollArea::max_height` can cap the
viewport but cannot make the containing popup grow again.
PR #8315 taught `Popup` to rerun its sizing pass after closing and
reopening. That fixes the same cached-size feedback loop when
`was_open_last_frame` is false, but a continuously open popup keeps that
value true while its contents change. In that case the caller is the
component that knows the cached natural size is stale.
This API exposes the existing one-frame `Area` sizing mechanism through
`Popup`. It is additive, defaults to false, and combines with the
automatic reopen pass, so unrelated popups, menus, tooltips, and areas
keep their current behavior.
## Related
* Fixes a regression from #8152
* Part of #2921
Reported symptom: you can widen the Widget Gallery window, but it won't
shrink again.
I'm not sure this fix is the best one, but it does work.
# Claude says
## Cause
A `Grid` gives its **last** column all the available width, so a
width-filling widget in it (`Separator`, `TextEdit`, `ProgressBar`, …)
makes the `Grid` remember a `col_width` that is really just "however
wide we happened to be".
At the start of a resize drag, `Resize` runs a one-frame sizing pass
(#8152) to measure the minimum content width and clamps the drag against
it. But `GridLayout::next_cell` inflated every cell to
`prev_state.col_width`, so the `Grid` reported its previous width as its
minimum — even though it was only offered `min_size.x`. The clamp is a
lower bound, so widening kept working while shrinking was blocked at the
widened width.
## Fix
During an enclosing sizing pass, don't inflate the stretchy last column
to its remembered width, and don't store the measured (narrow) widths.
Minimal repro (fails before, passes after — added as a regression test):
```rust
Window::new("x").default_width(280.0).show(ctx, |ui| {
egui::Grid::new("grid").num_columns(2).show(ui, |ui| {
ui.label("Separator");
ui.separator(); // fills the last column
ui.end_row();
});
});
```
`Panel` is unaffected — it clamps only against the user's `min_size`,
with no content-min sizing pass.
* [x] I have followed the instructions in the PR template
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
## Summary
- Closes#8309
- [x] I have followed the instructions in the PR template
`TextEdit` hint text was always aligned to `Align2::LEFT_TOP`, ignoring
the alignment set via `TextEdit::horizontal_align` / `vertical_align`.
This caused the hint text, the cursor, and the typed text to disagree on
alignment: e.g. a centered `TextEdit` showed a left-aligned hint with a
centered cursor.
The hint text atoms now use the widget's `align`, so the hint matches
the input text alignment. The default `align` is still `LEFT_TOP`, so
multi line text edits (and the default styling) are unchanged.
### Root cause
In `crates/egui/src/widgets/text_edit/builder.rs`, the hint-text branch
hardcoded:
```rust
atoms.push_right(atom.atom_align(Align2::LEFT_TOP));
```
while the input-text branch used `.atom_align(self.align)`. The hint
path now uses `align` as well.
### Drive-by: silence `clippy::unnecessary_wraps` in
`egui_kittest::app_kind`
`AppKind::run` returns `Option<egui::Response>`. The `Option` wrap is
required when the `eframe` feature is enabled (the `Eframe` branch
returns `None`), but `clippy::unnecessary_wraps` fires when
`egui_kittest` is built standalone without the `eframe` feature (e.g.
`cargo clippy -p egui_kittest`). The workspace CI run doesn't hit it
because feature unification via `egui_demo_app` enables `eframe`, but
it's a real annoyance for anyone linting the crate on its own. Added a
scoped `#[cfg_attr(not(feature = "eframe"),
expect(clippy::unnecessary_wraps))]` with an explanatory comment.
## Test plan
- [x] Added `textedit_hint_text_should_follow_text_alignment` kittest
regression in `crates/egui_kittest/tests/regression_tests.rs`. It fails
before the fix (`hint_center_x=24.25` vs `edit_center_x=100`) and passes
after.
- [x] `cargo test -p egui`
- [x] `cargo test -p egui_kittest --all-features --test
regression_tests`
- [x] `cargo clippy -p egui_kittest --all-features --test
regression_tests -- -D warnings`
- [x] `RUSTFLAGS="-D warnings" cargo clippy -p egui_kittest --lib`
(pre-existing `unnecessary_wraps` now silenced)
- [x] `cargo clippy -p egui -- -D warnings`
- [x] `cargo fmt --check`
---------
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
When an image snapshot fails, you get the number of pixels differing by
more than the `threshold` you happened to configure — which doesn't tell
you what threshold *would* have passed. So picking
`SnapshotOptions::threshold` / `failed_pixel_count_threshold` is trial
and error, one CI round-trip per guess.
This measures the failing pixel count at a sweep of thresholds (new
public `THRESHOLD_SWEEP`) and includes it in `SnapshotError::Diff`:
```
'sweep_demo' Image did not match snapshot. Diff: 293, …/sweep_demo.diff.png.
Failing pixels by threshold: 0.0: 1522, 0.1: 1522, 0.2: 293, 0.4: 293, 0.6: 293, 1.0: 293, …
Run `UPDATE_SNAPSHOTS=1 cargo test --all-features` to update the snapshots.
```
The sweep only runs for snapshots that already failed, so passing tests
are unaffected.
Breaking: `SnapshotError::Diff` gained a `failing_pixels_by_threshold`
field.
* [x] I have followed the instructions in the PR template
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
We had a ton of issues around `TexturesDelta` that weren't properly
applied because we early-out of some function:
* https://github.com/emilk/egui/pull/8313
* https://github.com/emilk/egui/pull/8250
* https://github.com/emilk/egui/pull/8279
This PR changes texture updates, so that we always store them after
taking them out of `FullOutput` and keep the delta around until it's
actually applied (by passing &mut refs and draining instead of
iterating). So even if we add a new early return somewhere, that can't
break texture updates.
It also optimizes `TexturesDelta::append` by dropping any previous
deltas if there's a new `whole` delta or a `free`.
It also adds a debug assert that any `TexturesDelta` is empty when
dropped, as an additional safeguard in case the bug sneaks back in.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
- needs #8335
Previously it was impossible to correctly set modifiers via egui
inspection, since that only passes egui::Event and egui::Event had no
way to generally express modifiers.
Now modifiers are passed to egui as a Event and not as a field of
`RawInput`.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
* Closes <https://github.com/emilk/egui/issues/8115>
* [x] I have followed the instructions in the PR template
## Summary
- Recalculate a menu popup's cached `Area` size when it reopens.
- Preserve cached sizing for continuously open menus and leave tooltips
and general popups unchanged.
- Add a headless regression test covering a wider item added while the
menu is closed.
## Root cause
`Area` keeps its cached size after a menu closes. When that menu
reopened with wider content, the
cached width constrained the new item and caused it to wrap instead of
allowing the popup to grow.
The fix requests the same invisible sizing pass used for a first-open
`Area` whenever a menu was not
open during the previous frame.
## User impact
Menus now expand to fit newly added wider items after reopening.
Existing wrapping, explicit-width,
alignment, screen-constraining, and continuously open menu behavior
remain unchanged.
## Validation
- `cargo test -p egui_kittest --test menu`
- `cargo check -p egui`
- `cargo fmt --all -- --check`
The three methods for showing a `Panel` are now:
* `panel.show`: always show the panel.
* `panel.show_collapsible`: show or hide the panel, with a slide
animation in between.
* `Panel::show_switched`: animate between two different panels: a
thin/collapsed one and a thick/expanded one.
* 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>
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).
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;
}
}
```
* Related to #56 (Improve text — tracking issue)
## Summary
This PR integrates [harfrust](https://crates.io/crates/harfrust) (a
pure-Rust port of HarfBuzz) into epaint's text layout pipeline,
replacing the character-by-character glyph positioning with proper
OpenType text shaping.
### What this enables
- **GPOS kerning**: most modern fonts only ship kerning in GPOS tables
(not the legacy `kern` table). Pairs like "AV", "VA", "AT" are now
properly tightened.
- **GSUB substitutions**: ligatures (fi, fl), contextual alternates, and
other OpenType features.
- **Combining marks**: diacritics (e.g. ɔ̃) are positioned via anchor
tables instead of being rendered as standalone replacement glyphs.
### Before/After
#### Kerning, etc.
<img width="838" height="726" alt="before_main"
src="https://github.com/user-attachments/assets/f0f26d5f-b117-43a6-b39c-ea40d2e73836"
/>
<img width="838" height="726" alt="after_harfrust"
src="https://github.com/user-attachments/assets/d983e5da-486c-4f39-bd4f-5782a90c6b39"
/>
#### Ligatures
<img width="1117" height="698" alt="before_closeup"
src="https://github.com/user-attachments/assets/7a3b08b4-cf6f-45b7-98ba-07c473cd3b02"
/>
<img width="1117" height="698" alt="after_closeup"
src="https://github.com/user-attachments/assets/6cfc5f21-d32f-4f09-be0c-59c8c553d44f"
/>
### Architecture
The shaping integrates into the existing pipeline without changing the
public API:
1. **`Font::segment_into_runs`** — segments text into contiguous runs by
font face (grapheme-cluster aware, never splits combining sequences)
2. **`FontFace::shape_text`** — calls harfrust to shape each run,
returning glyph IDs + positioned advances/offsets
3. **`layout_shaped_run`** — emits `Glyph` structs from the shaping
output, with NOTDEF fallback to other font faces for missing glyphs
4. **Buffer recycling** — `FontsImpl` pools a `harfrust::UnicodeBuffer`
to avoid per-layout allocations
### Disclaimer
I'm far from being a good Rust programmer. Claude Code did most of the
heavy lifting here. I did my best and used my limited knowledge to avoid
making too many mistakes. If this PR isn't up to quality standards,
please don't hesitate to close it.
## Test plan
- [x] `cargo test -p epaint` — all 18 text tests pass, including 6 new
ones
- [x] `cargo clippy -p epaint --all-features` — clean
- [x] `cargo fmt` — clean
- [ ] Snapshot tests need regeneration (expected: shaping changes glyph
positions)
- New tests added:
- `test_gpos_kerning` — verifies GPOS kerning tightens "AV", "VA", "AT"
pairs
- `test_combining_diacritics` — combining tilde doesn't add extra width
- `test_shaping_basic_latin` — sanity check for Latin text
- `test_shaping_empty_string` — empty input doesn't panic
- `test_shaping_multiple_newlines` — newline splitting works correctly
- `test_shaping_mixed_font_fallback` — Latin + emoji in same string
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Enable these new clippy lints and fix all warnings:
* `format_push_string` — use `write!` instead of `s += &format!(…)` to
avoid extra allocations
* `ignored_unit_patterns` — use `()` instead of `_` when matching unit
* `missing_fields_in_debug` — ensure manual `Debug` impls account for
all fields
* `needless_raw_string_hashes` — remove unnecessary `r#` on string
literals
* `ref_option` — prefer `Option<&T>` over `&Option<T>` in function
signatures
Allows to override the default `PREDICTABLE` render options, e.g. if
it's desired to create snapshots with the exact texture options used by
the app.
See #7630 for details / examples.
Summary
This PR fixes submenu keyboard parity: pressing Enter/Space on an
already-open submenu button now collapses that submenu (matching
top-level menu button behavior).
What changed
Updated submenu interaction logic to distinguish pointer primary clicks
from keyboard/accessibility-triggered clicks.
Kept pointer/touch behavior unchanged (submenu button clicks still don’t
auto-close submenu).
Added regression tests for:
keyboard open of nested submenu,
keyboard close (second Enter) of nested submenu,
pointer clicks on submenu button keeping submenu open.
Validation
cargo test -p egui_kittest --test regression_tests
Breaking changes
None. Behavior change is limited to keyboard/accessibility activation of
already-open submenu buttons.
* Closes <https://github.com/emilk/egui/issues/7926>
* [ X ] I have followed the instructions in the PR template
---------
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
* [x] I have followed the instructions in the PR template
This updates wgpu to v29 across the egui crate stack.
There a a few API changes due to the requirement to provide a display
handle up front to properly support GLES on linux. I have done my best
to make the api changes as reasonable as possible, but I don't have all
the greater project context, so lmk if things should be done a bit
differently.
I've also updated glow to 0.17 to make cargo deny happy, there are no
source changes. I'm not sure how you want to land these.
---------
Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
- fix for https://github.com/rerun-io/reality/pull/1075
The galleys row size was calculated by looking at the last glyphs pos_x,
which got changed to be rounded to integers when we added subpixel
binning. This introduced a subtle bug which caused the width of galleys
to be slightly off.
This PR fixes this by looking at the actual cursor position instead,
which is not rounded.
Also added a test to ensure this is correct. Previously, for the second
and last line, the `x` was too close to the `0`.
<img width="48" height="67" alt="image"
src="https://github.com/user-attachments/assets/a69a4cc3-b3f3-4553-ab92-73cb2e7a358c"
/>
---------
Co-authored-by: lucasmerlin <8009393+lucasmerlin@users.noreply.github.com>
## Summary
- Ignore raw device mouse motion unless the window is focused and the
pointer is inside it
- Also handles pointers starting down and then moving into or out of the
window (drag & drop)
- Prevents global mouse motion from triggering continuous repaint loops
- Applies to both glow and wgpu backends
## Testing
- I ran the check script, nothing seemed to fail
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
<!--
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!
-->
* [x] I have followed the instructions in the PR template
A number of separate commits to possibly easily revert some of them.
General idea: selectively update dependencies to remove transitive
dependencies and multiple versions etc. As well as updating "major" (the
one that `cargo update` doesn't update) version for some in Cargo.toml.
Rendering pipelines in `vello_cpu` wasn't obvious. Now both are used.
The image file written by debug_open_snapshot was being kept open by the
`tempfile` object. On Windows, this prevented `open::that` from
successfully launching the viewer sometimes because the file remained
locked, which can be avoided by first releasing the file handle.
* Part of https://github.com/emilk/egui/issues/5605
This changes the default style of egui.
The problem with "expanding" widgets is that they now want to paint
outside their own bounds, which then requires all parent UIs to have
proper margins.
It also means hovered things are no longer properly aligned with every
other widget.