Same as the native backends: tick `App::logic` via `Context::run_logic`
and leave all ui state alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add `Context::run_logic`, for ticking app logic without running a pass,
and use it in the native eframe backends when a viewport is minimized or
occluded (and has no visible descendant viewport).
Since no pass runs, all ui state is left untouched: nothing to
special-case inside egui, and the app finds everything where it left it
once the window is visible again.
`App::logic` is now always called outside of the egui pass. Any viewport
commands it sends (e.g. `ViewportCommand::Focus`) come out of
`LogicOutput` when there is no pass to carry them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Closes#4654
* Related #4667
* [x] I have followed the instructions in the PR template
This PR avoids materializing the contents of a file that was dragged
into an egui application on the web. It does so by storing the
`web_sys::File` handle directly on WASM.
This breaks the existing API of `DroppedFile` on the web, because there
is no way to retrieve the bytes synchronously form a `DroppedFile`
anymore, forcing handling call sites to become asynchronous.
The native API remains the same.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Replaces `home::home_dir` with `std::env::home_dir` as these functions
do the exact same thing
<!--
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!
-->
Removes dependency home from eframe by replacing `home::home_dir` with
`std::env::home_dir`. `home` was probably originally used since
`std::env::home_dir` was once deprecated because of a bug. Post Rust
version 1.87 this has been fixed and now these two functions do exactly
the same thing.
* [X] I have followed the instructions in the PR template
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!
-->
* Closes#8371
* [x] I have followed the instructions in the PR template
Adds a `link` feature to eframe to allow disabling links on egui-winit.
The feature is enabled by default so nothing changes for existing users
of eframe.
<!--
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!
-->
* Fix [the Samsung Keyboard Korean Cheonjiin layout
bug](https://github.com/emilk/egui/pull/7967#issuecomment-4098503570)
* Partially fix (does not close) #8046
* Supersedes #7914
* Supersedes #8047
* Related: #8068
* Related: #7983
* Related: #8078
* [x] I have followed the instructions in the PR template
This PR reworks the text input handling logic in eframe's web
integration, primarily in `text_agent.rs`.
It also adds a new `ImeEvent::DeleteSurrounding` variant, along with the
corresponding handling logic in `egui` to support the changes.
## Fix: Samsung Keyboard Cheonjiin issue
This PR fixes a bug reported by @rustbasic when using Samsung Keyboard's
Cheonjiin Korean layout.
Since Samsung Keyboard is only available on Samsung devices, I wasn't
able to verify it myself. The fix is based on @rustbasic's testing and
confirmation.
The root cause is that the layout relies on the preceding text to
correctly handle batchim composition. Previously, the text agent eagerly
cleared the text input after every IME composition, removing the context
too early. This PR makes that cleanup more conservative, preserving the
text when it may still be needed.
My understanding is that this is a quirk specific to Samsung Keyboard:
The IME reports that composition has finished even though it is
effectively still active and the composed text may continue to change.
## Fix: Keystrokes resetting keyboard layout (numpad/symbols/etc.)
Partially fixes#8046.
Previously, keystrokes would cause the on-screen keyboard to switch back
to its primary layout.
One remaining issue is that tapping within the active `TextEdit` to
reposition the cursor still resets the keyboard to its primary layout.
## About text suggestions
I originally planned to include text suggestion support in this PR
because #8068 implemented it.
However, adding text suggestion support would broaden the scope of this
PR, so I think it is better addressed in a separate PR.
For reference, [the reverted
implementation](8a4f70859c)
works fine on Android (Gboard), but not on iOS (iPadOS 17 + SwiftKey).
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>
* Follow-up to #8296, part of
<https://github.com/emilk/egui/issues/8295>
* [x] I have followed the instructions in the PR template
Stacked on #8296 (base branch), kept as a separate PR so it can be
reverted independently.
The hidden text-agent `<input>` is now inserted as a *sibling of the
canvas* (instead of appended to `document.body`) and positioned with
`offsetLeft`/`offsetTop` instead of `getBoundingClientRect`. Since the
input and the canvas share the same containing block, the input stays
anchored to the canvas top-left corner no matter how the page is
scrolled or how the canvas is embedded.
Consequences:
* Fixes the IME popup position when the host page is scrolled —
`move_to` previously wrote *viewport* coordinates from
`getBoundingClientRect` into document-absolute `left`/`top`.
* Subsumes the mobile Safari virtual-keyboard workaround (it replaced
the flapping `getBoundingClientRect` y with `offsetTop`, which is now
used everywhere), so `is_mobile_safari()` is removed.
* Removes the special-casing of document vs shadow DOM roots — sibling
insertion works uniformly in both.
* The input is `position: absolute`, so it does not participate in
flex/grid layout of the canvas' parent and causes no layout shift.
Caveat: host CSS selectors like `div > canvas:only-child` would no
longer match.
Verified with `cargo clippy -p eframe --target wasm32-unknown-unknown
--all-features` and `cargo fmt --all`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The `textures_delta` in `AppRunner::logic()` comes from
`TextureManager::take_delta()` via `end_pass()`. Previously, if
`is_visible` was `false`, these deltas would have been dropped and never
applied, so the `TextureManager` state gets out of sync with the
`Renderer`.
That causes https://github.com/emilk/egui/issues/8228: When the app is
loaded in a background tab and the first frame occurs via the
`setTimeout` path prior to the tab becoming visible, it loses the
`ImageDelta` representing the initial creation of the font atlas
texture. When the user then switches to the tab, it panics at
crates/egui-wgpu/src/renderer.rs:669:18 with `Tried to update a texture
that has not been allocated yet` when attempting to update the texture
that the wgpu renderer never saw when it was created.
Testing: To make it load in a background tab, put `data:text/html,<a
href=http://127.0.0.1:8765/>Click</a>` in the address bar and then
middle click the resulting link. On Chrome you also have to hover the
tab preview before switching to the tab to reproduce the issue.
<!--
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!
-->
* Closes <https://github.com/emilk/egui/issues/8228> and its duplicate
https://github.com/emilk/egui/issues/8278
* [x] I have followed the instructions in the PR template
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.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>
## Summary
- [x] I have followed the instructions in the PR template
Fixes comment-style violations of the `CONTRIBUTING.md` code style rule:
> `// Comment like this.` and not `//like this`
### Files changed
- `crates/epaint/src/shapes/bezier_shape.rs`: `//temporary solution` →
`// temporary solution`, and 22× `//add the start point` → `// add the
start point`
- `crates/egui/src/animation_manager.rs`: `//start new animation…` → `//
start new animation…`
- `crates/eframe/src/web/web_painter_wgpu.rs`:
`//create_new.instance_descriptor…` → `//
create_new.instance_descriptor…`
No code logic changes — only adding the missing space after `//` in
inline and standalone comments.
## Test plan
- [x] `cargo fmt --check`
- [x] `cargo clippy -p epaint -p egui`
- [x] `cargo test -p epaint --lib`
* Closes <https://github.com/emilk/egui/issues/8295>
* [x] I have followed the instructions in the PR template
When an eframe app is embedded in a scrollable host page, the host page
jumped to the top whenever the app booted or grabbed keyboard focus.
Cause: the hidden text-agent `<input>` sat at (0,0) of `<body>` with the
`autofocus` attribute, and all focus calls (text agent and canvas) used
plain `focus()`, so the browser scrolled the focused element into view —
i.e. to the top of the page.
Changes:
* All focus calls in `eframe` web (text agent, canvas, Gboard
blur/refocus workaround) now go through a new `focus_without_scroll()`
helper that uses `focus_with_options` with `preventScroll: true`
(supported by all evergreen browsers).
* The `autofocus` attribute is replaced with an explicit focus call
after DOM insertion — the browser-internal autofocus path always scrolls
the element into view and cannot be prevented by any focus option.
Boot-focus behavior is preserved.
* The text-agent input is initially parked at the canvas origin instead
of (0,0) of the page, so any residual scroll-into-view would target the
canvas rather than the top of the host page. (`move_to` keeps
repositioning it for IME as before.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
This fixes macOS live-resize behavior for the `eframe`/`egui-wgpu` path
when using the low-latency wgpu surface configuration.
The problem I was seeing is that native window resize can look visibly
below the baseline expected from a desktop GUI: stale or stretched
frames (manifesting as wobble/jitter), or severe lag while dragging a
window edge.
The fix has three parts:
- use `CAMetalLayer.presentsWithTransaction` during live resize to avoid
stale/stretched frames
- temporarily use at least `desired_maximum_frame_latency = 2` while
live resize is active, so transaction presentation does not stall when
the app normally uses `SurfaceConfig::LOW_LATENCY`
- treat macOS `WindowEvent::Moved` as part of the live-resize event
stream, since resizing from the top or left edge changes the window
origin
This PR depends on the winit-side AppKit live-resize timing fix in
[rust-windowing/winit#4588](https://github.com/rust-windowing/winit/pull/4588)
A renderer-only frame-latency change is not enough by itself. The
temporary latency bump only solves the drawable starvation caused by
combining `presentsWithTransaction` with `SurfaceConfig::LOW_LATENCY`.
It does not change when winit emits resize/redraw events, whether
redraws are delivered during AppKit's live-resize event-tracking loop,
or whether the surface size is derived from the current backing rect.
That is why the winit fix is needed first: it makes the windowing layer
report the current AppKit backing size and request redraws from the
live-resize/display callbacks. egui-wgpu still needs this PR on top
because winit does not own the wgpu `Surface` or the underlying
`CAMetalLayer` presentation policy.
In other words: winit fixes when the windowing layer reports
resize/redraw work, while this PR fixes how egui-wgpu presents
Metal-backed wgpu frames during that resize.
## Why change the existing feature?
The existing `macos-window-resize-jitter-fix` feature addresses one
symptom by enabling transaction presentation during resize, but it is
not enough for the low-latency wgpu path.
In particular, `presentsWithTransaction` and
`SurfaceConfig::LOW_LATENCY` interact poorly during AppKit live resize.
The old code avoids that by [skipping transaction presentation when
latency is
`1`](71c4ff3c33/crates/egui-wgpu/src/winit.rs (L417)),
but that means low-latency users get the resize jitter/wobble back.
This PR keeps the low-latency path normally, but temporarily bumps frame
latency only while live resize is active. That gives the resize path
enough drawable slack without changing normal interaction latency.
I removed the `macos-window-resize-jitter-fix` feature because this
seems like the behavior the macOS wgpu path should have by default, not
a separate opt-in. If keeping the feature as a no-op compatibility alias
is preferred, I can adjust the PR.
## Validation
I created a small demo app that somewhat resembles the layout of my
actual app and highlights both horizontal and vertical resize jitter:
- a borderless macOS window
- a simple toolbar
- a scrolling side list
- `SurfaceConfig::LOW_LATENCY`
The toolbar and list make stale or stretched frames easy to see during
native resize. The jitter is visible even on the traffic light buttons.
Recordings:
### Before 1: no transaction presentation, low latency
Shows jitter/wobble and stale/stretched frames during live resize.
https://github.com/user-attachments/assets/2cf4467b-e14c-4f41-8021-0b8c23f41004
### Before 2: transaction presentation with low latency
Shows the other failure mode: live resize can become severely laggy when
transaction presentation is used while keeping
`SurfaceConfig::LOW_LATENCY`.
https://github.com/user-attachments/assets/2f866790-f472-4ede-a3c0-480e8f0f041a
### After: patched egui-wgpu + patched winit, low latency
No visible wobble/jitter and no severe live-resize lag.
https://github.com/user-attachments/assets/59e46e9f-7906-4b5c-a6c7-1d09eae644cd
---------
Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
<!--
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
This PR adds a `remove_string()` API to the Storage trait and also
implements it in the `FileStorage` and `LocalStorage` stucts.
## Symptom
Fix this long-standing, occasional bug, that can cause text to look
compressed and "drunk":
<img width="552" height="226" alt="Screenshot 2026-06-22 at 13 12 56"
src="https://github.com/user-attachments/assets/9b1abad4-5ef6-4771-8168-f201afc341ab"
/>
## Root cause
`epaint::TextureAtlas::take_delta` is fire-and-forget: it resets the
dirty region as soon as it hands out a delta, assuming the delta will be
uploaded. Atlas growth always emits a **full** `ImageDelta` (`pos:
None`) which recreates the GPU texture at the new size — *as long as it
is applied*. But both native integrations applied `textures_delta`
inside skippable code paths:
- **wgpu** (`egui-wgpu/src/winit.rs`): textures were uploaded only
*after* surface-dependent early-returns (`render_state` /
`surfaces.get_mut(viewport_id)` missing). Texture uploads are
device-level and don't need a surface.
- **glow** (`eframe/src/native/glow_integration.rs`): textures were
uploaded only inside `if is_visible { … }` (and after a viewport-missing
early-return), while `integration.update` still ran and grew the atlas.
The root window even starts hidden on purpose (`with_visible(false)`, to
avoid a startup white flash), so the very first frames hit this.
When the delta was dropped, the GPU font texture stayed smaller than the
CPU-side atlas; every glyph UV (normalized by the CPU atlas size) then
sampled the wrong rows until the next full atlas recreation. wgpu/Metal
can't detect this — the read is in-bounds, just the wrong row.
## Fixes
- **wgpu**: apply `textures_delta.set` right after `render_state` is
obtained, **before** any surface-dependent early-return. `free` still
runs after submit (unchanged).
- **glow**: apply `textures_delta.set` (and `free`) regardless of
`is_visible`, making the GL context current when there's anything to
upload; only tessellation/paint/swap stay gated on visibility.
- **debug assert** in `egui-wgpu`'s `Renderer::update_texture`: a full
delta must (re)create the GPU texture at exactly the delta size —
catches any future CPU/GPU size desync at the source.
## wgpu ruled out
Confirmed the desync is **not** inside wgpu: Metal `create_texture` uses
the exact descriptor size, and `queue.write_texture` validates against
the texture's own live `desc` — a single texture can't have CPU/GPU
sizes disagree. The mismatch is born at the egui boundary (atlas size
for UVs vs. last-applied upload), which wgpu cannot see.
## Testing note
A headless regression test of `paint_and_update_textures` isn't
practical (it needs a real winit window; `render_state` is private with
no surface-less setter). I verified the failure *mechanism* separately
on macOS/Metal (texture lagging the atlas → silent wrong-row sampling,
no wgpu error), but that demo did not exercise the fixed code path, so
it's not included. The fixes rest on the reasoning above.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hey,
this is my fist PR to fix the bug I just published #8225
I hope it helps.
I tested the fix with wgpu but not glow.
I must say that I am not very happy to have very similar function
is_viewport_or_descendant_visible in both case.
Let me know if you would prefer that I try to factorize it.
* Closes <https://github.com/emilk/egui/issues/8225>
* [X] I have followed the instructions in the PR template
---------
Co-authored-by: Matthieu Casanova <public@kpouer.com>
Introduces live inspection for running egui apps over a small TCP
request/response protocol, plus the `egui::Plugin` that serves it.
This is the minimal surface to get the egui mcp in, we may want to
extend this in the future to add support for the inspection gui.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
**fix(web): prevent entire page from scrolling out of view in Chrome
(WASM)**
* Closes#7887
**Problem**
When using `egui` on the web, the browser (especially Chrome)
occasionally triggers an unwanted page-level scroll. This happens
because the hidden input element (text agent) used for IME/text input is
sometimes positioned outside the visible bounds of the canvas, causing
the browser to "scroll it into view."
**Solution**
I modified the `move_to` function in the web's `text_agent` to ensure
the input element's position stays within the canvas height. By clamping
the `top` property between `0.0` and `canvas_height`, we prevent the
browser from incorrectly scrolling the entire page when the text agent
moves.
- **Specific change:** Applied `clamp(0.0, canvas_height)` to the
`clamped_y` value before setting the CSS `top` property.
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.
## What
Adds a way for apps to push an RGBA bitmap as the OS cursor — the
missing companion to `Context::set_cursor_icon`. The integration
translates it into a real `winit::CustomCursor`, so the cursor is drawn
by the compositor and can extend past the egui window edge like any
native cursor.
## Why
Apps with custom-shaped windows (Winamp-style skins, themed launchers,
kiosk apps) currently have no clean way to display a custom cursor:
- `CursorIcon` is limited to the standard system enum.
- Painting the cursor sprite via `egui::Painter` works inside the canvas
but gets clipped at the window edge — the bottom/right of the cursor
disappears the moment the pointer is near the boundary, and there's no
way to render onto the desktop area exposed by a
transparent/region-shaped window.
`winit` 0.30+ already supports `CustomCursor::from_rgba` +
`ActiveEventLoop::create_custom_cursor`, but `egui-winit` doesn't
surface it. This PR exposes it through egui.
### Visual demonstration
Driving use case: a Winamp WSZ skin player
([all3f0r1/oneamp](https://github.com/all3f0r1/oneamp)) with a
transparent + region-shaped window where the skin ships its own `.cur`
files. The bottom-right corner of the playlist exposes the resize cursor
— notice how it gets clipped at the window edge in the painter-based
approach.
| Before (cursor painted via `egui::Painter`) | After (cursor pushed via
`set_cursor_image`) |
| --- | --- |
| 
| 
|
## API
```rust
// new in egui::data::output
pub struct CustomCursorImage {
pub rgba: std::sync::Arc<[u8]>,
pub size: [u16; 2], // matches winit's u16 to avoid lossy casts
pub hotspot: [u16; 2],
}
// new field on PlatformOutput (skipped from serde — ephemeral)
pub cursor_image: Option<CustomCursorImage>,
// new method on Context
ctx.set_cursor_image(Some(image)); // overrides cursor_icon for this frame
ctx.set_cursor_image(None); // revert to cursor_icon
```
`Arc<[u8]>` is intentional: the integration dedupes by `Arc::as_ptr`, so
reusing the same Arc across frames means the bitmap is only uploaded to
the OS once per skin, not once per frame.
## Integration changes
- `egui_winit::State::handle_platform_output_with_event_loop(window,
Option<&ActiveEventLoop>, ...)` is a new method that threads the active
event loop so it can call `event_loop.create_custom_cursor(...)`.
- The legacy `handle_platform_output(window, ...)` delegates with `None`
and silently drops `cursor_image`. **No existing callers break.**
- The icon and bitmap paths are unified in a private `apply_cursor`. The
no-flicker dedupe of the old `set_cursor_icon` is preserved on both
paths.
- If `CustomCursor::from_rgba` rejects the bitmap (bad dimensions,
hotspot OOB, etc.), we log a warning and fall back to the icon path.
- eframe's wgpu + glow integrations thread `&ActiveEventLoop` through
`run_ui_and_paint` (glow already had it; wgpu needed one extra
parameter) and call the new method.
- Immediate viewports keep the old path because they're invoked from a
`Context` callback that doesn't have an event loop reference. Custom
cursors are a no-op in immediate viewports — acceptable since they're a
niche path.
## Fallback semantics
| backend / context | what happens |
|--------------------------------|-------------------------------|
| eframe wgpu/glow main viewport | bitmap displayed via OS |
| eframe immediate viewport | falls back to `cursor_icon` |
| eframe web | falls back to `cursor_icon` |
| custom integrations not opted in | falls back to `cursor_icon` |
| `from_rgba` returns `BadImage` | warning + falls back to icon |
## Verification
- `cargo fmt --all -- --check` ✅
- `cargo clippy -p egui -p egui-winit -p eframe --all-targets
--all-features -- -D warnings` ✅
- `cargo doc --lib --no-deps -p egui -p egui-winit -p eframe
--all-features` ✅
- `cargo check -p egui --no-default-features --features serde` ✅
(validates the `serde(skip)` on `cursor_image`)
- Interactive validation on Linux/Wayland with the OneAmp WSZ skin
player — see screenshots above.
I haven't run the full snapshot test suite (`scripts/check.sh`) because
we're on Linux and the snapshots are macOS-rendered — happy to run it if
you'd like.
## Notes
Drafted per the contributing guide ("open a draft PR, you may get
helpful feedback early"). Open to design feedback on:
1. Whether `CustomCursorImage` should live in `egui::viewport` rather
than `egui::data::output`.
2. Whether the legacy `handle_platform_output` should grow `event_loop`
directly (breaking) instead of getting a sibling method (non-breaking,
what I did).
3. Whether to also wire it through eframe-web (probably not —
`wasm-bindgen-cursor` would need its own path).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
### 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.
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"
/>
* 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`).
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>
<!--
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
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
<!--
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`
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!