`capture_backdrop` copied the whole target into the backdrop texture for every
callback that asked for one, so a panel-sized blur on a large screen paid for
the entire frame, once per blur.
Ask the callback what it needs instead: `CallbackTrait::backdrop_rect` is given
the rect the callback will be drawn into, its own rect already cut down by the
clip rect, and returns the region it needs to read. Those are different
questions — a blur reads its radius beyond every edge it draws — and keeping
them apart is what lets the margin survive clipping, which inflating the
callback's own rect could not do: that rect is also the viewport it draws into.
`Backdrop::valid_in_pixels` reports what was captured. The texture stays
full-size so a pixel is still where the effect expects it; only the region that
was asked for holds this frame.
The default answer is the rect the callback is drawn into, which is right for an
effect that only reads the pixels it covers, so nothing has to change unless it
reads further out.
With nine blurred panels of 240x150 at radius 24 on a 1600x1200 canvas, this
plus scissoring the blur passes in regui takes the frame rate from 146 to 1690
fps on WebGPU and 135 to 616 on WebGL2, measured in Chrome; the marginal cost of
one blur drops about twentyfold. egui_kittest's snapshots of the same blurs come
out pixel for pixel identical.
The web painter called `Renderer::render`, which tells the renderer that no
backdrop can be captured, so a callback asking for one silently fell back to
`CallbackTrait::paint` and drew nothing. A blur behind a panel worked natively
and came out as plain glass in a browser.
Do on web what the winit painter already does: when a callback needs a
backdrop, render into our own texture rather than straight into the canvas
surface, which cannot be sampled, then run the
`render_from` / `capture_backdrop` loop and blit the result onto the surface.
The screen capture machinery is the same shape, so this reuses it, and frames
with no backdrop effect still go straight into the surface with no blit.
Checked in Chrome on both backends, WebGPU and WebGL2.
A pass ends by taking the `Context`'s texture uploads for the backend to apply.
That is wrong for a hosted viewport: the application paints it, and paints the
viewport hosting it, so the uploads were taken by code that has no way to give
them back. `TextureManager::set` and `free` both assert on ids the pass had
already finished with, and a freed texture cannot be re-queued at all, since
`free` decrements a retain count that has already reached zero.
Leaving them in place means the hosting viewport's pass takes them, as it would
have without the hosted viewport, and its backend applies them as usual.
Immediate viewports keep taking theirs, because they are painted during the
nested pass and their painter needs the uploads by then.
`TextureManager::pending_delta` lets an application that paints part way through
a frame apply what is pending without taking it. Applying an upload twice is
harmless; the second time re-uploads the same data.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `CallbackTrait::needs_backdrop`, so an effect can transform what egui has
already drawn rather than draw over it. Background blur behind a window or a
panel is the motivating case.
Nothing can sample the texture it is currently drawing into, so this needs the
render pass interrupted and the half-drawn frame copied aside. The renderer
does not own its pass, so the integration has to drive that: `render_from`
draws until it reaches a callback that wants a backdrop and stops, and
`capture_backdrop` copies the frame and lets the callback run its own passes
over it. `render` still works as before for integrations that do not care;
callbacks that asked for a backdrop just get a plain `paint`.
The surface texture cannot be copied from on every platform, which is why
`CaptureState` exists for screenshots. `Painter` now renders into that texture
whenever a backdrop is needed too, and blits it onto the surface afterwards, so
`copy_textures` is split into `copy_to_buffer` and `blit_to_surface`.
`egui_kittest`'s wgpu renderer drives the same loop, so backdrop effects can be
snapshot-tested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hosted viewports are left out of FullOutput::viewport_output, so there was no
way for the application that hosts one to see that it asked for a delayed
repaint. Without this, a blinking text cursor inside a hosted viewport stops
blinking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a way to run a pass for a viewport that the application paints itself,
e.g. into a texture. This is the building block for embedding one egui UI
inside another: a scaled, rotated or blurred sub-UI, a minimap, a live
preview, or an off-screen capture.
The nested-pass machinery already existed for immediate viewports, but two
things were missing for an application to use it:
* Nothing public sets `ViewportState::used`, so `end_pass` threw the child
viewport's state away every pass. Hover, click and drag could never work.
* The child appeared in `FullOutput::viewport_output`, so an integration
like eframe would open a native window for it.
`ViewportClass::Hosted` marks these viewports and keeps them out of
`viewport_output`, so the integration never learns they exist. In return the
application takes on what a backend normally does: supply the `RawInput`,
paint the shapes, apply the texture delta, and act on the platform output.
Also derives `Debug` for `ViewportClass`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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!
-->
* 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>
After upgrading to wgpu v30, it requires to drop the `BufferView` return
by `get_mapped_range` before unmap the buffer. Fix it in this pr, making
screenshot event crash no more
- 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`
## Summary
- [x] I have followed the instructions in the PR template
Fixes the 12 failing `cargo-deny` CI checks on `main` (introduced by
#8289).
### Root cause
`rustybuzz` was flagged as unmaintained by
[RUSTSEC-2026-0206](https://rustsec.org/advisories/RUSTSEC-2026-0206),
reported on 2026-07-12 — after the previous cargo-deny fix (#8300) was
merged on 2026-07-07. The advisory fires on all 12 cargo-deny target
triples.
`rustybuzz` is pulled in transitively via `resvg` → `usvg`, which
`egui_extras` uses for SVG support.
### Why ignore (not fix)?
- `resvg` is pinned at `0.45.1` and cannot be bumped: `0.47` needs
`tiny-skia 0.12`, but `winit 0.30`'s `sctk-adwaita` is stuck on
`tiny-skia 0.11` (see comment in `Cargo.toml` line 128).
- The advisory's recommended replacement is
[`harfrust`](https://github.com/harfbuzz/harfrust), which `resvg` has
not adopted yet.
- This is the same pattern used for the other transitively-unmaintained
advisories already in the ignore list (`ttf-parser` via winit,
`quick-xml` via accesskit/wayland, `bincode`, `yaml-rust`).
### Change
Added `RUSTSEC-2026-0206` to the `[advisories] ignore` list in
`deny.toml` with an explanatory comment.
## Test plan
- [x] `cargo deny check advisories` — passes (`advisories ok`)
- [x] `cargo deny check` — all sections pass (`advisories ok, bans ok,
licenses ok, sources ok`)
---------
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
* 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`
* 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>
Semver-compatible dependency updates (~200 transitive crates in
`Cargo.lock`), plus housekeeping of the version pins in `Cargo.toml`.
## Changes
* Normalize version strings in `[workspace.dependencies]`
(`similar-asserts = "2.0"`, `unicode_names2 = "3.1"`)
* `cargo update`: bump all semver-compatible transitive dependencies
* `itertools` 0.14 → 0.15; the duplicate with puffin's 0.14 /
criterion's 0.13 is now allowed in `deny.toml`
* `rand_core` duplicate (0.6 via phf_generator's rand 0.8, 0.10 via
getrandom 0.4/chacha20) also allowed in `deny.toml` — rand ecosystem is
mid-migration
* `toml` lock moved to 1.1.2 — the old winnow-duplicate blocker is gone
(winit's `toml_edit` now uses winnow 1.x), so the stale comment is
removed
* `wgpu` got a lock-only patch bump 29.0.1 → 29.0.4 (still on the 29.x
line; wgpu 30 intentionally not taken)
## Held back (would fail `cargo deny` with duplicate crates)
* `env_logger` 0.11.8 — newer pulls env_filter 1.x, duplicating the
0.1.x that android_logger needs
* `image` 0.25.6 — needs png 0.18, blocked on resvg
* `smithay-clipboard` 0.7.2 — pulls calloop 0.14, duplicating winit's
0.13
* accesskit platform adapters — newer ones need accesskit_consumer
0.36/0.37, duplicating the 0.35 that kittest 0.4 pins (comments added
for `accesskit_consumer`/`accesskit_winit`)
* wasm-bindgen family kept at 0.2.108 per the sync comment
Majors still blocked (documented in `Cargo.toml`):
`font-types`/`skrifa`/`harfrust` (vello_cpu 0.0.9), `resvg` (winit
0.30).
Verified: `scripts/cargo_deny.sh` green on all targets, `cargo clippy
--all-features --all-targets` clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* [x] I have followed the instructions in the PR template
Just a small docs change, since I saw this trip someone up. The ones on
`Response` may be redundant.
## 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.
A get atoms trait for Button, checkbox etc. I made it because I had time
to kill after I tried sorting buttons in a Vec by the image in their
atoms, but couldn't get to it because it was private.
---------
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This is a bit like my previous pr
(<https://github.com/emilk/egui/pull/8037>), but addresses the final bit
of 64 bit atomic dependent code, the svg loader in the `egui_extras`
crate.
As with the previous pr, this improves egui's compatibility on platforms
without 64 bit atomics.
* Closes <https://github.com/emilk/egui/issues/7692>
* [x] I have followed the instructions in the PR template
Fixes#8079, where font hinting only sharpens the vertical axis
(vertical stems stay blurry), and none of the skrifa knobs were
reachable — only switching to `Target::Mono` helped, but that wasn't
exposed.
This makes the hinting target configurable instead of hardcoding
`Target::Smooth { symmetric_rendering: true, preserve_linear_metrics:
true }`.
### API
- New `epaint::text::HintingTarget` mirroring `skrifa::outline::Target`:
- `Mono`
- `Smooth(SmoothHinting)` where `SmoothHinting { light,
symmetric_rendering, preserve_linear_metrics }`
- New field `FontTweak::hinting_target: HintingTarget`.
- Each variant/field is documented with what it does and the
egui-specific caveats (e.g. `symmetric_rendering` only affects
interpreter-hinted fonts; egui positions glyphs from shaper advances so
`preserve_linear_metrics` mostly affects sharpness, not layout).
### Render
- `font.rs` converts `HintingTarget` → `skrifa::outline::Target` and
threads it into the per-glyph `reconfigure` call; the hinting instance
is also reconfigured when the target changes.
### UI
- The font-tweak settings panel gets a `hinting_target` row: Smooth/Mono
radios, `light` / `symmetric_rendering` / `preserve_linear_metrics`
checkboxes, and a `Reset` button — all with tooltips.
### Behavior
- `HintingTarget::default()` matches egui's previous hardcoded target,
so **rendering is unchanged** unless you opt in. To fix the horizontal
blur from #8079, uncheck `preserve_linear_metrics` (or pick `Mono`).
Whether to flip the *default* is left as a follow-up.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `FontTweak` settings UI previously let you edit variable-font
variation coordinates only via free-form tag + value entry — you had to
*know* that e.g. `wght` exists and what range is valid.
This PR queries the font's actual variation axes and pre-populates the
UI.
### Changes
- **`epaint`**: new `FontData::variation_axes() ->
Vec<FontVariationAxis>` (skrifa-backed). Each `FontVariationAxis`
exposes the axis `tag`, human-readable `name`, `min`/`default`/`max`,
and `hidden`. Empty for static (non-variable) fonts.
- **`egui`**: extracted the `FontTweak` body into a public
`style::font_tweak_ui(ui, tweak, axes)`. When `axes` is non-empty, each
axis is shown as a named **slider** pre-filled with the font's default
and clamped to its valid range, with a ⟲ button to drop the override.
`impl Widget for &mut FontTweak` still exists and delegates with no axes
(free-form fallback, also used for unknown/manual tags).
- The font settings panel (`Context::fonts_tweak_ui`) now passes
`data.variation_axes()`.
- UI label renamed `coords` → **Axes** (matching Google Fonts'
terminology); the underlying `FontTweak.coords` field keeps the OpenType
"design coordinates" name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Example (Weight and Width):
<img width="340" height="239" alt="Screenshot 2026-06-24 at 11 51 33"
src="https://github.com/user-attachments/assets/f898289a-e329-453a-ba86-c60858901466"
/>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `Button` used as a toggle (via `Button::selected(true)`,
`Button::selectable(...)`, or `Ui::selectable_label`) now announces its
pressed / not-pressed state to screen readers. Plain buttons that never
call `.selected(...)` stay un-toggled, so their announcement doesn't
change. `Checkbox` and the pre-existing selectable-label code path
already did this; `Button` was the odd one out.
No public API change: the field is private, `Button::selected(bool)`
keeps its signature, and visuals are identical. Internally the field
becomes `Option<bool>` so we can distinguish "plain button" from "toggle
button currently off".
Regression test added in `regression_tests.rs`, do let me know if some
other file would be a better location.
### Note for manual testing
`egui_demo_app` pulls in `eframe` with `default-features = false` and
doesn't re-enable `accesskit`, so `cargo run -p egui_demo_app` publishes
no AccessKit tree at all. To verify manually:
`cargo run -p egui_demo_app --features accessibility_inspector`
Happy to send a small follow-up PR enabling `accesskit` in the demo
app's defaults if that's desirable, since that makes a11y work much
easier to smoke-test locally.
### Use of AI
This PR was drafted with Claude Code. I understand different projects
have different policies regarding AI generated code. Do let me know if
this is not acceptable here. Also happy to take any other feedback.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## 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>
When an agent screenshots the app using the mcp and then interacts with
the app by clicking at coords, they see the coords in the native image
coords. Since the mcp does everything else in logical coordinates, it
helps if the image they see is also in logical resoltution, so we always
downscale it to 1.0.
I've added this here to avioid having to decode and re-encode the image
in the mcp.
Unfortunately it only does downscaling for now, since adding some way to
upscale the image just for the screenshot would add a lot of complexity,
and might be invasive from a plugin.
I've also changed the submit call to take a closure, to make it easier
to use other transport channel (makes the implementation for reruns mcp
nicer).
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Lucas Meurer <lucas@rerun.io>
The font-shaping and SVG crate families can't be bumped right now
without splitting a transitive crate into two versions in `Cargo.lock`.
Every blocker is an upstream crate that hasn't caught up, so this PR
just records the reasons inline in `Cargo.toml` instead of forcing
duplicates.
## Blockers
| Crate | Wanted | Blocked by |
|---|---|---|
| `skrifa` 0.42 → 0.43 | | `vello_cpu` → `glifo` 0.1.1 pins `skrifa
^0.42` (no newer `glifo`) |
| `font-types` 0.11 → 0.12 | | same `glifo` chain pins `font-types` 0.11
|
| `harfrust` 0.7 → 0.10 | | needs `read-fonts` 0.40 / `font-types` 0.12;
`glifo` pins 0.39 / 0.11 |
| `resvg` 0.45 → 0.47 | | `winit` 0.30's `sctk-adwaita` stuck on
`tiny-skia ^0.11`; resvg 0.47 needs 0.12 |
| `image` 0.25.6 → 0.25.10 | | needs `png` 0.18, which only matches
resvg once resvg is on `tiny-skia` 0.12 |
On `main`, epaint and `glifo` *share* `skrifa` 0.42 / `font-types` 0.11,
so there is a single copy of each. Any epaint-side bump splits them.
resvg 0.46 avoids the tiny-skia split but its `fontconfig-parser`
duplicates `roxmltree`.
Revisit once `glifo` (for the font stack) and `sctk-adwaita` (for
tiny-skia) update.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This allows you to put an `AtomLayout` inside another `AtomLayout`.
Right now this has limited use, but once we add wrapping, vertical
`AtomLayout` and `AtomUi`, this will be a really powerful new layouting
primitive for egui.
Added a test for this in https://github.com/emilk/egui/pull/8221
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>
Label text selection before the fix in a deferred viewport:
<img width="484" height="172" alt="before_the_fix"
src="https://github.com/user-attachments/assets/2214a7d9-9585-497d-9920-dd336a7df7ea"
/>
After the fix:
<img width="484" height="172" alt="after_the_fix"
src="https://github.com/user-attachments/assets/0999ed8e-22d4-4109-a5b5-f468f99e692d"
/>
## What changed
- Keep label text-selection state separate for each viewport.
- Route pass lifecycle and label painting through the current
`ViewportId`.
- Drop inactive per-viewport state after its pass.
- Add a regression test that verifies an unrelated viewport pass cannot
clear a child viewport's
selection, while the owning viewport still clears selections whose
labels disappear.
## Why
Issue #4758 identified that deferred viewports need independent
label-selection state. PR #4760
fixed it by keying the temporary state by viewport. The plugin refactor
in PR #7385 moved that
state into one context-wide `LabelSelectionState`, which accidentally
removed the viewport
isolation. A pass in another viewport then fails to encounter the
selected widgets and clears the
selection.
This restores the behavior of #4760 within the current plugin
architecture. Applications do not
need any special handling.
Less risk of confusing the two.
Found and fix a couple real bugs in the process!
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>