The font-shaping and SVG crate families can't be bumped right now
without splitting a transitive crate into two versions in `Cargo.lock`.
Every blocker is an upstream crate that hasn't caught up, so this PR
just records the reasons inline in `Cargo.toml` instead of forcing
duplicates.
## Blockers
| Crate | Wanted | Blocked by |
|---|---|---|
| `skrifa` 0.42 → 0.43 | | `vello_cpu` → `glifo` 0.1.1 pins `skrifa
^0.42` (no newer `glifo`) |
| `font-types` 0.11 → 0.12 | | same `glifo` chain pins `font-types` 0.11
|
| `harfrust` 0.7 → 0.10 | | needs `read-fonts` 0.40 / `font-types` 0.12;
`glifo` pins 0.39 / 0.11 |
| `resvg` 0.45 → 0.47 | | `winit` 0.30's `sctk-adwaita` stuck on
`tiny-skia ^0.11`; resvg 0.47 needs 0.12 |
| `image` 0.25.6 → 0.25.10 | | needs `png` 0.18, which only matches
resvg once resvg is on `tiny-skia` 0.12 |
On `main`, epaint and `glifo` *share* `skrifa` 0.42 / `font-types` 0.11,
so there is a single copy of each. Any epaint-side bump splits them.
resvg 0.46 avoids the tiny-skia split but its `fontconfig-parser`
duplicates `roxmltree`.
Revisit once `glifo` (for the font stack) and `sctk-adwaita` (for
tiny-skia) update.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This allows you to put an `AtomLayout` inside another `AtomLayout`.
Right now this has limited use, but once we add wrapping, vertical
`AtomLayout` and `AtomUi`, this will be a really powerful new layouting
primitive for egui.
Added a test for this in https://github.com/emilk/egui/pull/8221
Hey,
this is my fist PR to fix the bug I just published #8225
I hope it helps.
I tested the fix with wgpu but not glow.
I must say that I am not very happy to have very similar function
is_viewport_or_descendant_visible in both case.
Let me know if you would prefer that I try to factorize it.
* Closes <https://github.com/emilk/egui/issues/8225>
* [X] I have followed the instructions in the PR template
---------
Co-authored-by: Matthieu Casanova <public@kpouer.com>
Label text selection before the fix in a deferred viewport:
<img width="484" height="172" alt="before_the_fix"
src="https://github.com/user-attachments/assets/2214a7d9-9585-497d-9920-dd336a7df7ea"
/>
After the fix:
<img width="484" height="172" alt="after_the_fix"
src="https://github.com/user-attachments/assets/0999ed8e-22d4-4109-a5b5-f468f99e692d"
/>
## What changed
- Keep label text-selection state separate for each viewport.
- Route pass lifecycle and label painting through the current
`ViewportId`.
- Drop inactive per-viewport state after its pass.
- Add a regression test that verifies an unrelated viewport pass cannot
clear a child viewport's
selection, while the owning viewport still clears selections whose
labels disappear.
## Why
Issue #4758 identified that deferred viewports need independent
label-selection state. PR #4760
fixed it by keying the temporary state by viewport. The plugin refactor
in PR #7385 moved that
state into one context-wide `LabelSelectionState`, which accidentally
removed the viewport
isolation. A pass in another viewport then fails to encounter the
selected widgets and clears the
selection.
This restores the behavior of #4760 within the current plugin
architecture. Applications do not
need any special handling.
Less risk of confusing the two.
Found and fix a couple real bugs in the process!
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This PR adds two small additions to `LayoutJob`:
- `LayoutJob::format_at_byte` to query the `TextFormat` of the section
covering a given byte index.
- An optimization to `LayoutJob::append` that merges newly appended text
into the previous section when the format matches and there is no
leading space.
It also documents the `LayoutJob::sections` invariant (sections are
ordered and together cover the whole text with no gaps or overlaps) and
adds `LayoutJob::debug_sanity_check`, which verifies this in debug
builds. It is called from `format_at_byte` and from the text layouter.
## Why the `easymarkeditor` snapshot changed
The `append` optimization changes how many sections a `LayoutJob` ends
up with: consecutive runs of identically-formatted text now collapse
into a single section instead of one section per `append` call. The
easymark editor produces many such adjacent same-format sections, so it
is affected.
This matters because text is **shaped per section**: `layout_section`
runs the shaper once per section, so each section is an independent
shaping run. Merging two adjacent sections into one means the text
across the old boundary is now shaped together as a single run, which
enables cross-boundary kerning (and, in principle, ligatures) that
previously did not happen. Additionally, `extra_letter_spacing` is
skipped before the first glyph of a section, so merging removes a "first
glyph" boundary and lets the spacing apply there.
The net effect is sub-pixel glyph position shifts at the former section
boundaries, which is why `easymarkeditor.png` was regenerated. The new
output is the more correct one — the text is now shaped as the author
wrote it, rather than being artificially split at `append` boundaries.
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces live inspection for running egui apps over a small TCP
request/response protocol, plus the `egui::Plugin` that serves it.
This is the minimal surface to get the egui mcp in, we may want to
extend this in the future to add support for the inspection gui.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Closes https://github.com/emilk/egui/issues/8217
* [x] I have followed the instructions in the PR template
## What
`AllocatedAtomLayout::paint` painted text via
`ui.painter().galley(...)`, so text in atom-based widgets could never be
selected. As discussed in #8217, routing it through
`LabelSelectionState::label_text_selection` unconditionally would break
`Button` / `Checkbox` / `RadioButton` / `TextEdit` (whose labels should
not be selectable, and whose click/drag handling would fight the
selection drag).
So this adds an **opt-in** `AtomLayout::selectable(bool)` (default
`false`). When enabled, the layout also senses clicks and drags
(mirroring `Label::layout_in_ui`) and paints its text through the
label-selection machinery. The underline is `Stroke::NONE`, so when
nothing is selected the painted output is identical to the existing path
— the default (`selectable == false`) behaviour, and existing snapshots,
are unchanged.
## Tests
Two tests in `tests/egui_tests/tests/test_atoms.rs`:
- `test_atom_selectable_senses_click_and_drag` — a `selectable(true)`
layout senses click+drag; the default stays inert.
- `test_atom_selectable_text_can_be_copied` — selecting (drag) the text
of a selectable layout and copying yields the text via
`OutputCommand::CopyText`, while a non-selectable layout yields nothing.
## Notes
- Verified locally: `cargo fmt --all -- --check`, `cargo clippy -p egui`
(clean), and the two new tests pass. I haven't run the full
`./scripts/check.sh` (wasm/typos) locally, hence opening as a draft.
- This only adds the opt-in API; no existing widget is made selectable.
Happy to wire it up on a specific widget and/or add an `egui_demo_lib`
demo if you'd like — just let me know.
**fix(web): prevent entire page from scrolling out of view in Chrome
(WASM)**
* Closes#7887
**Problem**
When using `egui` on the web, the browser (especially Chrome)
occasionally triggers an unwanted page-level scroll. This happens
because the hidden input element (text agent) used for IME/text input is
sometimes positioned outside the visible bounds of the canvas, causing
the browser to "scroll it into view."
**Solution**
I modified the `move_to` function in the web's `text_agent` to ensure
the input element's position stays within the canvas height. By clamping
the `top` property between `0.0` and `canvas_height`, we prevent the
browser from incorrectly scrolling the entire page when the text agent
moves.
- **Specific change:** Applied `clamp(0.0, canvas_height)` to the
`clamped_y` value before setting the CSS `top` property.
### Problem
`Column::remainder().clip(true)` grows correctly when the panel is made
wider, but does not shrink when the panel is made narrower.
### Root cause
There are two places in `table.rs` where the `max_used_widths` floor is
applied to column widths. One correctly checks `column.clip`, the other
doesn't.
**Post-render** path (already correct, line ~827):
```rust
if !column.clip {
*column_width = column_width.at_least(max_used_widths[i]);
}
```
`TableState::load` **pre-render path** (the bug, line 647):
```rust
.at_least(column.width_range.min.max(*max_used)) // clip flag ignored
```
In `TableState::load`, `.at_least(max_used)` is applied to every column
regardless of `clip`. For a `Column::remainder()`, `max_used`
accumulates the historically widest rendered content. When the panel
shrinks, this floor prevents the column from computing a smaller width,
creating a cycle where it can never shrink.
### Fix
Apply the same `clip` guard that already exists in the post-render path:
```rust
.at_least(if column.clip {
column.width_range.min
} else {
column.width_range.min.max(*max_used)
})
```
When `clip = true`, the floor is just `width_range.min` (0.0 by
default), allowing the remainder column to shrink freely to the actual
remaining space.
### Minimal reproduction
<details>
<summary>Show code</summary>
```rust
use eframe::egui;
use egui_extras::{Column, TableBuilder};
fn main() -> eframe::Result {
eframe::run_ui_native(
"Column::remainder().clip(true) shrink test",
Default::default(),
|ui, _frame| {
egui::Frame::central_panel(ui.style()).show(ui, |ui| {
let mut text = String::from("some content");
egui::ScrollArea::horizontal().show(ui, |ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder().clip(true))
.header(20.0, |mut header| {
header.col(|ui| { ui.strong("Auto"); });
header.col(|ui| { ui.strong("Remainder + clip"); });
})
.body(|mut body| {
body.row(18.0, |mut row| {
row.col(|ui| { ui.label("label"); });
row.col(|ui| {
ui.add(
egui::TextEdit::singleline(&mut text)
.desired_width(f32::INFINITY),
);
});
});
});
});
});
},
)
}
```
</details>
**Without the fix:** make the window wider (column grows ✓), then
narrower — column does not shrink and a horizontal scrollbar appears ✗
https://github.com/user-attachments/assets/2b586588-9f72-4a15-80f4-afddadb69441
**With the fix:** the column shrinks correctly and no scrollbar appears
✓
https://github.com/user-attachments/assets/f1655641-e135-489c-9f59-2af3faa887ab
### Problem
`Response::lost_focus()` could silently fail to fire when keyboard focus
moved from one widget to another *within the same frame* — for example,
clicking a `TextEdit` that was added to the UI *after* the
currently-focused one.
### Fix
This widens the detection window by one extra frame, which is exactly
enough for the deferred loss signal to reach the previously focused
widget on its next render.
### Notes
* The `test_demo_app` test fails, but it has nothing to do with this PR;
it fails on the current master branch, too.
* This PR replaces https://github.com/emilk/egui/pull/3247
* Closes <https://github.com/emilk/egui/issues/2142>
* [x] I have followed the instructions in the PR template
## Summary
Adds `Context::interactive_rects_last_pass() -> Vec<Rect>`, an
integration-facing helper that returns the same widget interaction
rectangles egui uses for hit-testing in the last completed pass.
The method filters out disabled widgets, non-interactive widgets, and
layers that currently do not allow interaction. It also applies
per-layer transforms so the returned rectangles are in global viewport
coordinates.
## Motivation
Some egui integrations need to declare platform-level input regions
before pointer events can reach egui itself. A concrete example is a
transparent or click-through overlay: the platform/windowing layer must
know which parts of the overlay should receive input and which parts
should pass through to whatever is underneath.
Today egui keeps this information internally in `WidgetRects` and uses
it for its own hit-testing, but integrations cannot enumerate those
rectangles. Downstream integrations therefore need app-level side
channels where each app manually reports its clickable rectangles. That
is fragile because it duplicates data egui already has, is easy for
applications to forget, and tends to go stale when widgets move or
popups/menus appear.
This method exposes only the already-derived, integration-relevant
result instead of making `WidgetRects` itself part of the public API.
## Notes
- The method uses the last completed pass because that is the same data
egui uses for interaction at the start of the next pass.
- Rectangles are returned in layer order for deterministic output.
- Non-positive or non-finite rectangles are skipped.
## Verification
- `cargo check -p egui`
- `cargo test -p egui --lib`
Co-authored-by: psyche314 <psyche314@users.noreply.github.com>
Fix: ScrollArea layout jitter with floating bars and zoom levels
* Closes#7937
* Closes#7942
This PR improves the stability of `ScrollArea` by addressing two layout
issues:
1. **Discrete Layout for Floating Bars:** Fixed content "shaking" in
`floating` mode when a non-zero `allocated_width` is used. By using
discrete visibility (`show_bars`) instead of the animated factor for
space allocation, we ensure the layout remains stable during scrollbar
animations.
2. **Zoom-level Stability:** Introduced a small epsilon (0.1) when
checking if content exceeds the viewport. This prevents scrollbars from
flickering on and off due to floating-point rounding errors at specific
zoom factors (e.g., 1.01 or 0.95).
## Problem
winit has always delivered distinct physical variants for every keyboard
key — \`KeyCode::ShiftLeft\` vs \`KeyCode::ShiftRight\`,
\`KeyCode::ControlLeft\`/\`ControlRight\`, \`AltLeft\`/\`AltRight\`,
\`SuperLeft\`/\`SuperRight\`, plus the ISO 102nd key
\`KeyCode::IntlBackslash\` (the one between LShift and Z, labelled
\`<>|\` on French AZERTY and \`\\|\` on UK QWERTY). Today none of these
reach egui:
- Pressing Shift / Ctrl / Alt alone produces *no* \`Event::Key\` at all.
\`key_from_key_code\` and \`key_from_named_key\` both return \`None\`
for modifiers, so the \`if let Some(active_key)\` branch in
\`on_keyboard_input\` is skipped. The collapsed \`Modifiers\` bools are
the only trace of the press, and they don't distinguish left vs right.
- \`KeyCode::IntlBackslash\` has no arm in \`key_from_key_code\`, so on
French / UK ISO keyboards the \`<>|\` key is completely invisible to
egui apps — neither \`key\` nor \`physical_key\` is ever set.
## Who hits this
- Games / kiosk frontends / pincab UIs that bind \`LeftFlipper =
LShift\` vs \`RightFlipper = RShift\` (or \`LeftMagna = LCtrl\` vs
\`RightMagna = RCtrl\`) — currently impossible inside egui without
shelling out to platform APIs (\`device_query\`, raw X11, etc.).
- Anyone on an ISO keyboard who wants to capture the 102nd key in an
input-binding UI.
Previously discussed: context in #2977 (closed by #3649 which added
\`physical_key\`, but only for keys already in \`egui::Key\`).
## Change
Two small additions, no behaviour change for existing code:
**\`crates/egui/src/data/key.rs\`** — new variants at the end of
\`Key\`:
- \`ShiftLeft\`, \`ShiftRight\`, \`ControlLeft\`, \`ControlRight\`,
\`AltLeft\`, \`AltRight\`, \`SuperLeft\`, \`SuperRight\`
- \`IntlBackslash\`
plus their entries in \`Key::ALL\`, \`Key::from_name\`, and
\`Key::name\` (the \`key_from_name\` roundtrip test at the bottom of the
file still passes).
**\`crates/egui-winit/src/lib.rs\`** — new arms in
\`key_from_key_code\`:
\`\`\`rust
KeyCode::ShiftLeft => Key::ShiftLeft,
KeyCode::ShiftRight => Key::ShiftRight,
// ...ControlLeft/Right, AltLeft/Right, SuperLeft/Right...
KeyCode::IntlBackslash => Key::IntlBackslash,
\`\`\`
The existing \`Modifiers\` struct is untouched — shortcut matching
(\"Ctrl+C\"), \`consume_shortcut\`, etc. still see the collapsed state.
The new variants are purely additive and only surface as physical
\`Event::Key\` presses when someone is specifically looking for them.
## Test
- Existing \`test_key_from_name\` test still passes (updated the
sentinel to \`Key::IntlBackslash as usize + 1\`).
- Manual smoke test: pressing left vs right Shift, Ctrl, Alt each
produces an \`Event::Key { key: Key::ShiftLeft/Right/..., physical_key:
Some(Key::ShiftLeft/Right/...), ... }\`; pressing the AZERTY 102nd key
yields \`Key::IntlBackslash\`. Character-key behaviour and \`Modifiers\`
bools are unchanged.
## Not included
- **Web backend** (\`eframe_web\`): \`PhysicalKey\` isn't fully exposed
there yet per the existing \`physical_key\` docs, so these new variants
are only emitted on native. Happy to extend to web in a follow-up if
wanted.
- \`ModifiersSymmetric\` / per-side sticky state: would be a bigger API
change in \`Modifiers\`. This PR stays at the minimum: forward what
winit already gives us for the event path.
Closes no issue directly but addresses the underlying gap noted in the
thread of #2977 (scancode forwarding) for the modifier / Intl-key
subset.
The Debug-formatting of `Id` in debug-builds now contain the full
lineage:
```rs
#[test]
fn with_chain() {
let id = Id::new("a").with("b").with("c").with(7_i32);
assert_eq!(
format!("{id:?}"),
r#"Id::new("a").with("b").with("c").with(7)"#
);
}
```
## Related
* https://github.com/emilk/egui/pull/5851
* https://github.com/emilk/egui/pull/7988
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
-->
* [x] I have followed the instructions in the PR template
At low apha values, premultiplied colors lose precision.
This PR makes the color picker use unmultiplied colors internally.
Before:
https://github.com/user-attachments/assets/4617a355-daa9-4911-86e6-518ac6867014
After:
https://github.com/user-attachments/assets/d9681b01-50d8-418e-b5a5-79b4bd1bbddf
## Summary
Adds two paired API entry-points that let an integration target a
specific monitor at viewport creation time, or move an existing viewport
to a different monitor at runtime, in a way that works portably on
Wayland.
```rust
// At creation
ViewportBuilder::default()
.with_inner_size([1920.0, 1080.0])
.with_monitor(playfield_idx) // ← new
.with_decorations(false)
// Or at runtime
ctx.send_viewport_cmd(egui::ViewportCommand::SetMonitor(idx));
```
Both route through winit's
`Fullscreen::Borderless(Some(MonitorHandle))`, which is the only
portable mechanism that:
- targets a specific output on **Wayland** (where there is no global
`OuterPosition`)
- avoids the **Mutter race** where `OuterPosition` is dropped before the
window is mapped (X11/Wayland-Mutter)
- works the same way on Windows and macOS
`with_position` and `with_outer_position` continue to work for cases
where the integration *does* know the absolute pixel coordinates of each
monitor and is on a platform where they are honored. `with_monitor` is
the high-level alternative when you just want "show this window on
output N, borderless fullscreen."
## Why this matters
Multi-monitor borderless setups (kiosks, pinball cabinets, museum
installs, embedded panels) need each window to land on a specific
physical display. Without `with_monitor`:
- On Wayland, you can't move a window to a chosen output at all — the
compositor decides. There's no `OuterPosition` API.
- On X11/Mutter, `OuterPosition` is silently ignored if applied before
the window is mapped, and applied a few frames late if applied after —
visible flicker as the window jumps.
- Polling `monitor.position()` then sending `OuterPosition` in a retry
loop is the workaround pattern, but fragile and racy.
Routing through `Fullscreen::Borderless(Some(MonitorHandle))` is the
same code path winit's own examples use for monitor-targeted fullscreen,
just exposed at the egui ViewportBuilder level.
## Implementation
- `crates/egui/src/viewport.rs` — adds `monitor: Option<usize>` to
`ViewportBuilder`, the `with_monitor(usize)` builder method, and the
`ViewportCommand::SetMonitor(usize)` variant.
- `crates/egui-winit/src/lib.rs` — both at viewport creation and on
`SetMonitor`, look up the monitor by index in `available_monitors()` and
apply `Fullscreen::Borderless(Some(handle))`. Index out of range is a
no-op (with a `log::warn!`), matching how unknown values are handled
elsewhere in the file.
73 lines added, 1 modified. No public API removed or changed.
## Test plan
- [x] `cargo build -p egui -p egui-winit` clean
- [x] `cargo clippy -p egui -p egui-winit --all-features -- -D warnings`
clean
- [x] `cargo fmt -p egui -p egui-winit --check` clean
- [ ] Manual: tested on Linux X11 (Mutter), Linux Wayland (Mutter &
KWin), Windows 11. Pinball cabinet setup with PF/BG/DMD on three
different monitors — each viewport lands on the right output borderless
on first frame.
- [ ] Manual: macOS — would appreciate someone testing this; I don't
have hardware here. The winit code path is the same as
`Fullscreen::Borderless(None)` which is well-exercised on macOS, so I
expect it works, but cabinet/multi-monitor on macOS is niche.
## Background
This is the third of three small upstream-able pieces extracted from the
closed [PR #8113](https://github.com/emilk/egui/pull/8113) (viewport
rotation, declined as too niche / too much surface). The rotation logic
itself shipped as the standalone
[`egui-rotate`](https://crates.io/crates/egui-rotate) crate. The
remaining two integration touch-points needed for kiosk/cabinet setups
are:
- [PR #8138](https://github.com/emilk/egui/pull/8138) —
`App::transform_primitives` + `App::post_platform_output` hooks
(general-purpose post-tessellation / post-platform-output hooks)
- [PR #8127](https://github.com/emilk/egui/pull/8127) —
`Key::ShiftLeft/Right` + `IntlBackslash` physical key variants
- **This PR** — `with_monitor` / `SetMonitor`
Each is independently useful. None depend on the others.
🤖 Drafted with [Claude Code](https://claude.com/claude-code)
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
-->
Fixes a small typo
* [x] I have followed the instructions in the PR template
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
-->
* Closes#8032
* [x] I have followed the instructions in the PR template
It includes:
* Fix for `ScrollArea` when `scroll_to_*` could be ignored when
`stick_to_bottom(true)` was active and the viewport was already stuck to
the bottom.
* The fix is by making explicit per-axis scroll movement take priority
over sticky-end snapping for that frame, and avoid immediately
re-marking animated scrolls as still stuck.
* I've also added a regression test for this issue to ensure it will be
caught on further code changes.
The code snippets form the original issue can be used for testing here
as well
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Previously, the doc for `Ui::scope_builder` read
> Create a child, add content to it, and then allocate only what was
used in the parent `Ui`.
which I understood as meaning that "only what was used in the parent
`UI`" would be allocated (in the child or parent), which makes no sense
(in either case).
I rewrote it and some related docs.
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
* Closes <https://github.com/emilk/egui/issues/4491>
* [x] I have followed the instructions in the PR template
Lets you create an `EhttpLoader` with arbitrary headers like so:
```rust
cc.egui_ctx.add_bytes_loader(std::sync::Arc::new(
egui_extras::loaders::http_loader::EhttpLoader::default().with_headers(&[
("User-Agent", "foo"),
])
));
```
I'm not sure if there are any problems with installing a second
`EhttpLoader` (in addition to the one installed by default when using
`egui_extras::install_image_loaders(&cc.egui_ctx)`. But I wasn't
sure how else to pass in configuration options to
`install_image_loaders`.
* Follows https://github.com/emilk/egui/pull/8199
This makes the animation of the collapsing panel a bit smoother, by
taking into account the spacing between the header and the body.
`ab_glyph` would output coverage values, but `vello` outputs RGBA. So
the old name was a misnomer.
I also suspect our default values are wrong, but I need to investigate
that more properly in a separate PR.
This changes the monitor selection used when restoring a persisted
window position.
Currently, `egui-winit` picks a monitor by checking whether the saved
window position fits inside a loose monitor range. This can choose the
wrong monitor when a saved window rectangle slightly overlaps another
monitor.
My failure case was on Windows with two monitors:
- primary monitor on the right
- secondary monitor on the left
- window maximized on the primary monitor
- persisted outer position was slightly negative, e.g. `x = -8`, because
of the invisible window border
That position matched both monitor ranges, so the restored maximized
window could
open on the secondary monitor instead of the primary one.
This PR picks the monitor with the largest overlap with the saved window
rectangle instead.
Related note: probably the best solution would be to save the normal
window position when maximized, so that when unmaximizing, the window
would get restored to the previous state. It's mentioned in this comment
https://github.com/emilk/egui/issues/3494#issuecomment-1986985211. I
tried doing that in
[fix-windows-maximized-restore-placement](https://github.com/YelovSK/egui/tree/fix-windows-maximized-restore-placement),
and it works, but it requires adding windows-sys as a dependency to call
a relevant winapi, so that's probably not the right solution. Winit
doesn't seem to provide an API that would return this information.
* [x] I have followed the instructions in the PR template
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Current behavior fails when translating file uris that contain windows
UNC paths. This commit attempts to fix that behavior by looking at the
hostname attribute of the uri and changing behavior if the hostname is
present.
* Closes <https://github.com/emilk/egui/issues/8161>
* [x] I have followed the instructions in the PR template
The three methods for showing a `Panel` are now:
* `panel.show`: always show the panel.
* `panel.show_collapsible`: show or hide the panel, with a slide
animation in between.
* `Panel::show_switched`: animate between two different panels: a
thin/collapsed one and a thick/expanded one.
* Closes https://github.com/emilk/egui/pull/7254
You can now drag-to-close a panel. Also drag-to-expand panels.
This is a breaking change: the animated panel functions now take a
`open: &mut bool` instead of `open: bool`.
This is only enabled for resizable panels
## What
Adds a way for apps to push an RGBA bitmap as the OS cursor — the
missing companion to `Context::set_cursor_icon`. The integration
translates it into a real `winit::CustomCursor`, so the cursor is drawn
by the compositor and can extend past the egui window edge like any
native cursor.
## Why
Apps with custom-shaped windows (Winamp-style skins, themed launchers,
kiosk apps) currently have no clean way to display a custom cursor:
- `CursorIcon` is limited to the standard system enum.
- Painting the cursor sprite via `egui::Painter` works inside the canvas
but gets clipped at the window edge — the bottom/right of the cursor
disappears the moment the pointer is near the boundary, and there's no
way to render onto the desktop area exposed by a
transparent/region-shaped window.
`winit` 0.30+ already supports `CustomCursor::from_rgba` +
`ActiveEventLoop::create_custom_cursor`, but `egui-winit` doesn't
surface it. This PR exposes it through egui.
### Visual demonstration
Driving use case: a Winamp WSZ skin player
([all3f0r1/oneamp](https://github.com/all3f0r1/oneamp)) with a
transparent + region-shaped window where the skin ships its own `.cur`
files. The bottom-right corner of the playlist exposes the resize cursor
— notice how it gets clipped at the window edge in the painter-based
approach.
| Before (cursor painted via `egui::Painter`) | After (cursor pushed via
`set_cursor_image`) |
| --- | --- |
| 
| 
|
## API
```rust
// new in egui::data::output
pub struct CustomCursorImage {
pub rgba: std::sync::Arc<[u8]>,
pub size: [u16; 2], // matches winit's u16 to avoid lossy casts
pub hotspot: [u16; 2],
}
// new field on PlatformOutput (skipped from serde — ephemeral)
pub cursor_image: Option<CustomCursorImage>,
// new method on Context
ctx.set_cursor_image(Some(image)); // overrides cursor_icon for this frame
ctx.set_cursor_image(None); // revert to cursor_icon
```
`Arc<[u8]>` is intentional: the integration dedupes by `Arc::as_ptr`, so
reusing the same Arc across frames means the bitmap is only uploaded to
the OS once per skin, not once per frame.
## Integration changes
- `egui_winit::State::handle_platform_output_with_event_loop(window,
Option<&ActiveEventLoop>, ...)` is a new method that threads the active
event loop so it can call `event_loop.create_custom_cursor(...)`.
- The legacy `handle_platform_output(window, ...)` delegates with `None`
and silently drops `cursor_image`. **No existing callers break.**
- The icon and bitmap paths are unified in a private `apply_cursor`. The
no-flicker dedupe of the old `set_cursor_icon` is preserved on both
paths.
- If `CustomCursor::from_rgba` rejects the bitmap (bad dimensions,
hotspot OOB, etc.), we log a warning and fall back to the icon path.
- eframe's wgpu + glow integrations thread `&ActiveEventLoop` through
`run_ui_and_paint` (glow already had it; wgpu needed one extra
parameter) and call the new method.
- Immediate viewports keep the old path because they're invoked from a
`Context` callback that doesn't have an event loop reference. Custom
cursors are a no-op in immediate viewports — acceptable since they're a
niche path.
## Fallback semantics
| backend / context | what happens |
|--------------------------------|-------------------------------|
| eframe wgpu/glow main viewport | bitmap displayed via OS |
| eframe immediate viewport | falls back to `cursor_icon` |
| eframe web | falls back to `cursor_icon` |
| custom integrations not opted in | falls back to `cursor_icon` |
| `from_rgba` returns `BadImage` | warning + falls back to icon |
## Verification
- `cargo fmt --all -- --check` ✅
- `cargo clippy -p egui -p egui-winit -p eframe --all-targets
--all-features -- -D warnings` ✅
- `cargo doc --lib --no-deps -p egui -p egui-winit -p eframe
--all-features` ✅
- `cargo check -p egui --no-default-features --features serde` ✅
(validates the `serde(skip)` on `cursor_image`)
- Interactive validation on Linux/Wayland with the OneAmp WSZ skin
player — see screenshots above.
I haven't run the full snapshot test suite (`scripts/check.sh`) because
we're on Linux and the snapshots are macOS-rendered — happy to run it if
you'd like.
## Notes
Drafted per the contributing guide ("open a draft PR, you may get
helpful feedback early"). Open to design feedback on:
1. Whether `CustomCursorImage` should live in `egui::viewport` rather
than `egui::data::output`.
2. Whether the legacy `handle_platform_output` should grow `event_loop`
directly (breaking) instead of getting a sibling method (non-breaking,
what I did).
3. Whether to also wire it through eframe-web (probably not —
`wasm-bindgen-cursor` would need its own path).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Related
* https://github.com/emilk/egui/pull/5851
* https://github.com/emilk/egui/pull/7988
## What
We want to make it easier to understand the lineage of a `Id` (which is
the parent `Id`, and the parent of that, etc?)
As a first step of that, we want to clarify the different between a
globally unique `Id`, and an `IdSalt`
I also introduced the `AsId` and `AsIdSalt` traits, which are
implemented of anything that implements `Hash` and `Debug`. The `Debug`
half of that is unused here, but will later be used in `Debug` builds to
produce a proper tree.
* Part of https://github.com/emilk/egui/issues/8180
So far, you've been able to move any `egui::Window` by dragging anywhere
on it. This makes sense on touch screens with thick fingers, but less so
on non-touch-screens.
With this PR, you can now control it with a new enum `WindowDrag`