1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 12:50:04 -04:00
Commit Graph

2001 Commits

Author SHA1 Message Date
Lucas Meurer
cf4b8de8bd Hand out an AccessKit focus request at the start of the pass
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.
2026-08-25 17:36:16 +02:00
Lucas Meurer
887d9b0415 Give TextEdit a min_size and a gap, via a shared LayoutStyle
`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>
2026-08-25 13:30:18 +02:00
Lucas Meurer
5c08eb5684 Add some more helpers 2026-08-25 09:49:37 +02:00
Lucas Meurer
487c8f1dda Let the theme set a button's atom gap
`Button` hard-coded `spacing.icon_spacing` as the gap between its icon and
its text. A theme that gives a button its own metrics wants to set that gap
too, so it becomes a `ButtonStyle` field, overridden by `Button::gap`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:49:37 +02:00
Lucas Meurer
bf7a71d0da Let the theme decide a button's minimum size 2026-08-25 09:49:36 +02:00
Lucas Meurer
8db6ccad4a Let DragValue set a min_size, and honor TextEdit::min_size height 2026-08-25 09:49:36 +02:00
Lucas Meurer
83fe9e0267 Add StyleProvider support for TextEdit, and classes for DragValue 2026-08-25 09:49:36 +02:00
rustbasic
7aa7f84858 Fix transparent child viewports on Windows with glow (#8423)
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
2026-08-24 12:49:33 +02:00
Emil Ernerfeldt
726b995608 Round the corners of the color picker (#8439)
The color picker's gradients and background checkers now follow the
style's corner radius, giving it a softer look.

This required changing the painting from being Mesh-based to being
texture-based

it's subtle with the default settings:

<img width="286" height="397" alt="Screenshot 2026-08-21 at 15 08 18"
src="https://github.com/user-attachments/assets/3a9d5289-71fb-4f34-8a20-1fe96891ba36"
/>


But you can [increase it](https://github.com/emilk/egui/pull/8445):


<img width="285" height="391" alt="Screenshot 2026-08-21 at 15 08 41"
src="https://github.com/user-attachments/assets/b0dfa7da-5b96-40e0-a80d-1da385d57a1a"
/>


### Before
for reference
<img width="286" height="392" alt="Screenshot 2026-08-21 at 19 08 27"
src="https://github.com/user-attachments/assets/7c754f36-795c-4381-9f9f-1fc8dd9a0264"
/>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 09:28:14 +00:00
Emil Ernerfeldt
a15a7e6611 Increase widget corner radius to 4 (#8445)
A little more round and friendly!

* See also https://github.com/emilk/egui/pull/8439
2026-08-24 11:07:06 +02:00
YouStones
97603fc082 Theme plugin system (experimental) (#8153)
<!--
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>
2026-08-21 13:05:45 +00:00
Emil Ernerfeldt
f5c9373e26 Add epaint::RoundedRect primitive (#8440) 2026-08-21 01:33:54 -07:00
Emil Ernerfeldt
00dc6e814b Make kittest's predictable texture filtering honor TextureOptions::NEAREST (#8441) 2026-08-21 01:33:29 -07:00
Keith
38c4ab7b3d Simplify and optimize Color32::from_rgba_unmultiplied (#8427)
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.
2026-08-20 20:36:39 +00:00
Recoordinate
b9a0723d88 Fix documentation typos (#8404)
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.
2026-08-18 12:52:07 +00:00
Emil Ernerfeldt
34b39d564b Enable the clippy::pedantic lint group (#8429)
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>
2026-08-18 06:55:53 +02:00
Emil Ernerfeldt
9bb36b0ac2 Match image file extensions case-insensitively (#8430)
`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>
2026-08-18 06:41:09 +02:00
Teddy Tennant
d802a982ce Don't revert external changes to a focused DragValue (#8403)
* 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.
2026-08-11 14:04:09 +02:00
rustbasic
3c69fb4833 Fallback window_title_frame to window_frame when unspecified (#8400)
### 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
2026-08-11 14:03:53 +02:00
Vitaly Kravchenko
6d98e1dccb Allow explicit popup sizing passes (#8407)
* [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.
2026-08-11 13:54:44 +02:00
42Pupusas
b42d2ef4f0 Don't busy-loop a CPU core while waiting for a redraw (#8398)
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
2026-08-11 11:52:12 +00:00
Lucas Meurer
46ba6405bf Release 0.36.1 - Fix Sense::drag bug (#8397) 2026-08-07 15:08:38 +02:00
Lucas Meurer
65e827e23a Fix Sense::drag detecting drags when clicking widget above it (#8396)
Co-authored-by: Lucas Meurer <lucas@rerun.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:26:26 +02:00
Emil Ernerfeldt
6aea7eff94 Enable the clippy::std_instead_of_core lint (#8394)
Prefer `core::` over `std::` where either work

* Part of https://github.com/emilk/egui/issues/5735
2026-08-06 13:19:13 +02:00
Lucas Meurer
2397194d5e Release 0.36.0 - Improved mobile keyboard support (#8390) 2026-08-05 10:55:25 +00:00
Emil Ernerfeldt
e37d44ad8a Never run an egui pass when nothing will be shown (#8387)
* Closes <https://github.com/emilk/egui/issues/8266>
* Alternative to #8385

Not the most simple or beautiful code, but it works, and makes sense.

What makes it complex: `app.logic` should still see some input (e.g.
what viewports are visible) and emit some output (e.g. "open this link",
or "focus and repaint").

## TODO
* [x] test multiple viewports

## Clanker says
Instead of teaching egui to skip book-keeping during a pass where no ui
is shown, we simply run no pass at all. Then there is nothing to
special-case: all ui state is left untouched, and the app finds
everything where it left it when the window is shown again.

* New `Context::run_logic(&raw_input, f)`: ticks app logic without a
pass, returning the `LogicOutput` (platform output + viewport commands)
that a pass would otherwise have carried, so e.g.
`ViewportCommand::Focus` still reaches the integration.
* All three eframe backends (glow, wgpu, web) call `run_logic` instead
of `run_ui` when the viewport is minimized/occluded (and has no visible
descendant viewport) or, on web, when the tab is hidden.
* `App::logic` is still called from inside the pass when the window is
visible, so it sees the current frame's input.

While hidden, `run_logic` fills in only the window state
(`RawInput::viewports` / `focused`), so the app can tell that it is
hidden. The ui input (events, time, …) is not interpreted, and is
instead given to the next real pass.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 09:14:35 +02:00
Emil Ernerfeldt
90e03028f7 Fix a few nightly clippy lints (#8388) 2026-08-04 21:40:56 +02:00
Emil Ernerfeldt
5347b0a4ac Fix window with a Grid being widenable but not shrinkable again (#8386)
## Related

* Fixes a regression from #8152
* Part of #2921

Reported symptom: you can widen the Widget Gallery window, but it won't
shrink again.

I'm not sure this fix is the best one, but it does work.

# Claude says
## Cause

A `Grid` gives its **last** column all the available width, so a
width-filling widget in it (`Separator`, `TextEdit`, `ProgressBar`, …)
makes the `Grid` remember a `col_width` that is really just "however
wide we happened to be".

At the start of a resize drag, `Resize` runs a one-frame sizing pass
(#8152) to measure the minimum content width and clamps the drag against
it. But `GridLayout::next_cell` inflated every cell to
`prev_state.col_width`, so the `Grid` reported its previous width as its
minimum — even though it was only offered `min_size.x`. The clamp is a
lower bound, so widening kept working while shrinking was blocked at the
widened width.

## Fix

During an enclosing sizing pass, don't inflate the stretchy last column
to its remembered width, and don't store the measured (narrow) widths.

Minimal repro (fails before, passes after — added as a regression test):

```rust
Window::new("x").default_width(280.0).show(ctx, |ui| {
    egui::Grid::new("grid").num_columns(2).show(ui, |ui| {
        ui.label("Separator");
        ui.separator(); // fills the last column
        ui.end_row();
    });
});
```

`Panel` is unaffected — it clamps only against the user's `min_size`,
with no content-min sizing pass.

* [x] I have followed the instructions in the PR template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
2026-08-04 16:54:06 +02:00
Emil Ernerfeldt
622bbbeccc Add drag-to-open for collapsible panels (#8363)
A fully collapsed `show_collapsible` panel now leaves a thin grab handle
at its fixed edge, invisible until hovered. Dragging it out past
`min_size` (or double-clicking it) reopens the panel. Opt out with
`panel.drag_to_open(false)`.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 09:20:25 +00:00
Emil Ernerfeldt
98eab50577 Treat a press that leaves a widget as a drag (#8365)
A widget that senses both clicks and drags postpones the
click-versus-drag decision until the pointer moves past `max_click_dist`
or is held for `max_click_duration`. But a click has to be released *on*
the widget — so once the pointer leaves, the gesture can only be a drag,
and there is nothing left to wait for.

This matters for widgets thinner than `max_click_dist` (6px), such as
panel resize handles. The pointer leaves such a widget almost
immediately, which hands the hover to whatever is underneath, while
`dragged()` was not true yet. So a handle highlighting on `hovered() ||
dragged()` blinked out mid-gesture.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:09:27 +02:00
rustbasic
78c0e39d1d Fix ScrollArea failure by handling horizontal and vertical scrolling separately in the missing place (#8275)
Fix ScrollArea failure by handling horizontal and vertical scrolling
separately in the missing place

Everywhere in `ScrollArea`, horizontal and vertical scrolling are
handled separately.
However, because there is a single place where they are not handled
separately, when trying to process horizontal and vertical scrolls
independently, one of the dimensions fails to scroll.

This Pull Request ensures that horizontal and vertical scrolling are
handled separately in this area, just like in the rest of the codebase.

* Closes #5289
* Closes #5307
* Closes #8274
2026-08-04 10:10:34 +02:00
rustbasic
5c0b690dab Add extra_text_line_spacing to control vertical spacing between text lines (#8040)
Add `extra_text_line_spacing` to control vertical spacing between text
lines

**Description**
This PR adds a new `Spacing::extra_text_line_spacing` field to control
additional vertical spacing between lines of text.

The spacing is applied to text layout by adjusting
`TextFormat::line_height` based on the font row height plus the
configured extra spacing.

This improves text readability and allows consistent line spacing
customization for widgets such as `TextEdit` and `Label`.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2026-08-04 10:06:47 +02:00
Emil Ernerfeldt
dae9adf307 Rename failed_pixel_count_threshold to max_failed_pixels (#8383)
It was confusing that both tolerances had "threshold" in the name

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 16:02:41 +00:00
Jochen Görtler
49d4befe6b Store web_sys::File inside of DroppedFile (#8354)
* Closes #4654
* Related #4667
* [x] I have followed the instructions in the PR template

This PR avoids materializing the contents of a file that was dragged
into an egui application on the web. It does so by storing the
`web_sys::File` handle directly on WASM.

This breaks the existing API of `DroppedFile` on the web, because there
is no way to retrieve the bytes synchronously form a `DroppedFile`
anymore, forcing handling call sites to become asynchronous.

The native API remains the same.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2026-08-03 15:07:37 +00:00
limo520
2e7a92bc37 Fix incorrect feature name in the code editor demo (#8330)
Change the feature name from syntax_highlighting to syntect.

<!--
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

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2026-08-03 14:51:17 +02:00
Sybrand Aarnoutse
ef846f53e6 Remove dependency on memoffset (#8304)
Hi, I may or may not have used your crate but I'd like to say a quick
thank you for it anyway!
I'm going down the list of reverse dependencies on `memoffset`.

This PR aims to remove the `memoffset` crate from your dependencies.

[`core::mem::offset_of`](https://doc.rust-lang.org/core/mem/macro.offset_of.html)
was stabilised in rustc 1.77 which I believe is at or below your MSRV.

The `memoffset` crate 0.9.1 says that

> If you're using a rustc version greater or equal to 1.77,
> this crate's offset_of!() macro simply forwards to
core::mem::offset_of!().

I consider it very unlikely (see
[here](https://github.com/rust-lang/rust/issues/111839)) for any usage
of the `offset_of!` macro to break but please check anyway.
I hope we can all enjoy the benefits of one less dependency :)

---
<!--
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 *except for
`./scripts/check.sh` which doesn't run in my environment* (I'm unwilling
to chase it down because I'm firing off a whole bunch of these PRs to
various repositories, sorry.)

`cargo clippy` gives 1 unrelated warning.

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2026-08-03 12:37:09 +00:00
Calbabreaker
5f75aa29d3 Remove dependency home (#8307)
Replaces `home::home_dir` with `std::env::home_dir` as these functions
do the exact same thing

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

Removes dependency home from eframe by replacing `home::home_dir` with
`std::env::home_dir`. `home` was probably originally used since
`std::env::home_dir` was once deprecated because of a bug. Post Rust
version 1.87 this has been fixed and now these two functions do exactly
the same thing.

* [X] I have followed the instructions in the PR template

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2026-08-03 12:29:11 +00:00
n4n5
fa608a1b40 Add egui::Window::title_frame (#8353)
* [X] I have followed the instructions in the PR template


Add a way to set the frame for the content and for the title of the
window
- `self.frame` will be used for the margins of the body
- `self.title_frame` will be used for the margins of the header (title)
2026-08-03 14:15:07 +02:00
Emil Ernerfeldt
eba2780dba Fix egui_kittest failing to compile without the wgpu feature (#8381)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 13:58:01 +02:00
oleflb
3d70aa1123 Make wgpu Instance public (#8321) 2026-08-03 11:53:28 +00:00
Emil Ernerfeldt
ddec5f3e4c Fix where Panel puts its separator line, and how much room it reserves (#8382)
Two fixes to the separator line of `Panel` (`resolve_frame` was added in
#8367):

* **Reserve room only when the line is always drawn.** Before,
`show_separator_line || resizable` reserved the line's thickness, so a
resizable panel that opted out still got a permanently visible gap along
its inner edge — space held for a line only drawn transiently, while
hovering or dragging the resize handle.
* **Paint the line outside the frame's outline**, in room reserved in
`Frame::outer_margin` rather than `inner_margin`, so going outwards from
the panel contents you get:

  `contents | inner_margin | stroke | separator line | outer_margin`

Previously the line landed on top of the frame's outline (or outside its
outer margin). Default panels — no stroke, no outer margin — are
unchanged pixel-wise.

Found in the Rerun viewer: the time panel is
`.resizable(true).show_separator_line(false)` and draws its own top
line, so the extra 1pt landed above the top bar's buttons, making them
look 1pt too low.

Tests in `tests/egui_tests/tests/test_panel_separator_line.rs`, both
spanning `show_separator_line` on/off × resize handle hovered/not:
snapshots of a top panel with a garish outline, plus a pixel probe
across the inner edge of a panel on each of the four sides. Both fail on
`main`.

* [x] I have followed the instructions in the PR template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 10:32:56 +00:00
Davy
dcd0c72d53 Fix TextEdit hint text not following horizontal_align/vertical_align (#8332)
## Summary

- Closes #8309
- [x] I have followed the instructions in the PR template

`TextEdit` hint text was always aligned to `Align2::LEFT_TOP`, ignoring
the alignment set via `TextEdit::horizontal_align` / `vertical_align`.
This caused the hint text, the cursor, and the typed text to disagree on
alignment: e.g. a centered `TextEdit` showed a left-aligned hint with a
centered cursor.

The hint text atoms now use the widget's `align`, so the hint matches
the input text alignment. The default `align` is still `LEFT_TOP`, so
multi line text edits (and the default styling) are unchanged.

### Root cause

In `crates/egui/src/widgets/text_edit/builder.rs`, the hint-text branch
hardcoded:

```rust
atoms.push_right(atom.atom_align(Align2::LEFT_TOP));
```

while the input-text branch used `.atom_align(self.align)`. The hint
path now uses `align` as well.

### Drive-by: silence `clippy::unnecessary_wraps` in
`egui_kittest::app_kind`

`AppKind::run` returns `Option<egui::Response>`. The `Option` wrap is
required when the `eframe` feature is enabled (the `Eframe` branch
returns `None`), but `clippy::unnecessary_wraps` fires when
`egui_kittest` is built standalone without the `eframe` feature (e.g.
`cargo clippy -p egui_kittest`). The workspace CI run doesn't hit it
because feature unification via `egui_demo_app` enables `eframe`, but
it's a real annoyance for anyone linting the crate on its own. Added a
scoped `#[cfg_attr(not(feature = "eframe"),
expect(clippy::unnecessary_wraps))]` with an explanatory comment.

## Test plan

- [x] Added `textedit_hint_text_should_follow_text_alignment` kittest
regression in `crates/egui_kittest/tests/regression_tests.rs`. It fails
before the fix (`hint_center_x=24.25` vs `edit_center_x=100`) and passes
after.
- [x] `cargo test -p egui`
- [x] `cargo test -p egui_kittest --all-features --test
regression_tests`
- [x] `cargo clippy -p egui_kittest --all-features --test
regression_tests -- -D warnings`
- [x] `RUSTFLAGS="-D warnings" cargo clippy -p egui_kittest --lib`
(pre-existing `unnecessary_wraps` now silenced)
- [x] `cargo clippy -p egui -- -D warnings`
- [x] `cargo fmt --check`

---------

Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
2026-08-03 08:55:18 +00:00
Emil Ernerfeldt
c676d939ca Report failing pixels by threshold when a kittest snapshot fails (#8360)
When an image snapshot fails, you get the number of pixels differing by
more than the `threshold` you happened to configure — which doesn't tell
you what threshold *would* have passed. So picking
`SnapshotOptions::threshold` / `failed_pixel_count_threshold` is trial
and error, one CI round-trip per guess.

This measures the failing pixel count at a sweep of thresholds (new
public `THRESHOLD_SWEEP`) and includes it in `SnapshotError::Diff`:

```
'sweep_demo' Image did not match snapshot. Diff: 293, …/sweep_demo.diff.png.
  Failing pixels by threshold: 0.0: 1522, 0.1: 1522, 0.2: 293, 0.4: 293, 0.6: 293, 1.0: 293, …
  Run `UPDATE_SNAPSHOTS=1 cargo test --all-features` to update the snapshots.
```

The sweep only runs for snapshots that already failed, so passing tests
are unaffected.

Breaking: `SnapshotError::Diff` gained a `failing_pixels_by_threshold`
field.

* [x] I have followed the instructions in the PR template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 10:08:09 +02:00
Emil Ernerfeldt
967aa1137a Re-add Visuals::clip_rect_margin as a deprecated no-op (#8380)
Follow-up to #8366, which removed `Visuals::clip_rect_margin` outright

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 07:57:35 +00:00
Lucas Meurer
998b413739 Sync window theme with egui theme (#8299)
Adds a new option to sync the window theme with the egui theme, enabled
by default. Works across viewports.

 


https://github.com/user-attachments/assets/513c2318-cd6e-4e2b-805d-04002c375a10

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 09:40:29 +02:00
Calin P
a80ed6bab7 Eframe: make webbrowser dependency optional (#8372)
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* Closes #8371
* [x] I have followed the instructions in the PR template

Adds a `link` feature to eframe to allow disabling links on egui-winit.
The feature is enabled by default so nothing changes for existing users
of eframe.
2026-08-03 09:00:02 +02:00
Emil Ernerfeldt
65109a0da0 Update crates (#8379)
Routine dependency update.

Updated: `font-types` 0.12, `harfrust` 0.12, `jiff` 0.2.35, `open` 5.4,
`pollster` 1.0, `rand` 0.10.2, `self_cell` 1.3, `skrifa` 0.44, `tokio`
1.53, `toml` 1.1, `vello_cpu` 0.1.
2026-08-03 06:38:37 +00:00
Emil Ernerfeldt
cb2b306f11 Add LayoutJob::clear (#8376)
## Summary

- Add `LayoutJob::clear` to reuse layout settings while rebuilding text.
- Cover preservation of every layout setting.

## Test

- `cargo clippy -p epaint --all-features --all-targets`
- `cargo test -p epaint --all-features`

* [x] I have followed the instructions in the PR template
2026-08-02 19:26:27 +00:00
Emil Ernerfeldt
7f30623cff Add WidgetText::size (#8377)
## Summary

- Add `WidgetText::size` for sizing plain, rich, and layout-job text
uniformly.
- Preserve already-laid-out galleys.

## Test

- `cargo clippy -p egui --all-features --all-targets`
- `cargo test -p egui --all-features`

* [x] I have followed the instructions in the PR template
2026-08-02 19:25:32 +00:00
Emil Ernerfeldt
7fd54ef741 Add BoxedWidget: dynamically dispatched widgets (#8378)
## Summary

- Add `BoxedWidget` and `Widget::boxed` for heterogeneous widget
collections.
2026-08-02 19:22:59 +00:00