Log `localStorage` write failures in `local_storage_set`
**Description**
This PR improves `local_storage_set()` logging in
`eframe/src/web/storage.rs`.
It logs:
- write failures with key and browser error
- unavailable local storage
This helps diagnose web persistence issues such as `QuotaExceededError`
when `localStorage.setItem()` fails.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This lets you turn off subpixel horizontal binning of glyphs. The option
is a trade-off between even kerning and sharp text.
* Closes https://github.com/emilk/egui/issues/8034
* 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>
<!--
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!
-->
* Depends on #7967
* Closes#7485
* Should fix#7906 (This issue doesn't seem to have been resolved, but
the author closed it; I personally don't have the environment to verify
whether it is fixed.)
* Replaces #4137, #4896, and partially #7810
* [x] I have followed the instructions in the PR template
This PR started as a fix for #7485, but has since evolved into a broader
rewrite of IME-related logic.
## Overview
This PR primarily introduces a new public method, `owns_ime_events`, on
[`Memory`], and refactors parts of [`TextEdit`] to integrate with it.
Previously, each [`TextEdit`] widget independently determined whether to
handle IME events and stored its own IME-related state. This approach
made ownership-handling fragmented and was therefore error-prone.
With this PR:
- IME event ownership is centralized, ensuring that at most a single
widget owns IME events per frame.
- [`PlatformOutput`]'s `ime` field can be set to `None` for at least one
frame when IME composition is interrupted, allowing the IME to be
properly dismissed.
## Details
Two new public methods are introduced on [`Memory`]:
- `fn owns_ime_events(&self, id: Id) -> bool`: check IME event ownership
for the current frame for the widget with the given `id`.
- `fn interrupt_ime(&mut self)`: interrupt the current IME composition,
if any.
Since the newly added methods on [`Memory`] are public, other widgets
can also participate in IME handling without risking ownership conflicts
of IME events.
I also added an internal (`pub(crate)`) field on [`TextEditState`],
called `cursor_purpose`, to distinguish the role of the [`TextEdit`]
cursor.
Additionally, `egui::ImeEvent::Enabled` and `egui::ImeEvent::Disabled`
have been removed, as they are no longer used anywhere.
## Demonstrations
### Windows: The Korean IME text duplication bug fixed in #4137 does not
reappear.
<table>
<thead>
<tr>
<th></th>
<th>With this PR</th>
<th>Without this PR</th>
</tr>
</thead>
<tbody>
<tr>
<th>Behavior</th>
<td>Correct (no regression)</td>
<td>Correct</td>
</tr>
<tr>
<th>Screencast</th>
<td>

</td>
<td>

</td>
</tr>
</tbody>
</table>
### Windows: Chinese and Japanese IMEs now behave more consistently with
the Korean IME in similar scenarios.
This change does not matter much, as composition is rarely interrupted
mid-process with these IMEs in typical usage.
<table>
<thead>
<tr>
<th></th>
<th>With this PR</th>
<th>Without this PR</th>
</tr>
</thead>
<tbody>
<tr>
<th>Behavior</th>
<td>Composition can be interrupted by clicking (like Korean IMEs)</td>
<td>Composition can not interrupted by clicking</td>
</tr>
<tr>
<th>Screencast (Builtin Chinese IME)</th>
<td>

</td>
<td>

</td>
</tr>
<tr>
<th>Screencast (Builtin Japanese IME)</th>
<td>

</td>
<td>

</td>
</tr>
</tbody>
</table>
### macOS: was buggy, still buggy
Likely due to this upstream bug in `winit`:
https://github.com/rust-windowing/winit/issues/4432
Once `winit` is updated to a version that includes the fix, the behavior
should become correct with this PR.
<table>
<thead>
<tr>
<th></th>
<th>With this PR</th>
<th>Without this PR</th>
</tr>
</thead>
<tbody>
<tr>
<th>Behavior</th>
<td>Buggy as before</td>
<td>Buggy: Characters are duplicated</td>
</tr>
<tr>
<th>Screencast</th>
<td>

</td>
<td>

</td>
</tr>
</tbody>
</table>
### Wayland + iBus: Korean IME duplication bug fixed
<table>
<thead>
<tr>
<th></th>
<th>With this PR</th>
<th>Without this PR</th>
</tr>
</thead>
<tbody>
<tr>
<th>Behavior</th>
<td>Correct</td>
<td>Buggy: Characters are duplicated</td>
</tr>
<tr>
<th>Screencast</th>
<td>

