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>
`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>
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)
* 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>
## 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>
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>
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>
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
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>
* 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>
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>
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>
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>
* [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)
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>
## 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>
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>