`ColorImage` fields are now private. Use `resolution()`, `pixels()`,
`pixels_mut()`, and `source_size()` instead.
Also renames `size` to `resolution` in several texture-related APIs
(`ImageData`, `TextureHandle`, `TextureMeta`, `TextureAtlas`,
`Fonts::font_image_resolution`).
`ColorImage::new` now panics if `width * height != pixels.len()`.
## Motivation
A) Make `ColorImage` always valid
B) Stop confusing using the confusing word `size` where `resolution` is
better
The name egui_system_fonts is already taken on crates.io. The new name
matches the crate's `SystemFontProvider` type.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cargo-deny is failing on all PRs due to new rustls advisories
(RUSTSEC-2026-0346, -0347, -0348, -0349). This bumps rustls 0.23.45 →
0.23.46 in `Cargo.lock`.
* [x] I have followed the instructions in the PR template
Co-authored-by: Claude <noreply@anthropic.com>
I don't think `Label::text_color(c)` method exists.
---
<!--
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!
* Keep PR description short and to the point! No long LLM slop.
* 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 a human has reviewed it
* 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/THE_RELEVANT_ISSUE>
* [x] I have followed the instructions in the PR template
`Clipboard::set_image` passed premultiplied `Color32` bytes to arboard,
which expects straight (unmultiplied) RGBA. Semi-transparent pixels came
out darkened. Pasting was already correct.
* [x] I have followed the instructions in the PR template
🤖 Generated with [Claude Code](https://claude.com/claude-code)
eframe now remembers the window's last *normal* (not maximized, not
fullscreen) size and position. After a relaunch, un-maximizing or
leaving fullscreen goes back to that size instead of a window that is
"maximized but not maximized".
Other fixes:
* A maximized or fullscreen window no longer paints its first frame
small.
* Leaving a restored fullscreen goes straight back to the normal size,
in one step.
* The window reopens on the correct monitor, also with monitors of
different scale factors on macOS. The position is saved in pixels
together with the scale factor, and converted on restore.
* On macOS and Windows, the window is placed exactly after creation,
then maximized or made fullscreen.
* On Windows, a window restored maximized no longer flashes on screen
(shown, hidden, then shown again) during startup. winit maximizes with
`ShowWindow(SW_MAXIMIZE)`, which shows the still-hidden window before
hiding it again. The new `egui_winit::set_maximized` cloaks a hidden
window (`DWMWA_CLOAK`) while maximizing it, so it stays invisible but is
already maximized for the first frame. This is a workaround until winit
has rust-windowing/winit#4575 or rust-windowing/winit#4587.
* On X11, a window saved maximized or fullscreen now restores as such.
winit sends these as `_NET_WM_STATE` client messages, which window
managers ignore while the window is still hidden (eframe keeps it hidden
until the first frame is painted). The new
`WindowSettings::after_first_show` re-applies them right after the
window is shown. `before_first_show` replaces the macOS-only
`enter_fullscreen_after_first_frame` / `enter_deferred_fullscreen`.
The save format changed, so old saved window settings are ignored once
(the window opens at its default size).
Known limitations:
* The normal geometry is sampled on save. If you resize, maximize before
the next autosave, and quit while maximized, un-maximizing after a
relaunch goes back to the size from before the resize.
* On X11, a restored maximized window may show un-maximized for a frame
before it maximizes.
* On X11 with GNOME (mutter), un-maximizing a window that was restored
maximized puts it in the top-left corner of its monitor, not at its
saved position. The size is right. mutter places windows that get
maximized right after mapping at the work area origin.
* On Wayland, the position (and so the monitor) can't be restored,
because Wayland doesn't let clients read or set window positions. Size
and maximized/fullscreen state are restored.
* On GNOME, a restored window that is close to the screen size gets
auto-maximized by mutter (`org.gnome.mutter auto-maximize`).
Tested on macOS, wayland, x11 (XWayland on GNOME, including two
monitors) and Windows (wgpu and glow).
* [x] I have followed the instructions in the PR template
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_019yMtdim1FPCsviY1FWfdTk
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Related: #5233, upstream
https://github.com/linebender/parley/issues/644
* [ ] I have followed the instructions in the PR template
On Windows with a non-Chinese locale, fontique's Han fallback is a
single Japanese font (Yu Gothic UI), which lacks many Simplified Chinese
characters like 这 and 还. These rendered as tofu even with Microsoft
YaHei installed.
The example text from the added demo without the fix fails to show some
glyphs:
<img width="582" height="146" alt="image"
src="https://github.com/user-attachments/assets/1f37a778-6d67-431f-9aad-c7bd23e0e360"
/>
`SystemFontProvider` now tries the other Han locales
(Simplified/Traditional Chinese, Japanese, Korean) when the font for the
preferred locale can't render a char.
Simplified Chinese is looked up as `zh-SG` instead of `zh-CN`: fontique
treats `zh-CN` as the default Han locale and caches the result of the
locale-less Han lookup (which every `Query::set_fallbacks` makes) as
that default. So `zh-CN` lookups often returned the Japanese font,
depending on which scripts were looked up first. A TODO points at
linebender/parley#689, which should make this workaround unnecessary.
Also adds a collapsed "System font fallback" section to the Misc Demos
window to check the fallback by eye.
## Testing
Verified on Windows 11 (de-DE): all samples in Misc Demos → System font
fallback render, and `cargo test -p egui_system_fonts --
--include-ignored` passes.
**Still needs testing on macOS and Linux**: open Misc Demos → System
font fallback in `egui_demo_app` and run the ignored tests above. Both
platforms also pick a single fallback font per script in fontique, so
they could have the same problem in milder form, e.g. a Japanese-locale
Mac, or a Linux system with only a Japanese CJK font.
The Misc Demos snapshot was updated on Windows; if CI disagrees,
`./scripts/update_snapshots_from_ci.sh` should fix it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Improes non-`syntect` highlighter in `egui_extras::syntax_highlighting`.
* `TokenType` is now public, with new `CodeTheme::format` and
`CodeTheme::format_mut`, so apps can match the colors to their own theme
* New `TokenType::Function`: an identifier directly followed by `(`
* `/* block comments */` for C, C++, JavaScript, JSON and Rust
* Backslash escapes in strings, so `"say \"hi\""` is one string
* `'single'`, `"""triple"""` and `'''triple'''` strings in Python and
TOML, `'char'` literals in C/C++, and `` `template` `` strings in
JavaScript
* New languages: JavaScript/TypeScript, shell (bash, sh, zsh) and YAML
* `jsonc` and `json5` are now highlighted as JSON
The demo snapshots are rendered with `--all-features`, which uses
`syntect`, so they are unaffected.
The new `egui_extras/tests/syntax_highlighting.rs` snapshot test covers
the fallback highlighter, run by a new CI step without `syntect`.
* [x] I have followed the instructions in the PR template
This PR was written by Claude Code (an LLM) on behalf of @emilk, and
needs a human review before leaving draft.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Part of https://github.com/emilk/egui/issues/5735
Enables the `clippy::std_instead_of_alloc` lint in `egui`, `emath`,
`epaint`, `ecolor`, `epaint_default_fonts` and `egui_extras`, and fixes
all the warnings (`std::sync::Arc` → `alloc::sync::Arc`, etc). Each
crate gets `extern crate alloc;` where needed.
This is enabled per-crate (via `#![warn(…)]` in `lib.rs`) rather than
workspace-wide, to avoid sprinkling `extern crate alloc;` across every
example and test.
* Closes <https://github.com/emilk/egui/issues/8699>
* [x] I have followed the instructions in the PR template
`TextEdit` now handles AccessKit's `SetValue` (replaces the whole text,
even when the field isn't focused, since assistive technologies set
values without focusing first) and `ReplaceSelectedText` (replaces the
selection, like a paste). Both are advertised on editable fields only,
respect `char_limit`, and turn newlines into spaces in single-line
fields. Paste now shares the same helper.
Tested with new `egui_kittest` tests, using two small helpers added next
to `set_value_accesskit`: `set_text_value_accesskit` and
`replace_selected_text_accesskit`. Also tested on Windows with UI
Automation, using the repro from the issue:
`ValuePattern.SetValue("hello")` now fills the field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
target_os is unknown on the web, so Cmd (Super) was not mapped to
Modifiers::command there: Cmd-C, Cmd-V, Cmd-X and Cmd-A did not work in
web builds, while Ctrl wrongly acted as the shortcut modifier.
Web integrations already set the operating system in Context::os()
(eframe derives it from the user-agent), so consult that instead of
target_os. Native behavior is unchanged: OperatingSystem::default() is
still derived from target_os.
* Closes <https://github.com/emilk/egui/issues/8676>
* [X] I have followed the instructions in the PR template
* [x] I have followed the instructions in the PR template
A `Harness` runs one pass as soon as it is built, and the root `Ui`
takes its style from the `Context` at the start of the pass.
So a style, fonts or image loaders installed from the ui closure miss
that first pass, and widgets that keep their first layout keep the wrong
one.
In Rerun this made a snapshot test flaky: `egui_table` sized its columns
with the default monospace font whenever the table's data arrived in
time for the first pass, and columns only grow after that.
`HarnessBuilder::with_context_setup(|ctx| …)` runs once on the new
`Context` before that pass, for `build_ui`, `build_ui_state` and
`build_eframe` (before the app is created).
The harness's own test settings (theme, no animations, no cursor
blinking) are still applied after it.
🤖 Written by Claude Code (Claude Opus 5.5); not yet reviewed by a human,
hence the draft.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`CCursor::new` was a bit of a foot-gun: it had no docstring, and it's
easy to accidentally pass it a byte offset instead of a character
offset.
* Document that `CCursor::new` takes a character offset, NOT a byte
offset
* Add `CCursor::end_of_str(text)`: a cursor after the last character of
`text` (i.e. `text.chars().count()`)
* Add `CCursorRange::select_all_str(text)`: select all of `text`
* Use these in `DragValue` and the `TextEdit` demo
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Closes#8327.
Handle arrow-key focus in the menu code: Up/Down follow tab order and
wrap within the current menu, Left returns to its opener, and Right
opens a submenu and focuses its first enabled entry. This also works
when a submenu opens to the left of its parent.
Preserve arrow keys used by text fields and sliders, and prevent a
stationary pointer from closing keyboard-opened submenus. Added
regression coverage for entry order, nested navigation, reopening, focus
notifications, and pointer interactions.
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
This makes some improvements to the scroll bouncing to make it feel more
consistent.
Previously, when scrolling back down quickly during a bounce, momentum
scroll wouldn't work making it feel stuck to the top. Scrolling back
(opposite direction of the bounce) is now always regarded as a normal
scroll.
The bounce felt very different depending on when you lifted the finger.
If you lifted the finger before/after the scroll started bouncing, you'd
get widely different bounces. Now bounce is consistent no matter if we
started overscrolling already or not.
This is me doing quick up/down scroll gestures. Before, on main:
https://github.com/user-attachments/assets/fd076b4c-99ba-4b14-bbf6-fad9dadb58bd
With the fixes:
https://github.com/user-attachments/assets/db70246b-f7de-427d-90f3-c92cffe3be35
Draft of the hand-written parts of the 0.37.0 changelog: the highlights
(colored emoji and glyph support, the theme plugin system, accessibility
improvements) and the breaking or deprecated changes.
It might be a bit long though.
---------
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
Adds `eframe::Frame::set_traffic_lights_position` for placing the native
macOS "traffic lights" (close/minimize/maximize buttons) inside a custom
title bar, e.g. when using
`ViewportBuilder::with_fullsize_content_view`.
## New API
```rust
impl eframe::Frame {
/// Center the traffic lights vertically in `title_bar_y` (measured from the top of the window),
/// with the close button `left_margin` from the left edge. Both in egui points.
/// Call it every frame. Does nothing on other platforms.
pub fn set_traffic_lights_position(
&self,
egui_ctx: &egui::Context,
title_bar_y: egui::Rangef,
left_margin: f32,
);
}
```
Usage (from the `custom_window_frame` example):
```rust
frame.set_traffic_lights_position(
ui.ctx(),
title_bar_rect.y_range(),
title_bar_rect.left() + 12.0,
);
```
## Details
* The buttons keep AppKit's native spacing.
* The native title bar is resized to cover the buttons, so they stay
clickable in title bars taller than the default.
* Units are egui points; the zoom factor is applied internally.
* It is immediate mode: AppKit resets the button positions whenever it
re-lays out the window (e.g. on resize), so call it every frame.
* No-op in fullscreen (the traffic lights live in a separate window
there) and on non-macOS platforms.
## Other changes
* `custom_window_frame` example: on macOS, uses a full-size content view
with the native traffic lights centered in the custom title bar, instead
of drawing its own buttons.
* `emath::Rangef` now supports `Rangef * f32`, `f32 * Rangef`, and
`Rangef / f32`.
* `WindowChromeMetrics::traffic_lights_size.y` now measures the top
margin from the top of the title bar (previously it was taken from the
bottom in AppKit's non-flipped coordinates), so the reported height may
change.
## Testing
Ran `custom_window_frame` on macOS: traffic lights stay centered when
resizing and zooming (cmd +/-).
* [x] I have followed the instructions in the PR template
🤖 This PR was written with Codex and Claude Code; Marten remains
responsible for the change.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
The non-`syntect` fallback highlighter in `egui_extras` now supports SQL
(`"sql"`):
* `-- line comments`
* `'single-quoted'` strings
* Case-insensitive keywords (`SELECT`, `select`, …)
Also: `_` is now treated as part of a word for all languages, so
identifiers like `order_id` (SQL) or `self_ref` (Rust) no longer get a
keyword highlighted inside them.
Snapshot test in `egui_demo_lib/tests/misc.rs` renders SQL with both
backends (`sql_fallback_*` / `sql_syntect_*`). Note: CI runs
`--all-features`, so only the syntect snapshots are checked there; the
fallback ones are checked by `cargo test -p egui_demo_lib`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
* Part of #5136
* Closes https://github.com/emilk/egui/pull/8414
On Wayland a compositor may stop sending frame callbacks to a window it
isn't showing, without telling the client that the window is occluded.
Hyprland does this when another window on the same workspace goes
fullscreen or maximized. With the glow backend, eframe then blocks in a
vsync `eglSwapBuffers` until the window is shown again. Nothing runs,
pings go unanswered, and the compositor reports the app as not
responding. With wgpu, `pre_present_notify` keeps eframe from blocking,
but `RedrawRequested` never arrives, so `App::logic` stops running.
In rust-windowing/winit#4668 the winit maintainers said that winit
should keep holding `RedrawRequested` until the frame callback arrives,
and that a timeout belongs in the toolkit. This PR adds that timeout:
* glow calls `Window::pre_present_notify` before swapping, like
egui-wgpu already does, so winit paces `RedrawRequested` by frame
callbacks.
* On Wayland, glow swaps with interval 0. The frame callbacks already
pace it. With the NVIDIA driver (egl-wayland2), a vsync swap also blocks
forever for a window that starts on a hidden workspace: the compositor
sends a frame callback for the first buffer but never presents it, and
the next swap waits for that buffer.
* If a `RedrawRequested` we asked for hasn't arrived within 250 ms,
eframe runs `App::logic` without an egui pass, the same as for other
hidden windows. Once the compositor sends a frame callback again, the
window paints normally.
`glow_options.vsync` therefore has no effect on Wayland. That's what
vsync means there anyway, and the docs now say so.
## Testing
I tested with a small probe that calls `request_repaint` on every
`App::logic` call and counts `logic` and `ui` calls. It ran on Hyprland
0.56.2 with NVIDIA and a headless output, and I counted pings and pongs
with `WAYLAND_DEBUG=1`. Each phase lasted 10 s:
| Scenario | Before | After |
| --- | --- | --- |
| Visible (glow and wgpu) | 60 fps | 60 fps |
| Covered by a fullscreen window, glow | blocked in `eglSwapBuffers`, 0
pings answered | about 4 `logic`/s, no paints, all pings answered |
| Covered by a maximized window, glow | same (Hyprland stops the
callbacks) | same as above |
| Covered, wgpu | responsive, but 0 `logic` calls | about 4 `logic`/s |
| Window on a hidden workspace | glow blocks | about 4 `logic`/s, all
pings answered |
| Started on a hidden workspace, glow | blocked on the second swap |
about 4 `logic`/s, all pings answered |
| Shown again | resumes | resumes on the next frame callback |
| Idle | 0 CPU | 0 CPU |
X11 and other platforms: I only checked that they compile. Nothing
changes there except that `run_logic` may run if a `RedrawRequested`
takes more than 250 ms to arrive, which only calls `App::logic` one
extra time.
When winit reports `Occluded` on Wayland (rust-windowing/winit#4709),
eframe's existing occluded handling takes over for hidden workspaces.
This timeout still covers windows the compositor hides without
suspending them.
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
A Wayland compositor refuses focus to a window it did not see the user
ask for. A launcher hands its own focus over instead: it asks the
compositor for an
[xdg-activation](https://wayland.app/protocols/xdg-activation-v1) token
and passes it to the child process in `XDG_ACTIVATION_TOKEN`. On X11 the
same role is played by a startup-notification id, in
`DESKTOP_STARTUP_ID`.
winit implements both —
`WindowExtStartupNotify::request_activation_token()` and
`WindowEvent::ActivationTokenDone` — but the reply arrives as a window
event that eframe consumes, so an app has no way to see it.
Two methods on `Frame`:
```rust
frame.request_activation_token(); // no-op off Linux
// ...one or more frames later:
if let Some(token) = frame.take_activation_token() {
Command::new("some-app").env("XDG_ACTIVATION_TOKEN", token).spawn()?;
}
```
Both native backends intercept `ActivationTokenDone`, hold the token on
`Frame`, and **request a repaint**: a token is single-use and
short-lived, so an idle app must not have to wait for an unrelated event
to notice it. Nothing is delivered on platforms without an activation
protocol, so an app should give up after a deadline of its own rather
than wait forever.
Nothing to add on the app side beyond these two calls — no winit
dependency, no `cfg` of its own.
### What this replaces
The first version of this PR put a
`ViewportCommand::RequestActivationToken` and an
`Event::ActivationTokenReceived` in `egui` itself. @lucasmerlin pointed
out this belongs in eframe rather than in the core, which is right:
`egui` has no business knowing what xdg-activation is, and eframe is
already where the native-specific surface lives.
### Checked
- both backends, glow and wgpu
- both arms of the `cfg_select!`: the Linux one as-is, the fallback by
temporarily pointing the predicate at another `target_os` (winit won't
build on Linux without `wayland` or `x11`)
- the real case this comes from: a launcher on GNOME/mutter spawning a
fullscreen app, which opens behind the launcher without a token and in
front with one
<!--
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
I'm the author of the Chinese README translation. For the following
reasons, I changed `README.md`.
1. The translation only covers README. However, the description in the
`README.md` is somewhat inaccurate, causing some people to
misunderstand. (The previous description “egui 的中文翻译文档” refers to "the
Chinese document translation for egui")
2. I changed my username. Although Github created a redirect, it's
better to change the link.
The new description is simple and straightforward: “中文 README” refers to
"Chinese README".
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 <https://github.com/emilk/egui/issues/8391>
* [x] I have followed the instructions in the PR template
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Fixes `eframe::Frame::set_wgpu_surface_config()` not changing the
present mode or frame latency.
`eframe::Frame::set_wgpu_surface_config()` wrote to the `surface_config`
of the `RenderState` inside `Frame` itself, but the one actually used to
configure the window is `egui_wgpu::winit::Painter::config.surface`.
Nothing ever propagated that write to the painter, so the two were never
synchronized.
This PR also trims the number of `SurfaceConfig`s scattered across the
structs.
## What was changed
- `RenderState` no longer stores `SurfaceConfig` **(BREAKING CHANGE)**;
- `Painter` gains `surface_config()` and `set_surface_config()`;
- `Frame::set_wgpu_surface_config()` and `Frame::wgpu_surface_config()`
now use a new field, `wgpu_surface_config: Option<SurfaceConfig>`;
- `Painter::config.surface` is now the only source of truth for the
applied `SurfaceConfig`.
* [X] I have followed the instructions in the PR template
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
`RawInput::safe_area_insets` documents `None` as reusing the previous
value, but `RawInput::append` replaced pending insets with `None` when
batching inputs.
Preserve the previous insets when newer input does not specify them,
matching `screen_rect` and `max_texture_side`.
Adds regression coverage for preservation, replacement, and explicit
reset to zero.
- [x] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
## Summary
I would like to build `egui` for `wasm32-unknown-unknown`, but targeting
an environment which isn't necessarily a browser. Right now, `egui`
unconditionally depends on `web-sys` for the `DroppedFile::web_file`
function. This adds a bunch of extra dependencies and causes the export
of about 1400 `__wbindgen_*` symbols in the final WASM binary. The
existence of those symbols is causing some problems for me.
As a fix, this puts `web-sys` behind a new `web_file` feature and gates
`web_file` on it. `eframe` enables the feature on wasm32, so existing
web users are unaffected.
## Changes
- Gate `DroppedFile::web_file` behind a `web_file` feature
## Checklist
<!--
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!
* Keep PR description short and to the point! No long LLM slop.
* 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 a human has reviewed it
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
-->
* No associated issue that I could find
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
* Closes <https://github.com/emilk/egui/issues/2550>
* [x] I have followed the instructions in the PR template
Double-click-and-drag now extends the selection by whole words, and
triple-click-and-drag by whole lines — in both `TextEdit` and labels,
including selections spanning multiple labels.

The reason this was missing: clicks are only counted on pointer
*release*, so at the start of a drag nothing knew it began with a
double-click.
* `PointerState` now also computes the would-be click count at *press*
time (`pub(crate)` only, same heuristics as the click counting on
release).
* On a second/third press, `TextCursorState` selects the word/line under
the pointer immediately and remembers it as the anchor; while dragging,
the selection is the union of the anchor and the word/line under the
pointer. A side effect is that plain double-click selection now appears
on press rather than release, matching other UI toolkits.
* `LabelSelectionState` carries the anchor across passes, so the drag
can extend into neighboring labels.
No public API changes. Covered by unit tests for the range math and
kittest tests for `TextEdit` (forward/backward/lines) and cross-label
drags in `tests/egui_tests/tests/test_text_selection.rs`.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
* Closes <https://github.com/emilk/egui/issues/5852>
* [x] I have followed the instructions in the PR template
Selecting text in a `Label` or a `TextEdit` now fills the PRIMARY
selection, and middle-clicking a `TextEdit` pastes it. This is what all
other X11 and Wayland applications do.
Testing: `tests/egui_tests/tests/test_primary_selection.rs`. Publishing
from labels and text edits, no publishing of passwords, no re-claiming
of an unchanged selection, nothing at all on non-PRIMARY platforms,
pasting at the click position rather than at the cursor, and no paste
from a click outside the widget.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Currently `ImageCrateLoader` spawns threads to load every image, while
`WebPLoader` doesn't.
This is particularly bad since the webp loader is used for animations,
which take longer than normal images and can *really* stall the app on
load.
* Related to <https://github.com/emilk/egui/issues/5375>
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Follow-up to #8316. cc @vlansco
#8316 made `warn_if_rect_changes_id` skip the warning whenever a new ID
at a rect had existed *anywhere* in the previous pass. That also
silences the bug class the warning exists to catch (#8092, #8084): when
one hidden auto ID gets consumed, every following widget in the same
parent keeps its rect but takes the ID of its next sibling. Each of
those new IDs "existed elsewhere last pass", so nothing was reported.
### The fix
Now we only skip if the widget's **previous rect is now vacated** (or it
came from another layer). That is a real move into a freed slot, which
is the case #8316 was after. If the old rect is still taken by some
other ID, nothing moved. The IDs just shifted, so we warn.
Trade-off: if a keyed list removes one item, and every row after it
shifts up by exactly one row height, the result looks the same as an
auto-ID shift. That case warns again, just as it did before #8316.
`Context::allow_widget_id_changes_in` covers it.
### Test
Added `rect_id_change_warns_for_auto_id_shift`: 5 widgets in one parent,
IDs shifted by one, same rects. It fails on `main` and passes here. The
existing tests from #8316 still pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`FileLoader` strips the `file://` prefix but does not percent-decode the
path,
so a `file://` URI with an escape such as `%20` fails to resolve on disk
(the
function's own doc noted "percent un-encoding are not handled").
`convert_uri_to_path` now decodes `%XX` escapes (e.g. `%20` -> space)
after
stripping the scheme, using a small inline decoder so no dependency is
added.
Invalid or truncated escapes are left as-is, and UTF-8 multibyte
sequences round
-trip. Added `%20` cases to the existing `check_convert_uri_to_path`
test for
both the Windows and non-Windows paths.
`cargo test`, `clippy`, and `fmt` pass for `egui_extras`.
Closes#8288
---------
Signed-off-by: 林晨 (Leo Cheng) <leo-cheng@vip.qq.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
I have found that `ui.horizontal(...).response.on_hover_text(...)`
doesn't work, although [documentation says
otherwise](https://docs.rs/egui/latest/egui/struct.Ui.html#method.horizontal):
> The returned
[Response](https://docs.rs/egui/latest/egui/response/struct.Response.html)
will only have checked for mouse hover but can be used for tooltips
(on_hover_text). It also contains the
[Rect](https://docs.rs/egui/latest/egui/struct.Rect.html) used by the
horizontal layout.The returned
[Response](https://docs.rs/egui/latest/egui/response/struct.Response.html)
will only have checked for mouse hover but can be used for tooltips
(on_hover_text). It also contains the
[Rect](https://docs.rs/egui/latest/egui/struct.Rect.html) used by the
horizontal layout.
So I added fix to this into tooltip, but this looks like a hack, and I
don't know a better place to do so. Do you have a better idea?
This PR is split into two commits: first commit adds test, and you can
see wrong behaviour in the image. Second commit fixes this and also
regenerates the image and you can now see tooltip.
I have used LLM to help me with this PR (codex). And I retained LLM
comment, because such hacks should have some comments, and it seems ok
to me.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Fix: TextFormat::valign not applied correctly with custom line_height
This PR resolves an issue where `TextFormat::valign` (vertical
alignment) was not correctly applied when `TextFormat::line_height` was
explicitly set, especially when it matched the `max_row_height`.
**Problem:**
The vertical positioning logic in `galley_from_rows()` calculated
`valign` based on the difference `(max_row_height - glyph.line_height)`.
When `glyph.line_height` was explicitly set to a larger value (e.g.,
`row_height + 6.0`) and became the `max_row_height` for the row, this
difference became zero. As a result, `valign` had no effect, and glyphs
consistently appeared top-aligned within the allocated line space.
**Solution:**
The `valign` calculation is now updated to use `glyph.font_face_height`
instead of `glyph.line_height`. This correctly applies vertical
alignment to the glyph's actual visual content within the
`max_row_height`, ensuring `Align::Center` and `Align::Max` (Bottom)
settings function as intended for customized line heights.
---------
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!
* Keep PR description short and to the point! No long LLM slop.
* 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 a human has reviewed it
* 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/8546>
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Closes#4173.
## The problem
`change_gl_context` takes an early-out when the surface is already
current, but that early-out is disabled on Windows for **every** case:
```rust
if !cfg!(target_os = "windows") {
// According to https://github.com/emilk/egui/issues/4289
// we cannot do this early-out on Windows.
```
So on Windows every frame pays `make_not_current` + `make_current` — two
`wglMakeCurrent` calls — whether or not anything changed.
## What this changes
`change_gl_context` takes a `single_viewport: bool`, and the early-out
is allowed on Windows when it is true. Call sites compute it as
`viewports.len() == 1`.
With a second viewport present the condition is false and the old path
runs unchanged, so the corruption #4289 describes is out of scope by
construction. On the `render_immediate_viewport` path the count is two
or more by definition, so it is never affected.
## Measurements
Windows 11, RTX 3060 (NVIDIA 610.74), release builds, window focused,
pointer parked off the window.
A minimal app — one viewport, one label, `request_repaint()` every
frame, i.e. the "light application" from #4173:
```rust
fn main() -> eframe::Result {
eframe::run_ui_native("bench", eframe::NativeOptions::default(), |ui, _frame| {
ui.label("bench");
ui.ctx().request_repaint();
})
}
```
Interleaved runs, alternating binaries, 8 s samples, process CPU:
| | runs (% of one core) | median |
|---|---|---|
| `main` | 104.1, 100.0, 97.0, 96.0, 98.3 | **98.3** |
| this PR | 2.3, 10.1, 20.2, 9.3, 3.1 | **9.3** |
The two sets do not overlap. `main` sits at a whole core for an
application that draws one label.
Per-scope numbers from a real application (a launcher with an animated
panel, 30 fps, `profiling` + puffin), µs of CPU per frame:
| scope | `main` | this PR |
|---|---|---|
| `change_gl_context` | 550 | **1.0** |
| egui pass (incl. the app's own `ui`) | 152 | 141 |
| `swap_buffers` | 21 | 103 |
| `update_viewport_info` | 65 | 65 |
| tessellation | 59 | 53 |
| painting | 26 | 22 |
| **whole frame** | **917** | **422** |
`swap_buffers` rises because it now does the waiting the context switch
used to absorb — a blocking wait, not work. That application went from
7.4–11.3% to 2.7–3.5% of one core.
## Checking #4289 has not come back
`examples/multiple_viewports`, forced onto the glow backend, with both
the immediate and the deferred child viewport opened from the first
frame, plus a temporary log of which branch `change_gl_context` took:
```
Count Name
----- ----
22 single_viewport=false <- three viewports open: old path
1 single_viewport=true <- first frame, before the children exist
```
Ran for 7 s with three viewports, no crash, no flicker, nothing on
stderr. Neither the log nor the forced-open change is part of this PR.
## Caveats, honestly
- One machine, one GPU vendor, one Windows version. I have no way to
test AMD or Intel, or an older Windows.
- In #4173, @gerwin3 reported that #4284 — which enabled this early-out
without the viewport-count guard — "did not noticeably decrease" CPU for
them. I cannot reproduce that; the numbers above are unambiguous on my
machine. My guess is that they were also hitting a second, unrelated
thing: `Context::request_repaint_after` subtracts `predicted_dt`, and
nothing in `egui-winit` ever assigns it, so it stays at its `1/60`
default and a throttled application still runs at vsync (see #5353).
That would put a `hello_world` measurement at the display's rate
regardless of this change. That is a hypothesis, not something I have
verified on their setup.
- `cargo fmt --all --check` is clean and `cargo clippy -p eframe
--no-default-features --features glow,default_fonts --all-targets`
reports no warnings. I have not run the full `scripts/check.sh` (wasm
toolchain).
Happy to reshape this if you would rather thread the flag differently,
or gate it on something other than the viewport count.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
* [x] I have followed the instructions in the PR template
`DeviceEvent::MouseMotion` is device-level relative motion with no
window attached. eframe used to deliver it only to the focused viewport
and dropped it when no viewport was focused. That made raw mouse deltas
unusable in multi-viewport apps on Wayland, where another viewport can
steal focus while the pointer stays over the window the user is
interacting with.
Now eframe delivers it to the focused viewport if that viewport has the
pointer or an active drag, and otherwise to the first viewport that
does. Focus is no longer required. Only that window is repainted. glow
and wgpu behave the same.
egui-winit's guard from #7866 is kept: motion is still ignored unless
the pointer is in the window or a button is held, so moving the mouse
elsewhere on screen doesn't cause repaints.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
* Fixes#8522
* [x] I have followed the instructions in the PR template
After a restart, a `Window`'s `AreaState` is restored with its position
but without its size, because the size isn't persisted. The first frame
is an invisible sizing pass that uses a guessed size, and that pass
constrained the window to the screen with the guessed size. So a window
that was shrunk and placed against the right or bottom screen edge got
pushed inward, and the next frame, which knows the real size, couldn't
undo it.
Now `Area` skips the screen constraint on that one sizing pass after a
restart. The position was already constrained last session, and the next
frame constrains it again with the real size, so the window still stays
on screen if the screen got smaller. Size is still not persisted, so the
saved-memory format is unchanged.
Tests:
* `shrunk_window_keeps_its_position_after_restart` covers the case from
#8522.
* `windows_keep_their_position_after_restart` covers default,
`auto_sized`, `resizable(false)`, collapsed and large-`default_size`
windows across a restart.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
-->
* [x] I have followed the instructions in the PR template
Factor out common constants to reduce the number of multiplications.
During checking the results (comparing my hand-optimized code with the
previous version using AI), the AI pointed out that d is always 0, so I
removed that as well.
It is possible to rewrite the equations a bit further, like:
`let p = (a / c - h * h);`
but not sure that it will be beneficial (on superscalar machines) due to
introduced data dependencies.
I also have a doubt what will happen when p = 0. I think it may have to
be special cased as p = 0 => r = 0 => division by zero (before taking
acos).
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
`CollapsingHeader::custom(ui, id_salt, add_header, add_body)` is a
convenience wrapper around `CollapsingState::show_header(…).body(…)`,
letting you put arbitrary widgets (checkboxes, buttons, …) in the header
of a collapsing section without manually managing a `CollapsingState`.
The "Custom Collapsing Header" demo in the misc demo window now uses it.
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
The gap between a menu and the submenu it opens is hardcoded in
`SubMenu::show`
(`frame.total_margin().sum().x / 2.0 + 2.0`), so a submenu can't be
placed flush
against its parent — even with a zero menu margin a ~2px gap remains.
This adds an optional `submenu_gap: Option<f32>` to `MenuConfig` (plus a
`MenuConfig::submenu_gap(f32)` builder). When set it overrides the
offset; when
`None` it falls back to the existing computed value, so existing menus
are
unaffected. Because a menu's `MenuConfig` propagates to its submenus,
setting it
once on the root menu applies to the whole tree.
```rust
egui::MenuButton::new("Add")
.config(egui::MenuConfig::new().submenu_gap(0.0)) // flush submenus
.ui(ui, |ui| { /* … */ });
```
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
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!
* Keep PR description short and to the point! No long LLM slop.
* 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 a human has reviewed it
* 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 adds an iterator (and its mutable counterpart) to IdTypeMap, which
filters for all values of the given type. Serialized / not yet
deserialized values are ignored.
It also introduces two helper functions to assert the contents of an
iterator and adds them to the relevant test cases.
* Closes <https://github.com/emilk/egui/issues/8323>
* [X] I have followed the instructions in the PR template