</td>
<td>

</td>
</tr>
</tbody>
</table>
### Wayland + iBus: #7485 is fixed
<table>
<thead>
<tr>
<th></th>
<th>With this PR</th>
<th>Without this PR</th>
</tr>
</thead>
<tbody>
<tr>
<th>Behavior</th>
<td>Correct</td>
<td>Buggy: Only a single ASCII character can be typed after
<code>TextEdit</code> is focused</td>
</tr>
<tr>
<th>Screencast</th>
<td>

</td>
<td>

</td>
</tr>
</tbody>
</table>
### Wayland + iBus: selection is also not broken
This PR does not reintroduce the selection bug fixed in #7973.
<table>
<thead>
<tr>
<th></th>
<th>With this PR</th>
</tr>
</thead>
<tbody>
<tr>
<th>Behavior</th>
<td>Correct</td>
</tr>
<tr>
<th>Screencast</th>
<td>

</td>
</tr>
</tbody>
</table>
### X11 + Fcitx5: IME composition can be interrupted
But due to #7975, the experience is still subpar. (Uncommitted text is
lost after interruption.)
<table>
<thead>
<tr>
<th></th>
<th>With this PR</th>
<th>Without this PR</th>
</tr>
</thead>
<tbody>
<tr>
<th>Screencast</th>
<td>

</td>
<td>

