## 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>
This PR adds two small additions to `LayoutJob`:
- `LayoutJob::format_at_byte` to query the `TextFormat` of the section
covering a given byte index.
- An optimization to `LayoutJob::append` that merges newly appended text
into the previous section when the format matches and there is no
leading space.
It also documents the `LayoutJob::sections` invariant (sections are
ordered and together cover the whole text with no gaps or overlaps) and
adds `LayoutJob::debug_sanity_check`, which verifies this in debug
builds. It is called from `format_at_byte` and from the text layouter.
## Why the `easymarkeditor` snapshot changed
The `append` optimization changes how many sections a `LayoutJob` ends
up with: consecutive runs of identically-formatted text now collapse
into a single section instead of one section per `append` call. The
easymark editor produces many such adjacent same-format sections, so it
is affected.
This matters because text is **shaped per section**: `layout_section`
runs the shaper once per section, so each section is an independent
shaping run. Merging two adjacent sections into one means the text
across the old boundary is now shaped together as a single run, which
enables cross-boundary kerning (and, in principle, ligatures) that
previously did not happen. Additionally, `extra_letter_spacing` is
skipped before the first glyph of a section, so merging removes a "first
glyph" boundary and lets the spacing apply there.
The net effect is sub-pixel glyph position shifts at the former section
boundaries, which is why `easymarkeditor.png` was regenerated. The new
output is the more correct one — the text is now shaped as the author
wrote it, rather than being artificially split at `append` boundaries.
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.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>
* Closes https://github.com/emilk/egui/issues/8217
* [x] I have followed the instructions in the PR template
## What
`AllocatedAtomLayout::paint` painted text via
`ui.painter().galley(...)`, so text in atom-based widgets could never be
selected. As discussed in #8217, routing it through
`LabelSelectionState::label_text_selection` unconditionally would break
`Button` / `Checkbox` / `RadioButton` / `TextEdit` (whose labels should
not be selectable, and whose click/drag handling would fight the
selection drag).
So this adds an **opt-in** `AtomLayout::selectable(bool)` (default
`false`). When enabled, the layout also senses clicks and drags
(mirroring `Label::layout_in_ui`) and paints its text through the
label-selection machinery. The underline is `Stroke::NONE`, so when
nothing is selected the painted output is identical to the existing path
— the default (`selectable == false`) behaviour, and existing snapshots,
are unchanged.
## Tests
Two tests in `tests/egui_tests/tests/test_atoms.rs`:
- `test_atom_selectable_senses_click_and_drag` — a `selectable(true)`
layout senses click+drag; the default stays inert.
- `test_atom_selectable_text_can_be_copied` — selecting (drag) the text
of a selectable layout and copying yields the text via
`OutputCommand::CopyText`, while a non-selectable layout yields nothing.
## Notes
- Verified locally: `cargo fmt --all -- --check`, `cargo clippy -p egui`
(clean), and the two new tests pass. I haven't run the full
`./scripts/check.sh` (wasm/typos) locally, hence opening as a draft.
- This only adds the opt-in API; no existing widget is made selectable.
Happy to wire it up on a specific widget and/or add an `egui_demo_lib`
demo if you'd like — just let me know.
**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.
### Problem
`Column::remainder().clip(true)` grows correctly when the panel is made
wider, but does not shrink when the panel is made narrower.
### Root cause
There are two places in `table.rs` where the `max_used_widths` floor is
applied to column widths. One correctly checks `column.clip`, the other
doesn't.
**Post-render** path (already correct, line ~827):
```rust
if !column.clip {
*column_width = column_width.at_least(max_used_widths[i]);
}
```
`TableState::load` **pre-render path** (the bug, line 647):
```rust
.at_least(column.width_range.min.max(*max_used)) // clip flag ignored
```
In `TableState::load`, `.at_least(max_used)` is applied to every column
regardless of `clip`. For a `Column::remainder()`, `max_used`
accumulates the historically widest rendered content. When the panel
shrinks, this floor prevents the column from computing a smaller width,
creating a cycle where it can never shrink.
### Fix
Apply the same `clip` guard that already exists in the post-render path:
```rust
.at_least(if column.clip {
column.width_range.min
} else {
column.width_range.min.max(*max_used)
})
```
When `clip = true`, the floor is just `width_range.min` (0.0 by
default), allowing the remainder column to shrink freely to the actual
remaining space.
### Minimal reproduction
<details>
<summary>Show code</summary>
```rust
use eframe::egui;
use egui_extras::{Column, TableBuilder};
fn main() -> eframe::Result {
eframe::run_ui_native(
"Column::remainder().clip(true) shrink test",
Default::default(),
|ui, _frame| {
egui::Frame::central_panel(ui.style()).show(ui, |ui| {
let mut text = String::from("some content");
egui::ScrollArea::horizontal().show(ui, |ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder().clip(true))
.header(20.0, |mut header| {
header.col(|ui| { ui.strong("Auto"); });
header.col(|ui| { ui.strong("Remainder + clip"); });
})
.body(|mut body| {
body.row(18.0, |mut row| {
row.col(|ui| { ui.label("label"); });
row.col(|ui| {
ui.add(
egui::TextEdit::singleline(&mut text)
.desired_width(f32::INFINITY),
);
});
});
});
});
});
},
)
}
```
</details>
**Without the fix:** make the window wider (column grows ✓), then
narrower — column does not shrink and a horizontal scrollbar appears ✗
https://github.com/user-attachments/assets/2b586588-9f72-4a15-80f4-afddadb69441
**With the fix:** the column shrinks correctly and no scrollbar appears
✓
https://github.com/user-attachments/assets/f1655641-e135-489c-9f59-2af3faa887ab
### Problem
`Response::lost_focus()` could silently fail to fire when keyboard focus
moved from one widget to another *within the same frame* — for example,
clicking a `TextEdit` that was added to the UI *after* the
currently-focused one.
### Fix
This widens the detection window by one extra frame, which is exactly
enough for the deferred loss signal to reach the previously focused
widget on its next render.
### Notes
* The `test_demo_app` test fails, but it has nothing to do with this PR;
it fails on the current master branch, too.
* This PR replaces https://github.com/emilk/egui/pull/3247
* Closes <https://github.com/emilk/egui/issues/2142>
* [x] I have followed the instructions in the PR template
## Summary
Adds `Context::interactive_rects_last_pass() -> Vec<Rect>`, an
integration-facing helper that returns the same widget interaction
rectangles egui uses for hit-testing in the last completed pass.
The method filters out disabled widgets, non-interactive widgets, and
layers that currently do not allow interaction. It also applies
per-layer transforms so the returned rectangles are in global viewport
coordinates.
## Motivation
Some egui integrations need to declare platform-level input regions
before pointer events can reach egui itself. A concrete example is a
transparent or click-through overlay: the platform/windowing layer must
know which parts of the overlay should receive input and which parts
should pass through to whatever is underneath.
Today egui keeps this information internally in `WidgetRects` and uses
it for its own hit-testing, but integrations cannot enumerate those
rectangles. Downstream integrations therefore need app-level side
channels where each app manually reports its clickable rectangles. That
is fragile because it duplicates data egui already has, is easy for
applications to forget, and tends to go stale when widgets move or
popups/menus appear.
This method exposes only the already-derived, integration-relevant
result instead of making `WidgetRects` itself part of the public API.
## Notes
- The method uses the last completed pass because that is the same data
egui uses for interaction at the start of the next pass.
- Rectangles are returned in layer order for deterministic output.
- Non-positive or non-finite rectangles are skipped.
## Verification
- `cargo check -p egui`
- `cargo test -p egui --lib`
Co-authored-by: psyche314 <psyche314@users.noreply.github.com>
Fix: ScrollArea layout jitter with floating bars and zoom levels
* Closes#7937
* Closes#7942
This PR improves the stability of `ScrollArea` by addressing two layout
issues:
1. **Discrete Layout for Floating Bars:** Fixed content "shaking" in
`floating` mode when a non-zero `allocated_width` is used. By using
discrete visibility (`show_bars`) instead of the animated factor for
space allocation, we ensure the layout remains stable during scrollbar
animations.
2. **Zoom-level Stability:** Introduced a small epsilon (0.1) when
checking if content exceeds the viewport. This prevents scrollbars from
flickering on and off due to floating-point rounding errors at specific
zoom factors (e.g., 1.01 or 0.95).
## Problem
winit has always delivered distinct physical variants for every keyboard
key — \`KeyCode::ShiftLeft\` vs \`KeyCode::ShiftRight\`,
\`KeyCode::ControlLeft\`/\`ControlRight\`, \`AltLeft\`/\`AltRight\`,
\`SuperLeft\`/\`SuperRight\`, plus the ISO 102nd key
\`KeyCode::IntlBackslash\` (the one between LShift and Z, labelled
\`<>|\` on French AZERTY and \`\\|\` on UK QWERTY). Today none of these
reach egui:
- Pressing Shift / Ctrl / Alt alone produces *no* \`Event::Key\` at all.
\`key_from_key_code\` and \`key_from_named_key\` both return \`None\`
for modifiers, so the \`if let Some(active_key)\` branch in
\`on_keyboard_input\` is skipped. The collapsed \`Modifiers\` bools are
the only trace of the press, and they don't distinguish left vs right.
- \`KeyCode::IntlBackslash\` has no arm in \`key_from_key_code\`, so on
French / UK ISO keyboards the \`<>|\` key is completely invisible to
egui apps — neither \`key\` nor \`physical_key\` is ever set.
## Who hits this
- Games / kiosk frontends / pincab UIs that bind \`LeftFlipper =
LShift\` vs \`RightFlipper = RShift\` (or \`LeftMagna = LCtrl\` vs
\`RightMagna = RCtrl\`) — currently impossible inside egui without
shelling out to platform APIs (\`device_query\`, raw X11, etc.).
- Anyone on an ISO keyboard who wants to capture the 102nd key in an
input-binding UI.
Previously discussed: context in #2977 (closed by #3649 which added
\`physical_key\`, but only for keys already in \`egui::Key\`).
## Change
Two small additions, no behaviour change for existing code:
**\`crates/egui/src/data/key.rs\`** — new variants at the end of
\`Key\`:
- \`ShiftLeft\`, \`ShiftRight\`, \`ControlLeft\`, \`ControlRight\`,
\`AltLeft\`, \`AltRight\`, \`SuperLeft\`, \`SuperRight\`
- \`IntlBackslash\`
plus their entries in \`Key::ALL\`, \`Key::from_name\`, and
\`Key::name\` (the \`key_from_name\` roundtrip test at the bottom of the
file still passes).
**\`crates/egui-winit/src/lib.rs\`** — new arms in
\`key_from_key_code\`:
\`\`\`rust
KeyCode::ShiftLeft => Key::ShiftLeft,
KeyCode::ShiftRight => Key::ShiftRight,
// ...ControlLeft/Right, AltLeft/Right, SuperLeft/Right...
KeyCode::IntlBackslash => Key::IntlBackslash,
\`\`\`
The existing \`Modifiers\` struct is untouched — shortcut matching
(\"Ctrl+C\"), \`consume_shortcut\`, etc. still see the collapsed state.
The new variants are purely additive and only surface as physical
\`Event::Key\` presses when someone is specifically looking for them.
## Test
- Existing \`test_key_from_name\` test still passes (updated the
sentinel to \`Key::IntlBackslash as usize + 1\`).
- Manual smoke test: pressing left vs right Shift, Ctrl, Alt each
produces an \`Event::Key { key: Key::ShiftLeft/Right/..., physical_key:
Some(Key::ShiftLeft/Right/...), ... }\`; pressing the AZERTY 102nd key
yields \`Key::IntlBackslash\`. Character-key behaviour and \`Modifiers\`
bools are unchanged.
## Not included
- **Web backend** (\`eframe_web\`): \`PhysicalKey\` isn't fully exposed
there yet per the existing \`physical_key\` docs, so these new variants
are only emitted on native. Happy to extend to web in a follow-up if
wanted.
- \`ModifiersSymmetric\` / per-side sticky state: would be a bigger API
change in \`Modifiers\`. This PR stays at the minimum: forward what
winit already gives us for the event path.
Closes no issue directly but addresses the underlying gap noted in the
thread of #2977 (scancode forwarding) for the modifier / Intl-key
subset.
The Debug-formatting of `Id` in debug-builds now contain the full
lineage:
```rs
#[test]
fn with_chain() {
let id = Id::new("a").with("b").with("c").with(7_i32);
assert_eq!(
format!("{id:?}"),
r#"Id::new("a").with("b").with("c").with(7)"#
);
}
```
## Related
* https://github.com/emilk/egui/pull/5851
* https://github.com/emilk/egui/pull/7988
<!--
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
At low apha values, premultiplied colors lose precision.
This PR makes the color picker use unmultiplied colors internally.
Before:
https://github.com/user-attachments/assets/4617a355-daa9-4911-86e6-518ac6867014
After:
https://github.com/user-attachments/assets/d9681b01-50d8-418e-b5a5-79b4bd1bbddf
## Summary
Adds two paired API entry-points that let an integration target a
specific monitor at viewport creation time, or move an existing viewport
to a different monitor at runtime, in a way that works portably on
Wayland.
```rust
// At creation
ViewportBuilder::default()
.with_inner_size([1920.0, 1080.0])
.with_monitor(playfield_idx) // ← new
.with_decorations(false)
// Or at runtime
ctx.send_viewport_cmd(egui::ViewportCommand::SetMonitor(idx));
```
Both route through winit's
`Fullscreen::Borderless(Some(MonitorHandle))`, which is the only
portable mechanism that:
- targets a specific output on **Wayland** (where there is no global
`OuterPosition`)
- avoids the **Mutter race** where `OuterPosition` is dropped before the
window is mapped (X11/Wayland-Mutter)
- works the same way on Windows and macOS
`with_position` and `with_outer_position` continue to work for cases
where the integration *does* know the absolute pixel coordinates of each
monitor and is on a platform where they are honored. `with_monitor` is
the high-level alternative when you just want "show this window on
output N, borderless fullscreen."
## Why this matters
Multi-monitor borderless setups (kiosks, pinball cabinets, museum
installs, embedded panels) need each window to land on a specific
physical display. Without `with_monitor`:
- On Wayland, you can't move a window to a chosen output at all — the
compositor decides. There's no `OuterPosition` API.
- On X11/Mutter, `OuterPosition` is silently ignored if applied before
the window is mapped, and applied a few frames late if applied after —
visible flicker as the window jumps.
- Polling `monitor.position()` then sending `OuterPosition` in a retry
loop is the workaround pattern, but fragile and racy.
Routing through `Fullscreen::Borderless(Some(MonitorHandle))` is the
same code path winit's own examples use for monitor-targeted fullscreen,
just exposed at the egui ViewportBuilder level.
## Implementation
- `crates/egui/src/viewport.rs` — adds `monitor: Option<usize>` to
`ViewportBuilder`, the `with_monitor(usize)` builder method, and the
`ViewportCommand::SetMonitor(usize)` variant.
- `crates/egui-winit/src/lib.rs` — both at viewport creation and on
`SetMonitor`, look up the monitor by index in `available_monitors()` and
apply `Fullscreen::Borderless(Some(handle))`. Index out of range is a
no-op (with a `log::warn!`), matching how unknown values are handled
elsewhere in the file.
73 lines added, 1 modified. No public API removed or changed.
## Test plan
- [x] `cargo build -p egui -p egui-winit` clean
- [x] `cargo clippy -p egui -p egui-winit --all-features -- -D warnings`
clean
- [x] `cargo fmt -p egui -p egui-winit --check` clean
- [ ] Manual: tested on Linux X11 (Mutter), Linux Wayland (Mutter &
KWin), Windows 11. Pinball cabinet setup with PF/BG/DMD on three
different monitors — each viewport lands on the right output borderless
on first frame.
- [ ] Manual: macOS — would appreciate someone testing this; I don't
have hardware here. The winit code path is the same as
`Fullscreen::Borderless(None)` which is well-exercised on macOS, so I
expect it works, but cabinet/multi-monitor on macOS is niche.
## Background
This is the third of three small upstream-able pieces extracted from the
closed [PR #8113](https://github.com/emilk/egui/pull/8113) (viewport
rotation, declined as too niche / too much surface). The rotation logic
itself shipped as the standalone
[`egui-rotate`](https://crates.io/crates/egui-rotate) crate. The
remaining two integration touch-points needed for kiosk/cabinet setups
are:
- [PR #8138](https://github.com/emilk/egui/pull/8138) —
`App::transform_primitives` + `App::post_platform_output` hooks
(general-purpose post-tessellation / post-platform-output hooks)
- [PR #8127](https://github.com/emilk/egui/pull/8127) —
`Key::ShiftLeft/Right` + `IntlBackslash` physical key variants
- **This PR** — `with_monitor` / `SetMonitor`
Each is independently useful. None depend on the others.
🤖 Drafted with [Claude Code](https://claude.com/claude-code)
<!--
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!
-->
Fixes a small typo
* [x] I have followed the instructions in the PR template
<!--
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#8032
* [x] I have followed the instructions in the PR template
It includes:
* Fix for `ScrollArea` when `scroll_to_*` could be ignored when
`stick_to_bottom(true)` was active and the viewport was already stuck to
the bottom.
* The fix is by making explicit per-axis scroll movement take priority
over sticky-end snapping for that frame, and avoid immediately
re-marking animated scrolls as still stuck.
* I've also added a regression test for this issue to ensure it will be
caught on further code changes.
The code snippets form the original issue can be used for testing here
as well
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Previously, the doc for `Ui::scope_builder` read
> Create a child, add content to it, and then allocate only what was
used in the parent `Ui`.
which I understood as meaning that "only what was used in the parent
`UI`" would be allocated (in the child or parent), which makes no sense
(in either case).
I rewrote it and some related docs.
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
* Closes <https://github.com/emilk/egui/issues/4491>
* [x] I have followed the instructions in the PR template
Lets you create an `EhttpLoader` with arbitrary headers like so:
```rust
cc.egui_ctx.add_bytes_loader(std::sync::Arc::new(
egui_extras::loaders::http_loader::EhttpLoader::default().with_headers(&[
("User-Agent", "foo"),
])
));
```
I'm not sure if there are any problems with installing a second
`EhttpLoader` (in addition to the one installed by default when using
`egui_extras::install_image_loaders(&cc.egui_ctx)`. But I wasn't
sure how else to pass in configuration options to
`install_image_loaders`.
* Follows https://github.com/emilk/egui/pull/8199
This makes the animation of the collapsing panel a bit smoother, by
taking into account the spacing between the header and the body.
`ab_glyph` would output coverage values, but `vello` outputs RGBA. So
the old name was a misnomer.
I also suspect our default values are wrong, but I need to investigate
that more properly in a separate PR.