A drag value is not a widget of its own: it shows a `Button` while you drag
it and a `TextEdit` while you type into it. A `StyleProvider` asked to style
either had no way to tell it apart from a stand-alone one, so it could not
style the two states as one control.
Set `egui::drag_value` on the drag value itself; it already forwards its
classes to both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`TextEdit` unpacked `AtomLayoutStyle` and re-applied the pieces by hand,
which silently dropped `align2` and `image_tint` and made a per-widget gap
lose to the theme's. Go through `AtomLayoutStyle::apply`, like `Button` and
`Checkbox` do, and add a `prefix_suffix_color` so the atoms around the input
keep the general text color rather than the text edit's own.
Tidy the provider while here: use `Widgets::state`, split the read-only
`fill`/`stroke` decision in two, and take the expansion correction from
`Frame::expand_in_place`.
Drop `TextVisuals::from_style`, `UiStack::inherited`, `StyleArgs::inherited`
and `HasClasses::last_class`, none of which have callers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AccessKit asks for focus by node id, and `Focus::interested_in_focus` only picks
the request up once the widget itself runs. Everything that reads the focus
before the widget — styling it, for one — therefore saw the old focus, so a text
edit was styled unfocused in the very pass it got focused, and lost its focus
ring for a frame.
AccessKit names the widget by the node id it read from the last tree we sent, so
the widget has been here before and is in `focus_widgets_cache`: `begin_pass` can
hand out the focus right away, the same way a `request_focus` from the last pass
is handed out. Memory then holds the focus before any widget runs, and
`Ui::widget_style` needs to know nothing about focus at all.
For the same reason the request never has to outlive the pass it arrives in — a
widget that has not run yet cannot have been named by it — so the pending
`id_requested_by_accesskit` field becomes a local.
The four `text_edit_*` snapshots gain a caret along with the ring, because the
field is now focused for real in that pass rather than a pass later.
Also drops `fallback_gap` from `TextEdit`, which has no `gap` of its own to defer
to, and moves the `DragValue` `HasClasses` impl above its test module.
AccessKit asks for focus by node id, and `Focus::interested_in_focus` only picks
the request up once the widget itself runs. Everything that reads the focus
before the widget — styling it, for one — therefore saw the old focus, so a text
edit was styled unfocused in the very pass it got focused, and lost its focus
ring for a frame.
A widget in `focus_widgets_cache` has asked for focus in an earlier pass, so it
is one we can hand focus to in `begin_pass`, the same way a `request_focus` from
the last pass is handed out. Memory then holds the focus before any widget runs,
and `Ui::widget_style` needs to know nothing about focus at all.
`interested_in_focus` keeps the same grant as a fallback, for the first pass a
widget is focusable in, and the two share `take_focus_from_accesskit`.
The four `text_edit_*` snapshots gain a caret along with the ring, because the
field is now focused for real in that pass rather than a pass later.
Also drops `fallback_gap` from `TextEdit`, which has no `gap` of its own to defer
to, and moves the `DragValue` `HasClasses` impl above its test module.
`ButtonStyle` carried a `min_size` and a gap; `TextEditStyle` carried
neither, so a themed text edit could set its padding but not its height or
the gap to its prefix — the height had to travel separately on the widget,
and the gap fell back to the ambient `spacing.icon_spacing`.
A theme decides the two together, so they move into a `LayoutStyle` that
both styles embed. `TextEdit` now honors both, with defaults that keep
today's rendering: no floor of egui's own, and the gap `AtomLayout` was
already falling back to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`AtomLayout::gap` is an `Option`, where `None` means "use the theme value".
`AtomLayoutStyle::apply` set it unconditionally, so it clobbered the gap a
widget had asked for. `DragValue` sets `gap(0.0)` to keep its prefix, value and
suffix flush, and lost it, which widened every `DragValue` in the demos.
`apply` now fills the gap in only when the widget left it unset. This also
reverts the snapshot updates that had captured the wider gap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* `Frame::invisible` needs `#[inline]` like every other builder method.
* `TextVisuals::new` took a `TextStyle` by value but only borrowed it. Take
an `impl Into<FontSelection>` instead, which consumes it and lets a theme
hand over a `FontId` directly.
* A hovered small button now paints its border a pixel further out, since
its padding is zeroed before the expansion correction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A `StyleProvider` reads a class either off the widget or off the `Ui`s it
sits in — the equivalent of a descendant selector. Give it one call that
covers both, next to the classes it queries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The expansion/stroke correction on a widget's margins was written out by
hand. Move it into `Frame::expand_in_place`, which knows the frame's own
stroke width, so a caller states the intent instead of the arithmetic.
`Frame::invisible` goes with it: a frame that keeps its layout but drops
its paint.
Two fixes fall out of it in the button style:
* A small button zeroed its vertical padding after the correction, which
left the frame a `stroke.width - expansion` tall. Zeroing before the
correction makes it exactly zero in every state, which is what "must
not add any height" means.
* The invisible frame of a button that hides its frame when inactive
dropped the outer margin and the stroke width, so it was not, in fact,
as big as the painted one. It now keeps the whole frame and only drops
the paint.
Also give `TextVisuals` a `new` and a `from_widget_visuals`, replacing the
local `text_visuals` helper, and add `HasClasses::add_classes` and
`HasClasses::with_classes`.
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!
-->
`constrain_to` overrides `constrain`, so I adjusted it to call
`constrain_to` before `constrain`.
* [x] I have followed the instructions in the PR template
Signed-off-by: limo520 <247564335@qq.com>
The Egui web example was throwing an error in WGPU. In this case, the
error was
```
panicked at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-30.0.0/src/backend/wgpu_core.rs:1414:26: wgpu error: Validation Error Caused by: In Device::create_render_pipeline, label = ‘egui_pipeline’ In the provided shader, the type given for group 1 binding 2 has a size of 4. Since the device does not support DownlevelFlags::BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED`, the type must have a size that is a multiple of 16 bytes
```
One reference I used was the following Pull Request:
<https://github.com/viridia/quill/pull/14>
The biggest change I made was in `egui-wgpu` to
`r_tex_nearest_filtering`
Shader (`egui.wgsl`)
```wgsl
// Old
@group(1) @binding(2) var<uniform> r_tex_nearest_filtering: u32;
// New
@group(1) @binding(2) var<uniform> r_tex_nearest_filtering: vec4<u32>;
```
And with the corresponding changes in `renderer.rs`, I ran it locally
and it worked perfectly in Firefox 149 and Chromium 151 (Both fail when
attempting to access https://www.egui.rs/#demo, returning the error
described above)
* Closes <https://github.com/emilk/egui/issues/8446>
* Fixes <https://github.com/emilk/egui/issues/8446>
* [X] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fix a build issue in the `egui-wgpu` crate when building for the
`wasm32-unknown-emscripten` target. WGPU does not support the WebGPU
backend on that target.
## Problem
`ViewportBuilder::with_monitor(idx)` is a **no-op on the glow backend**.
A window
built with `.with_monitor(...)` opens on the default monitor, not
fullscreen —
while the exact same code works on wgpu.
The monitor → `Fullscreen::Borderless(Some(monitor))` resolution only
lives in
`egui_winit::create_window`, which the **wgpu** backend uses. The
**glow**
backend builds its windows straight from
`egui_winit::create_winit_window_attributes`
at two sites in `glow_integration.rs` — the initial window and
per-viewport
deferred windows — so the monitor is never applied there.
## Repro
A multi-viewport app on the glow renderer with any viewport built via
`ViewportBuilder::default().with_monitor(1)` opens that window on
monitor 0,
un-fullscreen. Switching to the wgpu renderer places it correctly. Seen
on a
3-monitor GNOME/Wayland setup where the main window and secondary
viewports all
need to land borderless-fullscreen on specific outputs.
## Fix
Extract the resolution into a public
`apply_monitor_to_window_attributes` in
`egui-winit` and call it from `create_window` (unchanged behaviour) plus
both
glow window-creation sites, so `with_monitor` behaves the same on glow
and wgpu.
No public API change beyond the new helper; no behaviour change on wgpu.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fix transparent child viewports on `Windows` with `glow`
* Closes#3632
* Related #4451
* Related #5072
* Closes#7543
* Related #8116
Transparent native child viewports could become opaque on Windows when
the
selected GL config reports that it does not support transparency.
`glutin_winit::finalize_window` clears the native transparent window
attribute
in that case. However, on affected Windows GL paths, transparent native
windows
and their GL surfaces still composite correctly.
This change preserves the transparent window attribute for explicitly
transparent non-root viewports on Windows by creating those windows
directly.
Tested manually on Windows with glow:
- root transparent viewport
- deferred native child viewport
- immediate native child viewport
<!--
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!
-->
# What it does
Addition of a new system of theme plugins which allow the user to use
different rules engine to compute the style for the available
specialised widget style.
# How to use
Create a engine implementing the trait `ThemePlugin` and `ThemeStyle<S:
StyleStruct>` and implement the necessary methods, then register this
way (example for `ButtonStyle`):
```ui.add_theme::<ButtonStyle>(&mycustomengine);```
Now all button will call the `ThemeStyle<ButtonStyle>` method to compute the correct style and later use the cached value to avoid the costly computation.
If no valid `ThemeStyle<S>` or engine is available then it fallback to the default style.
* Closes part of <https://github.com/emilk/egui/issues/3284>
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: adrien <221212@umons.ac.be>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
So I first noticed that this function was using a large lookup table
behind a OnceLock. I initially thought about just making it a const, but
when looking at things further. I realized it could be made much
simpler.
If we just treated the numbers as fixed point we can get rid of any of
the floating point calculations and especially divisions. You can see
how efficiently this can compile down here:
https://llvm.godbolt.org/z/K83jEjvdq
You can see all three versions here: https://godbolt.org/z/nWc1as1nq
* First one is basically the original essentially being called by:
`ColorImage::from_rgba_unmultiplied()`
* Second is the const Lookup table instead of the OnceLock and runtime
generation.
* Third is the fixed point implementation.
At least looking at the bytes reported compiler explorer the OnceLock
and the const Table results are in similar size, and the const table is
surprisingly smaller when I compile to a binary object in compiler
explorer. However though the oncelock is producing a lot SIMD
instructions for initialization so I guess not too surprised. The fixed
point math is much smaller than both.
The const table is probably faster, but does bloat the binary images,
and again when it's this fast to compute:
https://llvm.godbolt.org/z/K83jEjvdq I am not sure the extra bytes are
worth it.
Next, what I did was merge `from_rgba_unmultiplied` and
`from_rgba_unmultiplied_const`. Moreover with the fixed point math the
`from_rgba_unmultiplied_const` is probably not necessary anymore, but
it's part of the public API so I left it. Lastly, I just added a sanity
test to make sure the math checks out which it does. You can even sweep
the 2^16 inputs to be sure.
* [x] I have followed the instructions in the PR template
Remove clippy lint groups already covered by `all` (`complexity`,
`perf`, `suspicious`, and the misspelled `correctnesss`), and two
individual lints covered by the `cargo` group (`negative_feature_names`,
`wildcard_dependencies`).
Add `publish = false` to the two internal crates that were missing it
(`popups`, `egui_tests`), which silences the
`clippy::cargo_common_metadata` warnings.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Two small documentation typos noticed while reading the docs:
- `crates/egui_extras/README.md`: "adds some features on top top of" ->
"adds some features on top of"
- `README.md`: "check out the [the egui web demo]" -> "check out [the
egui web demo]" (removes the duplicated "the"; link text and target
unchanged)
Documentation only; no code changes.
Instead of opting in to pedantic lints one by one, enable the whole
group and opt out of the noisy ones.
64% of the pedantic lints were already listed individually. This deletes
90 explicit lint lines, enables 51 pedantic lints we never listed, and
picks up new pedantic lints for free. Each opt-out carries its hit
count, so the cost of turning one back on is visible.
`restriction` and `nursery` stay opt-in per lint.
Stacked on top of #8430, which fixes the one real bug the new lints
found.
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`image.PNG` and `cat.SVG` were not recognized as images.
Adds `egui::load::has_extension(uri, extension)`, which ignores ASCII
case and any `#fragment`, and uses it for the `.svg`, `.gif`, `.webp`
and `.png` checks.
Note: gif/webp URIs like `a#b.gif` no longer match, since the fragment
is now excluded.
* [x] I have followed the instructions in the PR template
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Closes <https://github.com/emilk/egui/issues/8339>
* [x] I have followed the instructions in the PR template
## The bug
While a `DragValue` has focus it is rendered as a `TextEdit`, and the
text being
edited is stored in `Memory::data` between frames. That is needed so
that
half-finished input such as `"1."` or `"-"` isn't thrown away just
because it
doesn't parse to the current value.
The stored text was only discarded when the widget *gained* focus or
when the
widget itself changed the value. If something else changed the value
while the
`DragValue` was focused, the stored text was kept, shown to the user,
and
written back to the value when focus was lost — silently undoing the
external
change:
```rust
ui.add(egui::DragValue::new(&mut self.value));
if ui.button("increment").clicked() {
self.value += 1;
}
```
Click into the `DragValue` so it has focus, then press "increment": the
value
goes up for one frame and then snaps back. `Slider` shows the same
behaviour,
since it uses a `DragValue` for its value field.
## The fix
Store the value the text belongs to next to the text, and discard the
text when
the value no longer matches it. The remembered value is read back from
the
get/set closure *after* the widget has applied its own edits, so a
change the
widget made itself never looks like an external one — this matters for
values
that can't represent what was typed, e.g. `"12.5"` in a
`DragValue<i32>`.
This keeps the reason the text is stored in the first place intact: as
long as
nothing else touches the value, the text the user is typing is preserved
verbatim.
## Tests
Three tests in `crates/egui_kittest/tests/regression_tests.rs`:
* `drag_value_should_not_revert_external_changes_while_focused` — the
actual
regression. Fails on `main`:
```
---- drag_value_should_not_revert_external_changes_while_focused stdout
----
assertion `left == right` failed
left: Some("0")
right: Some("42")
```
and, with the display assertion removed so the test reaches the blur, on
the
value itself:
```
assertion `left == right` failed
left: 0
right: 42
```
* `drag_value_should_keep_text_while_typing` and
`drag_value_should_keep_text_the_value_cannot_represent` — guards for
the
behaviour the stored text exists for. Both pass on `main` and after the
fix,
and both fail if the text is re-read from the value too eagerly.
`cargo test -p egui_kittest` and `cargo test -p egui` pass, as do
`cargo fmt --all --check`, `scripts/lint.py` and
`cargo clippy -p egui -p egui_kittest --all-targets --all-features -- -D
warnings`.
## Not changed
`DragValue` still ignores the stored text when <kbd>Escape</kbd> is
pressed, and
`update_while_editing` still decides when typed text is applied —
neither is
touched here.
### Summary
When `title_frame` is not explicitly set, fall back to `window_frame`
instead of `Frame::window(&style)`.
### Motivation
If a custom `frame` is provided for a window, `window_title_frame`
should maintain visual consistency with it by default unless a separate
`title_frame` is specified.
* Related #8154
* Related #8353
* [x] I have followed the instructions in the PR template
## Summary
- Add an opt-in `Popup::sizing_pass(bool)` builder for remeasuring a
popup whose contents change while it remains open.
- Preserve the automatic first-open/reopen sizing pass and all existing
default behavior.
- Add a headless regression covering growth to a capped scroll viewport
with overflowing content remaining scrollable.
## Why this is necessary
A continuously open popup can first shrink around a short result set and
later receive more content, such as an autocomplete after its query
changes or a “show more” action. The cached `Area` height constrains the
`ScrollArea` input size, so `ScrollArea::max_height` can cap the
viewport but cannot make the containing popup grow again.
PR #8315 taught `Popup` to rerun its sizing pass after closing and
reopening. That fixes the same cached-size feedback loop when
`was_open_last_frame` is false, but a continuously open popup keeps that
value true while its contents change. In that case the caller is the
component that knows the cached natural size is stale.
This API exposes the existing one-frame `Area` sizing mechanism through
`Popup`. It is additive, defaults to false, and combines with the
automatic reopen pass, so unrelated popups, menus, tooltips, and areas
keep their current behavior.
<!--
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
Closes#8326
`check_redraw_requests` switched the event loop to `ControlFlow::Poll`
every time it called `request_redraw`, and only ever restored a sleeping
control flow when a *timed* repaint was still pending. Once the last
scheduled repaint had been consumed the `Poll` was never undone, so the
loop kept spinning.
This is most visible on Wayland, where `RedrawRequested` is only
delivered after the compositor sends a frame callback: between the
request and the callback eframe burns 100% of a CPU core, so simply
moving the mouse over a reactive app pegs a core.
`request_redraw` already wakes the event loop on its own, so the `Poll`
is not needed. Drop it, and always set an explicit sleeping control flow
at the end of `check_redraw_requests`: `WaitUntil` for the earliest
scheduled repaint, `Wait` when nothing is scheduled.
**Measured effect of this patch**
Two byte-identical eframe apps (a 400-row scrolling page, free-running
at 60fps), toggling only whether `eframe` resolves to stock 0.36.0 or
this patch. Same machine and session, native Wayland (niri,
wgpu/Vulkan). Whole-process CPU is `utime+stime` from `/proc/self/stat`,
so it counts every thread — what a system monitor sees.
| configuration | whole process |
|---|---|
| eframe 0.34.3, Wayland | ~12% of a core |
| stock 0.36.0, Wayland | **99–100% of a core** |
| stock 0.36.0, XWayland (same binary) | ~15% of a core |
| **0.36.0 + this patch, Wayland** | **15–16% of a core** |
The patch restores the 0.34 baseline and matches the XWayland figure for
the same binary — ~6.5× less CPU — with frame delivery unchanged at
60fps.
Two details worth noting: the same 0.36 binary is already fine on
XWayland, so this isn't application repaint behaviour; and the per-frame
*closure* cost rises slightly (1.55 to 2.15 ms) because those frames now
run on a CPU that isn't being held at max clocks by the spin loop.
* [X] I have followed the instructions in the PR template
30 lints in `[workspace.lints]` were set to `warn` despite already being
warn-by-default, or part of a lint group that is already enabled.
* `rust`: `elided_lifetimes_in_paths` and `unused_extern_crates` are in
`rust_2018_idioms`; `semicolon_in_expressions_from_macros` is in
`future_incompatible`; `unexpected_cfgs` and `unsafe_op_in_unsafe_fn`
are warn-by-default (the latter since edition 2024);
`rust_2021_prelude_collisions` never fires on edition 2021+.
* `rustdoc`: `broken_intra_doc_links` and `missing_crate_level_docs` are
in `rustdoc::all`.
* `clippy`: 22 lints that are in `clippy::all`.
The explicit `allow`s are kept, even though they are no-ops today, to
record our intent in case we ever enable `pedantic`.
No behavior change: `cargo clippy --all-features --all-targets` gives
the same output before and after.
* [x] I have followed the instructions in the PR template
🤖 Generated with [Claude Code](https://claude.com/claude-code)