</td>
</tr>
</tbody>
</table>
[`Memory`]: https://docs.rs/egui/latest/egui/struct.Memory.html
[`TextEdit`]:
https://docs.rs/egui/latest/egui/widgets/text_edit/struct.TextEdit.html
[`PlatformOutput`]:
https://docs.rs/egui/latest/egui/struct.PlatformOutput.html
[`TextEditState`]:
https://docs.rs/egui/latest/egui/widgets/text_edit/struct.TextEditState.html
<!--
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!
-->
Update some dependencies to get rid of old ones (especially nice to get
rid of windows-sys 0.45...).
* Closes <https://github.com/emilk/egui/issues/THE_RELEVANT_ISSUE>
* [x] I have followed the instructions in the PR template
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
As shown in the commit, this is exactly what it contains. There was a
redundant and seemingly meaningless `.clone()` in `about.rs`, which I
removed. Was it because the function signature of `Image::new()` was
different in older versions of egui?
Co-authored-by: EtherealPsyche <EtherealPsyche@users.noreply.github.com>
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.
<!--
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 improves cases where the canvas does not cover the full screen,
which was a goal in [release
0.28.0](https://github.com/emilk/egui/releases/tag/0.28.0).
* Closes <https://github.com/emilk/egui/issues/8035>
* [X] I have followed the instructions in the PR template
* Fix for https://github.com/emilk/eframe_template/issues/223
* Related: https://github.com/gfx-rs/wgpu/pull/9319
By default, we would only turn on the WebGPU backend on web, which means
browsers without WebGPU support would just crash.
You can still opt-out of all the default `wgpu` features by enabling
`eframe/wgpu_no_default_features` instead of `eframe/wgpu`
This PR fundamentally solves the same problem as
https://github.com/emilk/egui/pull/5827 just implemented with a lot less
ambition and api surface on my end. It contains the bare minimum amount
of changes that I need in order to be able to solve my problem.
My Problem:
I am still suffering from the problem that the TypeIdMap blows up over a
very long time when using my application. (The user generally never
turns off the application, it is intended to be just kept running
forever, some users also never restart their computers) My application
generates a lot of content dynamically so it may for some time display
widgets with a certain set of TypeId's + Id's later hiding them. Some of
the elements that were hidden may turn visible again once an external
event occurs, some may forever be discarded.
I do know myself when which sections of the UI have to be purged because
they will never become visible again, so this PR contains the minimum
amount of necessary functions that allow me to implement this
housekeeping logic on my end.
The existing facilities are insufficient to handle this as the type T
which the TypeId and the hash is derived from are sometimes pub(crate)
privates of widget subcrates or even pub(crate) of egui internals
itself, so its impossible to manually remove those from the TypeIdMap
the only build-in method to remove them is to call "clear" on the
TypeIdMap, however that gets rid of everything, even the elements that
are still shown and should still be in the TypeIdMap.
If the changes in this PR are agreeable to you and you want me to write
unit test for the 4 functions that I have added then tell me and I will
write the tests for you.
If you need anything else changed please tell me.
I ran cargo clippy and cargo fmt, but your check.sh does not work on my
computer.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Closes#7947
## Problem
`Visuals::interact_cursor` stopped working for buttons after the
`AtomLayout` refactor in commit 6eb7bb6e. Setting `interact_cursor` to
e.g. `CursorIcon::PointingHand` no longer changes the cursor when
hovering over a `Button`.
## Root Cause
When `Button` was rewritten to use `AtomLayout` in #5830, the
cursor-override block at the end of the old `Button::ui` was not carried
over to the new `Button::atom_ui` method.
The old code had:
```rust
if let Some(cursor) = ui.visuals().interact_cursor {
if response.hovered() {
ui.ctx().set_cursor_icon(cursor);
}
}
```
This was the only place `interact_cursor` was checked, so the setting
became entirely non-functional.
## Fix
Re-add the same `interact_cursor` check in `Button::atom_ui`, right
after painting and before `widget_info`, matching the original behavior.
---------
Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
When using `egui::ViewportBuilder::with_fullsize_content_view` one must
be careful not to paint anything where the "traffic light" buttons are:
<img width="87" height="47" alt="image"
src="https://github.com/user-attachments/assets/0e878c8e-7141-4fed-bbc8-4d542ddb5251"
/>
`eframe::WindowChromeMetrics` helps you with that!
## Summary
* Closes#5229
* Closes#7776
On Windows, once a window is hidden with
`ViewportCommand::Visible(false)`, two problems occur:
1. **Window can never be shown again** — Windows stops sending
`RedrawRequested` events to invisible windows, and viewport commands are
only processed during `run_ui_and_paint`, which is triggered by
`RedrawRequested`. This creates a deadlock:
```
Visible(false) → window hidden → no RedrawRequested → run_ui_and_paint never called → Visible(true) stuck in queue → window stays hidden forever
```
2. **High CPU usage** — The event loop spins at full speed with
`ControlFlow::Poll` even for invisible windows, and repaint requests are
scheduled immediately, causing a tight loop that burns CPU.
## Fix
**For #5229:** In `check_redraw_requests`, after calling
`window.request_redraw()`, detect invisible windows via
`window.is_visible() == Some(false)` and call `run_ui_and_paint`
directly for them. This ensures pending viewport commands (including
`Visible(true)`) are still processed even when the OS doesn't send
redraw events.
**For #7776:** Three layers of throttling for invisible windows:
- **Heartbeat scheduling:** After painting an invisible window, schedule
the next repaint 100ms in the future (instead of immediately). This
keeps viewport commands flowing while limiting to ~10 repaints/sec.
- **Event throttling:** In `user_event`, throttle `RequestRepaint`
events for invisible windows to at least 100ms delay, preventing egui's
repaint callback from bypassing the heartbeat.
- **ControlFlow fix:** Only set `ControlFlow::Poll` for visible windows.
Invisible windows use `WaitUntil` instead of spinning.
- **Backend sleep:** Add `is_visible() == Some(false)` alongside the
existing `is_minimized()` sleep check in both wgpu and glow backends
(defense in depth).
The fix is platform-agnostic: `is_visible()` returns `Some(false)` only
when the platform can confirm invisibility, so it won't trigger on
platforms where invisible windows still receive `RedrawRequested`.
## Test plan
- [x] `cargo fmt` passes
- [x] `cargo clippy -p eframe --all-features` passes with no warnings
- [x] Manual test on Windows: window reappears after `Visible(true)`
when hidden
- [x] Manual test on Windows: CPU stays near 0% while window is
invisible (was ~16% before fix)
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
feat: Add left_text() to egui::Button
This PR introduces the `left_text()` method to `egui::Button`. It
enables placing additional text content on the left side of the button's
primary label, which is useful for displaying auxiliary information,
labels, and for facilitating left-aligned text within the button.
```rust
let is_selected = true;
let selectable_label_widget = egui::Button::selectable(
is_selected,
"",
).left_text("Left");
let desired_width = ui.available_width();
let desired_height = ui.spacing().interact_size.y;
interaction_response = ui.add_sized(
egui::vec2(desired_width, desired_height),
selectable_label_widget,
);
```
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>