1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Merge branch 'main' into style_modifier

# Conflicts:
#	crates/egui/src/ui_stack.rs
This commit is contained in:
lucasmerlin
2026-05-07 17:06:28 +02:00
344 changed files with 6081 additions and 4883 deletions

View File

@@ -1,19 +0,0 @@
name: Cargo Machete
on: [push, pull_request]
jobs:
cargo-machete:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92
- name: Machete install
## The official cargo-machete action
uses: bnjbvr/cargo-machete@v0.9.1
- name: Checkout
uses: actions/checkout@v4
- name: Machete Check
run: cargo machete

25
.github/workflows/cargo_shear.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Looks for unused crates.
name: Cargo Shear
on:
push:
branches:
- "main"
pull_request:
types: [opened, synchronize]
jobs:
cargo-shear:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Cargo Shear
uses: taiki-e/install-action@v2.48.7
with:
tool: cargo-shear@1.11.2
- name: Run Cargo Shear
run: |
cargo shear

View File

@@ -4,17 +4,23 @@ on:
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
issues: write
jobs:
check-source-branch:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check PR source branch
env:
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
# Check if PR is from a fork
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
if [[ "$IS_FORK" == "true" ]]; then
# Check if PR is from the master/main branch of a fork
if [[ "${{ github.event.pull_request.head.ref }}" == "master" || "${{ github.event.pull_request.head.ref }}" == "main" ]]; then
if [[ "$HEAD_REF" == "master" || "$HEAD_REF" == "main" ]]; then
echo "ERROR: Pull requests from the master/main branch of forks are not allowed, because it prevents maintainers from contributing to your PR"
echo "Please create a feature branch in your fork and submit the PR from that branch instead."
exit 1

View File

@@ -94,7 +94,7 @@ jobs:
- name: wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.1.0
with:
version: "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
version: "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
- run: ./scripts/wasm_bindgen_check.sh --skip-setup

25
.github/workflows/taplo.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Checks that all TOML files are formatted with taplo.
name: Taplo
on:
push:
branches:
- "main"
pull_request:
types: [opened, synchronize]
jobs:
taplo:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Taplo
uses: taiki-e/install-action@v2.48.7
with:
tool: taplo-cli@0.9.3
- name: Check TOML formatting
run: |
taplo fmt --check

View File

@@ -14,6 +14,146 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.2 - 2026-05-04
### ⭐ Added
* Add regression test for O(n²) word boundary scan [#8077](https://github.com/emilk/egui/pull/8077) by [@hallyhaa](https://github.com/hallyhaa)
### 🐛 Fixed
* Fix wrong color of last glyph of selected text [#8075](https://github.com/emilk/egui/pull/8075) by [@emilk](https://github.com/emilk)
* Fix text selection of centered and right-aligned text [#8076](https://github.com/emilk/egui/pull/8076) by [@emilk](https://github.com/emilk)
* Fix `Context::is_pointer_over_egui` and `Context::egui_wants_pointer_input` [#8081](https://github.com/emilk/egui/pull/8081) by [@emilk](https://github.com/emilk)
* Fix centered & right aligned `TextEdit` [#8082](https://github.com/emilk/egui/pull/8082) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🚀 Performance
* Optimize text selection performance for large documents [#7917](https://github.com/emilk/egui/pull/7917) by [@rustbasic](https://github.com/rustbasic)
## 0.34.1 - 2026-03-27
Nothing new
## 0.34.0 - 2026-03-26
### Highlights from this release
- Sharper text unlocked by switching font rendering crate to [`skrifa`](https://crates.io/crates/skrifa)
- Fade out edges of `ScrollArea`s
- Use `Ui` as the main entrypoint
### Skrifa and font hinting
The font rendering backend was switched from `ab_glyph` to `skrifa` + `vello_cpu`. This enabled us support
font hinting and variations. It also paves the way for more font improvements in the future, like support for color
emojis and adding helpers for variations like `RichText::bold`.
Font hinting makes text more clear (look at the =):
https://github.com/user-attachments/assets/ea9151ec-869f-4c05-ab59-836114683417
We now support setting variable font parameters:
https://github.com/user-attachments/assets/0febde1c-ebf6-4d85-8f96-86ec0f934ecf
(Unfortunately there is currently a bug with variations, meaning changing them live like this won't work in practise.
There is a [draft PR](https://github.com/emilk/egui/pull/8029) to fix it, but it didn't make the release)
* Replace ab_glyph with Skrifa + vello_cpu; enable font hinting [#7694](https://github.com/emilk/egui/pull/7694) by [@valadaptive](https://github.com/valadaptive)
* Add font variations API [#7859](https://github.com/emilk/egui/pull/7859) by [@valadaptive](https://github.com/valadaptive)
### More `Ui`, less `Context`
egui has long had a confusing overlap in responsibilities between `Context` and `Ui`.
In particular, you could add panels to either one (or both!).
In this release, we switch from having `Context` be the main entrypoint, and instead provide whole-app `Ui`.
In egui we've replaced `Context::run` with `Context::run_ui`, and changed viewports to be given a `&mut Ui` instead of `Context`.
In `eframe` we've deprecated `App::update` replaced it with `App::ui` (which provides a `&mut Ui` instead of a `&Context`).
In addition to this, `Ui` now derefs to `Context`, so all code like `ui.ctx().input(…)` can now be written `ui.input(…)`.
This means you are much less likely to have to use naked `Context`s.
`Context` can still be useful though, since they implement `Clone` and can be sent to other threads so you can call `.request_repaint` on them.
* Add `Context::run_ui` [#7736](https://github.com/emilk/egui/pull/7736) by [@emilk](https://github.com/emilk)
* Add `Deref<Target = Context>` for `Ui` [#7770](https://github.com/emilk/egui/pull/7770) by [@emilk](https://github.com/emilk)
* Replace `App::update` with `fn logic` and `fn ui` [#7775](https://github.com/emilk/egui/pull/7775) by [@emilk](https://github.com/emilk)
* Rename `Context::style` to `global_style`; avoid confusion w/ `Ui::style` [#7772](https://github.com/emilk/egui/pull/7772) by [@emilk](https://github.com/emilk)
* Rename functions in `Context` to avoid confusion [#7773](https://github.com/emilk/egui/pull/7773) by [@emilk](https://github.com/emilk)
* Viewports: give the caller a `Ui` instead of `Context` [#7779](https://github.com/emilk/egui/pull/7779) by [@emilk](https://github.com/emilk)
### Changed panel API
As part of the above work, we have unified the panel API.
`SidePanel` and `TopBottomPanel` are deprecated, replaced by a single `Panel`.
Furthermore, it is now deprecated to use panels directly on `Context`. Use the `show_inside` functions instead, acting on `Ui`s.
This unification and simplification will make it easier to maintain and improve panels going forward.
* Add `Panel` to replace `SidePanel` and `TopBottomPanel` [#5659](https://github.com/emilk/egui/pull/5659) by [@sharky98](https://github.com/sharky98)
* Deprecate using `Panel` directly on a `Context` [#7781](https://github.com/emilk/egui/pull/7781) by [@emilk](https://github.com/emilk)
* Deprecate `CentralPanel::show` [#7783](https://github.com/emilk/egui/pull/7783) by [@emilk](https://github.com/emilk)
* Deprecate `Context::used_size` and `Context::available_rect` [#7788](https://github.com/emilk/egui/pull/7788) by [@emilk](https://github.com/emilk)
### ⭐ Added
* Add `is_scrolling`/`is_smooth_scrolling` util, checking for active scroll action [#7669](https://github.com/emilk/egui/pull/7669) by [@IsseW](https://github.com/IsseW)
* Allow multiple atoms in `Button::shortcut_text` and `right_text` [#7696](https://github.com/emilk/egui/pull/7696) by [@emilk](https://github.com/emilk)
* Add `ScrollArea::content_margin` [#7722](https://github.com/emilk/egui/pull/7722) by [@emilk](https://github.com/emilk)
* Per-widget style [#7667](https://github.com/emilk/egui/pull/7667) by [@AdrienZianne](https://github.com/AdrienZianne)
* Plugin: export `TypedPluginGuard` and `TypedPluginHandle` [#7780](https://github.com/emilk/egui/pull/7780) by [@apekros](https://github.com/apekros)
* Add `ViewportInfo::occluded` and `visible` [#7948](https://github.com/emilk/egui/pull/7948) by [@emilk](https://github.com/emilk)
* Add `Atom` prefix/suffix support to `DragValue` [#7949](https://github.com/emilk/egui/pull/7949) by [@lucasmerlin](https://github.com/lucasmerlin)
* ⚠️ Atom improvements: `Atom::id`, `align`, `closure`, `max_size` [#7958](https://github.com/emilk/egui/pull/7958) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add `DebugOptions::warn_if_rect_changes_id` [#7984](https://github.com/emilk/egui/pull/7984) by [@emilk](https://github.com/emilk)
* `TextEdit` `Atom` prefix/suffix [#7587](https://github.com/emilk/egui/pull/7587) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add `Button::left_text` [#7955](https://github.com/emilk/egui/pull/7955) by [@rustbasic](https://github.com/rustbasic)
* Add `Response::parent_id` [#8010](https://github.com/emilk/egui/pull/8010) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add `Context::text_edit_focused` [#8014](https://github.com/emilk/egui/pull/8014) by [@emilk](https://github.com/emilk)
* Add `Context::time` [#8017](https://github.com/emilk/egui/pull/8017) by [@emilk](https://github.com/emilk)
* Add `Ui::is_tooltip` [#8016](https://github.com/emilk/egui/pull/8016) by [@emilk](https://github.com/emilk)
* Add `UiStack::bg_color` [#8020](https://github.com/emilk/egui/pull/8020) by [@emilk](https://github.com/emilk)
* Make `egui::IdSet` public [#8019](https://github.com/emilk/egui/pull/8019) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add raw key methods to TypeIdMap [#8007](https://github.com/emilk/egui/pull/8007) by [@AlexanderSchuetz97](https://github.com/AlexanderSchuetz97)
### 🔧 Changed
* Remove `accesskit` feature and always depend on `accesskit` [#7701](https://github.com/emilk/egui/pull/7701) by [@emilk](https://github.com/emilk)
* Update MSRV from 1.88 to 1.92 [#7793](https://github.com/emilk/egui/pull/7793) by [@JasperBRiedel](https://github.com/JasperBRiedel)
* Improve modifier handling when scrolling [#7678](https://github.com/emilk/egui/pull/7678) by [@emilk](https://github.com/emilk)
* Apply preferred font weight when loading variable fonts [#7790](https://github.com/emilk/egui/pull/7790) by [@pmnxis](https://github.com/pmnxis)
* Make scroll bars and resize splitters visible to accesskit [#7804](https://github.com/emilk/egui/pull/7804) by [@emilk](https://github.com/emilk)
* Allow moving existing widgets to the top of interaction stack [#7805](https://github.com/emilk/egui/pull/7805) by [@emilk](https://github.com/emilk)
* Slightly change interact behavior around thin splitters [#7806](https://github.com/emilk/egui/pull/7806) by [@emilk](https://github.com/emilk)
* Move window resize interaction to be over contents [#7807](https://github.com/emilk/egui/pull/7807) by [@emilk](https://github.com/emilk)
* Don't expand widgets on hover [#7808](https://github.com/emilk/egui/pull/7808) by [@emilk](https://github.com/emilk)
* Make `FrameCache::get` return a reference instead of cloning the cached value [#7834](https://github.com/emilk/egui/pull/7834) by [@KonaeAkira](https://github.com/KonaeAkira)
* Update selected dependencies [#7920](https://github.com/emilk/egui/pull/7920) by [@oscargus](https://github.com/oscargus)
* Make `Galley::pos_from_layout_cursor` `pub` [#7864](https://github.com/emilk/egui/pull/7864) by [@dionb](https://github.com/dionb)
* Update accesskit to 0.24.0 (and related deps) [#7850](https://github.com/emilk/egui/pull/7850) by [@delan](https://github.com/delan)
* Quit on Ctrl-Q [#7985](https://github.com/emilk/egui/pull/7985) by [@emilk](https://github.com/emilk)
* Fade out the edges of `ScrollAreas` [#8018](https://github.com/emilk/egui/pull/8018) by [@emilk](https://github.com/emilk)
### 🔥 Removed
* Remove `CacheTrait::as_any_mut` [#7833](https://github.com/emilk/egui/pull/7833) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Fix: ensure `CentralPanel::show_inside` allocates space in parent [#7778](https://github.com/emilk/egui/pull/7778) by [@emilk](https://github.com/emilk)
* Heed constrain rect when auto-positioning windows [#7786](https://github.com/emilk/egui/pull/7786) by [@emilk](https://github.com/emilk)
* Fix jitter when hovering edge of scroll area close to resize splitter [#7803](https://github.com/emilk/egui/pull/7803) by [@emilk](https://github.com/emilk)
* Don't focus Areas, Windows and ScrollAreas [#7827](https://github.com/emilk/egui/pull/7827) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix backspacing leaving last character in IME prediction not removed on macOS native and Safari [#7810](https://github.com/emilk/egui/pull/7810) by [@umajho](https://github.com/umajho)
* Implemented distance threshold for double/triple clicks [#7817](https://github.com/emilk/egui/pull/7817) by [@bl4ze4447](https://github.com/bl4ze4447)
* Fix `CentralPanel::show_inside_dyn` to round `panel_rect` [#7868](https://github.com/emilk/egui/pull/7868) by [@ripopov](https://github.com/ripopov)
* Stop ctrl+arrow etc from moving focus [#7897](https://github.com/emilk/egui/pull/7897) by [@emilk](https://github.com/emilk)
* Fix scroll area not consuming scroll events [#7904](https://github.com/emilk/egui/pull/7904) by [@lucasmerlin](https://github.com/lucasmerlin)
* Pass in an explicit id in `UiBuilder`, to avoid wrapping passed in ids with Id::new() [#7925](https://github.com/emilk/egui/pull/7925) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix crash when dragging a DragValue through small floats [#7939](https://github.com/emilk/egui/pull/7939) by [@Fyrecean](https://github.com/Fyrecean)
* Fix emoji icon font [#7940](https://github.com/emilk/egui/pull/7940) by [@Jhynjhiruu](https://github.com/Jhynjhiruu)
* Fixes the overly aggressive overflow elision in `truncate()` and similar for os scaling other than 100% [#7867](https://github.com/emilk/egui/pull/7867) by [@RndUsr123](https://github.com/RndUsr123)
* Fix text color when selecting newline character [#7951](https://github.com/emilk/egui/pull/7951) by [@emilk](https://github.com/emilk)
* Fix: repaint on drag-and-drop files [#7953](https://github.com/emilk/egui/pull/7953) by [@emilk](https://github.com/emilk)
* Fix instable IDs following animated panels [#7994](https://github.com/emilk/egui/pull/7994) by [@emilk](https://github.com/emilk)
* Enables every combination of `TextEdit` and `LayoutJob` alignments [#7831](https://github.com/emilk/egui/pull/7831) by [@RndUsr123](https://github.com/RndUsr123)
* Fix `horizontal_wrapping` row height after using `text_edit_multiline` [#8000](https://github.com/emilk/egui/pull/8000) by [@optozorax](https://github.com/optozorax)
* Fix menu keyboard toggle for open submenus [#7957](https://github.com/emilk/egui/pull/7957) by [@fjkorf](https://github.com/fjkorf)
* Fix: `Visuals::interact_cursor` support in `Button` [#7986](https://github.com/emilk/egui/pull/7986) by [@mango766](https://github.com/mango766)
### 🚀 Performance
* Shrink the byte-size of `Response` slightly [#8011](https://github.com/emilk/egui/pull/8011) by [@emilk](https://github.com/emilk)
## 0.33.3 - 2025-12-11
* Treat `.` as a word-splitter in text navigation [#7741](https://github.com/emilk/egui/pull/7741) by [@emilk](https://github.com/emilk)
* Change text color of selected text [#7691](https://github.com/emilk/egui/pull/7691) by [@emilk](https://github.com/emilk)

1098
Cargo.lock

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ members = [
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.92"
version = "0.33.3"
version = "0.34.2"
[profile.release]
@@ -55,18 +55,18 @@ opt-level = 2
[workspace.dependencies]
emath = { version = "0.33.3", path = "crates/emath", default-features = false }
ecolor = { version = "0.33.3", path = "crates/ecolor", default-features = false }
epaint = { version = "0.33.3", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.33.3", path = "crates/epaint_default_fonts" }
egui = { version = "0.33.3", path = "crates/egui", default-features = false }
egui-winit = { version = "0.33.3", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.33.3", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.33.3", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.33.3", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.33.3", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.33.3", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.33.3", path = "crates/eframe", default-features = false }
emath = { version = "0.34.2", path = "crates/emath", default-features = false }
ecolor = { version = "0.34.2", path = "crates/ecolor", default-features = false }
epaint = { version = "0.34.2", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.34.2", path = "crates/epaint_default_fonts" }
egui = { version = "0.34.2", path = "crates/egui", default-features = false }
egui-winit = { version = "0.34.2", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.34.2", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.34.2", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.34.2", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.34.2", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.34.2", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.34.2", path = "crates/eframe", default-features = false }
accesskit = "0.24.0"
accesskit_consumer = "0.35.0"
@@ -80,34 +80,35 @@ arboard = { version = "3.6.1", default-features = false }
backtrace = "0.3.76"
bitflags = "2.9.4"
bytemuck = "1.24.0"
chrono = { version = "0.4.42", default-features = false }
cint = "0.3.1"
color-hex = "0.2.0"
criterion = { version = "0.7.0", default-features = false }
dify = { version = "0.8", default-features = false }
directories = "6.0.0"
document-features = "0.2.11"
ehttp = { version = "0.6.0", default-features = false }
ehttp = { version = "0.7.1", default-features = false }
enum-map = "2.7.3"
env_logger = { version = "0.11.8", default-features = false }
font-types = { version = "0.11.0", default-features = false, features = ["std"] }
glow = "0.16.0"
glow = "0.17.0"
glutin = { version = "0.32.3", default-features = false }
glutin-winit = { version = "0.5.0", default-features = false }
harfrust = "0.5.2"
home = "0.5.9"
image = { version = "0.25.6", default-features = false }
jiff = { version = "0.2.23", default-features = false }
js-sys = "0.3.77"
kittest = { version = "0.3.0" }
kittest = { version = "0.4.0" }
log = { version = "0.4.28", features = ["std"] }
memoffset = "0.9.1"
mimalloc = "0.1.48"
mime_guess2 = { version = "2.3.1", default-features = false }
mint = "0.5.9"
nohash-hasher = "0.2.0"
objc2 = "0.5.2"
objc2-app-kit = { version = "0.2.2", default-features = false }
objc2-foundation = { version = "0.2.2", default-features = false }
objc2-ui-kit = { version = "0.2.2", default-features = false }
objc2 = "0.6.4"
objc2-app-kit = { version = "0.3.2", default-features = false }
objc2-foundation = { version = "0.3.2", default-features = false }
objc2-ui-kit = { version = "0.3.2", default-features = false }
open = "5.3.2"
parking_lot = "0.12.5"
percent-encoding = "2.3.2"
@@ -121,7 +122,7 @@ raw-window-handle = "0.6.2"
rayon = "1.11.0"
resvg = { version = "0.45.1", default-features = false }
rfd = "0.17.2"
ron = "0.11.0"
ron = "0.12.0"
self_cell = "1.2.1"
serde = { version = "1.0.228", features = ["derive"] }
similar-asserts = "1.7.0"
@@ -133,23 +134,25 @@ syntect = { version = "5.3.0", default-features = false }
tempfile = "3.23.0"
thiserror = "2.0.17"
tokio = "1.49"
toml = {version = "1", default-features = false }
toml = { version = "1.0.0", default-features = false }
type-map = "0.5.1"
unicode_names2 = { version = "2.0.0", default-features = false }
unicode-general-category = "1.1.0"
unicode-segmentation = "1.12.0"
vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] }
wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
wasm-bindgen-futures = "0.4.0"
vello_cpu = { version = "0.0.7", default-features = false, features = [
"std",
"u8_pipeline",
"f32_pipeline",
] }
wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766
wasm-bindgen-futures = "0.4.58"
wayland-cursor = { version = "0.31.11", default-features = false }
web-sys = "0.3.77"
web-time = "1.1.0" # Timekeeping for native and web
webbrowser = "1.0.5"
wgpu = { version = "28.0.0", default-features = false, features = ["std"] }
wgpu = { version = "29.0.1", default-features = false, features = ["std"] }
windows-sys = "0.61.2"
winit = { version = "0.30.12", default-features = false }
[patch.crates-io]
kittest = { git = "https://github.com/rerun-io/kittest", branch = "main" }
winit = { version = "0.30.13", default-features = false }
[workspace.lints.rust]
unsafe_code = "deny"
@@ -221,10 +224,12 @@ flat_map_option = "warn"
float_cmp_const = "warn"
fn_params_excessive_bools = "warn"
fn_to_numeric_cast_any = "warn"
format_push_string = "warn"
from_iter_instead_of_collect = "warn"
get_unwrap = "warn"
if_let_mutex = "warn"
ignore_without_reason = "warn"
ignored_unit_patterns = "warn"
implicit_clone = "warn"
implied_bounds_in_impls = "warn"
imprecise_flops = "warn"
@@ -273,6 +278,7 @@ mismatching_type_param_order = "warn"
missing_assert_message = "warn"
missing_enforced_import_renames = "warn"
missing_errors_doc = "warn"
missing_fields_in_debug = "warn"
missing_safety_doc = "warn"
mixed_attributes_style = "warn"
mut_mut = "warn"
@@ -282,6 +288,7 @@ needless_continue = "warn"
needless_for_each = "warn"
needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
needless_raw_string_hashes = "warn"
negative_feature_names = "warn"
non_std_lazy_statics = "warn"
non_zero_suggestions = "warn"
@@ -302,6 +309,7 @@ rc_mutex = "warn"
readonly_write_lock = "warn"
redundant_type_annotations = "warn"
ref_as_ptr = "warn"
ref_option = "warn"
ref_option_ref = "warn"
ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn"

View File

@@ -10,16 +10,14 @@
[![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq)
<br/>
<div align="center">
<a href="https://www.rerun.io/"><img src="https://github.com/user-attachments/assets/78e79463-4357-461b-bbd1-31aa5ef5e1a2" width="250"></a>
<a href="https://www.egui.rs/"><img src="https://github.com/user-attachments/assets/cfaf1d43-9338-490f-ae82-99b420baa1b0" width="400"></a>
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
an SDK for visualizing streams of multimodal data.
</div>
---
<br/>
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
</div>
egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations).

View File

@@ -6,6 +6,18 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.2 - 2026-05-04
Nothing new
## 0.34.1 - 2026-03-27
Nothing new
## 0.34.0 - 2026-03-26
Nothing new
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui"
categories = ["mathematics", "encoding"]
keywords = ["gui", "color", "conversion", "gamedev", "images"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true

View File

@@ -7,6 +7,39 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.2 - 2026-05-04
* Document glow-only fields in `NativeOptions` [#8104](https://github.com/emilk/egui/pull/8104) by [@emilk](https://github.com/emilk)
## 0.34.1 - 2026-03-27
* `wgpu` backend: Enable WebGL fallback [#8038](https://github.com/emilk/egui/pull/8038) by [@emilk](https://github.com/emilk)
* Only apply cursor style to the `<canvas>` [#8036](https://github.com/emilk/egui/pull/8036) by [@mkeeter](https://github.com/mkeeter)
## 0.34.0 - 2026-03-26
### ⭐ Added
* Add feature `wgpu_no_default_features` [#7700](https://github.com/emilk/egui/pull/7700) by [@emilk](https://github.com/emilk)
* Add `ViewportInfo::occluded` and `visible` [#7948](https://github.com/emilk/egui/pull/7948) by [@emilk](https://github.com/emilk)
* Add `eframe::WindowChromeMetrics` (macOS only) [#8015](https://github.com/emilk/egui/pull/8015) by [@emilk](https://github.com/emilk)
### 🔧 Changed
* Replace `App::update` with `fn logic` and `fn ui` [#7775](https://github.com/emilk/egui/pull/7775) by [@emilk](https://github.com/emilk)
* Make `wgpu` the default renderer for `eframe` and egui.rs [#7615](https://github.com/emilk/egui/pull/7615) by [@emilk](https://github.com/emilk)
* Roll out new egui icon and logo [#7995](https://github.com/emilk/egui/pull/7995) by [@emilk](https://github.com/emilk)
* Update wasm-bindgen to 0.2.108, and ehttp to 0.7.1 [#7996](https://github.com/emilk/egui/pull/7996) by [@emilk](https://github.com/emilk)
* Update to `wgpu` 29 [#7990](https://github.com/emilk/egui/pull/7990) by [@cwfitzgerald](https://github.com/cwfitzgerald)
* Allow fallback from smithay to arboard when getting clipboard [#7976](https://github.com/emilk/egui/pull/7976) by [@wizzeh](https://github.com/wizzeh)
### 🐛 Fixed
* Fix: update get_proc_address to use Arc for better ownership management [#7922](https://github.com/emilk/egui/pull/7922) by [@Wybxc](https://github.com/Wybxc)
* Much improved IME [#7967](https://github.com/emilk/egui/pull/7967) by [@umajho](https://github.com/umajho)
* Improve behavior of invisible windows [#7905](https://github.com/emilk/egui/pull/7905) by [@gcailly](https://github.com/gcailly)
### 🚀 Performance
* Avoid repaints on device mouse motion outside window [#7866](https://github.com/emilk/egui/pull/7866) by [@inktomi](https://github.com/inktomi)
* Only run `App::ui` if the application is visible [#7950](https://github.com/emilk/egui/pull/7950) by [@emilk](https://github.com/emilk)
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
[package.metadata.docs.rs]
all-features = true
@@ -89,7 +89,7 @@ web_screen_reader = ["web-sys/SpeechSynthesis", "web-sys/SpeechSynthesisUtteranc
## See <https://github.com/emilk/egui/issues/5889> for more details.
##
## By default, eframe will prefer WebGPU over WebGL, but
## you can configure this at run-time with [`NativeOptions::wgpu_options`].
## you can configure this at run-time with `WebOptions::wgpu_options`.
wgpu = ["wgpu_no_default_features", "egui-wgpu/default"]
## This is exactly like the `wgpu` feature, but does NOT enable the default features of `wgpu` and `egui-wgpu`.
@@ -155,7 +155,6 @@ glutin-winit = { workspace = true, optional = true, default-features = false, fe
"wgl",
] }
home = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true }
# mac:
[target.'cfg(any(target_os = "macos"))'.dependencies]
@@ -169,10 +168,17 @@ objc2-foundation = { workspace = true, default-features = false, features = [
objc2-app-kit = { workspace = true, default-features = false, features = [
"std",
"NSApplication",
"NSBitmapImageRep",
"NSButton",
"NSControl",
"NSGraphics",
"NSImage",
"NSImageRep",
"NSMenu",
"NSMenuItem",
"NSResponder",
"NSView",
"NSWindow",
] }
# windows:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -2,7 +2,7 @@
//!
//! `epi` provides interfaces for window management and serialization.
//!
//! Start by looking at the [`App`] trait, and implement [`App::update`].
//! Start by looking at the [`App`] trait, and implement [`App::ui`].
#![warn(missing_docs)] // Let's keep `epi` well-documented.
@@ -161,22 +161,6 @@ pub trait App {
/// (A "viewport" in egui means an native OS window).
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut Frame);
/// Called each time the UI needs repainting, which may be many times per second.
///
/// Put your widgets into a [`egui::Panel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`].
///
/// The [`egui::Context`] can be cloned and saved if you like.
///
/// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
///
/// This is called for the root viewport ([`egui::ViewportId::ROOT`]).
/// Use [`egui::Context::show_viewport_deferred`] to spawn additional viewports (windows).
/// (A "viewport" in egui means an native OS window).
#[deprecated = "Use Self::ui instead"]
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
_ = (ctx, frame);
}
/// Get a handle to the app.
///
/// Can be used from web to interact or other external context.
@@ -256,7 +240,7 @@ pub trait App {
true
}
/// A hook for manipulating or filtering raw input before it is processed by [`Self::update`].
/// A hook for manipulating or filtering raw input before it is processed by [`Self::ui`].
///
/// This function provides a way to modify or filter input events before they are processed by egui.
///
@@ -275,22 +259,6 @@ pub trait App {
fn raw_input_hook(&mut self, _ctx: &egui::Context, _raw_input: &mut egui::RawInput) {}
}
/// Selects the level of hardware graphics acceleration.
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum HardwareAcceleration {
/// Require graphics acceleration.
Required,
/// Prefer graphics acceleration, but fall back to software.
Preferred,
/// Do NOT use graphics acceleration.
///
/// On some platforms (macOS) this is ignored and treated the same as [`Self::Preferred`].
Off,
}
/// Options controlling the behavior of a native window.
///
/// Additional windows can be opened using (egui viewports)[`egui::viewport`].
@@ -314,11 +282,6 @@ pub struct NativeOptions {
/// To avoid this, set the icon to [`egui::IconData::default`].
pub viewport: egui::ViewportBuilder,
/// Turn on vertical syncing, limiting the FPS to the display refresh rate.
///
/// The default is `true`.
pub vsync: bool,
/// Set the level of the multisampling anti-aliasing (MSAA).
///
/// Must be a power-of-two. Higher = more smooth 3D.
@@ -340,11 +303,6 @@ pub struct NativeOptions {
/// `egui` doesn't need the stencil buffer, so the default value is 0.
pub stencil_buffer: u8,
/// Specify whether or not hardware acceleration is preferred, required, or not.
///
/// Default: [`HardwareAcceleration::Preferred`].
pub hardware_acceleration: HardwareAcceleration,
/// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub renderer: Renderer,
@@ -381,13 +339,6 @@ pub struct NativeOptions {
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub window_builder: Option<WindowBuilderHook>,
#[cfg(feature = "glow")]
/// Needed for cross compiling for VirtualBox VMSVGA driver with OpenGL ES 2.0 and OpenGL 2.1 which doesn't support SRGB texture.
/// See <https://github.com/emilk/egui/pull/1993>.
///
/// For OpenGL ES 2.0: set this to [`egui_glow::ShaderVersion::Es100`] to solve blank texture problem (by using the "fallback shader").
pub shader_version: Option<egui_glow::ShaderVersion>,
/// On desktop: make the window position to be centered at initialization.
///
/// Platform specific:
@@ -395,6 +346,10 @@ pub struct NativeOptions {
/// Wayland desktop currently not supported.
pub centered: bool,
/// Configures glow instance.
#[cfg(feature = "glow")]
pub glow_options: egui_glow::GlowConfiguration,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
@@ -439,6 +394,9 @@ impl Clone for NativeOptions {
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
window_builder: None, // Skip any builder callbacks if cloning
#[cfg(feature = "glow")]
glow_options: self.glow_options.clone(),
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: self.wgpu_options.clone(),
@@ -458,11 +416,9 @@ impl Default for NativeOptions {
Self {
viewport: Default::default(),
vsync: true,
multisampling: 0,
depth_buffer: 0,
stencil_buffer: 0,
hardware_acceleration: HardwareAcceleration::Preferred,
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
renderer: Renderer::default(),
@@ -475,13 +431,14 @@ impl Default for NativeOptions {
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
window_builder: None,
#[cfg(feature = "glow")]
shader_version: None,
centered: false,
#[cfg(feature = "glow")]
glow_options: egui_glow::GlowConfiguration::default(),
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
wgpu_options: egui_wgpu::WgpuConfiguration::default()
.with_surface_config(egui_wgpu::SurfaceConfig::LOW_LATENCY),
persist_window: true,
@@ -516,6 +473,10 @@ pub struct WebOptions {
#[cfg(feature = "glow")]
pub webgl_context_option: WebGlContextOption,
/// Configures glow instance.
#[cfg(feature = "glow")]
pub glow_options: egui_glow::GlowConfiguration,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
@@ -560,6 +521,9 @@ impl Default for WebOptions {
#[cfg(feature = "glow")]
webgl_context_option: WebGlContextOption::BestFirst,
#[cfg(feature = "glow")]
glow_options: egui_glow::GlowConfiguration::default(),
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
@@ -776,7 +740,7 @@ impl Frame {
/// * Read the pixel buffer from the previous frame (`glow::Context::read_pixels`).
/// * Render things behind the egui windows.
///
/// Note that all egui painting is deferred to after the call to [`App::update`]
/// Note that all egui painting is deferred to after the call to [`App::ui`]
/// ([`egui`] only collects [`egui::Shape`]s and then eframe paints them all in one go later on).
///
/// To get a [`glow`] context you need to compile with the `glow` feature flag,
@@ -805,6 +769,28 @@ impl Frame {
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
self.wgpu_render_state.as_ref()
}
/// The currently-applied runtime surface config (present mode, frame latency)
/// used by the `wgpu` renderer, if any.
///
/// Returns `None` when not using the `wgpu` backend.
#[cfg(feature = "wgpu_no_default_features")]
pub fn wgpu_surface_config(&self) -> Option<egui_wgpu::SurfaceConfig> {
self.wgpu_render_state
.as_ref()
.map(|state| state.surface_config)
}
/// Set the runtime surface config (present mode, frame latency) for the `wgpu`
/// renderer. The surface is reconfigured on the next paint.
///
/// No-op when not using the `wgpu` backend.
#[cfg(feature = "wgpu_no_default_features")]
pub fn set_wgpu_surface_config(&mut self, config: egui_wgpu::SurfaceConfig) {
if let Some(state) = &mut self.wgpu_render_state {
state.surface_config = config;
}
}
}
/// Information about the web environment (if applicable).
@@ -882,7 +868,7 @@ pub struct IntegrationInfo {
/// Seconds of cpu usage (in seconds) on the previous frame.
///
/// This includes [`App::update`] as well as rendering (except for vsync waiting).
/// This includes [`App::ui`] as well as rendering (except for vsync waiting).
///
/// For a more detailed view of cpu usage, connect your preferred profiler by enabling it's feature in [`profiling`](https://crates.io/crates/profiling).
///

View File

@@ -6,7 +6,7 @@
//! To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
//!
//! In short, you implement [`App`] (especially [`App::update`]) and then
//! In short, you implement [`App`] (especially [`App::ui`]) and then
//! call [`crate::run_native`] from your `main.rs`, and/or use `eframe::WebRunner` from your `lib.rs`.
//!
//! ## Compiling for web
@@ -19,7 +19,7 @@
//!
//! ## Simplified usage
//! If your app is only for native, and you don't need advanced features like state persistence,
//! then you can use the simpler function [`run_simple_native`].
//! then you can use the simpler function [`run_ui_native`].
//!
//! ## Usage, native:
//! ``` no_run
@@ -159,7 +159,7 @@ pub use {egui, egui::emath, egui::epaint};
pub use {egui_glow, glow};
#[cfg(feature = "wgpu_no_default_features")]
pub use {egui_wgpu, wgpu};
pub use {egui_wgpu, egui_wgpu::SurfaceConfig, egui_wgpu::WgpuConfiguration, egui_wgpu::wgpu};
mod epi;
@@ -190,6 +190,9 @@ pub use web::{WebLogger, WebRunner};
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
mod native;
#[cfg(target_os = "macos")]
pub use native::macos::WindowChromeMetrics;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use native::run::EframeWinitApplication;
@@ -254,8 +257,27 @@ pub mod icon_data;
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
#[allow(clippy::allow_attributes, clippy::needless_pass_by_value)]
pub fn run_native(
app_name: &str,
native_options: NativeOptions,
app_creator: AppCreator<'_>,
) -> Result {
run_native_ext(app_name, native_options, None, app_creator)
}
/// Like [`run_native`], but lets you supply a pre-existing [`egui::Context`].
///
/// If `egui_ctx` is `Some`, that context will be used by eframe instead of creating a fresh one.
/// If it is `None`, eframe creates a new context (same behavior as [`run_native`]).
///
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
#[allow(clippy::allow_attributes, clippy::needless_pass_by_value)]
pub fn run_native_ext(
app_name: &str,
mut native_options: NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: AppCreator<'_>,
) -> Result {
let renderer = init_native(app_name, &mut native_options);
@@ -264,13 +286,13 @@ pub fn run_native(
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator)
native::run::run_glow(app_name, native_options, egui_ctx, app_creator)
}
#[cfg(feature = "wgpu_no_default_features")]
Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, native_options, app_creator)
native::run::run_wgpu(app_name, native_options, egui_ctx, app_creator)
}
}
}
@@ -443,67 +465,6 @@ pub fn run_ui_native(
)
}
/// The simplest way to get started when writing a native app.
///
/// This does NOT support persistence of custom user data. For that you need to use [`run_native`].
/// However, it DOES support persistence of egui data (window positions and sizes, how far the user has scrolled in a
/// [`ScrollArea`](egui::ScrollArea), etc.) if the persistence feature is enabled.
///
/// # Example
/// ``` no_run
/// fn main() -> eframe::Result {
/// // Our application state:
/// let mut name = "Arthur".to_owned();
/// let mut age = 42;
///
/// let options = eframe::NativeOptions::default();
/// eframe::run_simple_native("My egui App", options, move |ctx, _frame| {
/// egui::CentralPanel::default().show(ctx, |ui| {
/// ui.heading("My egui Application");
/// ui.horizontal(|ui| {
/// let name_label = ui.label("Your name: ");
/// ui.text_edit_singleline(&mut name)
/// .labelled_by(name_label.id);
/// });
/// ui.add(egui::Slider::new(&mut age, 0..=120).text("age"));
/// if ui.button("Increment").clicked() {
/// age += 1;
/// }
/// ui.label(format!("Hello '{name}', age {age}"));
/// });
/// })
/// }
/// ```
///
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[deprecated = "Use run_ui_native instead"]
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub fn run_simple_native(
app_name: &str,
native_options: NativeOptions,
update_fun: impl FnMut(&egui::Context, &mut Frame) + 'static,
) -> Result {
struct SimpleApp<U> {
update_fun: U,
}
impl<U: FnMut(&egui::Context, &mut Frame) + 'static> App for SimpleApp<U> {
fn ui(&mut self, _ui: &mut egui::Ui, _frame: &mut Frame) {}
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
(self.update_fun)(ctx, frame);
}
}
run_native(
app_name,
native_options,
Box::new(|_cc| Ok(Box::new(SimpleApp { update_fun }))),
)
}
// ----------------------------------------------------------------------------
/// The different problems that can occur when trying to run `eframe`.

View File

@@ -204,7 +204,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
use crate::icon_data::IconDataExt as _;
profiling::function_scope!();
use objc2::ClassType as _;
use objc2::AnyThread as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::NSString;

View File

@@ -214,15 +214,17 @@ impl EpiIntegration {
Self {
frame,
last_auto_save: Instant::now(),
egui_ctx,
pending_full_output: Default::default(),
close: false,
can_drag_window: false,
#[cfg(feature = "persistence")]
persist_window: native_options.persist_window,
app_icon_setter,
beginning: Instant::now(),
beginning: Instant::now()
.checked_sub(web_time::Duration::from_secs_f64(egui_ctx.time()))
.unwrap_or_else(Instant::now),
is_first_frame: true,
egui_ctx,
}
}
@@ -259,7 +261,7 @@ impl EpiIntegration {
/// Run user code - this can create immediate viewports, so hold no locks over this!
///
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::update`].
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`].
pub fn update(
&mut self,
app: &mut dyn epi::App,
@@ -287,12 +289,6 @@ impl EpiIntegration {
}
if is_visible {
{
profiling::scope!("App::update");
#[expect(deprecated)]
app.update(ui.ctx(), &mut self.frame);
}
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);

View File

@@ -207,7 +207,7 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
profiling::scope!("ron::serialize");
if let Err(err) = ron::Options::default()
.to_io_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
.and_then(|()| writer.flush().map_err(|err| err.into()))
{
log::warn!("Failed to serialize app state: {err}");
} else {

View File

@@ -34,7 +34,7 @@ use egui_winit::accesskit_winit;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::epi_integration::EpiIntegration,
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
};
use super::{
@@ -55,6 +55,10 @@ pub struct GlowWinitApp<'app> {
// re-initializing the `GlowWinitRunning` state on Android if the application
// suspends and resumes.
app_creator: Option<AppCreator<'app>>,
/// An optional pre-existing egui context. If `Some`, it is used instead of
/// creating a new one via [`create_egui_context`]. Taken during initialization.
egui_ctx: Option<egui::Context>,
}
/// State that is initialized when the application is first starts running via
@@ -128,6 +132,7 @@ impl<'app> GlowWinitApp<'app> {
event_loop: &EventLoop<UserEvent>,
app_name: &str,
native_options: NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: AppCreator<'app>,
) -> Self {
profiling::function_scope!();
@@ -137,6 +142,7 @@ impl<'app> GlowWinitApp<'app> {
native_options,
running: None,
app_creator: Some(app_creator),
egui_ctx,
}
}
@@ -184,7 +190,7 @@ impl<'app> GlowWinitApp<'app> {
let painter = egui_glow::Painter::new(
gl,
"",
native_options.shader_version,
native_options.glow_options.shader_version,
native_options.dithering,
)?;
@@ -209,7 +215,10 @@ impl<'app> GlowWinitApp<'app> {
)
};
let egui_ctx = create_egui_context(storage.as_deref());
let egui_ctx = self
.egui_ctx
.take()
.unwrap_or_else(|| create_egui_context(storage.as_deref()));
let (mut glutin, painter) = Self::create_glutin_windowed_context(
&egui_ctx,
@@ -761,9 +770,11 @@ impl GlowWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), Some(&window));
if window.is_minimized() == Some(true) {
if is_invisible_or_minimized(&window) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
@@ -950,12 +961,12 @@ impl GlutinWindowContext {
use glutin::prelude::*;
// convert native options to glutin options
let hardware_acceleration = match native_options.hardware_acceleration {
crate::HardwareAcceleration::Required => Some(true),
crate::HardwareAcceleration::Preferred => None,
crate::HardwareAcceleration::Off => Some(false),
let hardware_acceleration = match native_options.glow_options.hardware_acceleration {
egui_glow::HardwareAcceleration::Required => Some(true),
egui_glow::HardwareAcceleration::Preferred => None,
egui_glow::HardwareAcceleration::Off => Some(false),
};
let swap_interval = if native_options.vsync {
let swap_interval = if native_options.glow_options.vsync {
glutin::surface::SwapInterval::Wait(NonZeroU32::MIN)
} else {
glutin::surface::SwapInterval::DontWait

View File

@@ -0,0 +1,76 @@
use egui::Vec2;
use objc2_app_kit::{NSView, NSWindow, NSWindowButton};
use raw_window_handle::{AppKitWindowHandle, RawWindowHandle};
/// Size of the "traffic lights" (red/yellow/green close/minimize/maximize buttons)
/// on the native macOS window.
///
/// This is very useful together with [`egui::ViewportBuilder::with_fullsize_content_view`].
#[derive(Debug)]
pub struct WindowChromeMetrics {
/// Size of the "traffic lights" (red/yellow/green close/minimize/maximize buttons),
/// including margins.
///
/// The unit here is in "native scale", which means it needs to be divided by [`egui::Context::zoom_factor`]
/// to get the size in egui points.
pub traffic_lights_size: Vec2,
}
impl WindowChromeMetrics {
/// Get the window chrome metrics for a given window handle.
pub fn from_window_handle(window_handle: &RawWindowHandle) -> Option<Self> {
window_chrome_metrics(window_handle)
}
}
fn window_chrome_metrics(window_handle: &RawWindowHandle) -> Option<WindowChromeMetrics> {
let RawWindowHandle::AppKit(appkit_handle) = window_handle else {
return None;
};
let ns_view = ns_view_from_handle(appkit_handle)?;
let ns_window = ns_view.window()?;
Some(WindowChromeMetrics {
traffic_lights_size: traffic_lights_metrics(&ns_window)?,
})
}
fn traffic_lights_metrics(ns_window: &NSWindow) -> Option<Vec2> {
// Button order is CloseButton, MiniaturizeButton, ZoomButton:
let close_button = ns_window
.standardWindowButton(NSWindowButton::CloseButton)?
.frame();
let zoom_button = ns_window
.standardWindowButton(NSWindowButton::ZoomButton)?
.frame();
let left_margin = close_button.origin.x;
let right_margin = left_margin; // for symmetry
let total_width = zoom_button.origin.x + zoom_button.size.width + right_margin;
let top_margin = close_button.origin.y;
let bottom_margin = top_margin; // Usually symmetric
let total_height = top_margin + close_button.size.height + bottom_margin;
Some(Vec2::new(total_width as f32, total_height as f32))
}
fn ns_view_from_handle(handle: &AppKitWindowHandle) -> Option<&NSView> {
let ns_view_ptr = handle.ns_view.as_ptr().cast::<NSView>();
// Validate the pointer is non-null
if ns_view_ptr.is_null() {
None
} else {
// SAFETY:
// - We've verified the pointer is non-null
// - The pointer comes from the windowing system, so it should be valid
// - NSView pointers from AppKit are expected to remain valid for the window lifetime
#[expect(unsafe_code)]
unsafe {
ns_view_ptr.as_ref()
}
}
}

View File

@@ -3,6 +3,9 @@ mod epi_integration;
mod event_loop_context;
pub mod run;
#[cfg(target_os = "macos")]
pub(crate) mod macos;
/// File storage which can be used by native backends.
#[cfg(feature = "persistence")]
pub mod file_storage;

View File

@@ -1,4 +1,4 @@
use std::time::Instant;
use std::time::{Duration, Instant};
use winit::{
application::ApplicationHandler,
@@ -11,9 +11,20 @@ use ahash::HashMap;
use super::winit_integration::{UserEvent, WinitApp};
use crate::{
Result, epi,
native::{event_loop_context, winit_integration::EventResult},
native::{
event_loop_context,
winit_integration::{EventResult, is_invisible_or_minimized},
},
};
/// Minimum interval between repaints for invisible windows.
///
/// On Windows, invisible windows don't receive `RedrawRequested` events,
/// so we throttle their repaints to avoid busy-looping while still
/// processing viewport commands like `Visible(true)`.
/// See <https://github.com/emilk/egui/issues/7776>.
const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100);
// ----------------------------------------------------------------------------
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoop<UserEvent>> {
#[cfg(target_os = "android")]
@@ -177,23 +188,54 @@ impl<T: WinitApp> WinitAppWrapper<T> {
fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) {
let now = Instant::now();
let mut invisible_window_ids = Vec::new();
self.windows_next_repaint_times
.retain(|window_id, repaint_time| {
if now < *repaint_time {
return true; // not yet ready
}
event_loop.set_control_flow(ControlFlow::Poll);
if let Some(window) = self.winit_app.window(*window_id) {
log::trace!("request_redraw for {window_id:?}");
window.request_redraw();
// On Windows, invisible windows don't receive RedrawRequested
// events, so pending viewport commands (e.g. Visible(true)) would
// never be processed. We collect these windows to paint them
// directly below.
// See: https://github.com/emilk/egui/issues/5229
if is_invisible_or_minimized(&window) {
invisible_window_ids.push(*window_id);
} else {
log::trace!("request_redraw for {window_id:?}");
event_loop.set_control_flow(ControlFlow::Poll);
window.request_redraw();
}
} else {
log::trace!("No window found for {window_id:?}");
}
false
});
// Paint invisible windows directly, since they won't receive
// RedrawRequested events on Windows. This ensures that viewport
// commands like Visible(true) are still processed.
for window_id in &invisible_window_ids {
let event_result = self.winit_app.run_ui_and_paint(event_loop, *window_id);
self.handle_event_result(event_loop, event_result);
}
// Throttle any already-scheduled repaints for invisible windows
// to avoid busy-looping. If no repaint was requested by the app,
// the window will simply sleep.
// See: https://github.com/emilk/egui/issues/7776
if !invisible_window_ids.is_empty() {
let next_paint = Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL;
for window_id in &invisible_window_ids {
self.windows_next_repaint_times
.entry(*window_id)
.and_modify(|t| *t = (*t).min(next_paint));
}
}
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
if let Some(next_repaint_time) = next_repaint_time {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
@@ -270,6 +312,16 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
if let Some(window_id) =
self.winit_app.window_id_from_viewport_id(viewport_id)
{
// Throttle repaints for invisible windows to prevent
// high CPU usage on Windows.
// See: https://github.com/emilk/egui/issues/7776
let when = if let Some(window) = self.winit_app.window(window_id)
&& is_invisible_or_minimized(&window)
{
when.max(Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL)
} else {
when
};
Ok(EventResult::RepaintAt(window_id, when))
} else {
Ok(EventResult::Wait)
@@ -347,6 +399,7 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, winit_app: impl WinitApp) -> R
pub fn run_glow(
app_name: &str,
mut native_options: epi::NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: epi::AppCreator<'_>,
) -> Result {
use super::glow_integration::GlowWinitApp;
@@ -354,13 +407,15 @@ pub fn run_glow(
#[cfg(not(target_os = "ios"))]
if native_options.run_and_return {
return with_event_loop(native_options, |event_loop, native_options| {
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
let glow_eframe =
GlowWinitApp::new(event_loop, app_name, native_options, egui_ctx, app_creator);
run_and_return(event_loop, glow_eframe)
})?;
}
let event_loop = create_event_loop(&mut native_options)?;
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
let glow_eframe =
GlowWinitApp::new(&event_loop, app_name, native_options, egui_ctx, app_creator);
run_and_exit(event_loop, glow_eframe)
}
@@ -373,7 +428,7 @@ pub fn create_glow<'a>(
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::glow_integration::GlowWinitApp;
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, None, app_creator);
WinitAppWrapper::new(glow_eframe, true)
}
@@ -383,6 +438,7 @@ pub fn create_glow<'a>(
pub fn run_wgpu(
app_name: &str,
mut native_options: epi::NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: epi::AppCreator<'_>,
) -> Result {
use super::wgpu_integration::WgpuWinitApp;
@@ -390,13 +446,15 @@ pub fn run_wgpu(
#[cfg(not(target_os = "ios"))]
if native_options.run_and_return {
return with_event_loop(native_options, |event_loop, native_options| {
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
let wgpu_eframe =
WgpuWinitApp::new(event_loop, app_name, native_options, egui_ctx, app_creator);
run_and_return(event_loop, wgpu_eframe)
})?;
}
let event_loop = create_event_loop(&mut native_options)?;
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
let wgpu_eframe =
WgpuWinitApp::new(&event_loop, app_name, native_options, egui_ctx, app_creator);
run_and_exit(event_loop, wgpu_eframe)
}
@@ -409,7 +467,7 @@ pub fn create_wgpu<'a>(
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::wgpu_integration::WgpuWinitApp;
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, None, app_creator);
WinitAppWrapper::new(wgpu_eframe, true)
}

View File

@@ -27,7 +27,10 @@ use winit_integration::UserEvent;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
native::{
epi_integration::EpiIntegration,
winit_integration::{EventResult, is_invisible_or_minimized},
},
};
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
@@ -45,6 +48,10 @@ pub struct WgpuWinitApp<'app> {
/// Set when we are actually up and running.
running: Option<WgpuWinitRunning<'app>>,
/// An optional pre-existing egui context. If `Some`, it is used instead of
/// creating a new one via [`winit_integration::create_egui_context`]. Taken during initialization.
egui_ctx: Option<egui::Context>,
}
/// State that is initialized when the application is first starts running via
@@ -102,6 +109,7 @@ impl<'app> WgpuWinitApp<'app> {
event_loop: &EventLoop<UserEvent>,
app_name: &str,
native_options: NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: AppCreator<'app>,
) -> Self {
profiling::function_scope!();
@@ -118,6 +126,7 @@ impl<'app> WgpuWinitApp<'app> {
native_options,
running: None,
app_creator: Some(app_creator),
egui_ctx,
}
}
@@ -184,9 +193,17 @@ impl<'app> WgpuWinitApp<'app> {
builder: ViewportBuilder,
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
profiling::function_scope!();
// Inject the display handle into the wgpu setup so that wgpu can create
// surfaces on platforms that require it (e.g. GLES on Wayland).
let mut wgpu_options = self.native_options.wgpu_options.clone();
if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
&& create_new.display_handle.is_none()
{
create_new.display_handle = Some(Box::new(event_loop.owned_display_handle()));
}
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
self.native_options.wgpu_options.clone(),
wgpu_options,
self.native_options.viewport.transparent.unwrap_or(false),
egui_wgpu::RendererOptions {
msaa_samples: self.native_options.multisampling as _,
@@ -417,7 +434,10 @@ impl WinitApp for WgpuWinitApp<'_> {
.unwrap_or(&self.app_name),
)
};
let egui_ctx = winit_integration::create_egui_context(storage.as_deref());
let egui_ctx = self
.egui_ctx
.take()
.unwrap_or_else(|| winit_integration::create_egui_context(storage.as_deref()));
let (window, builder) = create_window(
&egui_ctx,
event_loop,
@@ -711,6 +731,7 @@ impl WgpuWinitRunning<'_> {
&clipped_primitives,
&textures_delta,
screenshot_commands,
window,
);
for action in viewport.actions_requested.drain(..) {
@@ -770,10 +791,12 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window
&& window.is_minimized() == Some(true)
&& is_invisible_or_minimized(window)
{
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
@@ -1098,6 +1121,7 @@ fn render_immediate_viewport(
&clipped_primitives,
&textures_delta,
vec![],
window,
);
egui_winit.handle_platform_output(window, platform_output);

View File

@@ -9,6 +9,14 @@ use egui::ViewportId;
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
/// Returns `true` if the window is invisible or minimized.
///
/// These windows don't receive `RedrawRequested` events on Windows,
/// so they need special handling to keep processing viewport commands.
pub fn is_invisible_or_minimized(window: &Window) -> bool {
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
}
/// Create an egui context, restoring it from storage if possible.
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
profiling::function_scope!();

View File

@@ -284,9 +284,6 @@ impl AppRunner {
self.app.logic(ui.ctx(), &mut self.frame);
if is_visible {
#[expect(deprecated)]
self.app.update(ui.ctx(), &mut self.frame);
self.app.ui(ui, &mut self.frame);
}
});
@@ -393,7 +390,7 @@ impl AppRunner {
}
}
super::set_cursor_icon(cursor_icon);
super::set_cursor_icon(self.canvas(), cursor_icon);
if self.has_focus() {
// The eframe app has focus.

View File

@@ -1114,16 +1114,16 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32
} else if JsValue::from_str("contentBoxSize").js_in(entry.as_ref()) {
let content_box_size = entry.content_box_size();
let idx0 = content_box_size.at(0);
if !idx0.is_undefined() {
let size: web_sys::ResizeObserverSize = idx0.dyn_into()?;
width = size.inline_size();
height = size.block_size();
} else {
if idx0.is_undefined() {
// legacy
let size = JsValue::clone(content_box_size.as_ref());
let size: web_sys::ResizeObserverSize = size.dyn_into()?;
width = size.inline_size();
height = size.block_size();
} else {
let size: web_sys::ResizeObserverSize = idx0.dyn_into()?;
width = size.inline_size();
height = size.block_size();
}
if DEBUG_RESIZE {
log::info!("contentBoxSize {width}x{height}");

View File

@@ -38,6 +38,7 @@ mod web_painter_wgpu;
pub use backend::*;
use egui::Theme;
use js_sys::Object;
use wasm_bindgen::prelude::*;
use web_sys::{Document, MediaQueryList, Node};
@@ -177,10 +178,8 @@ fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, ctx: &egui::Contex
// ----------------------------------------------------------------------------
/// Set the cursor icon.
fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> {
let document = web_sys::window()?.document()?;
document
.body()?
fn set_cursor_icon(canvas: &web_sys::HtmlCanvasElement, cursor: egui::CursorIcon) -> Option<()> {
canvas
.style()
.set_property("cursor", cursor_web_name(cursor))
.ok()
@@ -370,5 +369,5 @@ pub fn percent_decode(s: &str) -> String {
/// Are we running inside the Safari browser?
pub fn is_safari_browser() -> bool {
web_sys::window().is_some_and(|window| window.has_own_property(&JsValue::from("safari")))
web_sys::window().is_some_and(|window| Object::has_own(&window, &JsValue::from("safari")))
}

View File

@@ -9,7 +9,19 @@ pub fn local_storage_get(key: &str) -> Option<String> {
/// Write data to local storage.
pub fn local_storage_set(key: &str, value: &str) {
local_storage().map(|storage| storage.set_item(key, value));
match local_storage() {
Some(storage) => {
if let Err(err) = storage.set_item(key, value) {
log::warn!(
"local_storage_set failed: key={key}, err={}",
crate::web::string_from_js_value(&err)
);
}
}
None => {
log::warn!("local_storage unavailable");
}
}
}
#[cfg(feature = "persistence")]

View File

@@ -56,8 +56,13 @@ impl TextAgent {
let input = input.clone();
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
let text = input.value();
// Fix android virtual keyboard Gboard
// This removes the virtual keyboard's suggestion.
// Workaround for an Android Gboard issue: after typing a word,
// the user has to delete invisible characters (whose count
// matches the length of the current suggestion) before actual
// characters are deleted, unless the focus has been reset.
//
// this issue appears to have been fixed in Gboard sometime
// between versions 14.7.09 and 17.0.12.
if !event.is_composing() {
input.blur().ok();
input.focus().ok();
@@ -75,11 +80,7 @@ impl TextAgent {
};
let on_composition_start = {
let input = input.clone();
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
input.set_value("");
let event = egui::Event::Ime(egui::ImeEvent::Enabled);
runner.input.raw.events.push(event);
// Repaint moves the text agent into place,
// see `move_to` in `AppRunner::handle_platform_output`.
runner.needs_repaint.repaint_asap();
@@ -136,6 +137,12 @@ impl TextAgent {
let Some(ime) = ime else { return Ok(()) };
if ime.should_interrupt_composition {
// no-op for now: currently, the text agent is sizeless, so any
// click shifts focus to the canvas, which naturally interrupts the
// composition.
}
let mut canvas_rect = super::canvas_content_rect(canvas);
// Fix for safari with virtual keyboard flapping position
if is_mobile_safari() {

View File

@@ -31,8 +31,13 @@ impl WebPainterGlow {
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
let gl = std::sync::Arc::new(gl);
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)
.map_err(|err| format!("Error starting glow painter: {err}"))?;
let painter = egui_glow::Painter::new(
gl,
shader_prefix,
options.glow_options.shader_version,
options.dithering,
)
.map_err(|err| format!("Error starting glow painter: {err}"))?;
Ok(Self {
canvas,

View File

@@ -15,13 +15,32 @@ pub(crate) struct WebPainterWgpu {
surface: wgpu::Surface<'static>,
surface_configuration: wgpu::SurfaceConfiguration,
render_state: Option<RenderState>,
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
on_surface_status: Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction>,
depth_stencil_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
screen_capture_state: Option<CaptureState>,
capture_tx: CaptureSender,
capture_rx: CaptureReceiver,
ctx: egui::Context,
needs_reconfigure: bool,
}
/// Owned web display handle that is `Send + Sync`.
///
/// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum
/// contains platform variants with raw pointers. On web the handle is always empty,
/// so this wrapper is safe.
#[cfg(target_arch = "wasm32")]
#[derive(Clone, Debug)]
struct WebDisplay;
#[cfg(target_arch = "wasm32")]
impl egui_wgpu::wgpu::rwh::HasDisplayHandle for WebDisplay {
fn display_handle(
&self,
) -> Result<egui_wgpu::wgpu::rwh::DisplayHandle<'_>, egui_wgpu::wgpu::rwh::HandleError> {
Ok(egui_wgpu::wgpu::rwh::DisplayHandle::web())
}
}
impl WebPainterWgpu {
@@ -63,7 +82,17 @@ impl WebPainterWgpu {
) -> Result<Self, String> {
log::debug!("Creating wgpu painter");
let instance = options.wgpu_options.wgpu_setup.new_instance().await;
// Inject the display handle into the wgpu setup so that wgpu can create surfaces on WebGL.
let mut wgpu_options = options.wgpu_options.clone();
if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
&& create_new.display_handle.is_none()
{
// Force WebGL, useful for quick & dirty testing:
//create_new.instance_descriptor.backends = wgpu::Backends::GL;
create_new.display_handle = Some(Box::new(WebDisplay));
}
let instance = wgpu_options.wgpu_setup.new_instance().await;
let surface = instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
@@ -71,7 +100,7 @@ impl WebPainterWgpu {
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
let render_state = RenderState::create(
&options.wgpu_options,
&wgpu_options,
&instance,
Some(&surface),
egui_wgpu::RendererOptions {
@@ -89,7 +118,7 @@ impl WebPainterWgpu {
let surface_configuration = wgpu::SurfaceConfiguration {
format: render_state.target_format,
present_mode: options.wgpu_options.present_mode,
present_mode: wgpu_options.surface.present_mode,
view_formats: vec![render_state.target_format],
..default_configuration
};
@@ -105,11 +134,12 @@ impl WebPainterWgpu {
surface_configuration,
depth_stencil_format,
depth_texture_view: None,
on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _,
on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _,
screen_capture_state: None,
capture_tx,
capture_rx,
ctx,
needs_reconfigure: false,
})
}
}
@@ -195,18 +225,28 @@ impl WebPainter for WebPainterWgpu {
);
}
if self.needs_reconfigure {
self.surface
.configure(&render_state.device, &self.surface_configuration);
self.needs_reconfigure = false;
}
let output_frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(err) => match (*self.on_surface_error)(err) {
SurfaceErrorAction::RecreateSurface => {
self.surface
.configure(&render_state.device, &self.surface_configuration);
return Ok(());
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
self.needs_reconfigure = true;
frame
}
other => {
match (*self.on_surface_status)(&other) {
SurfaceErrorAction::RecreateSurface => {
self.surface
.configure(&render_state.device, &self.surface_configuration);
}
SurfaceErrorAction::SkipFrame => {}
}
SurfaceErrorAction::SkipFrame => {
return Ok(());
}
},
return Ok(());
}
};
{

View File

@@ -6,6 +6,29 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.2 - 2026-05-04
* Update to wgpu 29.0.1 [#8073](https://github.com/emilk/egui/pull/8073) by [@emilk](https://github.com/emilk)
* Warn if using a software rasterizer [#8101](https://github.com/emilk/egui/pull/8101) by [@emilk](https://github.com/emilk)
## 0.34.1 - 2026-03-27
* `wgpu` backend: Enable WebGL fallback [#8038](https://github.com/emilk/egui/pull/8038) by [@emilk](https://github.com/emilk)
## 0.34.0 - 2026-03-26
### ⭐ Added
* Add error message when calling `.render()` without `.update_buffers()` [#8005](https://github.com/emilk/egui/pull/8005) by [@emilk](https://github.com/emilk)
### 🔧 Changed
* Put the `capture` module behind a feature flag, make the `egui` dependency optional [#7698](https://github.com/emilk/egui/pull/7698) by [@StT191](https://github.com/StT191)
* Attach stencil buffer [#7702](https://github.com/emilk/egui/pull/7702) by [@jgraef](https://github.com/jgraef)
* Update wgpu to 28.0.0 [#7853](https://github.com/emilk/egui/pull/7853) by [@SuchAFuriousDeath](https://github.com/SuchAFuriousDeath)
* Update to wgpu 29 [#7990](https://github.com/emilk/egui/pull/7990) by [@cwfitzgerald](https://github.com/cwfitzgerald)
### 🐛 Fixed
* Fix wgpu memory leak leading to panic when window is minimized (#7434) [#7928](https://github.com/emilk/egui/pull/7928) by [@landaire](https://github.com/landaire)
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -15,7 +15,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
[lints]
workspace = true
@@ -25,7 +25,12 @@ all-features = true
rustdoc-args = ["--generate-link-to-definition"]
[features]
default = ["fragile-send-sync-non-atomic-wasm", "macos-window-resize-jitter-fix", "wgpu/default"]
default = [
"fragile-send-sync-non-atomic-wasm",
"macos-window-resize-jitter-fix",
"wgpu/default",
"wgpu/webgl", # A very important fallback for web support
]
## Enables the `capture` module for capturing screenshots.
capture = ["dep:egui"]

View File

@@ -24,7 +24,10 @@ mod renderer;
mod setup;
pub use renderer::*;
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
pub use setup::{
EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew,
WgpuSetupExisting,
};
/// Helpers for capturing screenshots of the UI.
#[cfg(feature = "capture")]
@@ -61,6 +64,43 @@ pub enum WgpuError {
HandleError(#[from] ::winit::raw_window_handle::HandleError),
}
/// Runtime-mutable subset of [`WgpuConfiguration`].
///
/// Edit any field to have the surface reconfigured on the next paint.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SurfaceConfig {
/// Present mode used for the primary surface.
pub present_mode: wgpu::PresentMode,
/// Desired maximum number of frames that the presentation engine should queue in advance.
///
/// Use `1` for low-latency, and `2` for high-throughput.
///
/// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details.
///
/// `None` => Let `wgpu` pick a default (currently `2`).
pub desired_maximum_frame_latency: Option<u32>,
}
impl SurfaceConfig {
/// Good default for GUIs with very little (or no) extra GPU work.
pub const LOW_LATENCY: Self = Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: if cfg!(target_os = "ios") {
None // The default is good on iOS, while `Some(1)` cuts FPS in half
} else {
Some(1) // Low-latency by default.
},
};
/// Good default for GUIs with a lot of extra GPU work,
/// or that want to prioritize smoothness over latency.
pub const HIGH_THROUGHPUT: Self = Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: Some(2), // High-throughput.
};
}
/// Access to the render state for egui.
#[derive(Clone)]
pub struct RenderState {
@@ -85,6 +125,11 @@ pub struct RenderState {
/// Egui renderer responsible for drawing the UI.
pub renderer: Arc<RwLock<Renderer>>,
/// Runtime-mutable subset of the wgpu configuration.
///
/// Update this to have the surface reconfigured on the next paint.
pub surface_config: SurfaceConfig,
}
async fn request_adapter(
@@ -135,29 +180,12 @@ async fn request_adapter(
}
})?;
if cfg!(target_arch = "wasm32") {
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
if 1 < available_adapters.len() {
log::info!(
"There are {} available wgpu adapters: {}",
available_adapters.len(),
describe_adapters(available_adapters)
);
} else {
// native:
if available_adapters.len() == 1 {
log::debug!(
"Picked the only available wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
log::info!(
"There were {} available wgpu adapters: {}",
available_adapters.len(),
describe_adapters(available_adapters)
);
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
}
}
Ok(adapter)
@@ -191,6 +219,7 @@ impl RenderState {
let (adapter, device, queue) = match config.wgpu_setup.clone() {
WgpuSetup::CreateNew(WgpuSetupCreateNew {
instance_descriptor: _,
display_handle: _,
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
@@ -232,6 +261,8 @@ impl RenderState {
}) => (adapter, device, queue),
};
log_adapter_info(&adapter.get_info());
let surface_formats = {
profiling::scope!("get_capabilities");
compatible_surface.map_or_else(
@@ -254,6 +285,7 @@ impl RenderState {
queue,
target_format,
renderer: Arc::new(RwLock::new(renderer)),
surface_config: config.surface,
})
}
}
@@ -272,7 +304,7 @@ fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
}
}
/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
/// Specifies which action should be taken as consequence of a surface error.
pub enum SurfaceErrorAction {
/// Do nothing and skip the current frame.
SkipFrame,
@@ -284,23 +316,24 @@ pub enum SurfaceErrorAction {
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
#[derive(Clone)]
pub struct WgpuConfiguration {
/// Present mode used for the primary surface.
pub present_mode: wgpu::PresentMode,
/// Desired maximum number of frames that the presentation engine should queue in advance.
/// Runtime-mutable configuration for the surface (present mode, frame latency).
///
/// Use `1` for low-latency, and `2` for high-throughput.
///
/// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details.
///
/// `None` = `wgpu` default.
pub desired_maximum_frame_latency: Option<u32>,
/// These are the fields exposed via [`RenderState::surface_config`] for live
/// reconfiguration at runtime.
pub surface: SurfaceConfig,
/// How to create the wgpu adapter & device
pub wgpu_setup: WgpuSetup,
/// Callback for surface errors.
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction + Send + Sync>,
/// Callback for surface status changes.
///
/// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame
/// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For
/// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and
/// defers surface reconfiguration to the next frame — the callback is not invoked
/// in that case either.
pub on_surface_status:
Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction + Send + Sync>,
}
#[test]
@@ -312,36 +345,49 @@ fn wgpu_config_impl_send_sync() {
impl std::fmt::Debug for WgpuConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
present_mode,
desired_maximum_frame_latency,
surface,
wgpu_setup,
on_surface_error: _,
on_surface_status: _,
} = self;
f.debug_struct("WgpuConfiguration")
.field("present_mode", &present_mode)
.field(
"desired_maximum_frame_latency",
&desired_maximum_frame_latency,
)
.field("surface", &surface)
.field("wgpu_setup", &wgpu_setup)
.finish_non_exhaustive()
}
}
impl WgpuConfiguration {
#[inline]
pub fn with_surface_config(mut self, surface_config: SurfaceConfig) -> Self {
self.surface = surface_config;
self
}
}
impl Default for WgpuConfiguration {
fn default() -> Self {
Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: None,
wgpu_setup: Default::default(),
on_surface_error: Arc::new(|err| {
if err == wgpu::SurfaceError::Outdated {
// This error occurs when the app is minimized on Windows.
// Silently return here to prevent spamming the console with:
// "The underlying surface has changed, and therefore the swap chain must be updated"
} else {
log::warn!("Dropped frame with error: {err}");
surface: SurfaceConfig::HIGH_THROUGHPUT,
// No display handle available at this point — callers should replace this with
// `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
wgpu_setup: WgpuSetup::without_display_handle(),
on_surface_status: Arc::new(|status| {
match status {
wgpu::CurrentSurfaceTexture::Outdated => {
// This error occurs when the app is minimized on Windows.
// Silently return here to prevent spamming the console with:
// "The underlying surface has changed, and therefore the swap chain must be updated"
}
wgpu::CurrentSurfaceTexture::Occluded => {
// This error occurs when the application is occluded (e.g. minimized or behind another window).
log::debug!("Dropped frame with error: {status:?}");
}
_ => {
log::warn!("Dropped frame with error: {status:?}");
}
}
SurfaceErrorAction::SkipFrame
}),
}
@@ -385,6 +431,18 @@ pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option<wg
// ---------------------------------------------------------------------------
fn log_adapter_info(info: &wgpu::AdapterInfo) {
let summary = adapter_info_summary(info);
let is_test = cfg!(test); // Software rasterizers are expected (and preferred) during testing!
if info.device_type == wgpu::DeviceType::Cpu && !is_test {
log::warn!("Software rasterizer detected - loss of performance expected. {summary}");
} else {
log::debug!("wgpu adapter: {summary}");
}
}
/// A human-readable summary about an adapter
pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
let wgpu::AdapterInfo {
@@ -406,37 +464,52 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
// > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
// > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
use std::fmt::Write as _;
let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");
if !name.is_empty() {
summary += &format!(", name: {name:?}");
write!(summary, ", name: {name:?}").ok();
}
if !driver.is_empty() {
summary += &format!(", driver: {driver:?}");
write!(summary, ", driver: {driver:?}").ok();
}
if !driver_info.is_empty() {
summary += &format!(", driver_info: {driver_info:?}");
write!(summary, ", driver_info: {driver_info:?}").ok();
}
if *vendor != 0 {
#[cfg(not(target_arch = "wasm32"))]
{
summary += &format!(", vendor: {} (0x{vendor:04X})", parse_vendor_id(*vendor));
write!(
summary,
", vendor: {} (0x{vendor:04X})",
parse_vendor_id(*vendor)
)
.ok();
}
#[cfg(target_arch = "wasm32")]
{
summary += &format!(", vendor: 0x{vendor:04X}");
write!(summary, ", vendor: 0x{vendor:04X}").ok();
}
}
if *device != 0 {
summary += &format!(", device: 0x{device:02X}");
write!(summary, ", device: 0x{device:02X}").ok();
}
if !device_pci_bus_id.is_empty() {
summary += &format!(", pci_bus_id: {device_pci_bus_id:?}");
write!(summary, ", pci_bus_id: {device_pci_bus_id:?}").ok();
}
if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
summary += &format!(", subgroup_size: {subgroup_min_size}..={subgroup_max_size}");
write!(
summary,
", subgroup_size: {subgroup_min_size}..={subgroup_max_size}"
)
.ok();
}
summary += &format!(", transient_saves_memory: {transient_saves_memory}");
write!(
summary,
", transient_saves_memory: {transient_saves_memory}"
)
.ok();
summary
}

View File

@@ -1,5 +1,3 @@
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
@@ -352,7 +350,10 @@ impl Renderer {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
bind_group_layouts: &[
Some(&uniform_bind_group_layout),
Some(&texture_bind_group_layout),
],
immediate_size: 0,
});
@@ -360,8 +361,8 @@ impl Renderer {
.depth_stencil_format
.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
@@ -469,6 +470,9 @@ impl Renderer {
/// The render pass internally keeps all referenced resources alive as long as necessary.
/// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error
/// instead of a compile time error.
///
/// # Panic
/// Always ensure that [`Renderer::update_buffers`] has been called otherwise calling [`Renderer::render`] will panic!
pub fn render(
&self,
render_pass: &mut wgpu::RenderPass<'static>,
@@ -513,8 +517,12 @@ impl Renderer {
// Skip rendering zero-sized clip areas.
if let Primitive::Mesh(_) = primitive {
// If this is a mesh, we need to advance the index and vertex buffer iterators:
index_buffer_slices.next().unwrap();
vertex_buffer_slices.next().unwrap();
index_buffer_slices
.next()
.expect("You must call .update_buffers() before .render()");
vertex_buffer_slices
.next()
.expect("You must call .update_buffers() before .render()");
}
continue;
}
@@ -524,8 +532,12 @@ impl Renderer {
match primitive {
Primitive::Mesh(mesh) => {
let index_buffer_slice = index_buffer_slices.next().unwrap();
let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
let index_buffer_slice = index_buffer_slices
.next()
.expect("You must call .update_buffers() before .render()");
let vertex_buffer_slice = vertex_buffer_slices
.next()
.expect("You must call .update_buffers() before .render()");
if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) {
render_pass.set_bind_group(1, bind_group, &[]);
@@ -951,6 +963,7 @@ impl Renderer {
let index_buffer_staging = queue.write_buffer_with(
&self.index_buffer.buffer,
0,
#[expect(clippy::unwrap_used)] // Checked above
NonZeroU64::new(required_index_buffer_size).unwrap(),
);
@@ -968,7 +981,8 @@ impl Renderer {
Primitive::Mesh(mesh) => {
let size = mesh.indices.len() * std::mem::size_of::<u32>();
let slice = index_offset..(size + index_offset);
index_buffer_staging[slice.clone()]
index_buffer_staging
.slice(slice.clone())
.copy_from_slice(bytemuck::cast_slice(&mesh.indices));
self.index_buffer.slices.push(slice);
index_offset += size;
@@ -994,6 +1008,7 @@ impl Renderer {
let vertex_buffer_staging = queue.write_buffer_with(
&self.vertex_buffer.buffer,
0,
#[expect(clippy::unwrap_used)] // Checked above
NonZeroU64::new(required_vertex_buffer_size).unwrap(),
);
@@ -1011,7 +1026,8 @@ impl Renderer {
Primitive::Mesh(mesh) => {
let size = mesh.vertices.len() * std::mem::size_of::<Vertex>();
let slice = vertex_offset..(size + vertex_offset);
vertex_buffer_staging[slice.clone()]
vertex_buffer_staging
.slice(slice.clone())
.copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
self.vertex_buffer.slices.push(slice);
vertex_offset += size;

View File

@@ -1,5 +1,43 @@
use std::sync::Arc;
/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`].
///
/// [`wgpu::InstanceDescriptor`] stores its display handle as a non-cloneable
/// `Box<dyn WgpuHasDisplayHandle>`. This trait wraps it so it can be cloned
/// alongside the rest of the egui wgpu configuration.
///
/// Automatically implemented for all types that satisfy the bounds
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
pub trait EguiDisplayHandle:
wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static
{
/// Clone into a `Box<dyn WgpuHasDisplayHandle>` for [`wgpu::InstanceDescriptor::display`].
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
/// Clone into a new `Box<dyn EguiDisplayHandle>`.
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle>;
}
impl Clone for Box<dyn EguiDisplayHandle> {
fn clone(&self) -> Self {
// We need to deref here, otherwise this causes infinite recursion stack overflow.
(**self).clone_display_handle()
}
}
impl<T> EguiDisplayHandle for T
where
T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static,
{
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle> {
Box::new(self.clone())
}
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub enum WgpuSetup {
/// Construct a wgpu setup using some predefined settings & heuristics.
@@ -22,9 +60,19 @@ pub enum WgpuSetup {
Existing(WgpuSetupExisting),
}
impl Default for WgpuSetup {
fn default() -> Self {
Self::CreateNew(WgpuSetupCreateNew::default())
impl WgpuSetup {
/// Creates a new [`WgpuSetup::CreateNew`] with the given display handle.
///
/// See [`WgpuSetupCreateNew::from_display_handle`] for details.
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle))
}
/// Creates a new [`WgpuSetup::CreateNew`] without a display handle.
///
/// See [`WgpuSetupCreateNew::without_display_handle`] for details.
pub fn without_display_handle() -> Self {
Self::CreateNew(WgpuSetupCreateNew::without_display_handle())
}
}
@@ -65,8 +113,18 @@ impl WgpuSetup {
}
log::debug!("Creating wgpu instance with backends {backends:?}");
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
.await
let desc = &create_new.instance_descriptor;
let descriptor = wgpu::InstanceDescriptor {
backends: desc.backends,
flags: desc.flags,
backend_options: desc.backend_options.clone(),
memory_budget_thresholds: desc.memory_budget_thresholds,
display: create_new
.display_handle
.as_ref()
.map(|handle| handle.clone_for_wgpu()),
};
wgpu::util::new_instance_with_webgpu_detection(descriptor).await
}
Self::Existing(existing) => existing.instance.clone(),
}
@@ -98,18 +156,35 @@ pub type NativeAdapterSelectorMethod = Arc<
/// Configuration for creating a new wgpu setup.
///
/// Used for [`WgpuSetup::CreateNew`].
///
/// Prefer [`Self::from_display_handle`] when you have a display handle available.
/// Most platforms work without one, but some (e.g. Wayland with GLES, or WebGL)
/// require it, so providing one ensures maximum compatibility.
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
///
/// Note: The display handle is stored in [`Self::display_handle`] rather than in
/// [`Self::instance_descriptor`] so the config can be cloned
/// ([`wgpu::InstanceDescriptor`] is not `Clone`). It is injected at instance creation time.
pub struct WgpuSetupCreateNew {
/// Instance descriptor for creating a wgpu instance.
/// Descriptor for the wgpu instance.
///
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which
/// controls which backends are supported (wgpu will pick one of these).
/// If you only want to support WebGL (and not WebGPU),
/// you can set this to [`wgpu::Backends::GL`].
/// By default on web, WebGPU will be used if available.
/// WebGL will only be used as a fallback,
/// and only if you have enabled the `webgl` feature of crate `wgpu`.
/// Leave [`wgpu::InstanceDescriptor::display`] as `None` — use [`Self::display_handle`]
/// instead (injected at instance creation time).
///
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which controls
/// which backends are supported (wgpu will pick one of these). For example, set it to
/// [`wgpu::Backends::GL`] to use only WebGL. By default on web, WebGPU is preferred
/// with WebGL as a fallback (requires the `webgl` feature of crate `wgpu`).
pub instance_descriptor: wgpu::InstanceDescriptor,
/// Display handle passed to wgpu at instance creation time.
///
/// Required on some platforms (e.g. Wayland with GLES, WebGL); optional elsewhere.
/// With winit, use [`winit::event_loop::OwnedDisplayHandle`].
///
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
pub display_handle: Option<Box<dyn EguiDisplayHandle>>,
/// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web.
pub power_preference: wgpu::PowerPreference,
@@ -128,32 +203,37 @@ pub struct WgpuSetupCreateNew {
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
}
impl Clone for WgpuSetupCreateNew {
fn clone(&self) -> Self {
impl WgpuSetupCreateNew {
/// Creates a new configuration with the given display handle.
///
/// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web)
/// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require
/// one. Providing it unconditionally ensures your app works everywhere.
///
/// If you don't have a display handle available, use [`Self::without_display_handle`]
/// instead — it will still work on the majority of platforms.
///
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
Self {
instance_descriptor: self.instance_descriptor.clone(),
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: Arc::clone(&self.device_descriptor),
display_handle: Some(Box::new(display_handle)),
..Self::without_display_handle()
}
}
}
impl std::fmt::Debug for WgpuSetupCreateNew {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WgpuSetupCreateNew")
.field("instance_descriptor", &self.instance_descriptor)
.field("power_preference", &self.power_preference)
.field(
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
)
.finish()
}
}
impl Default for WgpuSetupCreateNew {
fn default() -> Self {
/// Creates a new configuration without a display handle.
///
/// A display handle is not required for headless operation (offscreen rendering, tests,
/// compute-only workloads). It also isn't needed on most platforms even when presenting
/// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one.
///
/// If you do have a display handle available, prefer [`Self::from_display_handle`] for
/// maximum compatibility.
///
/// With winit you can obtain one via [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
///
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
pub fn without_display_handle() -> Self {
Self {
instance_descriptor: wgpu::InstanceDescriptor {
// Add GL backend, primarily because WebGPU is not stable enough yet.
@@ -163,8 +243,11 @@ impl Default for WgpuSetupCreateNew {
flags: wgpu::InstanceFlags::from_build_config().with_env(),
backend_options: wgpu::BackendOptions::from_env_or_default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
display: None,
},
display_handle: None,
power_preference: wgpu::PowerPreference::from_env()
.unwrap_or(wgpu::PowerPreference::HighPerformance),
@@ -192,6 +275,46 @@ impl Default for WgpuSetupCreateNew {
}
}
impl Clone for WgpuSetupCreateNew {
fn clone(&self) -> Self {
let desc = &self.instance_descriptor;
Self {
instance_descriptor: wgpu::InstanceDescriptor {
backends: desc.backends,
flags: desc.flags,
backend_options: desc.backend_options.clone(),
memory_budget_thresholds: desc.memory_budget_thresholds,
display: None,
},
display_handle: self.display_handle.clone(),
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: Arc::clone(&self.device_descriptor),
}
}
}
impl std::fmt::Debug for WgpuSetupCreateNew {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
instance_descriptor,
display_handle,
power_preference,
native_adapter_selector,
device_descriptor: _,
} = self;
f.debug_struct("WgpuSetupCreateNew")
.field("instance_descriptor", instance_descriptor)
.field("display_handle", display_handle)
.field("power_preference", power_preference)
.field(
"native_adapter_selector",
&native_adapter_selector.is_some(),
)
.finish_non_exhaustive()
}
}
/// Configuration for using an existing wgpu setup.
///
/// Used for [`WgpuSetup::Existing`].

View File

@@ -3,7 +3,7 @@
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
#![expect(unsafe_code)]
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
use crate::{
RendererOptions,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
@@ -17,6 +17,7 @@ struct SurfaceState {
width: u32,
height: u32,
resizing: bool,
needs_reconfigure: bool,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
@@ -26,7 +27,7 @@ struct SurfaceState {
/// NOTE: all egui viewports share the same painter.
pub struct Painter {
context: Context,
configuration: WgpuConfiguration,
config: WgpuConfiguration,
options: RendererOptions,
support_transparent_backbuffer: bool,
screen_capture_state: Option<CaptureState>,
@@ -57,16 +58,16 @@ impl Painter {
/// associated.
pub async fn new(
context: Context,
configuration: WgpuConfiguration,
config: WgpuConfiguration,
support_transparent_backbuffer: bool,
options: RendererOptions,
) -> Self {
let (capture_tx, capture_rx) = capture_channel();
let instance = configuration.wgpu_setup.new_instance().await;
let instance = config.wgpu_setup.new_instance().await;
Self {
context,
configuration,
config,
options,
support_transparent_backbuffer,
screen_capture_state: None,
@@ -93,17 +94,22 @@ impl Painter {
fn configure_surface(
surface_state: &SurfaceState,
render_state: &RenderState,
config: &WgpuConfiguration,
config: &SurfaceConfig,
) {
profiling::function_scope!();
let SurfaceConfig {
present_mode,
desired_maximum_frame_latency,
} = *config;
let width = surface_state.width;
let height = surface_state.height;
let mut surf_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: render_state.target_format,
present_mode: config.present_mode,
present_mode,
alpha_mode: surface_state.alpha_mode,
view_formats: vec![render_state.target_format],
..surface_state
@@ -112,7 +118,7 @@ impl Painter {
.expect("The surface isn't supported by this adapter")
};
if let Some(desired_maximum_frame_latency) = config.desired_maximum_frame_latency {
if let Some(desired_maximum_frame_latency) = desired_maximum_frame_latency {
surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency;
}
@@ -200,13 +206,9 @@ impl Painter {
let render_state = if let Some(render_state) = &self.render_state {
render_state
} else {
let render_state = RenderState::create(
&self.configuration,
&self.instance,
Some(&surface),
self.options,
)
.await?;
let render_state =
RenderState::create(&self.config, &self.instance, Some(&surface), self.options)
.await?;
self.render_state.get_or_insert(render_state)
};
let alpha_mode = if self.support_transparent_backbuffer {
@@ -234,6 +236,7 @@ impl Painter {
height: size.height,
alpha_mode,
resizing: false,
needs_reconfigure: false,
},
);
let Some(width) = NonZeroU32::new(size.width) else {
@@ -276,7 +279,7 @@ impl Painter {
surface_state.width = width;
surface_state.height = height;
Self::configure_surface(surface_state, render_state, &self.configuration);
Self::configure_surface(surface_state, render_state, &self.config.surface);
if let Some(depth_format) = self.options.depth_stencil_format {
self.depth_texture_view.insert(
@@ -368,12 +371,12 @@ impl Painter {
hal_surface
.render_layer()
.lock()
.set_presents_with_transaction(resizing);
.setPresentsWithTransaction(resizing);
Self::configure_surface(
state,
self.render_state.as_ref().unwrap(),
&self.configuration,
&self.config.surface,
);
}
}
@@ -409,6 +412,7 @@ impl Painter {
/// and the captures captured screenshot if it was requested.
///
/// If `capture_data` isn't empty, a screenshot will be captured.
#[expect(clippy::too_many_arguments)]
pub fn paint_and_update_textures(
&mut self,
viewport_id: ViewportId,
@@ -417,6 +421,7 @@ impl Painter {
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
capture_data: Vec<UserData>,
window: &winit::window::Window,
) -> f32 {
profiling::function_scope!();
@@ -445,6 +450,20 @@ impl Painter {
let capture = !capture_data.is_empty();
let mut vsync_sec = 0.0;
// Apply any runtime changes requested via `RenderState::surface_config`.
// We diff against the already-applied values in `self.config.surface`
// and, if anything differs, mark every surface as needing reconfiguration so
// the existing `needs_reconfigure` pathway below picks them up.
if let Some(render_state) = self.render_state.as_ref()
&& render_state.surface_config != self.config.surface
{
self.config.surface = render_state.surface_config;
#[expect(clippy::iter_over_hash_type)]
for surface in self.surfaces.values_mut() {
surface.needs_reconfigure = true;
}
}
let Some(render_state) = self.render_state.as_mut() else {
return vsync_sec;
};
@@ -454,7 +473,7 @@ impl Painter {
commands_submitted: false,
};
let Some(surface_state) = self.surfaces.get(&viewport_id) else {
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
return vsync_sec;
};
@@ -491,6 +510,11 @@ impl Painter {
)
};
if surface_state.needs_reconfigure {
Self::configure_surface(surface_state, render_state, &self.config.surface);
surface_state.needs_reconfigure = false;
}
let output_frame = {
profiling::scope!("get_current_texture");
// This is what vsync-waiting happens on my Mac.
@@ -501,16 +525,20 @@ impl Painter {
};
let output_frame = match output_frame {
Ok(frame) => frame,
Err(err) => match (*self.configuration.on_surface_error)(err) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
return vsync_sec;
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
surface_state.needs_reconfigure = true;
frame
}
other => {
match (*self.config.on_surface_status)(&other) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.config.surface);
}
SurfaceErrorAction::SkipFrame => {}
}
SurfaceErrorAction::SkipFrame => {
return vsync_sec;
}
},
return vsync_sec;
}
};
let mut capture_buffer = None;
@@ -643,6 +671,8 @@ impl Painter {
);
}
window.pre_present_notify();
{
profiling::scope!("present");
// wgpu doesn't document where vsync can happen. Maybe here?

View File

@@ -5,6 +5,21 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.2 - 2026-05-04
Nothing new
## 0.34.1 - 2026-03-27
Nothing new
## 0.34.0 - 2026-03-26
* Add `is_scrolling`/`is_smooth_scrolling` util, checking for active scroll action [#7669](https://github.com/emilk/egui/pull/7669) by [@IsseW](https://github.com/IsseW)
* Fix backspacing leaving last character in IME prediction not removed on macOS native and Safari [#7810](https://github.com/emilk/egui/pull/7810) by [@umajho](https://github.com/umajho)
* Much improved IME [#7967](https://github.com/emilk/egui/pull/7967) by [@umajho](https://github.com/umajho)
* Allow fallback from smithay to arboard when getting clipboard [#7976](https://github.com/emilk/egui/pull/7976) by [@wizzeh](https://github.com/wizzeh)
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true
@@ -20,6 +20,9 @@ workspace = true
all-features = true
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-shear]
ignored = ["wayland-cursor"] # TODO(emilk): remove when we update winit
[features]
default = ["clipboard", "links", "wayland", "winit/default", "x11"]
@@ -81,6 +84,7 @@ objc2.workspace = true
objc2-foundation = { workspace = true, features = ["std", "NSThread"] }
objc2-ui-kit = { workspace = true, features = [
"std",
"objc2-core-foundation",
"UIApplication",
"UIGeometry",
"UIResponder",

View File

@@ -65,13 +65,12 @@ impl Clipboard {
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
return match clipboard.load() {
Ok(text) => Some(text),
match clipboard.load() {
Ok(text) => return Some(text),
Err(err) => {
log::error!("smithay paste error: {err}");
None
}
};
}
}
#[cfg(all(

View File

@@ -9,6 +9,9 @@
#![expect(clippy::manual_range_contains)]
#[cfg(target_os = "windows")]
use std::collections::HashSet;
#[cfg(feature = "accesskit")]
pub use accesskit_winit;
pub use egui;
@@ -98,14 +101,17 @@ pub struct State {
/// Only one touch will be interpreted as pointer at any time.
pointer_touch_id: Option<u64>,
/// track ime state
has_sent_ime_enabled: bool,
#[cfg(feature = "accesskit")]
pub accesskit: Option<accesskit_winit::Adapter>,
allow_ime: bool,
ime_rect_px: Option<egui::Rect>,
/// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
/// release events that should be filtered out. See comments in that method
/// for details.
#[cfg(target_os = "windows")]
pressed_processed_physical_keys: HashSet<winit::keyboard::PhysicalKey>,
}
impl State {
@@ -126,9 +132,11 @@ impl State {
};
let mut slf = Self {
egui_ctx,
viewport_id,
start_time: web_time::Instant::now(),
start_time: web_time::Instant::now()
.checked_sub(web_time::Duration::from_secs_f64(egui_ctx.time()))
.unwrap_or_else(web_time::Instant::now),
egui_ctx,
egui_input,
pointer_pos_in_points: None,
any_pointer_button_down: false,
@@ -141,13 +149,13 @@ impl State {
simulate_touch_screen: false,
pointer_touch_id: None,
has_sent_ime_enabled: false,
#[cfg(feature = "accesskit")]
accesskit: None,
allow_ime: false,
ime_rect_px: None,
#[cfg(target_os = "windows")]
pressed_processed_physical_keys: HashSet::new(),
};
slf.egui_input
@@ -364,25 +372,33 @@ impl State {
is_synthetic,
..
} => {
// Winit generates fake "synthetic" KeyboardInput events when the focus
// is changed to the window, or away from it. Synthetic key presses
// represent no real key presses and should be ignored.
// See https://github.com/rust-windowing/winit/issues/3543
if *is_synthetic && event.state == ElementState::Pressed {
// Winit generates fake "synthetic" KeyboardInput events when the focus
// is changed to the window, or away from it. Synthetic key presses
// represent no real key presses and should be ignored.
// See https://github.com/rust-windowing/winit/issues/3543
EventResponse {
repaint: true,
consumed: false,
}
} else {
self.on_keyboard_input(event);
let egui_wants_keyboard_input = self.egui_ctx.egui_wants_keyboard_input();
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
let consumed = self.egui_ctx.egui_wants_keyboard_input()
|| event.logical_key
== winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
EventResponse {
repaint: true,
consumed,
if let Some(response) =
self.try_on_ime_processed_keyboard_input(event, egui_wants_keyboard_input)
{
response
} else {
self.on_keyboard_input(event);
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
let consumed = egui_wants_keyboard_input
|| event.logical_key
== winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
EventResponse {
repaint: true,
consumed,
}
}
}
}
@@ -526,6 +542,91 @@ impl State {
}
}
#[cfg(not(target_os = "windows"))]
#[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)]
#[inline(always)]
fn try_on_ime_processed_keyboard_input(
&mut self,
_event: &winit::event::KeyEvent,
_egui_wants_keyboard_input: bool,
) -> Option<EventResponse> {
// `KeyboardInput` events processed by the IME are not emitted by
// `winit` on non-Windows platforms, so we don't need to do anything
// here.
None
}
#[cfg(target_os = "windows")]
#[inline(always)]
fn try_on_ime_processed_keyboard_input(
&mut self,
event: &winit::event::KeyEvent,
egui_wants_keyboard_input: bool,
) -> Option<EventResponse> {
if !self.allow_ime {
None
} else if event.logical_key == winit::keyboard::NamedKey::Process {
// On Windows, the current version of `winit` (0.30.12) has a bug
// where `KeyboardInput` events processed by the IME are still
// emitted. [^1]
//
// As a workaround, we detect these events by checking whether their
// `logical_key` is `winit::keyboard::NamedKey::Process`, and filter
// them out to keep behavior consistent with other platforms.
//
// `winit::keyboard::NamedKey::Process` is not documented in
// `winit`. Reading through its source code, we find that it is
// mapped from `VK_PROCESSKEY` on Windows [^2]. (On an unrelated
// note, Web is the only other platform that also uses it [^3].)
// According to Microsoft, “the IME sets the virtual key value
// to `VK_PROCESSKEY` after processing a key input message” [^4].
// See also [^5].
// (I can't find a documentation page dedicated to this value.)
//
// TODO(umajho): Remove this workaround once the `winit` bug is fixed
// and we've updated to a version that includes the fix. NOTE: Don't
// forget to also remove the `pressed_processed_physical_keys` field
// and its related code.
//
// [^1]: https://github.com/rust-windowing/winit/issues/4508
// [^2]: https://github.com/rust-windowing/winit/blob/e9809ef54b18499bb4f2cac945719ecc2a61061b/src/platform_impl/windows/keyboard_layout.rs#L946
// [^3]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
// [^4]: https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetvirtualkey#remarks
// [^5]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/keyboard-input#character-messages
self.pressed_processed_physical_keys
.insert(event.physical_key);
Some(EventResponse {
repaint: false,
consumed: egui_wants_keyboard_input,
})
} else if event.state == ElementState::Released
&& self
.pressed_processed_physical_keys
.remove(&event.physical_key)
{
// Unlike key-presses, we can not tell whether a key-release event
// is processed by the IME or not by looking at its `logical_key`,
// because their `logical_key` is the original value (e.g.
// `winit::keyboard::Key::Character(…)`) rather than
// `winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process)`.
// (See the screencast for Windows in [^1].)
// So we track the physical keys of processed key-presses and
// filter out the corresponding key-releases.
//
// [^1]: https://github.com/rust-windowing/winit/issues/4508
Some(EventResponse {
repaint: false,
consumed: egui_wants_keyboard_input,
})
} else {
None
}
}
/// ## NOTE
///
/// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
@@ -585,17 +686,11 @@ impl State {
// }
match ime {
winit::event::Ime::Enabled => {
if cfg!(target_os = "linux") {
// This event means different things in X11 and Wayland, but we can just
// ignore it and enable IME on the preedit event.
// See <https://github.com/rust-windowing/winit/issues/2498>
} else {
self.ime_event_enable();
}
}
winit::event::Ime::Preedit(text, Some(_cursor)) => {
self.ime_event_enable();
// [`winit::event::Ime::Enabled`] means different things in X11 and
// Wayland, but it doesn't matter to us.
// See <https://github.com/rust-windowing/winit/issues/2498>
winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
winit::event::Ime::Preedit(text, _) => {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
@@ -604,53 +699,10 @@ impl State {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone())));
self.ime_event_disable();
}
winit::event::Ime::Disabled => {
self.ime_event_disable();
}
winit::event::Ime::Preedit(_, None) => {
if cfg!(target_os = "macos") {
// On macOS, when the user presses backspace to delete the
// last character in an IME composition, `winit` only emits
// `winit::event::Ime::Preedit("", None)` without a
// preceding `winit::event::Ime::Preedit("", Some(0, 0))`.
//
// The current implementation of `egui::TextEdit` relies on
// receiving an `egui::ImeEvent::Preedit("")` to remove the
// last character in the composition in this case, so we
// emit it here.
//
// This is guarded to macOS-only, as applying it on other
// platforms is unnecessary and can cause undesired
// behavior.
// See: https://github.com/emilk/egui/pull/7973
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new())));
}
self.ime_event_disable();
}
}
}
pub fn ime_event_enable(&mut self) {
if !self.has_sent_ime_enabled {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Enabled));
self.has_sent_ime_enabled = true;
}
}
pub fn ime_event_disable(&mut self) {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Disabled));
self.has_sent_ime_enabled = false;
}
/// Returns `true` if the event was sent to egui.
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
if !self.is_pointer_in_window() && !self.any_pointer_button_down {
@@ -999,13 +1051,32 @@ impl State {
self.set_cursor_icon(window, cursor_icon);
let allow_ime = ime.is_some();
if self.allow_ime != allow_ime {
let is_toggling_ime = self.allow_ime != allow_ime;
if is_toggling_ime {
self.allow_ime = allow_ime;
#[cfg(target_os = "windows")]
if !self.allow_ime {
// Defensively clear the set to avoid unexpected behavior.
//
// We don't do the same in `ime_event_disable` because the key
// release events for IME confirmation keys arrive after
// `winit::event::Ime::Disabled`.
self.pressed_processed_physical_keys.clear();
}
profiling::scope!("set_ime_allowed");
window.set_ime_allowed(allow_ime);
}
if let Some(ime) = ime {
if !is_toggling_ime && ime.should_interrupt_composition {
// TODO(umajho): use a more proper way to interrupt composition
// if `winit` provides one in the future.
window.set_ime_allowed(false);
window.set_ime_allowed(true);
}
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
let ime_rect_px = pixels_per_point * ime.rect;
if self.ime_rect_px != Some(ime_rect_px)

View File

@@ -36,8 +36,8 @@ mod ios {
| UISceneActivationState::ForegroundInactive
)
{
// Safe to cast, the class kind was checked above
let window_scene = Retained::cast::<UIWindowScene>(scene.clone());
// SAFETY: class kind was checked above with `isKindOfClass`
let window_scene = Retained::cast_unchecked::<UIWindowScene>(scene.clone());
if let Some(window) = window_scene.keyWindow() {
let insets = window.safeAreaInsets();
return SafeAreaInsets(MarginF32 {

View File

@@ -11,7 +11,7 @@ readme = "../../README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true

View File

@@ -4,7 +4,21 @@ use epaint::text::TextWrapMode;
/// A low-level ui building block.
///
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
/// This can be a piece of text, an image, or even a custom widget.
/// It can be decorated with various layout hints, such as `grow`, `shrink`, `align`, and more.
///
/// `Atom` implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
///
/// Many widgets take an `impl` [`crate::IntoAtoms`] parameter,
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
/// ```
/// # use egui::{Vec2, AtomExt, AtomKind, Atom, Image, Id};
/// # egui::__run_test_ui(|ui| {
/// let image = egui::include_image!("../../../eframe/data/icon.png");
/// ui.button((image, "Click me!"));
/// # });
/// ```
///
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
/// ```
/// # use egui::{Image, emath::Vec2};

View File

@@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
// If none is found, mark the first text item as `shrink`.
if wrap_mode != TextWrapMode::Extend {
let any_shrink = atoms.iter().any(|a| a.shrink);
let any_shrink = atoms.any_shrink();
if !any_shrink {
let first_text = atoms
.iter_mut()
@@ -318,8 +318,9 @@ impl<'a> AtomLayout<'a> {
let (_, rect) = ui.allocate_space(frame_size);
let mut response = ui.interact(rect, id, sense);
response.intrinsic_size =
Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));
response.set_intrinsic_size(
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size),
);
AllocatedAtomLayout {
sized_atoms: sized_items,
@@ -520,6 +521,20 @@ impl AtomLayoutResponse {
}
}
impl Deref for AtomLayoutResponse {
type Target = Response;
fn deref(&self) -> &Self::Target {
&self.response
}
}
impl DerefMut for AtomLayoutResponse {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.response
}
}
impl Widget for AtomLayout<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response

View File

@@ -8,6 +8,15 @@ use std::ops::{Deref, DerefMut};
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
/// A list of [`Atom`]s.
///
/// Many widgets take an `impl` [`IntoAtoms`] parameter,
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
/// ```
/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2};
/// # egui::__run_test_ui(|ui| {
/// let image = egui::include_image!("../../../eframe/data/icon.png");
/// ui.button((image, "Click me!"));
/// # });
#[derive(Clone, Debug, Default)]
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
@@ -69,6 +78,11 @@ impl<'a> Atoms<'a> {
string
}
/// Do any of the atoms have shrink set to `true`?
pub fn any_shrink(&self) -> bool {
self.iter().any(|a| a.shrink)
}
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
self.0.iter().map(|atom| &atom.kind)
}
@@ -187,6 +201,16 @@ where
}
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
///
/// Many widgets take an `impl` [`IntoAtoms`] parameter,
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
/// ```
/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2};
/// # egui::__run_test_ui(|ui| {
/// let image = egui::include_image!("../../../eframe/data/icon.png");
/// ui.button((image, "Click me!"));
/// # });
/// ```
pub trait IntoAtoms<'a> {
fn collect(self, atoms: &mut Atoms<'a>);

View File

@@ -1,3 +1,5 @@
use std::fmt::Write as _;
#[derive(Clone)]
struct Frame {
/// `_main` is usually as the deepest depth.
@@ -23,7 +25,7 @@ pub fn capture() -> String {
if let Some(file_and_line) = &mut file_and_line
&& let Some(line_nr) = symbol.lineno()
{
file_and_line.push_str(&format!(":{line_nr}"));
write!(file_and_line, ":{line_nr}").ok();
}
let file_and_line = file_and_line.unwrap_or_default();
@@ -130,12 +132,14 @@ pub fn capture() -> String {
if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth {
// Show that some frames were elided
formatted.push_str(&format!("{:widest_depth$}\n", ""));
writeln!(formatted, "{:widest_depth$} …", "").ok();
}
formatted.push_str(&format!(
"{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}\n"
));
writeln!(
formatted,
"{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}"
)
.ok();
last_depth = frame.depth;
}

View File

@@ -280,7 +280,7 @@ impl Area {
self
}
/// Constrains this area to [`Context::screen_rect`]?
/// Constrains this area to [`Context::content_rect`]?
///
/// Default: `true`.
#[inline]
@@ -291,7 +291,7 @@ impl Area {
/// Constrain the movement of the window to the given rectangle.
///
/// For instance: `.constrain_to(ctx.screen_rect())`.
/// For instance: `.constrain_to(ctx.content_rect())`.
#[inline]
pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
self.constrain = true;
@@ -516,6 +516,7 @@ impl Area {
let move_response = ctx.create_widget(
WidgetRect {
id: interact_id,
parent_id: id,
layer_id,
rect: state.rect(),
interact_rect: state.rect().intersect(constrain_rect),

View File

@@ -451,15 +451,6 @@ impl CollapsingHeader {
self
}
/// Explicitly set the source of the [`Id`] of this widget, instead of using title label.
/// This is useful if the title label is dynamic or not unique.
#[deprecated = "Renamed id_salt"]
#[inline]
pub fn id_source(mut self, id_salt: impl Hash) -> Self {
self.id_salt = Id::new(id_salt);
self
}
/// If you set this to `false`, the [`CollapsingHeader`] will be grayed out and un-clickable.
///
/// This is a convenience for [`Ui::disable`].

View File

@@ -94,12 +94,6 @@ impl ComboBox {
}
}
/// Without label.
#[deprecated = "Renamed from_id_salt"]
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
Self::from_id_salt(id_salt)
}
/// Set the outer width of the button and menu.
///
/// Default is [`Spacing::combo_width`].

View File

@@ -174,11 +174,6 @@ impl Frame {
Self::NONE
}
#[deprecated = "Use `Frame::NONE` or `Frame::new()` instead."]
pub const fn none() -> Self {
Self::NONE
}
/// For when you want to group a few widgets together within a frame.
pub fn group(style: &Style) -> Self {
Self::new()
@@ -283,16 +278,6 @@ impl Frame {
self
}
/// The rounding of the _outer_ corner of the [`Self::stroke`]
/// (or, if there is no stroke, the outer corner of [`Self::fill`]).
///
/// In other words, this is the corner radius of the _widget rect_.
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
/// Margin outside the painted frame.
///
/// Similar to what is called `margin` in CSS.

View File

@@ -10,7 +10,7 @@
use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{Align, RectAlign, Vec2, vec2};
@@ -197,7 +197,7 @@ impl MenuState {
/// Horizontal menu bar where you can add [`MenuButton`]s.
///
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
/// The menu bar goes well in a [`crate::Panel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
///
@@ -219,9 +219,6 @@ pub struct MenuBar {
style: StyleModifier,
}
#[deprecated = "Renamed to `egui::MenuBar`"]
pub type Bar = MenuBar;
impl Default for MenuBar {
fn default() -> Self {
Self {
@@ -458,6 +455,7 @@ impl SubMenu {
let is_any_open = open_item.is_some();
let mut is_open = open_item == Some(id);
let was_open = is_open;
let mut set_open = None;
// We expand the button rect so there is no empty space where no menu is shown
@@ -470,9 +468,21 @@ impl SubMenu {
// But since we check if no other menu is open, nothing should be able to cover the button
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
// `clicked` includes keyboard and accessibility click actions.
// We want Enter/Space to toggle an already open submenu, while pointer clicks should keep
// the submenu open (for touch and pointer interactions).
let clicked = button_response.clicked();
let clicked_by_pointer = button_response.clicked_by(PointerButton::Primary);
let clicked_by_keyboard_or_access = clicked && !clicked_by_pointer;
if ui.is_enabled() && is_open && clicked_by_keyboard_or_access {
set_open = Some(false);
is_open = false;
}
// The clicked handler is there for accessibility (keyboard navigation)
let should_open =
ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open));
ui.is_enabled() && ((!was_open && clicked) || (is_hovered && !is_any_open));
if should_open {
set_open = Some(true);
is_open = true;

View File

@@ -9,7 +9,6 @@ mod combo_box;
pub mod frame;
pub mod menu;
pub mod modal;
pub mod old_popup;
pub mod panel;
mod popup;
pub(crate) mod resize;
@@ -26,7 +25,6 @@ pub use {
combo_box::*,
frame::Frame,
modal::{Modal, ModalResponse},
old_popup::*,
panel::*,
popup::*,
resize::Resize,

View File

@@ -1,212 +0,0 @@
//! Old and deprecated API for popups. Use [`Popup`] instead.
#![expect(deprecated)]
use crate::containers::tooltip::Tooltip;
use crate::{
Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect,
Response, Ui, Widget as _, WidgetText,
};
use emath::RectAlign;
// ----------------------------------------------------------------------------
/// Show a tooltip at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_ui`].
///
/// See also [`show_tooltip_text`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[expect(deprecated)]
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text");
/// });
/// }
/// # });
/// ```
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
}
/// Show a tooltip at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_ui`].
///
/// See also [`show_tooltip_text`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text");
/// });
/// }
/// # });
/// ```
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip_at_pointer<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
.gap(12.0)
.show(add_contents)
.map(|response| response.inner)
}
/// Show a tooltip under the given area.
///
/// If the tooltip does not fit under the area, it tries to place it above it instead.
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip_for<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, *widget_rect)
.show(add_contents)
.map(|response| response.inner)
}
/// Show a tooltip at the given position.
///
/// Returns `None` if the tooltip could not be placed.
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip_at<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, suggested_position)
.show(add_contents)
.map(|response| response.inner)
}
/// Show some text at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_text`].
///
/// See also [`show_tooltip`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text");
/// }
/// # });
/// ```
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip_text(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
text: impl Into<WidgetText>,
) -> Option<()> {
show_tooltip(ctx, parent_layer, widget_id, |ui| {
crate::widgets::Label::new(text).ui(ui);
})
}
/// Was this tooltip visible last frame?
#[deprecated = "Use `Tooltip::was_tooltip_open_last_frame` instead"]
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
Tooltip::was_tooltip_open_last_frame(ctx, widget_id)
}
/// Indicate whether a popup will be shown above or below the box.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AboveOrBelow {
Above,
Below,
}
/// Helper for [`popup_above_or_below_widget`].
#[deprecated = "Use `egui::Popup` instead"]
pub fn popup_below_widget<R>(
ui: &Ui,
popup_id: Id,
widget_response: &Response,
close_behavior: PopupCloseBehavior,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
popup_above_or_below_widget(
ui,
popup_id,
widget_response,
AboveOrBelow::Below,
close_behavior,
add_contents,
)
}
/// Shows a popup above or below another widget.
///
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
///
/// The opened popup will have a minimum width matching its parent.
///
/// You must open the popup with [`crate::Memory::open_popup`] or [`crate::Memory::toggle_popup`].
///
/// Returns `None` if the popup is not open.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// let response = ui.button("Open popup");
/// let popup_id = ui.make_persistent_id("my_unique_id");
/// if response.clicked() {
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
/// }
/// let below = egui::AboveOrBelow::Below;
/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside;
/// # #[expect(deprecated)]
/// egui::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| {
/// ui.set_min_width(200.0); // if you want to control the size
/// ui.label("Some more info, or things you can select:");
/// ui.label("…");
/// });
/// # });
/// ```
#[deprecated = "Use `egui::Popup` instead"]
pub fn popup_above_or_below_widget<R>(
_parent_ui: &Ui,
popup_id: Id,
widget_response: &Response,
above_or_below: AboveOrBelow,
close_behavior: PopupCloseBehavior,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let response = Popup::from_response(widget_response)
.layout(Layout::top_down_justified(Align::LEFT))
.open_memory(None)
.close_behavior(close_behavior)
.id(popup_id)
.align(match above_or_below {
AboveOrBelow::Above => RectAlign::TOP_START,
AboveOrBelow::Below => RectAlign::BOTTOM_START,
})
.width(widget_response.rect.width())
.show(|ui| {
ui.set_min_width(ui.available_width());
add_contents(ui)
})?;
Some(response.inner)
}

View File

@@ -18,9 +18,8 @@
use emath::{GuiRounding as _, Pos2};
use crate::{
Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef,
Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetType, lerp,
vec2,
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect, Sense,
Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, vec2,
};
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
@@ -451,59 +450,6 @@ impl Panel {
}
}
// Deprecated
impl Panel {
#[deprecated = "Renamed default_size"]
pub fn default_width(self, default_size: f32) -> Self {
self.default_size(default_size)
}
#[deprecated = "Renamed min_size"]
pub fn min_width(self, min_size: f32) -> Self {
self.min_size(min_size)
}
#[deprecated = "Renamed max_size"]
pub fn max_width(self, max_size: f32) -> Self {
self.max_size(max_size)
}
#[deprecated = "Renamed size_range"]
pub fn width_range(self, size_range: impl Into<Rangef>) -> Self {
self.size_range(size_range)
}
#[deprecated = "Renamed exact_size"]
pub fn exact_width(self, size: f32) -> Self {
self.exact_size(size)
}
#[deprecated = "Renamed default_size"]
pub fn default_height(self, default_size: f32) -> Self {
self.default_size(default_size)
}
#[deprecated = "Renamed min_size"]
pub fn min_height(self, min_size: f32) -> Self {
self.min_size(min_size)
}
#[deprecated = "Renamed max_size"]
pub fn max_height(self, max_size: f32) -> Self {
self.max_size(max_size)
}
#[deprecated = "Renamed size_range"]
pub fn height_range(self, size_range: impl Into<Rangef>) -> Self {
self.size_range(size_range)
}
#[deprecated = "Renamed exact_size"]
pub fn exact_height(self, size: f32) -> Self {
self.exact_size(size)
}
}
// Public showing methods
impl Panel {
/// Show the panel inside a [`Ui`].
@@ -515,41 +461,6 @@ impl Panel {
self.show_inside_dyn(ui, Box::new(add_contents))
}
/// Show the panel at the top level.
#[deprecated = "Use show_inside() instead"]
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
/// Show the panel if `is_expanded` is `true`,
/// otherwise don't show it, but with a nice animation between collapsed and expanded.
#[deprecated = "Use show_animated_inside() instead"]
pub fn show_animated<R>(
self,
ctx: &Context,
is_expanded: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
#![expect(deprecated)]
let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
let animated_panel = self.get_animated_panel(ctx, is_expanded)?;
if how_expanded < 1.0 {
// Show a fake panel in this in-between animation state:
animated_panel.show(ctx, |_ui| {});
None
} else {
// Show the real panel:
Some(animated_panel.show(ctx, add_contents))
}
}
/// Show the panel if `is_expanded` is `true`,
/// otherwise don't show it, but with a nice animation between collapsed and expanded.
pub fn show_animated_inside<R>(
@@ -561,7 +472,11 @@ impl Panel {
let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
// Get either the fake or the real panel to animate
let animated_panel = self.get_animated_panel(ui.ctx(), is_expanded)?;
let Some(animated_panel) = self.get_animated_panel(ui.ctx(), is_expanded) else {
// Make sure the ids of the next widgets are the same whether we show the panel or not:
ui.skip_ahead_auto_ids(1);
return None;
};
if how_expanded < 1.0 {
// Show a fake panel in this in-between animation state:
@@ -573,34 +488,6 @@ impl Panel {
}
}
/// Show either a collapsed or a expanded panel, with a nice animation between.
#[deprecated = "Use show_animated_between_inside() instead"]
pub fn show_animated_between<R>(
ctx: &Context,
is_expanded: bool,
collapsed_panel: Self,
expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> Option<InnerResponse<R>> {
#![expect(deprecated)]
let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
// Get either the fake or the real panel to animate
let animated_between_panel =
Self::get_animated_between_panel(ctx, is_expanded, collapsed_panel, expanded_panel);
if 0.0 == how_expanded {
Some(animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
} else if how_expanded < 1.0 {
// Show animation:
animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded));
None
} else {
Some(animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
}
}
/// Show either a collapsed or a expanded panel, with a nice animation between.
pub fn show_animated_between_inside<R>(
ui: &mut Ui,
@@ -752,59 +639,6 @@ impl Panel {
inner_response
}
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
#![expect(deprecated)]
let side = self.side;
let available_rect = ctx.available_rect();
let mut panel_ui = Ui::new(
ctx.clone(),
self.id,
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(available_rect),
);
panel_ui.set_clip_rect(ctx.content_rect());
panel_ui
.response()
.widget_info(|| WidgetInfo::new(WidgetType::Panel));
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
let rect = inner_response.response.rect;
match side {
PanelSide::Vertical(side) => match side {
VerticalSide::Left => ctx.pass_state_mut(|state| {
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
}),
VerticalSide::Right => ctx.pass_state_mut(|state| {
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
}),
},
PanelSide::Horizontal(side) => match side {
HorizontalSide::Top => {
ctx.pass_state_mut(|state| {
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
});
}
HorizontalSide::Bottom => {
ctx.pass_state_mut(|state| {
state.allocate_bottom_panel(Rect::from_min_max(
rect.min,
available_rect.max,
));
});
}
},
}
inner_response
}
fn prepare_resizable_panel(&self, panel_sizer: &mut PanelSizer<'_>, ui: &Ui) {
let resize_id = self.id.with("__resize");
let resize_response = ui.ctx().read_response(resize_id);
@@ -1040,61 +874,9 @@ impl CentralPanel {
response
}
/// Show the panel at the top level.
#[deprecated = "Use show_inside() instead"]
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
#![expect(deprecated)]
let id = Id::new((ctx.viewport_id(), "central_panel"));
let mut panel_ui = Ui::new(
ctx.clone(),
id,
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(ctx.available_rect()),
);
panel_ui.set_clip_rect(ctx.content_rect());
if false {
// TODO(emilk): @lucasmerlin shouldn't we enable this?
panel_ui
.response()
.widget_info(|| WidgetInfo::new(WidgetType::Panel));
}
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
// Only inform ctx about what we actually used, so we can shrink the native window to fit.
ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
inner_response
}
}
fn clamp_to_range(x: f32, range: Rangef) -> f32 {
let range = range.as_positive();
x.clamp(range.min, range.max)
}
// ----------------------------------------------------------------------------
#[deprecated = "Use Panel::left or Panel::right instead"]
pub type SidePanel = super::Panel;
#[deprecated = "Use Panel::top or Panel::bottom instead"]
pub type TopBottomPanel = super::Panel;

View File

@@ -1,5 +1,3 @@
#![expect(deprecated)] // This is a new, safe wrapper around the old `Memory::popup` API.
use std::iter::once;
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
@@ -87,7 +85,7 @@ pub enum PopupCloseBehavior {
/// but in the popup's body
CloseOnClickOutside,
/// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_all_popups`]
/// Clicks will be ignored. Popup might be closed manually by calling [`Popup::close_all`]
/// or by pressing the escape button
IgnoreClicks,
}
@@ -666,10 +664,6 @@ impl Popup<'_> {
}
/// Open the given popup and close all others.
///
/// If you are NOT using [`Popup::show`], you must
/// also call [`crate::Memory::keep_popup_open`] as long as
/// you're showing the popup.
pub fn open_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.open_popup(popup_id));
}

View File

@@ -69,13 +69,6 @@ impl Resize {
self
}
/// A source for the unique [`Id`], e.g. `.id_source("second_resize_area")` or `.id_source(loop_index)`.
#[inline]
#[deprecated = "Renamed id_salt"]
pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt(id_salt)
}
/// A source for the unique [`Id`], e.g. `.id_salt("second_resize_area")` or `.id_salt(loop_index)`.
#[inline]
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {

View File

@@ -5,7 +5,7 @@
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use emath::GuiRounding as _;
use epaint::Margin;
use epaint::{Color32, Direction, Margin, Shape};
use crate::{
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
@@ -423,13 +423,6 @@ impl ScrollArea {
self
}
/// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`.
#[inline]
#[deprecated = "Renamed id_salt"]
pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt(id_salt)
}
/// A source for the unique [`Id`], e.g. `.id_salt("second_scroll_area")` or `.id_salt(loop_index)`.
#[inline]
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
@@ -530,32 +523,6 @@ impl ScrollArea {
/// This can be used, for example, to optionally freeze scrolling while the user
/// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
///
/// This controls both scrolling directions.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn enable_scrolling(mut self, enable: bool) -> Self {
self.scroll_source = if enable {
ScrollSource::ALL
} else {
ScrollSource::NONE
};
self
}
/// Can the user drag the scroll area to scroll?
///
/// This is useful for touch screens.
///
/// If `true`, the [`ScrollArea`] will sense drags.
///
/// Default: `true`.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.scroll_source.drag = drag_to_scroll;
self
}
/// What sources does the [`ScrollArea`] use for scrolling the contents.
#[inline]
pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
@@ -1019,13 +986,17 @@ impl ScrollArea {
.inner;
let (content_size, state) = prepared.end(ui);
ScrollAreaOutput {
let output = ScrollAreaOutput {
inner,
id,
state,
content_size,
inner_rect,
}
};
paint_fade_areas(ui, &output);
output
}
}
@@ -1504,3 +1475,88 @@ impl Prepared {
(content_size, state)
}
}
/// Paint fade-out gradients at the top and/or bottom of a scroll area to
/// indicate that more content is available beyond the visible region.
fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
let crate::style::ScrollFadeStyle {
strength,
size: fade_size,
} = ui.spacing().scroll.fade;
if strength <= 0.0 {
return;
}
let bg = ui.stack().bg_color();
let offset = scroll_output.state.offset;
let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
let paint_rect = scroll_output
.inner_rect
.intersect(ui.min_rect())
.expand(ui.visuals().clip_rect_margin);
// Top fade: animate opacity based on how far we've scrolled down.
if 0.0 < offset.y {
let t = (offset.y / fade_size).clamp(0.0, 1.0) * strength;
let bg_faded = bg.gamma_multiply(t);
let rect = Rect::from_min_max(
paint_rect.left_top(),
pos2(paint_rect.right(), paint_rect.top() + fade_size),
);
ui.painter().add(Shape::gradient_rect(
rect,
Direction::TopDown,
[bg_faded, Color32::TRANSPARENT],
));
}
// Bottom fade: animate opacity based on distance from the bottom.
let distance_from_bottom = overflow.y - offset.y;
if 0.0 < distance_from_bottom {
let t = (distance_from_bottom / fade_size).clamp(0.0, 1.0) * strength;
let bg_faded = bg.gamma_multiply(t);
let rect = Rect::from_min_max(
pos2(paint_rect.left(), paint_rect.bottom() - fade_size),
paint_rect.right_bottom(),
);
ui.painter().add(Shape::gradient_rect(
rect,
Direction::BottomUp,
[bg_faded, Color32::TRANSPARENT],
));
}
// Left fade: animate opacity based on how far we've scrolled right.
if 0.0 < offset.x {
let t = (offset.x / fade_size).clamp(0.0, 1.0) * strength;
let bg_faded = bg.gamma_multiply(t);
let rect = Rect::from_min_max(
paint_rect.left_top(),
pos2(paint_rect.left() + fade_size, paint_rect.bottom()),
);
ui.painter().add(Shape::gradient_rect(
rect,
Direction::LeftToRight,
[bg_faded, Color32::TRANSPARENT],
));
}
// Right fade: animate opacity based on distance from the right edge.
let distance_from_right = overflow.x - offset.x;
if 0.0 < distance_from_right {
let t = (distance_from_right / fade_size).clamp(0.0, 1.0) * strength;
let bg_faded = bg.gamma_multiply(t);
let rect = Rect::from_min_max(
pos2(paint_rect.right() - fade_size, paint_rect.top()),
paint_rect.right_bottom(),
);
ui.painter().add(Shape::gradient_rect(
rect,
Direction::RightToLeft,
[bg_faded, Color32::TRANSPARENT],
));
}
}

View File

@@ -16,24 +16,6 @@ pub struct Tooltip<'a> {
}
impl Tooltip<'_> {
/// Show a tooltip that is always open.
#[deprecated = "Use `Tooltip::always_open` instead."]
pub fn new(
parent_widget: Id,
ctx: Context,
anchor: impl Into<PopupAnchor>,
parent_layer: LayerId,
) -> Self {
Self {
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.sense(Sense::hover()),
parent_layer,
parent_widget,
}
}
/// Show a tooltip that is always open.
pub fn always_open(
ctx: Context,

View File

@@ -262,7 +262,7 @@ impl<'open> Window<'open> {
self
}
/// Constrains this window to [`Context::screen_rect`].
/// Constrains this window to [`Context::content_rect`].
///
/// To change the area to constrain to, use [`Self::constrain_to`].
///
@@ -275,7 +275,7 @@ impl<'open> Window<'open> {
/// Constrain the movement of the window to the given rectangle.
///
/// For instance: `.constrain_to(ctx.screen_rect())`.
/// For instance: `.constrain_to(ctx.content_rect())`.
#[inline]
pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
self.area = self.area.constrain_to(constrain_rect);
@@ -427,7 +427,7 @@ impl<'open> Window<'open> {
/// Enable/disable scrolling on the window by dragging with the pointer. `true` by default.
///
/// See [`ScrollArea::drag_to_scroll`] for more.
/// See [`ScrollArea::scroll_source`] for more.
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.scroll = self.scroll.scroll_source(ScrollSource {
@@ -673,7 +673,7 @@ impl Window<'_> {
title_bar.ui(
&mut area_content_ui,
&content_response,
content_response.as_ref(),
open.as_deref_mut(),
&mut collapsing,
collapsible,
@@ -962,6 +962,7 @@ fn do_resize_interaction(
WidgetRect {
layer_id,
id,
parent_id: layer_id.id,
rect,
interact_rect: rect,
sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable
@@ -1255,7 +1256,7 @@ impl TitleBar {
fn ui(
self,
ui: &mut Ui,
content_response: &Option<Response>,
content_response: Option<&Response>,
open: Option<&mut bool>,
collapsing: &mut CollapsingState,
collapsible: bool,
@@ -1299,7 +1300,7 @@ impl TitleBar {
ui.visuals().text_color(),
);
if let Some(content_response) = &content_response {
if let Some(content_response) = content_response {
// Paint separator between title and content:
let content_rect = content_response.rect;
if false {

View File

@@ -300,7 +300,7 @@ impl RepaintCause {
struct ViewportRepaintInfo {
/// Monotonically increasing counter.
///
/// Incremented at the end of [`Context::run`].
/// Incremented at the end of [`Context::run_ui`].
/// This can be smaller than [`Self::cumulative_pass_nr`],
/// but never larger.
cumulative_frame_nr: u64,
@@ -463,7 +463,7 @@ impl ContextImpl {
let content_rect = viewport.input.content_rect();
viewport.this_pass.begin_pass(content_rect);
viewport.this_pass.begin_pass();
{
let mut layers: Vec<LayerId> = viewport.prev_pass.widgets.layer_ids().collect();
@@ -697,8 +697,8 @@ impl ContextImpl {
/// // Game loop:
/// loop {
/// let raw_input = egui::RawInput::default();
/// let full_output = ctx.run(raw_input, |ctx| {
/// egui::CentralPanel::default().show(&ctx, |ui| {
/// let full_output = ctx.run_ui(raw_input, |ui| {
/// egui::CentralPanel::default().show_inside(ui, |ui| {
/// ui.label("Hello world!");
/// if ui.button("Click me").clicked() {
/// // take some action here
@@ -780,9 +780,6 @@ impl Context {
/// });
/// // handle full_output
/// ```
///
/// ## See also
/// * [`Self::run`]
#[must_use]
pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput {
self.run_ui_dyn(new_input, &mut run_ui)
@@ -791,60 +788,28 @@ impl Context {
#[must_use]
fn run_ui_dyn(&self, new_input: RawInput, run_ui: &mut dyn FnMut(&mut Ui)) -> FullOutput {
let plugins = self.read(|ctx| ctx.plugins.ordered_plugins());
#[expect(deprecated)]
self.run(new_input, |ctx| {
let mut top_ui = Ui::new(
self.run_dyn(new_input, &mut |ctx| {
let mut root_ui = Ui::new(
ctx.clone(),
Id::new((ctx.viewport_id(), "__top_ui")),
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(ctx.available_rect()),
.max_rect(ctx.viewport_rect()),
);
{
plugins.on_begin_pass(&mut top_ui);
run_ui(&mut top_ui);
plugins.on_end_pass(&mut top_ui);
plugins.on_begin_pass(&mut root_ui);
run_ui(&mut root_ui);
plugins.on_end_pass(&mut root_ui);
}
// Inform ctx about what we actually used, so we can shrink the native window to fit.
// TODO(emilk): make better use of this somehow
ctx.pass_state_mut(|state| state.allocate_central_panel(top_ui.min_rect()));
ctx.pass_state_mut(|state| {
state.root_ui_available_rect = Some(root_ui.available_rect_before_wrap());
state.root_ui_min_rect = Some(root_ui.min_rect());
});
})
}
/// Run the ui code for one frame.
///
/// At most [`Options::max_passes`] calls will be issued to `run_ui`,
/// and only on the rare occasion that [`Context::request_discard`] is called.
/// Usually, it `run_ui` will only be called once.
///
/// Put your widgets into a [`crate::Panel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`].
///
/// Instead of calling `run`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`].
///
/// ```
/// // One egui context that you keep reusing:
/// let mut ctx = egui::Context::default();
///
/// // Each frame:
/// let input = egui::RawInput::default();
/// let full_output = ctx.run(input, |ctx| {
/// egui::CentralPanel::default().show(&ctx, |ui| {
/// ui.label("Hello egui!");
/// });
/// });
/// // handle full_output
/// ```
///
/// ## See also
/// * [`Self::run_ui`]
#[must_use]
#[deprecated = "Call run_ui instead"]
pub fn run(&self, new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput {
self.run_dyn(new_input, &mut run_ui)
}
#[must_use]
fn run_dyn(&self, mut new_input: RawInput, run_ui: &mut dyn FnMut(&Self)) -> FullOutput {
profiling::function_scope!();
@@ -914,10 +879,10 @@ impl Context {
output
}
/// An alternative to calling [`Self::run`].
/// An alternative to calling [`Self::run_ui`].
///
/// It is usually better to use [`Self::run`], because
/// `run` supports multi-pass layout using [`Self::request_discard`].
/// It is usually better to use [`Self::run_ui`], because
/// `run_ui` supports multi-pass layout using [`Self::request_discard`].
///
/// ```
/// // One egui context that you keep reusing:
@@ -927,9 +892,7 @@ impl Context {
/// let input = egui::RawInput::default();
/// ctx.begin_pass(input);
///
/// egui::CentralPanel::default().show(&ctx, |ui| {
/// ui.label("Hello egui!");
/// });
/// // … add panels and windows here …
///
/// let full_output = ctx.end_pass();
/// // handle full_output
@@ -942,12 +905,6 @@ impl Context {
self.write(|ctx| ctx.begin_pass(new_input));
}
/// See [`Self::begin_pass`].
#[deprecated = "Renamed begin_pass"]
pub fn begin_frame(&self, new_input: RawInput) {
self.begin_pass(new_input);
}
}
/// ## Borrows parts of [`Context`]
@@ -1048,7 +1005,7 @@ impl Context {
/// Read-only access to [`PassState`].
///
/// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]).
/// This is only valid during the call to [`Self::run_ui`] (between [`Self::begin_pass`] and [`Self::end_pass`]).
#[inline]
pub(crate) fn pass_state<R>(&self, reader: impl FnOnce(&PassState) -> R) -> R {
self.write(move |ctx| reader(&ctx.viewport().this_pass))
@@ -1056,7 +1013,7 @@ impl Context {
/// Read-write access to [`PassState`].
///
/// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]).
/// This is only valid during the call to [`Self::run_ui`] (between [`Self::begin_pass`] and [`Self::end_pass`]).
#[inline]
pub(crate) fn pass_state_mut<R>(&self, writer: impl FnOnce(&mut PassState) -> R) -> R {
self.write(move |ctx| writer(&mut ctx.viewport().this_pass))
@@ -1072,7 +1029,7 @@ impl Context {
/// Read-only access to [`Fonts`].
///
/// Not valid until first call to [`Context::run()`].
/// Not valid until first call to [`Context::run_ui()`].
/// That's because since we don't know the proper `pixels_per_point` until then.
#[inline]
pub fn fonts<R>(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R {
@@ -1089,7 +1046,7 @@ impl Context {
/// Read-write access to [`Fonts`].
///
/// Not valid until first call to [`Context::run()`].
/// Not valid until first call to [`Context::run_ui()`].
/// That's because since we don't know the proper `pixels_per_point` until then.
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R {
@@ -1360,6 +1317,7 @@ impl Context {
let WidgetRect {
id,
parent_id: _,
layer_id,
rect,
interact_rect,
@@ -1378,8 +1336,8 @@ impl Context {
interact_rect,
sense,
flags: Flags::empty(),
interact_pointer_pos: None,
intrinsic_size: None,
interact_pointer_pos_or_nan: Pos2::NAN,
intrinsic_size_or_nan: Vec2::NAN,
};
res.flags.set(Flags::ENABLED, enabled);
@@ -1470,14 +1428,11 @@ impl Context {
|| res.long_touched()
|| clicked
|| res.drag_stopped();
if is_interacted_with {
res.interact_pointer_pos = input.pointer.interact_pos();
if let (Some(to_global), Some(pos)) = (
memory.to_global.get(&res.layer_id),
&mut res.interact_pointer_pos,
) {
*pos = to_global.inverse() * *pos;
if is_interacted_with && let Some(mut pos) = input.pointer.interact_pos() {
if let Some(to_global) = memory.to_global.get(&res.layer_id) {
pos = to_global.inverse() * pos;
}
res.interact_pointer_pos_or_nan = pos;
}
if input.pointer.any_down() && !is_interacted_with {
@@ -1547,6 +1502,11 @@ impl Context {
crate::debug_text::print(self, text);
}
/// Current time in seconds, relative to some unknown epoch.
pub fn time(&self) -> f64 {
self.input(|i| i.time)
}
/// What operating system are we running on?
///
/// When compiling natively, this is
@@ -1662,7 +1622,7 @@ impl Context {
/// The total number of completed frames.
///
/// Starts at zero, and is incremented once at the end of each call to [`Self::run`].
/// Starts at zero, and is incremented once at the end of each call to [`Self::run_ui`].
///
/// This is always smaller or equal to [`Self::cumulative_pass_nr`].
pub fn cumulative_frame_nr(&self) -> u64 {
@@ -1671,7 +1631,7 @@ impl Context {
/// The total number of completed frames.
///
/// Starts at zero, and is incremented once at the end of each call to [`Self::run`].
/// Starts at zero, and is incremented once at the end of each call to [`Self::run_ui`].
///
/// This is always smaller or equal to [`Self::cumulative_pass_nr_for`].
pub fn cumulative_frame_nr_for(&self, id: ViewportId) -> u64 {
@@ -1691,7 +1651,7 @@ impl Context {
/// The total number of completed passes (usually there is one pass per rendered frame).
///
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once).
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run_ui`] (usually once).
///
/// If you instead want to know which pass index this is within the current frame,
/// use [`Self::current_pass_index`].
@@ -1701,7 +1661,7 @@ impl Context {
/// The total number of completed passes (usually there is one pass per rendered frame).
///
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once).
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run_ui`] (usually once).
pub fn cumulative_pass_nr_for(&self, id: ViewportId) -> u64 {
self.read(|ctx| {
ctx.viewports
@@ -1935,7 +1895,7 @@ impl Context {
}
}
/// Callbacks
/// Plugins
impl Context {
/// Call the given callback at the start of each pass of each viewport.
///
@@ -2076,7 +2036,7 @@ impl Context {
self.options(|opt| opt.theme())
}
/// The [`Theme`] used to select between dark and light [`Self::style`]
/// The [`Theme`] used to select between dark and light [`Self::global_style`]
/// as the active style used by all subsequent popups, menus, etc.
///
/// Example:
@@ -2093,12 +2053,6 @@ impl Context {
self.options(|opt| Arc::clone(opt.style()))
}
/// The currently active [`Style`] used by all subsequent popups, menus, etc.
#[deprecated = "Renamed to `global_style` to avoid confusion with `ui.style()`"]
pub fn style(&self) -> Arc<Style> {
self.options(|opt| Arc::clone(opt.style()))
}
/// Mutate the currently active [`Style`] used by all subsequent popups, menus, etc.
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
///
@@ -2113,21 +2067,6 @@ impl Context {
self.options_mut(|opt| mutate_style(Arc::make_mut(opt.style_mut())));
}
/// Mutate the currently active [`Style`] used by all subsequent popups, menus, etc.
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
///
/// Example:
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.global_style_mut(|style| {
/// style.spacing.item_spacing = egui::vec2(10.0, 20.0);
/// });
/// ```
#[deprecated = "Renamed to `global_style_mut` to avoid confusion with `ui.style_mut()`"]
pub fn style_mut(&self, mutate_style: impl FnOnce(&mut Style)) {
self.options_mut(|opt| mutate_style(Arc::make_mut(opt.style_mut())));
}
/// The currently active [`Style`] used by all new popups, menus, etc.
///
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
@@ -2139,18 +2078,6 @@ impl Context {
self.options_mut(|opt| *opt.style_mut() = style.into());
}
/// The currently active [`Style`] used by all new popups, menus, etc.
///
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
///
/// You can also change this using [`Self::style_mut`].
///
/// You can use [`Ui::style_mut`] to change the style of a single [`Ui`].
#[deprecated = "Renamed to `set_global_style` to avoid confusion with `ui.set_style()`"]
pub fn set_style(&self, style: impl Into<Arc<Style>>) {
self.options_mut(|opt| *opt.style_mut() = style.into());
}
/// Mutate the [`Style`]s used by all subsequent popups, menus, etc. in both dark and light mode.
///
/// Example:
@@ -2397,6 +2324,12 @@ impl Context {
crate::gui_zoom::zoom_with_keyboard(self);
}
for shortcut in self.options(|o| o.quit_shortcuts.clone()) {
if self.input_mut(|i| i.consume_shortcut(&shortcut)) {
self.send_viewport_cmd(ViewportCommand::Close);
}
}
#[cfg(debug_assertions)]
self.debug_painting();
@@ -2408,17 +2341,11 @@ impl Context {
output
}
/// Call at the end of each frame if you called [`Context::begin_pass`].
#[must_use]
#[deprecated = "Renamed end_pass"]
pub fn end_frame(&self) -> FullOutput {
self.end_pass()
}
/// Called at the end of the pass.
#[cfg(debug_assertions)]
fn debug_painting(&self) {
#![expect(clippy::iter_over_hash_type)] // ok to be sloppy in debug painting
use std::fmt::Write as _;
let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| {
let rect = widget.interact_rect;
@@ -2491,13 +2418,17 @@ impl Context {
for id in contains_pointer {
let mut widget_text = format!("{id:?}");
if let Some(rect) = widget_rects.get(id) {
widget_text +=
&format!(" {:?} {:?} {:?}", rect.layer_id, rect.rect, rect.sense);
write!(
widget_text,
" {:?} {:?} {:?}",
rect.layer_id, rect.rect, rect.sense
)
.ok();
}
if let Some(info) = widget_rects.info(id) {
widget_text += &format!(" {info:?}");
write!(widget_text, " {info:?}").ok();
}
debug_text += &format!("{widget_text}\n");
writeln!(debug_text, "{widget_text}").ok();
}
self.debug_text(debug_text);
}
@@ -2562,7 +2493,7 @@ impl Context {
);
self.viewport(|vp| {
for reason in &vp.output.request_discard_reasons {
warning += &format!("\n {reason}");
write!(warning, "\n {reason}").ok();
}
});
@@ -2597,6 +2528,12 @@ impl ContextImpl {
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
if self.memory.should_interrupt_ime()
&& let Some(ime) = &mut platform_output.ime
{
ime.should_interrupt_composition = true;
}
{
profiling::scope!("accesskit");
let state = viewport.this_pass.accesskit_state.take();
@@ -2636,6 +2573,19 @@ impl ContextImpl {
}
}
#[cfg(debug_assertions)]
let shapes = if self.memory.options.style().debug.warn_if_rect_changes_id {
let mut shapes = shapes;
warn_if_rect_changes_id(
&mut shapes,
&viewport.prev_pass.widgets,
&viewport.this_pass.widgets,
);
shapes
} else {
shapes
};
std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
if repaint_needed {
@@ -2815,24 +2765,14 @@ impl Context {
self.input(|i| i.viewport_rect()).round_ui()
}
/// Position and size of the egui area.
#[deprecated(
note = "screen_rect has been split into viewport_rect() and content_rect(). You likely should use content_rect()"
)]
pub fn screen_rect(&self) -> Rect {
self.input(|i| i.content_rect()).round_ui()
}
/// How much space is still available after panels have been added.
#[deprecated = "Use content_rect (or viewport_rect) instead"]
pub fn available_rect(&self) -> Rect {
self.pass_state(|s| s.available_rect()).round_ui()
}
/// How much space is used by windows and the top-level [`Ui`].
pub fn globally_used_rect(&self) -> Rect {
self.write(|ctx| {
let mut used = ctx.viewport().this_pass.used_by_panels;
let viewport = ctx.viewport();
let root_ui_min_rect =
(viewport.this_pass.root_ui_min_rect).or(viewport.prev_pass.root_ui_min_rect);
let mut used = root_ui_min_rect.unwrap_or(Rect::NOTHING);
for (_id, window) in ctx.memory.areas().visible_windows() {
used |= window.rect();
}
@@ -2840,46 +2780,33 @@ impl Context {
})
}
/// How much space is used by windows and the top-level [`Ui`].
#[deprecated = "Renamed to globally_used_rect"]
pub fn used_rect(&self) -> Rect {
self.globally_used_rect()
}
/// How much space is used by windows and the top-level [`Ui`].
///
/// You can shrink your egui area to this size and still fit all egui components.
#[deprecated = "Use globally_used_rect instead"]
pub fn used_size(&self) -> Vec2 {
(self.globally_used_rect().max - Pos2::ZERO).round_ui()
}
// ---------------------------------------------------------------------
/// Is the pointer (mouse/touch) over any egui area?
pub fn is_pointer_over_egui(&self) -> bool {
let pointer_pos = self.input(|i| i.pointer.interact_pos());
if let Some(pointer_pos) = pointer_pos {
if let Some(layer) = self.layer_id_at(pointer_pos) {
if layer.order == Order::Background {
!self.pass_state(|state| state.unused_rect.contains(pointer_pos))
} else {
true
}
let Some(pointer_pos) = pointer_pos else {
return false;
};
let Some(layer) = self.layer_id_at(pointer_pos) else {
return false;
};
if layer.order == Order::Background {
let root_ui_available_rect = self
.pass_state(|state| state.root_ui_available_rect)
.or_else(|| self.prev_pass_state(|state| state.root_ui_available_rect));
if let Some(root_ui_available_rect) = root_ui_available_rect {
// Modern `run_ui` code
!root_ui_available_rect.contains(pointer_pos)
} else {
false
true // We shouldn't get here, but who knows
}
} else {
false
true
}
}
/// Is the pointer (mouse/touch) over any egui area?
#[deprecated = "Renamed to is_pointer_over_egui"]
pub fn is_pointer_over_area(&self) -> bool {
self.is_pointer_over_egui()
}
/// True if egui is currently interested in the pointer (mouse or touch).
///
/// Could be the pointer is hovering over a [`crate::Window`] or the user is dragging a widget.
@@ -2891,17 +2818,6 @@ impl Context {
|| (self.is_pointer_over_egui() && !self.input(|i| i.pointer.any_down()))
}
/// True if egui is currently interested in the pointer (mouse or touch).
///
/// Could be the pointer is hovering over a [`crate::Window`] or the user is dragging a widget.
/// If `false`, the pointer is outside of any egui area and so
/// you may be interested in what it is doing (e.g. controlling your game).
/// Returns `false` if a drag started outside of egui and then moved over an egui area.
#[deprecated = "Renamed to egui_wants_pointer_input"]
pub fn wants_pointer_input(&self) -> bool {
self.egui_wants_pointer_input()
}
/// Is egui currently using the pointer position (e.g. dragging a slider)?
///
/// NOTE: this will return `false` if the pointer is just hovering over an egui area.
@@ -2909,23 +2825,18 @@ impl Context {
self.memory(|m| m.interaction().is_using_pointer())
}
/// Is egui currently using the pointer position (e.g. dragging a slider)?
///
/// NOTE: this will return `false` if the pointer is just hovering over an egui area.
#[deprecated = "Renamed to egui_is_using_pointer"]
pub fn is_using_pointer(&self) -> bool {
self.egui_is_using_pointer()
}
/// If `true`, egui is currently listening on text input (e.g. typing text in a [`crate::TextEdit`]).
pub fn egui_wants_keyboard_input(&self) -> bool {
self.memory(|m| m.focused().is_some())
}
/// If `true`, egui is currently listening on text input (e.g. typing text in a [`crate::TextEdit`]).
#[deprecated = "Renamed to egui_wants_keyboard_input"]
pub fn wants_keyboard_input(&self) -> bool {
self.egui_wants_keyboard_input()
/// Is the currently focused widget a text edit?
pub fn text_edit_focused(&self) -> bool {
if let Some(id) = self.memory(|mem| mem.focused()) {
crate::text_edit::TextEditState::load(self, id).is_some()
} else {
false
}
}
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
@@ -2938,18 +2849,6 @@ impl Context {
self.pass_state_mut(|fs| fs.highlight_next_pass.insert(id));
}
/// Is an egui context menu open?
///
/// This only works with the old, deprecated [`crate::menu`] API.
#[expect(deprecated)]
#[deprecated = "Use `any_popup_open` instead"]
pub fn is_context_menu_open(&self) -> bool {
self.data(|d| {
d.get_temp::<crate::menu::BarState>(crate::menu::CONTEXT_MENU_ID_STR.into())
.is_some_and(|state| state.has_root())
})
}
/// Is a popup or (context) menu open?
///
/// Will return false for [`crate::Tooltip`]s (which are technically popups as well).
@@ -2960,18 +2859,6 @@ impl Context {
.any(|layer| !layer.open_popups.is_empty())
})
}
/// Is a popup or (context) menu open?
///
/// Will return false for [`crate::Tooltip`]s (which are technically popups as well).
#[deprecated = "Renamed to any_popup_open"]
pub fn is_popup_open(&self) -> bool {
self.pass_state_mut(|fs| {
fs.layers
.values()
.any(|layer| !layer.open_popups.is_empty())
})
}
}
// Ergonomic methods to forward some calls often used in 'if let' without holding the borrow
@@ -3586,17 +3473,6 @@ impl Context {
}
});
#[expect(deprecated)]
ui.horizontal(|ui| {
ui.label(format!(
"{} menu bars",
self.data(|d| d.count::<crate::menu::BarState>())
));
if ui.button("Reset").clicked() {
self.data_mut(|d| d.remove_by_type::<crate::menu::BarState>());
}
});
ui.horizontal(|ui| {
ui.label(format!(
"{} scroll areas",
@@ -3949,8 +3825,8 @@ impl Context {
/// When called, the integration needs to:
/// * Check if there already is a window for this viewport id, and if not open one
/// * Set the window attributes (position, size, …) based on [`ImmediateViewport::builder`].
/// * Call [`Context::run`] with [`ImmediateViewport::viewport_ui_cb`].
/// * Handle the output from [`Context::run`], including rendering
/// * Call [`Context::run_ui`] with [`ImmediateViewport::viewport_ui_cb`].
/// * Handle the output from [`Context::run_ui`], including rendering
pub fn set_immediate_viewport_renderer(
callback: impl for<'a> Fn(&Self, ImmediateViewport<'a>) + 'static,
) {
@@ -4237,6 +4113,112 @@ fn context_impl_send_sync() {
assert_send_sync::<Context>();
}
/// Check if any [`Rect`] appears with different [`Id`]s between two passes.
///
/// This helps detect cases where the same screen area is claimed by different widget ids
/// across passes, which is often a sign of id instability.
#[cfg(debug_assertions)]
fn warn_if_rect_changes_id(
out_shapes: &mut Vec<ClippedShape>,
prev_widgets: &crate::WidgetRects,
new_widgets: &crate::WidgetRects,
) {
profiling::function_scope!();
use std::collections::BTreeMap;
/// A wrapper around [`Rect`] that implements [`Ord`] using the bit representation of its floats.
#[derive(Clone, Copy, PartialEq, Eq)]
struct OrderedRect(Rect);
impl PartialOrd for OrderedRect {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedRect {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let lhs = self.0;
let rhs = other.0;
lhs.min
.x
.to_bits()
.cmp(&rhs.min.x.to_bits())
.then(lhs.min.y.to_bits().cmp(&rhs.min.y.to_bits()))
.then(lhs.max.x.to_bits().cmp(&rhs.max.x.to_bits()))
.then(lhs.max.y.to_bits().cmp(&rhs.max.y.to_bits()))
}
}
fn create_lookup<'a>(
widgets: impl Iterator<Item = &'a WidgetRect>,
) -> BTreeMap<OrderedRect, Vec<&'a WidgetRect>> {
let mut lookup: BTreeMap<OrderedRect, Vec<&'a WidgetRect>> = BTreeMap::default();
for w in widgets {
lookup.entry(OrderedRect(w.rect)).or_default().push(w);
}
lookup
}
for (layer_id, new_layer_widgets) in new_widgets.layers() {
let prev = create_lookup(prev_widgets.get_layer(*layer_id));
let new = create_lookup(new_layer_widgets.iter());
for (hashable_rect, new_at_rect) in new {
let Some(prev_at_rect) = prev.get(&hashable_rect) else {
continue; // this rect did not exist in the previous pass
};
if prev_at_rect
.iter()
.any(|w| new_at_rect.iter().any(|nw| nw.id == w.id))
{
continue; // at least one id stayed the same, so this is not an id change
}
// Only warn if at least one of the previous ids is gone from this layer entirely.
// If they all still exist (just at a different rect), then the rect match
// is just a coincidence caused by widgets shifting (e.g. a window being dragged).
if prev_at_rect.iter().all(|w| new_widgets.contains(w.id)) {
continue;
}
// Only warn if at least one widget has the same parent_id in both frames.
// If all parent_ids changed too, this is a cascading id shift, not a widget bug.
if !prev_at_rect
.iter()
.any(|pw| new_at_rect.iter().any(|nw| nw.parent_id == pw.parent_id))
{
continue;
}
let rect = new_at_rect[0].rect;
log::warn!(
"Widget rect {rect:?} changed id between passes: prev ids: {:?}, new ids: {:?}",
prev_at_rect
.iter()
.map(|w| w.id.short_debug_format())
.collect::<Vec<_>>(),
new_at_rect
.iter()
.map(|w| w.id.short_debug_format())
.collect::<Vec<_>>(),
);
out_shapes.push(ClippedShape {
clip_rect: Rect::EVERYTHING,
shape: epaint::Shape::rect_stroke(
rect,
0,
(2.0, Color32::RED),
StrokeKind::Outside,
),
});
}
}
}
#[cfg(test)]
mod test {
use super::Context;

View File

@@ -12,7 +12,7 @@ use crate::{
/// Set the values that make sense, leave the rest at their `Default::default()`.
///
/// You can check if `egui` is using the inputs using
/// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`].
/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`].
///
/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner.
///
@@ -64,8 +64,8 @@ pub struct RawInput {
/// In-order events received this frame.
///
/// There is currently no way to know if egui handles a particular event,
/// but you can check if egui is using the keyboard with [`crate::Context::wants_keyboard_input`]
/// and/or the pointer (mouse/touch) with [`crate::Context::is_using_pointer`].
/// but you can check if egui is using the keyboard with [`crate::Context::egui_wants_keyboard_input`]
/// and/or the pointer (mouse/touch) with [`crate::Context::egui_is_using_pointer`].
pub events: Vec<Event>,
/// Dragged files hovering over egui.
@@ -376,12 +376,14 @@ impl ViewportInfo {
ui.label(opt_as_str(&visible));
ui.end_row();
#[expect(clippy::ref_option)]
fn opt_rect_as_string(v: &Option<Rect>) -> String {
v.as_ref().map_or(String::new(), |r| {
format!("Pos: {:?}, size: {:?}", r.min, r.size())
})
}
#[expect(clippy::ref_option)]
fn opt_as_str<T: std::fmt::Debug>(v: &Option<T>) -> String {
v.as_ref().map_or(String::new(), |v| format!("{v:?}"))
}
@@ -441,6 +443,10 @@ pub enum Event {
Text(String),
/// A key was pressed or released.
///
/// ## Note for integration authors
///
/// Key events that has been processed by IMEs should not be sent to `egui`.
Key {
/// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak
/// keyboard layout, it will be taken into account.
@@ -599,15 +605,22 @@ pub enum Event {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImeEvent {
/// Notifies when the IME was enabled.
#[deprecated = "No longer used by egui"]
Enabled,
/// A new IME candidate is being suggested.
///
/// An empty preedit string indicates that the IME has been dismissed, while
/// a non-empty preedit string indicates that the IME is active.
Preedit(String),
/// IME composition ended with this final result.
///
/// The IME is considered dismissed after this event.
Commit(String),
/// Notifies when the IME was disabled.
#[deprecated = "No longer used by egui"]
Disabled,
}

View File

@@ -2,7 +2,7 @@
use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
/// What egui emits each frame from [`crate::Context::run`].
/// What egui emits each frame from [`crate::Context::run_ui`].
///
/// The backend should use this.
#[derive(Clone, Default)]
@@ -79,6 +79,9 @@ pub struct IMEOutput {
///
/// This is a very thin rectangle.
pub cursor_rect: crate::Rect,
/// Whether any ongoing IME composition should be interrupted.
pub should_interrupt_composition: bool,
}
/// Commands that the egui integration should execute at the end of a frame.
@@ -123,6 +126,9 @@ pub struct PlatformOutput {
/// This is set if, and only if, the user is currently editing text.
///
/// Useful for IME.
///
/// This field should only be set by the widget that currently owns IME
/// events (see [`crate::Memory::owns_ime_events`]).
pub ime: Option<IMEOutput>,
/// The difference in the widget tree since last frame.

View File

@@ -2,7 +2,7 @@ use ahash::HashMap;
use emath::TSTransform;
use crate::{LayerId, Pos2, Sense, WidgetRect, WidgetRects, ahash, emath, id::IdSet};
use crate::{LayerId, Pos2, Sense, WidgetRect, WidgetRects, emath, id::IdSet};
/// Result of a hit-test against [`WidgetRects`].
///
@@ -450,6 +450,7 @@ mod tests {
fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect {
WidgetRect {
id,
parent_id: Id::NULL,
layer_id: LayerId::background(),
rect,
interact_rect: rect,

View File

@@ -209,7 +209,7 @@ impl InputOptions {
/// You can access this with [`crate::Context::input`].
///
/// You can check if `egui` is using the inputs using
/// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`].
/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct InputState {
@@ -522,14 +522,6 @@ impl InputState {
self.viewport_rect
}
/// Position and size of the egui area.
#[deprecated(
note = "screen_rect has been split into viewport_rect() and content_rect(). You likely should use content_rect()"
)]
pub fn screen_rect(&self) -> Rect {
self.content_rect()
}
/// Get the safe area insets.
///
/// This represents the area of the screen covered by status bars, navigation controls, notches,

View File

@@ -1,7 +1,7 @@
//! Handles paint layers, i.e. how things
//! are sometimes painted behind or in front of other things.
use crate::{Id, IdMap, Rect, ahash, epaint};
use crate::{Id, IdMap, Rect, epaint};
use epaint::{ClippedShape, Shape, emath::TSTransform};
/// Different layer categories
@@ -86,12 +86,6 @@ impl LayerId {
}
}
#[inline(always)]
#[deprecated = "Use `Memory::allows_interaction` instead"]
pub fn allow_interaction(&self) -> bool {
self.order.allow_interaction()
}
/// Short and readable summary
pub fn short_debug_format(&self) -> String {
format!(

View File

@@ -1,7 +1,7 @@
use emath::GuiRounding as _;
use crate::{
Align,
Align, Direction,
emath::{Align2, NumExt as _, Pos2, Rect, Vec2, pos2, vec2},
};
const INFINITY: f32 = f32::INFINITY;
@@ -87,36 +87,6 @@ impl Region {
// ----------------------------------------------------------------------------
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Direction {
LeftToRight,
RightToLeft,
TopDown,
BottomUp,
}
impl Direction {
#[inline(always)]
pub fn is_horizontal(self) -> bool {
match self {
Self::LeftToRight | Self::RightToLeft => true,
Self::TopDown | Self::BottomUp => false,
}
}
#[inline(always)]
pub fn is_vertical(self) -> bool {
match self {
Self::LeftToRight | Self::RightToLeft => false,
Self::TopDown | Self::BottomUp => true,
}
}
}
// ----------------------------------------------------------------------------
/// The layout of a [`Ui`][`crate::Ui`], e.g. "vertical & centered".
///
/// ```
@@ -623,12 +593,24 @@ impl Layout {
if (self.is_vertical() && self.horizontal_align() == Align::Center)
|| self.horizontal_justify()
{
frame_size.x = frame_size.x.max(available_rect.width()); // fill full width
// For wrapping layouts, fill the current column width, not the entire layout width.
let width = if self.main_wrap {
region.cursor.width()
} else {
available_rect.width()
};
frame_size.x = frame_size.x.max(width); // fill full width
}
if (self.is_horizontal() && self.vertical_align() == Align::Center)
|| self.vertical_justify()
{
frame_size.y = frame_size.y.max(available_rect.height()); // fill full height
// For wrapping layouts, fill the current row height, not the entire layout height.
let height = if self.main_wrap {
region.cursor.height()
} else {
available_rect.height()
};
frame_size.y = frame_size.y.max(height); // fill full height
}
let align2 = match self.main_dir {
@@ -791,14 +773,14 @@ impl Layout {
let new_top = region.cursor.bottom() + spacing.y;
region.cursor = Rect::from_min_max(
pos2(region.max_rect.left(), new_top),
pos2(INFINITY, new_top + region.cursor.height()),
pos2(INFINITY, new_top),
);
}
Direction::RightToLeft => {
let new_top = region.cursor.bottom() + spacing.y;
region.cursor = Rect::from_min_max(
pos2(-INFINITY, new_top),
pos2(region.max_rect.right(), new_top + region.cursor.height()),
pos2(region.max_rect.right(), new_top),
);
}
Direction::TopDown | Direction::BottomUp => {}

View File

@@ -112,8 +112,8 @@
//! loop {
//! let raw_input: egui::RawInput = gather_input();
//!
//! let full_output = ctx.run(raw_input, |ctx| {
//! egui::CentralPanel::default().show(&ctx, |ui| {
//! let full_output = ctx.run_ui(raw_input, |ui| {
//! egui::CentralPanel::default().show_inside(ui, |ui| {
//! ui.label("Hello world!");
//! if ui.button("Click me").clicked() {
//! // take some action here
@@ -407,8 +407,6 @@ pub mod layers;
mod layout;
pub mod load;
mod memory;
#[deprecated = "Use `egui::containers::menu` instead"]
pub mod menu;
pub mod os;
mod painter;
mod pass_state;
@@ -434,9 +432,6 @@ mod callstack;
pub use accesskit;
#[deprecated = "Use the ahash crate directly."]
pub use ahash;
pub use epaint;
pub use epaint::ecolor;
pub use epaint::emath;
@@ -449,7 +444,7 @@ pub use emath::{
remap_clamp, vec2,
};
pub use epaint::{
ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
ClippedPrimitive, ColorImage, CornerRadius, Direction, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
@@ -458,8 +453,8 @@ pub use epaint::{
pub mod text {
pub use crate::text_selection::CCursorRange;
pub use epaint::text::{
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TAB_SIZE,
TextFormat, TextWrapping, cursor::CCursor,
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat,
TextWrapping, cursor::CCursor,
};
}
@@ -478,7 +473,7 @@ pub use self::{
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,
grid::Grid,
id::{Id, IdMap},
id::{Id, IdMap, IdSet},
input_state::{InputOptions, InputState, MultiTouchInfo, PointerState, SurrenderFocusOn},
layers::{LayerId, Order},
layout::*,
@@ -499,9 +494,6 @@ pub use self::{
widgets::*,
};
#[deprecated = "Renamed to CornerRadius"]
pub type Rounding = CornerRadius;
// ----------------------------------------------------------------------------
/// Helper function that adds a label when compiling with debug assertions enabled.

View File

@@ -116,6 +116,11 @@ pub struct Memory {
/// (e.g. relative to some other widget).
#[cfg_attr(feature = "persistence", serde(skip))]
popups: ViewportIdMap<OpenPopup>,
/// Whether to inform the backend to interrupt any ongoing IME composition
/// this pass.
#[cfg_attr(feature = "persistence", serde(skip))]
requested_interrupt_ime: bool,
}
impl Default for Memory {
@@ -133,6 +138,7 @@ impl Default for Memory {
popups: Default::default(),
everything_is_visible: Default::default(),
add_fonts: Default::default(),
requested_interrupt_ime: Default::default(),
};
slf.interactions.entry(slf.viewport_id).or_default();
slf.areas.entry(slf.viewport_id).or_default();
@@ -193,7 +199,7 @@ pub struct Options {
#[cfg_attr(feature = "serde", serde(skip))]
pub light_style: std::sync::Arc<Style>,
/// Preference for selection between dark and light [`crate::Context::style`]
/// Preference for selection between dark and light [`crate::Context::global_style`]
/// as the active style used by all subsequent windows, panels, etc.
///
/// Default: `ThemePreference::System`.
@@ -234,6 +240,16 @@ pub struct Options {
#[cfg_attr(feature = "serde", serde(skip))]
pub zoom_with_keyboard: bool,
/// Keyboard shortcuts to close the application.
///
/// Pressing any of these will send [`crate::ViewportCommand::Close`]
/// to the root viewport.
///
/// Defaults to `Cmd-Q` (which is Ctrl-Q on Linux/Windows, Cmd-Q on Mac).
/// Set to empty to disable.
#[cfg_attr(feature = "serde", serde(skip))]
pub quit_shortcuts: Vec<crate::KeyboardShortcut>,
/// Controls the tessellator.
pub tessellation_options: epaint::TessellationOptions,
@@ -256,7 +272,7 @@ pub struct Options {
///
/// If this is `1`, [`crate::Context::request_discard`] will be ignored.
///
/// Multi-pass is supported by [`crate::Context::run`].
/// Multi-pass is supported by [`crate::Context::run_ui`].
///
/// See [`crate::Context::request_discard`] for more.
pub max_passes: NonZeroUsize,
@@ -304,6 +320,10 @@ impl Default for Options {
system_theme: None,
zoom_factor: 1.0,
zoom_with_keyboard: true,
quit_shortcuts: vec![crate::KeyboardShortcut::new(
crate::Modifiers::COMMAND,
crate::Key::Q,
)],
tessellation_options: Default::default(),
repaint_on_widget_change: false,
@@ -363,6 +383,7 @@ impl Options {
system_theme: _,
zoom_factor,
zoom_with_keyboard,
quit_shortcuts: _, // not shown in ui
tessellation_options,
repaint_on_widget_change,
max_passes,
@@ -746,6 +767,8 @@ impl Memory {
self.areas.entry(self.viewport_id).or_default();
self.requested_interrupt_ime = false;
// self.interactions is handled elsewhere
self.options.begin_pass(new_raw_input);
@@ -797,12 +820,6 @@ impl Memory {
}
}
/// The currently set transform of a layer.
#[deprecated = "Use `Context::layer_transform_to_global` instead"]
pub fn layer_transforms(&self, layer_id: LayerId) -> Option<TSTransform> {
self.to_global.get(&layer_id).copied()
}
/// An iterator over all layers. Back-to-front, top is last.
pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ {
self.areas().order().iter().copied()
@@ -860,9 +877,12 @@ impl Memory {
/// Give keyboard focus to a specific widget.
/// See also [`crate::Response::request_focus`].
///
/// Calling this will interrupt IME composition.
#[inline(always)]
pub fn request_focus(&mut self, id: Id) {
self.focus_mut().focused_widget = Some(FocusWidget::new(id));
self.interrupt_ime();
}
/// Surrender keyboard focus for a specific widget.
@@ -978,6 +998,28 @@ impl Memory {
pub(crate) fn focus_mut(&mut self) -> &mut Focus {
self.focus.entry(self.viewport_id).or_default()
}
/// Check if the widget owns IME events.
///
/// A widget should only consume IME events if this returns `true`. At most
/// one widget can own IME events for each frame.
#[inline(always)]
pub fn owns_ime_events(&self, id: Id) -> bool {
// Note: Even if the IME is being interrupted in the current frame, we
// should not return `false` here, since we still need
// `PlatformOutput::ime` to be set in such cases.
self.has_focus(id)
}
/// Interrupt the current IME composition, if any.
pub fn interrupt_ime(&mut self) {
self.requested_interrupt_ime = true;
}
pub(crate) fn should_interrupt_ime(&self) -> bool {
self.requested_interrupt_ime
}
}
/// State of an open popup.
@@ -1004,40 +1046,27 @@ impl OpenPopup {
}
}
/// ## Deprecated popup API
/// Use [`crate::Popup`] instead.
/// ## Popup state (internal API)
///
/// Used by [`crate::Popup`].
impl Memory {
/// Is the given popup open?
#[deprecated = "Use Popup::is_id_open instead"]
pub fn is_popup_open(&self, popup_id: Id) -> bool {
pub(crate) fn is_popup_open(&self, popup_id: Id) -> bool {
self.popups
.get(&self.viewport_id)
.is_some_and(|state| state.id == popup_id)
|| self.everything_is_visible()
}
/// Is any popup open?
#[deprecated = "Use Popup::is_any_open instead"]
pub fn any_popup_open(&self) -> bool {
pub(crate) fn any_popup_open(&self) -> bool {
self.popups.contains_key(&self.viewport_id) || self.everything_is_visible()
}
/// Open the given popup and close all others.
///
/// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open.
#[deprecated = "Use Popup::open_id instead"]
pub fn open_popup(&mut self, popup_id: Id) {
pub(crate) fn open_popup(&mut self, popup_id: Id) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, None));
}
/// Popups must call this every frame while open.
///
/// This is needed because in some cases popups can go away without `close_popup` being
/// called. For example, when a context menu is open and the underlying widget stops
/// being rendered.
#[deprecated = "Use Popup::show instead"]
pub fn keep_popup_open(&mut self, popup_id: Id) {
pub(crate) fn keep_popup_open(&mut self, popup_id: Id) {
if let Some(state) = self.popups.get_mut(&self.viewport_id)
&& state.id == popup_id
{
@@ -1045,43 +1074,27 @@ impl Memory {
}
}
/// Open the popup and remember its position.
#[deprecated = "Use Popup with PopupAnchor::Position instead"]
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
pub(crate) fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, pos.into()));
}
/// Get the position for this popup.
#[deprecated = "Use Popup::position_of_id instead"]
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
pub(crate) fn popup_position(&self, id: Id) -> Option<Pos2> {
let state = self.popups.get(&self.viewport_id)?;
if state.id == id { state.pos } else { None }
}
/// Close any currently open popup.
#[deprecated = "Use Popup::close_all instead"]
pub fn close_all_popups(&mut self) {
pub(crate) fn close_all_popups(&mut self) {
self.popups.clear();
}
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
#[deprecated = "Use Popup::close_id instead"]
pub fn close_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
pub(crate) fn close_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.popups.remove(&self.viewport_id);
}
}
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
#[deprecated = "Use Popup::toggle_id instead"]
pub fn toggle_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
pub(crate) fn toggle_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.close_popup(popup_id);
} else {

View File

@@ -1,781 +0,0 @@
#![expect(deprecated)]
//! Deprecated menu API - Use [`crate::containers::menu`] instead.
//!
//! Usage:
//! ```
//! fn show_menu(ui: &mut egui::Ui) {
//! use egui::{menu, Button};
//!
//! menu::bar(ui, |ui| {
//! ui.menu_button("File", |ui| {
//! if ui.button("Open").clicked() {
//! // …
//! }
//! });
//! });
//! }
//! ```
use super::{
Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response, Sense, TextStyle, Ui,
Vec2, style::WidgetVisuals,
};
use crate::{
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt as _, Order, Stroke, Style,
TextWrapMode, UiKind, WidgetText, epaint, vec2,
widgets::{Button, ImageButton},
};
use epaint::mutex::RwLock;
use std::sync::Arc;
/// What is saved between frames.
#[derive(Clone, Default)]
pub struct BarState {
open_menu: MenuRootManager,
}
impl BarState {
pub fn load(ctx: &Context, bar_id: Id) -> Self {
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
}
pub fn store(self, ctx: &Context, bar_id: Id) {
ctx.data_mut(|d| d.insert_temp(bar_id, self));
}
/// Show a menu at pointer if primary-clicked response.
///
/// Should be called from [`Context`] on a [`Response`]
pub fn bar_menu<R>(
&mut self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
MenuRoot::stationary_click_interaction(button, &mut self.open_menu);
self.open_menu.show(button, add_contents)
}
pub(crate) fn has_root(&self) -> bool {
self.open_menu.inner.is_some()
}
}
impl std::ops::Deref for BarState {
type Target = MenuRootManager;
fn deref(&self) -> &Self::Target {
&self.open_menu
}
}
impl std::ops::DerefMut for BarState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.open_menu
}
}
fn set_menu_style(style: &mut Style) {
if style.compact_menu_style {
style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
}
}
/// The menu bar goes well in a [`crate::Panel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
#[deprecated = "Use `egui::MenuBar::new().ui(` instead"]
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
ui.horizontal(|ui| {
set_menu_style(ui.style_mut());
// Take full width and fixed height:
let height = ui.spacing().interact_size.y;
ui.set_min_size(vec2(ui.available_width(), height));
add_contents(ui)
})
}
/// Construct a top level menu in a menu bar. This would be e.g. "File", "Edit" etc.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
pub fn menu_button<R>(
ui: &mut Ui,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_impl(ui, title, Box::new(add_contents))
}
/// Construct a top level menu with a custom button in a menu bar.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
pub fn menu_custom_button<R>(
ui: &mut Ui,
button: Button<'_>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_button_impl(ui, button, Box::new(add_contents))
}
/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
#[deprecated = "Use `menu_custom_button` instead"]
pub fn menu_image_button<R>(
ui: &mut Ui,
image_button: ImageButton<'_>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_button_impl(
ui,
Button::image(image_button.image),
Box::new(add_contents),
)
}
/// Construct a nested sub menu in another menu.
///
/// Opens on hover.
///
/// Returns `None` if the menu is not open.
pub fn submenu_button<R>(
ui: &mut Ui,
parent_state: Arc<RwLock<MenuState>>,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
SubMenu::new(parent_state, title).show(ui, add_contents)
}
/// wrapper for the contents of every menu.
fn menu_popup<'c, R>(
ctx: &Context,
parent_layer: LayerId,
menu_state_arc: &Arc<RwLock<MenuState>>,
menu_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> {
let pos = {
let mut menu_state = menu_state_arc.write();
menu_state.entry_count = 0;
menu_state.rect.min
};
let area_id = menu_id.with("__menu");
ctx.pass_state_mut(|fs| {
fs.layers
.entry(parent_layer)
.or_default()
.open_popups
.insert(area_id)
});
let area = Area::new(area_id)
.kind(UiKind::Menu)
.order(Order::Foreground)
.fixed_pos(pos)
.default_width(ctx.global_style().spacing.menu_width)
.sense(Sense::hover());
let mut sizing_pass = false;
let area_response = area.show(ctx, |ui| {
sizing_pass = ui.is_sizing_pass();
set_menu_style(ui.style_mut());
Frame::menu(ui.style())
.show(ui, |ui| {
ui.set_menu_state(Some(Arc::clone(menu_state_arc)));
ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
.inner
})
.inner
});
let area_rect = area_response.response.rect;
menu_state_arc.write().rect = if sizing_pass {
// During the sizing pass we didn't know the size yet,
// so we might have just constrained the position unnecessarily.
// Therefore keep the original=desired position until the next frame.
Rect::from_min_size(pos, area_rect.size())
} else {
// We knew the size, and this is where it ended up (potentially constrained to screen).
// Remember it for the future:
area_rect
};
area_response
}
/// Build a top level menu with a button.
///
/// Responds to primary clicks.
fn stationary_menu_impl<'c, R>(
ui: &mut Ui,
title: impl Into<WidgetText>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let title = title.into();
let bar_id = ui.id();
let menu_id = bar_id.with(title.text());
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let mut button = Button::new(title);
if bar_state.open_menu.is_menu_open(menu_id) {
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
}
let button_response = ui.add(button);
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Build a top level menu with an image button.
///
/// Responds to primary clicks.
fn stationary_menu_button_impl<'c, R>(
ui: &mut Ui,
button: Button<'_>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let button_response = ui.add(button);
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
pub(crate) const CONTEXT_MENU_ID_STR: &str = "__egui::context_menu";
/// Response to secondary clicks (right-clicks) by showing the given menu.
pub fn context_menu(
response: &Response,
add_contents: impl FnOnce(&mut Ui),
) -> Option<InnerResponse<()>> {
let menu_id = Id::new(CONTEXT_MENU_ID_STR);
let mut bar_state = BarState::load(&response.ctx, menu_id);
MenuRoot::context_click_interaction(response, &mut bar_state);
let inner_response = bar_state.show(response, add_contents);
bar_state.store(&response.ctx, menu_id);
inner_response
}
/// Returns `true` if the context menu is opened for this widget.
pub fn context_menu_opened(response: &Response) -> bool {
let menu_id = Id::new(CONTEXT_MENU_ID_STR);
let bar_state = BarState::load(&response.ctx, menu_id);
bar_state.is_menu_open(response.id)
}
/// Stores the state for the context menu.
#[derive(Clone, Default)]
pub struct MenuRootManager {
inner: Option<MenuRoot>,
}
impl MenuRootManager {
/// Show a menu at pointer if right-clicked response.
///
/// Should be called from [`Context`] on a [`Response`]
pub fn show<R>(
&mut self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
if let Some(root) = self.inner.as_mut() {
let (menu_response, inner_response) = root.show(button, add_contents);
if menu_response.is_close() {
self.inner = None;
}
inner_response
} else {
None
}
}
fn is_menu_open(&self, id: Id) -> bool {
self.inner.as_ref().map(|m| m.id) == Some(id)
}
}
impl std::ops::Deref for MenuRootManager {
type Target = Option<MenuRoot>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for MenuRootManager {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
/// Menu root associated with an Id from a Response
#[derive(Clone)]
pub struct MenuRoot {
pub menu_state: Arc<RwLock<MenuState>>,
pub id: Id,
}
impl MenuRoot {
pub fn new(position: Pos2, id: Id) -> Self {
Self {
menu_state: Arc::new(RwLock::new(MenuState::new(position))),
id,
}
}
pub fn show<R>(
&self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (MenuResponse, Option<InnerResponse<R>>) {
if self.id == button.id {
let inner_response = menu_popup(
&button.ctx,
button.layer_id,
&self.menu_state,
self.id,
add_contents,
);
let menu_state = self.menu_state.read();
let escape_pressed = button.ctx.input(|i| i.key_pressed(Key::Escape));
if menu_state.response.is_close()
|| escape_pressed
|| inner_response.response.should_close()
{
return (MenuResponse::Close, Some(inner_response));
}
}
(MenuResponse::Stay, None)
}
/// Interaction with a stationary menu, i.e. fixed in another Ui.
///
/// Responds to primary clicks.
fn stationary_interaction(button: &Response, root: &mut MenuRootManager) -> MenuResponse {
let id = button.id;
if (button.clicked() && root.is_menu_open(id))
|| button.ctx.input(|i| i.key_pressed(Key::Escape))
{
// menu open and button clicked or esc pressed
return MenuResponse::Close;
} else if (button.clicked() && !root.is_menu_open(id))
|| (button.hovered() && root.is_some())
{
// menu not open and button clicked
// or button hovered while other menu is open
let mut pos = button.rect.left_bottom();
let menu_frame = Frame::menu(&button.ctx.global_style());
pos.x -= menu_frame.total_margin().left; // Make fist button in menu align with the parent button
pos.y += button.ctx.global_style().spacing.menu_spacing;
if let Some(root) = root.inner.as_mut() {
let menu_rect = root.menu_state.read().rect;
let content_rect = button.ctx.input(|i| i.content_rect());
if pos.y + menu_rect.height() > content_rect.max.y {
pos.y = content_rect.max.y - menu_rect.height() - button.rect.height();
}
if pos.x + menu_rect.width() > content_rect.max.x {
pos.x = content_rect.max.x - menu_rect.width();
}
}
if let Some(to_global) = button.ctx.layer_transform_to_global(button.layer_id) {
pos = to_global * pos;
}
return MenuResponse::Create(pos, id);
} else if button
.ctx
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
&& let Some(pos) = button.ctx.input(|i| i.pointer.interact_pos())
&& let Some(root) = root.inner.as_mut()
&& root.id == id
{
// pressed somewhere while this menu is open
let in_menu = root.menu_state.read().area_contains(pos);
if !in_menu {
return MenuResponse::Close;
}
}
MenuResponse::Stay
}
/// Interaction with a context menu (secondary click).
pub fn context_interaction(response: &Response, root: &mut Option<Self>) -> MenuResponse {
let response = response.interact(Sense::click());
let hovered = response.hovered();
let secondary_clicked = response.secondary_clicked();
response.ctx.input(|input| {
let pointer = &input.pointer;
if let Some(pos) = pointer.interact_pos() {
let (in_old_menu, destroy) = if let Some(root) = root {
let in_old_menu = root.menu_state.read().area_contains(pos);
let destroy = !in_old_menu && pointer.any_pressed() && root.id == response.id;
(in_old_menu, destroy)
} else {
(false, false)
};
if !in_old_menu {
if hovered && secondary_clicked {
return MenuResponse::Create(pos, response.id);
} else if destroy || hovered && pointer.primary_down() {
return MenuResponse::Close;
}
}
}
MenuResponse::Stay
})
}
pub fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
match menu_response {
MenuResponse::Create(pos, id) => {
root.inner = Some(Self::new(pos, id));
}
MenuResponse::Close => root.inner = None,
MenuResponse::Stay => {}
}
}
/// Respond to secondary (right) clicks.
pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager) {
let menu_response = Self::context_interaction(response, root);
Self::handle_menu_response(root, menu_response);
}
// Responds to primary clicks.
pub fn stationary_click_interaction(button: &Response, root: &mut MenuRootManager) {
let menu_response = Self::stationary_interaction(button, root);
Self::handle_menu_response(root, menu_response);
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum MenuResponse {
Close,
Stay,
Create(Pos2, Id),
}
impl MenuResponse {
pub fn is_close(&self) -> bool {
*self == Self::Close
}
}
pub struct SubMenuButton {
text: WidgetText,
icon: WidgetText,
index: usize,
}
impl SubMenuButton {
/// The `icon` can be an emoji (e.g. `⏵` right arrow), shown right of the label
fn new(text: impl Into<WidgetText>, icon: impl Into<WidgetText>, index: usize) -> Self {
Self {
text: text.into(),
icon: icon.into(),
index,
}
}
fn visuals<'a>(
ui: &'a Ui,
response: &Response,
menu_state: &MenuState,
sub_id: Id,
) -> &'a WidgetVisuals {
if menu_state.is_open(sub_id) && !response.hovered() {
&ui.style().visuals.widgets.open
} else {
ui.style().interact(response)
}
}
#[inline]
pub fn icon(mut self, icon: impl Into<WidgetText>) -> Self {
self.icon = icon.into();
self
}
pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response {
let Self { text, icon, .. } = self;
let text_style = TextStyle::Button;
let sense = Sense::click();
let text_icon_gap = ui.spacing().item_spacing.x;
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let text_available_width = ui.available_width() - total_extra.x;
let text_galley = text.into_galley(
ui,
Some(TextWrapMode::Wrap),
text_available_width,
text_style.clone(),
);
let icon_available_width = text_available_width - text_galley.size().x;
let icon_galley = icon.into_galley(
ui,
Some(TextWrapMode::Wrap),
icon_available_width,
text_style,
);
let text_and_icon_size = Vec2::new(
text_galley.size().x + text_icon_gap + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y),
);
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| {
crate::WidgetInfo::labeled(
crate::WidgetType::Button,
ui.is_enabled(),
text_galley.text(),
)
});
if ui.is_rect_visible(rect) {
let visuals = Self::visuals(ui, &response, menu_state, sub_id);
let text_pos = Align2::LEFT_CENTER
.align_size_within_rect(text_galley.size(), rect.shrink2(button_padding))
.min;
let icon_pos = Align2::RIGHT_CENTER
.align_size_within_rect(icon_galley.size(), rect.shrink2(button_padding))
.min;
if ui.visuals().button_frame {
ui.painter().rect_filled(
rect.expand(visuals.expansion),
visuals.corner_radius,
visuals.weak_bg_fill,
);
}
let text_color = visuals.text_color();
ui.painter().galley(text_pos, text_galley, text_color);
ui.painter().galley(icon_pos, icon_galley, text_color);
}
response
}
}
pub struct SubMenu {
button: SubMenuButton,
parent_state: Arc<RwLock<MenuState>>,
}
impl SubMenu {
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl Into<WidgetText>) -> Self {
let index = parent_state.write().next_entry_index();
Self {
button: SubMenuButton::new(text, "", index),
parent_state,
}
}
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
let sub_id = ui.id().with(self.button.index);
let response = self.button.show(ui, &self.parent_state.read(), sub_id);
self.parent_state
.write()
.submenu_button_interaction(ui, sub_id, &response);
let inner =
self.parent_state
.write()
.show_submenu(ui.ctx(), ui.layer_id(), sub_id, add_contents);
InnerResponse::new(inner, response)
}
}
/// Components of menu state, public for advanced usage.
///
/// Usually you don't need to use it directly.
pub struct MenuState {
/// The opened sub-menu and its [`Id`]
sub_menu: Option<(Id, Arc<RwLock<Self>>)>,
/// Bounding box of this menu (without the sub-menu),
/// including the frame and everything.
pub rect: Rect,
/// Used to check if any menu in the tree wants to close
pub response: MenuResponse,
/// Used to hash different [`Id`]s for sub-menus
entry_count: usize,
}
impl MenuState {
pub fn new(position: Pos2) -> Self {
Self {
rect: Rect::from_min_size(position, Vec2::ZERO),
sub_menu: None,
response: MenuResponse::Stay,
entry_count: 0,
}
}
/// Close menu hierarchy.
pub fn close(&mut self) {
self.response = MenuResponse::Close;
}
fn show_submenu<R>(
&mut self,
ctx: &Context,
parent_layer: LayerId,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let (sub_response, response) = self.submenu(id).map(|sub| {
let inner_response = menu_popup(ctx, parent_layer, sub, id, add_contents);
if inner_response.response.should_close() {
sub.write().close();
}
(sub.read().response, inner_response.inner)
})?;
self.cascade_close_response(sub_response);
Some(response)
}
/// Check if position is in the menu hierarchy's area.
pub fn area_contains(&self, pos: Pos2) -> bool {
self.rect.contains(pos)
|| self
.sub_menu
.as_ref()
.is_some_and(|(_, sub)| sub.read().area_contains(pos))
}
fn next_entry_index(&mut self) -> usize {
self.entry_count += 1;
self.entry_count - 1
}
/// Sense button interaction opening and closing submenu.
fn submenu_button_interaction(&mut self, ui: &Ui, sub_id: Id, button: &Response) {
let pointer = ui.input(|i| i.pointer.clone());
let open = self.is_open(sub_id);
if self.moving_towards_current_submenu(&pointer) {
// We don't close the submenu if the pointer is on its way to hover it.
// ensure to repaint once even when pointer is not moving
ui.request_repaint();
} else if !open && button.hovered() {
// TODO(emilk): open menu to the left if there isn't enough space to the right
let mut pos = button.rect.right_top();
pos.x = self.rect.right() + ui.spacing().menu_spacing;
pos.y -= Frame::menu(ui.style()).total_margin().top; // align the first button in the submenu with the parent button
self.open_submenu(sub_id, pos);
} else if open
&& ui.response().contains_pointer()
&& !button.hovered()
&& !self.hovering_current_submenu(&pointer)
{
// We are hovering something else in the menu, so close the submenu.
self.close_submenu();
}
}
/// Check if pointer is moving towards current submenu.
fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool {
if pointer.is_still() {
return false;
}
if let Some(sub_menu) = self.current_submenu()
&& let Some(pos) = pointer.hover_pos()
{
let rect = sub_menu.read().rect;
return rect.intersects_ray(pos, pointer.direction().normalized());
}
false
}
/// Check if pointer is hovering current submenu.
fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
if let Some(sub_menu) = self.current_submenu()
&& let Some(pos) = pointer.hover_pos()
{
return sub_menu.read().area_contains(pos);
}
false
}
/// Cascade close response to menu root.
fn cascade_close_response(&mut self, response: MenuResponse) {
if response.is_close() {
self.response = response;
}
}
fn is_open(&self, id: Id) -> bool {
self.sub_id() == Some(id)
}
fn sub_id(&self) -> Option<Id> {
self.sub_menu.as_ref().map(|(id, _)| *id)
}
fn current_submenu(&self) -> Option<&Arc<RwLock<Self>>> {
self.sub_menu.as_ref().map(|(_, sub)| sub)
}
fn submenu(&self, id: Id) -> Option<&Arc<RwLock<Self>>> {
let (k, sub) = self.sub_menu.as_ref()?;
if id == *k { Some(sub) } else { None }
}
/// Open submenu at position, if not already open.
fn open_submenu(&mut self, id: Id, pos: Pos2) {
if !self.is_open(id) {
self.sub_menu = Some((id, Arc::new(RwLock::new(Self::new(pos)))));
}
}
fn close_submenu(&mut self) {
self.sub_menu = None;
}
}

View File

@@ -82,12 +82,6 @@ impl Painter {
self.layer_id = layer_id;
}
/// If set, colors will be modified to look like this
#[deprecated = "Use `multiply_opacity` instead"]
pub fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
self.fade_to_color = fade_to_color;
}
/// Set the opacity (alpha multiplier) of everything painted by this painter from this point forward.
///
/// `opacity` must be between 0.0 and 1.0, where 0.0 means fully transparent (i.e., invisible)
@@ -195,41 +189,6 @@ impl Painter {
pub fn round_to_pixel_center(&self, point: f32) -> f32 {
point.round_to_pixel_center(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels).
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
pos.round_to_pixel_center(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering of filled shapes.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_to_pixel(&self, point: f32) -> f32 {
point.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
vec.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
pos.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
rect.round_to_pixels(self.pixels_per_point())
}
}
/// ## Low level

View File

@@ -199,16 +199,15 @@ pub struct PassState {
pub tooltips: TooltipPassState,
/// Starts off as the `screen_rect`, shrinks as panels are added.
/// The [`crate::CentralPanel`] does not change this.
pub available_rect: Rect,
/// What the root UI had available at the end of the previous pass.
///
/// Only set if [`crate::Context::run_ui`] has been called.
pub root_ui_available_rect: Option<Rect>,
/// Starts off as the `screen_rect`, shrinks as panels are added.
/// The [`crate::CentralPanel`] retracts from this.
pub unused_rect: Rect,
/// How much space is used by panels.
pub used_by_panels: Rect,
/// What the root UI had used at the end of the previous pass.
///
/// Only set if [`crate::Context::run_ui`] has been called.
pub root_ui_min_rect: Option<Rect>,
/// The current scroll area should scroll to this range (horizontal, vertical).
pub scroll_target: [Option<ScrollTarget>; 2],
@@ -240,9 +239,8 @@ impl Default for PassState {
widgets: Default::default(),
layers: Default::default(),
tooltips: Default::default(),
available_rect: Rect::NAN,
unused_rect: Rect::NAN,
used_by_panels: Rect::NAN,
root_ui_available_rect: None,
root_ui_min_rect: None,
scroll_target: [None, None],
scroll_delta: (Vec2::default(), style::ScrollAnimation::none()),
accesskit_state: None,
@@ -255,16 +253,15 @@ impl Default for PassState {
}
impl PassState {
pub(crate) fn begin_pass(&mut self, content_rect: Rect) {
pub(crate) fn begin_pass(&mut self) {
profiling::function_scope!();
let Self {
used_ids,
widgets,
tooltips,
layers,
available_rect,
unused_rect,
used_by_panels,
root_ui_available_rect,
root_ui_min_rect,
scroll_target,
scroll_delta,
accesskit_state,
@@ -278,9 +275,8 @@ impl PassState {
widgets.clear();
tooltips.clear();
layers.clear();
*available_rect = content_rect;
*unused_rect = content_rect;
*used_by_panels = Rect::NOTHING;
*root_ui_available_rect = None;
*root_ui_min_rect = None;
*scroll_target = [None, None];
*scroll_delta = Default::default();
@@ -293,64 +289,4 @@ impl PassState {
highlight_next_pass.clear();
}
/// How much space is still available after panels has been added.
pub(crate) fn available_rect(&self) -> Rect {
debug_assert!(
self.available_rect.is_finite(),
"Called `available_rect()` before `Context::run()`"
);
self.available_rect
}
/// Shrink `available_rect`.
pub(crate) fn allocate_left_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.min.distance(self.available_rect.min) < 0.1,
"Mismatching left panel. You must not create a panel from within another panel."
);
self.available_rect.min.x = panel_rect.max.x;
self.unused_rect.min.x = panel_rect.max.x;
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.max.distance(self.available_rect.max) < 0.1,
"Mismatching right panel. You must not create a panel from within another panel."
);
self.available_rect.max.x = panel_rect.min.x;
self.unused_rect.max.x = panel_rect.min.x;
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.min.distance(self.available_rect.min) < 0.1,
"Mismatching top panel. You must not create a panel from within another panel."
);
self.available_rect.min.y = panel_rect.max.y;
self.unused_rect.min.y = panel_rect.max.y;
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.max.distance(self.available_rect.max) < 0.1,
"Mismatching bottom panel. You must not create a panel from within another panel."
);
self.available_rect.max.y = panel_rect.min.y;
self.unused_rect.max.y = panel_rect.min.y;
self.used_by_panels |= panel_rect;
}
pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
// Note: we do not shrink `available_rect`, because
// we allow windows to cover the CentralPanel.
self.unused_rect = Rect::NOTHING; // Nothing left unused after this
self.used_by_panels |= panel_rect;
}
}

View File

@@ -55,7 +55,7 @@ pub struct Response {
/// Where the pointer (mouse/touch) were when this widget was clicked or dragged.
/// `None` if the widget is not being interacted with.
#[doc(hidden)]
pub interact_pointer_pos: Option<Pos2>,
pub interact_pointer_pos_or_nan: Pos2,
/// The intrinsic / desired size of the widget.
///
@@ -67,12 +67,22 @@ pub struct Response {
/// At the time of writing, this is only used by external crates
/// for improved layouting.
/// See for instance [`egui_flex`](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex).
pub intrinsic_size: Option<Vec2>,
#[doc(hidden)]
pub intrinsic_size_or_nan: Vec2,
#[doc(hidden)]
pub flags: Flags,
}
#[test]
fn test_response_size() {
assert_eq!(
std::mem::size_of::<Response>(),
88,
"Keep Response small, because we create them often, and we want to keep it lean and fast"
);
}
/// A bit set for various boolean properties of `Response`.
#[doc(hidden)]
#[derive(Copy, Clone, Debug)]
@@ -141,6 +151,22 @@ bitflags::bitflags! {
}
impl Response {
/// The [`Id`] of the parent [`crate::Ui`] that hosts this widget.
///
/// Looks up the [`WidgetRect`] from the current (or previous) pass.
pub fn parent_id(&self) -> Id {
let id = self.ctx.viewport(|viewport| {
viewport
.this_pass
.widgets
.get(self.id)
.or_else(|| viewport.prev_pass.widgets.get(self.id))
.map(|w| w.parent_id)
});
debug_assert!(id.is_some(), "WidgetRect for Response not found!");
id.unwrap_or(Id::NULL)
}
/// Returns true if this widget was clicked this frame by the primary button.
///
/// A click is registered when the mouse or touch is released within
@@ -246,10 +272,10 @@ impl Response {
false
} else if let Some(pos) = pointer_interact_pos {
let layer_under_pointer = self.ctx.layer_id_at(pos);
if layer_under_pointer != Some(self.layer_id) {
true
} else {
if layer_under_pointer == Some(self.layer_id) {
!self.interact_rect.contains(pos)
} else {
true
}
} else {
false // clicked without a pointer, weird
@@ -489,7 +515,26 @@ impl Response {
/// `None` if the widget is not being interacted with.
#[inline]
pub fn interact_pointer_pos(&self) -> Option<Pos2> {
self.interact_pointer_pos
let pos = self.interact_pointer_pos_or_nan;
if pos.any_nan() { None } else { Some(pos) }
}
/// The intrinsic / desired size of the widget.
///
/// This is the size that a non-wrapped, non-truncated, non-justified version of the widget
/// would have.
///
/// If this is `None`, use [`Self::rect`] instead.
#[inline]
pub fn intrinsic_size(&self) -> Option<Vec2> {
let size = self.intrinsic_size_or_nan;
if size.any_nan() { None } else { Some(size) }
}
/// Set the intrinsic / desired size of the widget.
#[inline]
pub fn set_intrinsic_size(&mut self, size: Vec2) {
self.intrinsic_size_or_nan = size;
}
/// If it is a good idea to show a tooltip, where is pointer?
@@ -732,6 +777,7 @@ impl Response {
WidgetRect {
layer_id: self.layer_id,
id: self.id,
parent_id: self.parent_id(),
rect: self.rect,
interact_rect: self.interact_rect,
sense: self.sense | sense,
@@ -1007,8 +1053,10 @@ impl Response {
interact_rect: self.interact_rect.union(other.interact_rect),
sense: self.sense.union(other.sense),
flags: self.flags | other.flags,
interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos),
intrinsic_size: None,
interact_pointer_pos_or_nan: self
.interact_pointer_pos()
.unwrap_or(other.interact_pointer_pos_or_nan),
intrinsic_size_or_nan: Vec2::NAN,
}
}
}

View File

@@ -297,17 +297,6 @@ pub struct Style {
#[cfg_attr(feature = "serde", serde(skip))]
pub number_formatter: NumberFormatter,
/// If set, labels, buttons, etc. will use this to determine whether to wrap the text at the
/// right edge of the [`Ui`] they are in. By default, this is `None`.
///
/// **Note**: this API is deprecated, use `wrap_mode` instead.
///
/// * `None`: use `wrap_mode` instead
/// * `Some(true)`: wrap mode defaults to [`crate::TextWrapMode::Wrap`]
/// * `Some(false)`: wrap mode defaults to [`crate::TextWrapMode::Extend`]
#[deprecated = "Use wrap_mode instead"]
pub wrap: Option<bool>,
/// If set, labels, buttons, etc. will use this to determine whether to wrap or truncate the
/// text at the right edge of the [`Ui`] they are in, or to extend it. By default, this is
/// `None`.
@@ -586,6 +575,8 @@ pub struct ScrollStyle {
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub interact_handle_opacity: f32,
pub fade: ScrollFadeStyle,
}
impl Default for ScrollStyle {
@@ -616,6 +607,8 @@ impl ScrollStyle {
dormant_handle_opacity: 0.0,
active_handle_opacity: 0.6,
interact_handle_opacity: 1.0,
fade: Default::default(),
}
}
@@ -699,6 +692,8 @@ impl ScrollStyle {
dormant_handle_opacity,
active_handle_opacity,
interact_handle_opacity,
fade,
} = self;
ui.horizontal(|ui| {
@@ -772,6 +767,52 @@ impl ScrollStyle {
ui.label("Inner margin");
});
}
ui.separator();
fade.ui(ui);
}
}
/// Controls if and how to fade out the sides of a [`crate::ScrollArea`]
/// to indicate there is more there if you scroll.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ScrollFadeStyle {
/// Opacity of the fade effect at the outer edge, in 0.0-1.0.
///
/// Set to 0.0 to disable the fade effect.
pub strength: f32,
/// Size of the fade-area (height for vertical scrolling,
/// width for horizontal scrolling).
pub size: f32,
}
impl Default for ScrollFadeStyle {
fn default() -> Self {
Self {
strength: 0.5,
size: 20.0,
}
}
}
impl ScrollFadeStyle {
pub fn ui(&mut self, ui: &mut Ui) {
let Self { strength, size } = self;
ui.horizontal(|ui| {
ui.add(DragValue::new(strength).speed(0.01).range(0.0..=1.0));
ui.label("Fade strength");
});
if 0.0 < *strength {
ui.horizontal(|ui| {
ui.add(DragValue::new(size).range(0.0..=64.0));
ui.label("Fade size");
});
}
}
}
@@ -1114,13 +1155,6 @@ impl Visuals {
self.window_stroke
}
/// When fading out things, we fade the colors towards this.
#[inline(always)]
#[deprecated = "Use disabled_alpha(). Fading is now handled by modifying the alpha channel."]
pub fn fade_out_to_color(&self) -> Color32 {
self.widgets.noninteractive.weak_bg_fill
}
/// Disabled widgets have their alpha modified by this.
#[inline(always)]
pub fn disabled_alpha(&self) -> f32 {
@@ -1251,11 +1285,6 @@ impl WidgetVisuals {
pub fn text_color(&self) -> Color32 {
self.fg_stroke.color
}
#[deprecated = "Renamed to corner_radius"]
pub fn rounding(&self) -> CornerRadius {
self.corner_radius
}
}
/// Options for help debug egui by adding extra visualization
@@ -1303,6 +1332,10 @@ pub struct DebugOptions {
/// Show interesting widgets under the mouse cursor.
pub show_widget_hits: bool,
/// Show a warning if the same `Rect` had different `Id` and the same parent `Id` on the
/// previous frame.
pub warn_if_rect_changes_id: bool,
/// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`].
///
/// See [`emath::GuiRounding`] for more.
@@ -1329,6 +1362,7 @@ impl Default for DebugOptions {
show_resize: false,
show_interactive_widgets: false,
show_widget_hits: false,
warn_if_rect_changes_id: cfg!(debug_assertions),
show_unaligned: cfg!(debug_assertions),
show_focused_widget: false,
}
@@ -1353,7 +1387,6 @@ pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
impl Default for Style {
fn default() -> Self {
#[expect(deprecated)]
Self {
override_font_id: None,
override_text_style: None,
@@ -1361,7 +1394,6 @@ impl Default for Style {
text_styles: default_text_styles(),
drag_value_text_style: TextStyle::Button,
number_formatter: NumberFormatter(Arc::new(emath::format_with_decimals_in_range)),
wrap: None,
wrap_mode: None,
spacing: Spacing::default(),
interaction: Interaction::default(),
@@ -1667,7 +1699,6 @@ use crate::{
impl Style {
pub fn ui(&mut self, ui: &mut crate::Ui) {
#[expect(deprecated)]
let Self {
override_font_id,
override_text_style,
@@ -1675,7 +1706,6 @@ impl Style {
text_styles,
drag_value_text_style,
number_formatter: _, // can't change callbacks in the UI
wrap: _,
wrap_mode,
spacing,
interaction,
@@ -2287,11 +2317,13 @@ impl Visuals {
max_texture_side: _,
alpha_from_coverage,
font_hinting,
subpixel_binning,
} = text_options;
text_alpha_from_coverage_ui(ui, alpha_from_coverage);
ui.checkbox(font_hinting, "Enable font hinting");
ui.checkbox(font_hinting, "Font hinting (sharper text)");
ui.checkbox(subpixel_binning, "Sub-pixel binning (more even kerning)");
});
ui.collapsing("Text cursor", |ui| {
@@ -2491,6 +2523,7 @@ impl DebugOptions {
show_resize,
show_interactive_widgets,
show_widget_hits,
warn_if_rect_changes_id,
show_unaligned,
show_focused_widget,
} = self;
@@ -2522,6 +2555,11 @@ impl DebugOptions {
ui.checkbox(show_widget_hits, "Show widgets under mouse pointer");
ui.checkbox(
warn_if_rect_changes_id,
"Warn if a Rect changes Id between frames",
);
ui.checkbox(
show_unaligned,
"Show rectangles not aligned to integer point coordinates",
@@ -2850,8 +2888,11 @@ impl Widget for &mut FontTweak {
scale,
y_offset_factor,
y_offset,
hinting_override,
hinting,
coords,
thin_space_width,
tab_size,
subpixel_binning,
} = self;
ui.label("Scale");
@@ -2867,18 +2908,20 @@ impl Widget for &mut FontTweak {
ui.add(DragValue::new(y_offset).speed(-0.02));
ui.end_row();
ui.label("hinting_override");
ComboBox::from_id_salt("hinting_override")
.selected_text(match hinting_override {
None => "None",
Some(true) => "Enable",
Some(false) => "Disable",
})
.show_ui(ui, |ui| {
ui.selectable_value(hinting_override, None, "None");
ui.selectable_value(hinting_override, Some(true), "Enable");
ui.selectable_value(hinting_override, Some(false), "Disable");
});
ui.label("hinting");
ui.horizontal(|ui| {
ui.radio_value(hinting, Some(true), "on");
ui.radio_value(hinting, Some(false), "off");
ui.radio_value(hinting, None, "default");
});
ui.end_row();
ui.label("subpixel_binning");
ui.horizontal(|ui| {
ui.radio_value(subpixel_binning, Some(true), "on");
ui.radio_value(subpixel_binning, Some(false), "off");
ui.radio_value(subpixel_binning, None, "default");
});
ui.end_row();
ui.label("coords");
@@ -2924,6 +2967,21 @@ impl Widget for &mut FontTweak {
}
ui.end_row();
ui.label("thin_space_width");
ui.horizontal(|ui| {
ui.add(
DragValue::new(thin_space_width)
.range(0.0..=1.0)
.speed(0.01),
);
ui.label("1\u{2009}234\u{2009}567\u{2009}890");
});
ui.end_row();
ui.label("tab_size");
ui.add(DragValue::new(tab_size).range(0.0..=16.0).speed(0.1));
ui.end_row();
if ui.button("Reset").clicked() {
*self = Default::default();
}

View File

@@ -97,12 +97,6 @@ impl CCursorRange {
}
}
#[inline]
#[deprecated = "Use `self.sorted_cursors` instead."]
pub fn sorted(&self) -> [CCursor; 2] {
self.sorted_cursors()
}
pub fn slice_str<'s>(&self, text: &'s str) -> &'s str {
let [min, max] = self.sorted_cursors();
slice_char_range(text, min.index..max.index)

View File

@@ -49,10 +49,15 @@ fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
impl std::fmt::Debug for WidgetTextCursor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
widget_id,
ccursor,
pos: _,
} = self;
f.debug_struct("WidgetTextCursor")
.field("widget_id", &self.widget_id.short_debug_format())
.field("ccursor", &self.ccursor.index)
.finish()
.field("widget_id", &widget_id.short_debug_format())
.field("ccursor", &ccursor.index)
.finish_non_exhaustive()
}
}

View File

@@ -106,38 +106,26 @@ impl TextCursorState {
}
fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange {
if ccursor.index == 0 {
CCursorRange::two(ccursor, ccursor_next_word(text, ccursor))
} else {
let it = text.chars();
let mut it = it.skip(ccursor.index - 1);
if let Some(char_before_cursor) = it.next() {
if let Some(char_after_cursor) = it.next() {
if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) {
let min = ccursor_previous_word(text, ccursor + 1);
let max = ccursor_next_word(text, min);
CCursorRange::two(min, max)
} else if is_word_char(char_before_cursor) {
let min = ccursor_previous_word(text, ccursor);
let max = ccursor_next_word(text, min);
CCursorRange::two(min, max)
} else if is_word_char(char_after_cursor) {
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(ccursor, max)
} else {
let min = ccursor_previous_word(text, ccursor);
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(min, max)
}
} else {
let min = ccursor_previous_word(text, ccursor);
CCursorRange::two(min, ccursor)
}
} else {
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(ccursor, max)
}
if text.is_empty() {
return CCursorRange::one(ccursor);
}
let line_start = find_line_start(text, ccursor);
let line_end = ccursor_next_line(text, line_start);
let line_range = line_start.index..line_end.index;
let current_line_text = slice_char_range(text, line_range.clone());
let relative_idx = ccursor.index - line_start.index;
let relative_ccursor = CCursor::new(relative_idx);
let min = ccursor_previous_word(current_line_text, relative_ccursor);
let max = ccursor_next_word(current_line_text, relative_ccursor);
CCursorRange::two(
CCursor::new(line_start.index + min.index),
CCursor::new(line_start.index + max.index),
)
}
fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
@@ -156,13 +144,13 @@ fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
let min = ccursor_previous_line(text, ccursor);
let max = ccursor_next_line(text, min);
CCursorRange::two(min, max)
} else if !is_linebreak(char_after_cursor) {
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(ccursor, max)
} else {
} else if is_linebreak(char_after_cursor) {
let min = ccursor_previous_line(text, ccursor);
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(min, max)
} else {
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(ccursor, max)
}
} else {
let min = ccursor_previous_line(text, ccursor);
@@ -209,16 +197,20 @@ fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
}
fn next_word_boundary_char_index(text: &str, cursor_ci: usize) -> usize {
for (word_byte_index, word) in text.split_word_bound_indices() {
let word_ci = char_index_from_byte_index(text, word_byte_index);
let mut current_char_idx = 0;
for (_word_byte_index, word) in text.split_word_bound_indices() {
let word_ci = current_char_idx;
// We consider `.` a word boundary.
// At least that's how Mac works when navigating something like `www.example.com`.
for (dot_ci_offset, chr) in word.chars().enumerate() {
let dot_ci = word_ci + dot_ci_offset;
let mut word_char_count = 0;
for chr in word.chars() {
let dot_ci = word_ci + word_char_count;
if chr == '.' && cursor_ci < dot_ci {
return dot_ci;
}
word_char_count += 1;
}
// Splitting considers contiguous whitespace as one word, such words must be skipped,
@@ -228,9 +220,11 @@ fn next_word_boundary_char_index(text: &str, cursor_ci: usize) -> usize {
if cursor_ci < word_ci && !all_word_chars(word) {
return word_ci;
}
current_char_idx += word_char_count;
}
char_index_from_byte_index(text, text.len())
current_char_idx
}
fn all_word_chars(text: &str) -> bool {
@@ -265,22 +259,14 @@ fn is_linebreak(c: char) -> bool {
/// Accepts and returns character offset (NOT byte offset!).
pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
// We know that new lines, '\n', are a single byte char, but we have to
// work with char offsets because before the new line there may be any
// number of multi byte chars.
// We need to know the char index to be able to correctly set the cursor
// later.
let chars_count = text.chars().count();
let byte_idx = byte_index_from_char_index(text, current_index.index);
let text_before = &text[..byte_idx];
let position = text
.chars()
.rev()
.skip(chars_count - current_index.index)
.position(|x| x == '\n');
match position {
Some(pos) => CCursor::new(current_index.index - pos),
None => CCursor::new(0),
if let Some(last_newline_byte) = text_before.rfind('\n') {
let char_idx = char_index_from_byte_index(text, last_newline_byte + 1);
CCursor::new(char_idx)
} else {
CCursor::new(0)
}
}
@@ -329,7 +315,7 @@ pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
#[cfg(test)]
mod test {
use crate::text_selection::text_cursor_state::next_word_boundary_char_index;
use super::*;
#[test]
fn test_next_word_boundary_char_index() {
@@ -366,4 +352,117 @@ mod test {
assert_eq!(next_word_boundary_char_index(text, 19), 20);
assert_eq!(next_word_boundary_char_index(text, 20), 21);
}
#[test]
fn test_previous_word() {
let text = "abc def ghi";
assert_eq!(ccursor_previous_word(text, CCursor::new(7)).index, 4);
assert_eq!(ccursor_previous_word(text, CCursor::new(5)).index, 4);
assert_eq!(ccursor_previous_word(text, CCursor::new(4)).index, 0);
assert_eq!(ccursor_previous_word(text, CCursor::new(0)).index, 0);
}
#[test]
fn test_next_word() {
let text = "abc def ghi";
assert_eq!(ccursor_next_word(text, CCursor::new(0)).index, 3);
assert_eq!(ccursor_next_word(text, CCursor::new(3)).index, 7);
assert_eq!(ccursor_next_word(text, CCursor::new(7)).index, 11);
assert_eq!(ccursor_next_word(text, CCursor::new(11)).index, 11);
}
#[test]
fn test_select_word_at() {
// CCursorRange::two(min, max) sets primary=max, secondary=min
let text = "hello world";
let range = select_word_at(text, CCursor::new(2));
let (lo, hi) = (
range.primary.index.min(range.secondary.index),
range.primary.index.max(range.secondary.index),
);
assert_eq!(lo, 0);
assert_eq!(hi, 5);
let range = select_word_at(text, CCursor::new(8));
let (lo, hi) = (
range.primary.index.min(range.secondary.index),
range.primary.index.max(range.secondary.index),
);
assert_eq!(lo, 6);
assert_eq!(hi, 11);
}
#[test]
fn test_word_boundary_large_text_performance() {
// Before the O(n²) → O(n) fix, this would take minutes on large text.
let large_text = "word ".repeat(200_000); // ~1MB
let len = large_text.chars().count();
let start = std::time::Instant::now();
let next = ccursor_next_word(&large_text, CCursor::new(len - 10));
assert!(next.index <= len);
let prev = ccursor_previous_word(&large_text, CCursor::new(len - 10));
assert!(prev.index < len);
let range = select_word_at(&large_text, CCursor::new(len - 3));
let lo = range.primary.index.min(range.secondary.index);
let hi = range.primary.index.max(range.secondary.index);
assert!(lo < hi, "Expected a non-empty word selection");
let elapsed = start.elapsed();
assert!(
elapsed.as_secs() < 5,
"Word boundary operations on 1MB text took {elapsed:?}, expected < 5s"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_previous_word_graphemes() {
let cases = [
("", 0, 0),
("hello", 0, 0),
("hello", "hello".chars().count(), 0),
("hello world", 6, 0),
("hello world", 8, 6),
("hello world", "hello world".chars().count(), 6),
("hello world ", "hello world ".chars().count(), 6),
("hello world", "hello world".chars().count(), 8),
(" ", " ".chars().count(), 0),
("hello, world", "hello, world".chars().count(), 7),
("www.example.com", "www.example.com".chars().count(), 12),
("안녕! 😊 세상", 8, 6),
("❤️👍 skvělá knihovna 👍❤️", 18, 11),
(
"a e\u{301} b",
"a e\u{301} b".chars().count(),
"a e\u{301} ".chars().count(),
),
(
"hi 🙂 world",
"hi 🙂 world".chars().count(),
"hi 🙂 ".chars().count(),
),
(
"hi 👨‍👩‍👧‍👦 world",
"hi 👨‍👩‍👧‍👦 world".chars().count(),
"hi 👨‍👩‍👧‍👦 ".chars().count(),
),
];
for (text, cursor, expected) in cases {
let result = ccursor_previous_word(text, CCursor::new(cursor));
assert_eq!(
result.index, expected,
"text={text:?}, cursor={cursor}, got={}, expected={expected}",
result.index
);
}
}
}

View File

@@ -61,7 +61,7 @@ pub fn paint_text_selection(
let last_glyph_index = if ri == max.row {
max.column
} else {
row.glyphs.len() - 1
row.glyphs.len()
};
let first_vertex_index = row

View File

@@ -3,12 +3,10 @@
use std::{any::Any, hash::Hash, ops::Deref, sync::Arc};
use emath::GuiRounding as _;
use epaint::mutex::RwLock;
use crate::containers::menu;
use crate::widget_style::{HasClasses as _, ROOT_CLASS};
use crate::{containers::*, ecolor::*, layout::*, placer::Placer, widgets::*, *};
use emath::GuiRounding as _;
// ----------------------------------------------------------------------------
/// This is what you use to place widgets.
@@ -76,10 +74,6 @@ pub struct Ui {
/// where we size up the contents of the Ui, without actually showing it.
sizing_pass: bool,
/// Indicates whether this Ui belongs to a Menu.
#[expect(deprecated)]
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
/// The [`UiStack`] for this [`Ui`].
stack: Arc<UiStack>,
@@ -162,7 +156,6 @@ impl Ui {
placer,
enabled: true,
sizing_pass,
menu_state: None,
stack: Arc::new(ui_stack),
sense,
min_rect_already_remembered: false,
@@ -178,6 +171,7 @@ impl Ui {
ui.ctx().create_widget(
WidgetRect {
id: ui.unique_id,
parent_id: ui.id,
layer_id: ui.layer_id(),
rect: start_rect,
interact_rect: start_rect,
@@ -202,47 +196,6 @@ impl Ui {
ui
}
/// Create a new [`Ui`] at a specific region.
///
/// Note: calling this function twice from the same [`Ui`] will create a conflict of id. Use
/// [`Self::scope`] if needed.
///
/// When in doubt, use `None` for the `UiStackInfo` argument.
#[deprecated = "Use ui.new_child() instead"]
pub fn child_ui(
&mut self,
max_rect: Rect,
layout: Layout,
ui_stack_info: Option<UiStackInfo>,
) -> Self {
self.new_child(
UiBuilder::new()
.max_rect(max_rect)
.layout(layout)
.ui_stack_info(ui_stack_info.unwrap_or_default()),
)
}
/// Create a new [`Ui`] at a specific region with a specific id.
///
/// When in doubt, use `None` for the `UiStackInfo` argument.
#[deprecated = "Use ui.new_child() instead"]
pub fn child_ui_with_id_source(
&mut self,
max_rect: Rect,
layout: Layout,
id_salt: impl Hash,
ui_stack_info: Option<UiStackInfo>,
) -> Self {
self.new_child(
UiBuilder::new()
.id_salt(id_salt)
.max_rect(max_rect)
.layout(layout)
.ui_stack_info(ui_stack_info.unwrap_or_default()),
)
}
/// Create a child `Ui` with the properties of the given builder.
///
/// This is a very low-level function.
@@ -327,7 +280,6 @@ impl Ui {
placer,
enabled,
sizing_pass,
menu_state: self.menu_state.clone(),
stack: Arc::new(ui_stack),
sense,
min_rect_already_remembered: false,
@@ -347,6 +299,7 @@ impl Ui {
child_ui.ctx().create_widget(
WidgetRect {
id: child_ui.unique_id,
parent_id: self.id,
layer_id: child_ui.layer_id(),
rect: start_rect,
interact_rect: start_rect,
@@ -368,18 +321,6 @@ impl Ui {
// -------------------------------------------------
/// Set to true in special cases where we do one frame
/// where we size up the contents of the Ui, without actually showing it.
///
/// This will also turn the Ui invisible.
/// Should be called right after [`Self::new`], if at all.
#[inline]
#[deprecated = "Use UiBuilder.sizing_pass().invisible()"]
pub fn set_sizing_pass(&mut self) {
self.sizing_pass = true;
self.set_invisible();
}
/// Set to true in special cases where we do one frame
/// where we size up the contents of the Ui, without actually showing it.
#[inline]
@@ -418,7 +359,7 @@ impl Ui {
/// Style options for this [`Ui`] and its children.
///
/// Note that this may be a different [`Style`] than that of [`Context::style`].
/// Note that this may be a different [`Style`] than that of [`Context::global_style`].
#[inline]
pub fn style(&self) -> &Arc<Style> {
&self.style
@@ -493,6 +434,12 @@ impl Ui {
&mut self.style_mut().visuals
}
/// Is this [`Ui`] in a tooltip?
#[inline]
pub fn is_tooltip(&self) -> bool {
self.layer_id().order == Order::Tooltip
}
/// Get a reference to this [`Ui`]'s [`UiStack`].
#[inline]
pub fn stack(&self) -> &Arc<UiStack> {
@@ -554,33 +501,6 @@ impl Ui {
}
}
/// Calling `set_enabled(false)` will cause the [`Ui`] to deny all future interaction
/// and all the widgets will draw with a gray look.
///
/// Usually it is more convenient to use [`Self::add_enabled_ui`] or [`Self::add_enabled`].
///
/// Calling `set_enabled(true)` has no effect - it will NOT re-enable the [`Ui`] once disabled.
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut enabled = true;
/// ui.group(|ui| {
/// ui.checkbox(&mut enabled, "Enable subsection");
/// ui.set_enabled(enabled);
/// if ui.button("Button that is not always clickable").clicked() {
/// /* … */
/// }
/// });
/// # });
/// ```
#[deprecated = "Use disable(), add_enabled_ui(), or add_enabled() instead"]
pub fn set_enabled(&mut self, enabled: bool) {
if !enabled {
self.disable();
}
}
/// If `false`, any widgets added to the [`Ui`] will be invisible and non-interactive.
///
/// This is `false` if any parent had [`UiBuilder::invisible`]
@@ -597,7 +517,7 @@ impl Ui {
///
/// Once invisible, there is no way to make the [`Ui`] visible again.
///
/// Usually it is more convenient to use [`Self::add_visible_ui`] or [`Self::add_visible`].
/// Usually it is more convenient to use [`Self::add_visible`].
///
/// ### Example
/// ```
@@ -619,34 +539,6 @@ impl Ui {
self.disable();
}
/// Calling `set_visible(false)` will cause all further widgets to be invisible,
/// yet still allocate space.
///
/// The widgets will not be interactive (`set_visible(false)` implies `set_enabled(false)`).
///
/// Calling `set_visible(true)` has no effect.
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut visible = true;
/// ui.group(|ui| {
/// ui.checkbox(&mut visible, "Show subsection");
/// ui.set_visible(visible);
/// if ui.button("Button that is not always shown").clicked() {
/// /* … */
/// }
/// });
/// # });
/// ```
#[deprecated = "Use set_invisible(), add_visible_ui(), or add_visible() instead"]
pub fn set_visible(&mut self, visible: bool) {
if !visible {
self.painter.set_invisible();
self.disable();
}
}
/// Make the widget in this [`Ui`] semi-transparent.
///
/// `opacity` must be between 0.0 and 1.0, where 0.0 means fully transparent (i.e., invisible)
@@ -694,17 +586,8 @@ impl Ui {
///
/// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`].
pub fn wrap_mode(&self) -> TextWrapMode {
#[expect(deprecated)]
if let Some(wrap_mode) = self.style.wrap_mode {
wrap_mode
}
// `wrap` handling for backward compatibility
else if let Some(wrap) = self.style.wrap {
if wrap {
TextWrapMode::Wrap
} else {
TextWrapMode::Extend
}
} else if let Some(grid) = self.placer.grid() {
if grid.wrap_text() {
TextWrapMode::Wrap
@@ -721,14 +604,6 @@ impl Ui {
}
}
/// Should text wrap in this [`Ui`]?
///
/// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`].
#[deprecated = "Use `wrap_mode` instead"]
pub fn wrap_text(&self) -> bool {
self.wrap_mode() == TextWrapMode::Wrap
}
/// How to vertically align text
#[inline]
pub fn text_valign(&self) -> Align {
@@ -1051,6 +926,7 @@ impl Ui {
self.ctx().create_widget(
WidgetRect {
id,
parent_id: self.id,
layer_id: self.layer_id(),
rect,
interact_rect: self.clip_rect().intersect(rect),
@@ -1062,18 +938,6 @@ impl Ui {
)
}
/// Deprecated: use [`Self::interact`] instead.
#[deprecated = "The contains_pointer argument is ignored. Use `ui.interact` instead."]
pub fn interact_with_hovered(
&self,
rect: Rect,
_contains_pointer: bool,
id: Id,
sense: Sense,
) -> Response {
self.interact(rect, id, sense)
}
/// Read the [`Ui`]'s background [`Response`].
/// Its [`Sense`] will be based on the [`UiBuilder::sense`] used to create this [`Ui`].
///
@@ -1085,7 +949,7 @@ impl Ui {
pub fn response(&self) -> Response {
// This is the inverse of Context::read_response. We prefer a response
// based on last frame's widget rect since the one from this frame is Rect::NOTHING until
// Ui::interact_bg is called or the Ui is dropped.
// Ui::remember_min_rect is called or the Ui is dropped.
let mut response = self
.ctx()
.viewport(|viewport| {
@@ -1120,6 +984,7 @@ impl Ui {
let mut response = self.ctx().create_widget(
WidgetRect {
id: self.unique_id,
parent_id: self.id,
layer_id: self.layer_id(),
rect: self.min_rect(),
interact_rect: self.clip_rect().intersect(self.min_rect()),
@@ -1135,16 +1000,6 @@ impl Ui {
response
}
/// Interact with the background of this [`Ui`],
/// i.e. behind all the widgets.
///
/// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`].
#[deprecated = "Use UiBuilder::sense with Ui::response instead"]
pub fn interact_bg(&self, sense: Sense) -> Response {
// This will update the WidgetRect that was first created in `Ui::new`.
self.interact(self.min_rect(), self.unique_id, sense)
}
/// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]?
///
/// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance,
@@ -1289,7 +1144,7 @@ impl Ui {
pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response {
let (id, rect) = self.allocate_space(desired_size);
let mut response = self.interact(rect, id, sense);
response.intrinsic_size = Some(desired_size);
response.set_intrinsic_size(desired_size);
response
}
@@ -1497,34 +1352,6 @@ impl Ui {
)
}
/// Allocated the given rectangle and then adds content to that rectangle.
///
/// If the contents overflow, more space will be allocated.
/// When finished, the amount of space actually used (`min_rect`) will be allocated.
/// So you can request a lot of space and then use less.
#[deprecated = "Use `allocate_new_ui` instead"]
pub fn allocate_ui_at_rect<R>(
&mut self,
max_rect: Rect,
add_contents: impl FnOnce(&mut Self) -> R,
) -> InnerResponse<R> {
self.scope_builder(UiBuilder::new().max_rect(max_rect), add_contents)
}
/// Allocated space (`UiBuilder::max_rect`) and then add content to it.
///
/// If the contents overflow, more space will be allocated.
/// When finished, the amount of space actually used (`min_rect`) will be allocated in the parent.
/// So you can request a lot of space and then use less.
#[deprecated = "Use `scope_builder` instead"]
pub fn allocate_new_ui<R>(
&mut self,
ui_builder: UiBuilder,
add_contents: impl FnOnce(&mut Self) -> R,
) -> InnerResponse<R> {
self.scope_dyn(ui_builder, Box::new(add_contents))
}
/// Convenience function to get a region to paint on.
///
/// Note that egui uses screen coordinates for everything.
@@ -1815,7 +1642,7 @@ impl Ui {
/// If you call `add_visible` from within an already invisible [`Ui`],
/// the widget will always be invisible, even if the `visible` argument is true.
///
/// See also [`Self::add_visible_ui`], [`Self::set_visible`] and [`Self::is_visible`].
/// See also [`Self::set_invisible`] and [`Self::is_visible`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
@@ -1840,38 +1667,6 @@ impl Ui {
}
}
/// Add a section that is possibly invisible, i.e. greyed out and non-interactive.
///
/// An invisible ui still takes up the same space as if it were visible.
///
/// If you call `add_visible_ui` from within an already invisible [`Ui`],
/// the result will always be invisible, even if the `visible` argument is true.
///
/// See also [`Self::add_visible`], [`Self::set_visible`] and [`Self::is_visible`].
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut visible = true;
/// ui.checkbox(&mut visible, "Show subsection");
/// ui.add_visible_ui(visible, |ui| {
/// ui.label("Maybe you see this, maybe you don't!");
/// });
/// # });
/// ```
#[deprecated = "Use 'ui.scope_builder' instead"]
pub fn add_visible_ui<R>(
&mut self,
visible: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let mut ui_builder = UiBuilder::new();
if !visible {
ui_builder = ui_builder.invisible();
}
self.scope_builder(ui_builder, add_contents)
}
/// Add extra space before the next widget.
///
/// The direction is dependent on the layout.
@@ -2379,21 +2174,6 @@ impl Ui {
self.scope_dyn(UiBuilder::new().id_salt(id_salt), Box::new(add_contents))
}
/// Push another level onto the [`UiStack`].
///
/// You can use this, for instance, to tag a group of widgets.
#[deprecated = "Use 'ui.scope_builder' instead"]
pub fn push_stack_info<R>(
&mut self,
ui_stack_info: UiStackInfo,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.scope_dyn(
UiBuilder::new().ui_stack_info(ui_stack_info),
Box::new(add_contents),
)
}
/// Create a scoped child ui.
///
/// You can use this to temporarily change the [`Style`] of a sub-region, for instance:
@@ -2434,26 +2214,6 @@ impl Ui {
InnerResponse::new(ret, response)
}
/// Redirect shapes to another paint layer.
///
/// ```
/// # use egui::{LayerId, Order, Id};
/// # egui::__run_test_ui(|ui| {
/// let layer_id = LayerId::new(Order::Tooltip, Id::new("my_floating_ui"));
/// ui.with_layer_id(layer_id, |ui| {
/// ui.label("This is now in a different layer");
/// });
/// # });
/// ```
#[deprecated = "Use ui.scope_builder(UiBuilder::new().layer_id(…), …) instead"]
pub fn with_layer_id<R>(
&mut self,
layer_id: LayerId,
add_contents: impl FnOnce(&mut Self) -> R,
) -> InnerResponse<R> {
self.scope_builder(UiBuilder::new().layer_id(layer_id), add_contents)
}
/// A [`CollapsingHeader`] that starts out collapsed.
///
/// The name must be unique within the current parent,
@@ -3007,22 +2767,6 @@ impl Ui {
/// # Menus
impl Ui {
/// Close the menu we are in (including submenus), if any.
///
/// See also: [`Self::menu_button`] and [`Response::context_menu`].
#[deprecated = "Use `ui.close()` or `ui.close_kind(UiKind::Menu)` instead"]
pub fn close_menu(&self) {
self.close_kind(UiKind::Menu);
}
#[expect(deprecated)]
pub(crate) fn set_menu_state(
&mut self,
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
) {
self.menu_state = menu_state;
}
#[inline]
/// Create a menu button that when clicked will show the given menu.
///

View File

@@ -2,6 +2,8 @@ use std::sync::Arc;
use std::{any::Any, iter::FusedIterator};
use crate::widget_style::Classes;
use epaint::Color32;
use crate::{Direction, Frame, Id, Rect};
/// What kind is this [`crate::Ui`]?
@@ -255,6 +257,25 @@ impl UiStack {
pub fn has_visible_frame(&self) -> bool {
!self.info.frame.stroke.is_empty()
}
/// The background color of this [`crate::Ui`].
///
/// This blend together all [`Frame::fill`] colors
/// up to the root.
#[inline]
pub fn bg_color(&self) -> Color32 {
let mut total = Color32::TRANSPARENT;
for node in self.iter() {
let fill = node.frame().fill;
if fill != Color32::TRANSPARENT {
total = fill.blend(total);
if total.is_opaque() {
break;
}
}
}
total
}
}
// these methods act on the entire stack

View File

@@ -4,7 +4,6 @@
// This will also allow users to pick their own serialization format per type.
use std::{any::Any, sync::Arc};
// -----------------------------------------------------------------------------------------------
/// Like [`std::any::TypeId`], but can be serialized and deserialized.
@@ -182,6 +181,18 @@ impl Element {
}
}
pub fn is_temp(&self) -> bool {
match self {
#[cfg(feature = "persistence")]
Self::Value { serialize_fn, .. } => serialize_fn.is_none(),
#[cfg(not(feature = "persistence"))]
Self::Value { .. } => true,
Self::Serialized(_) => false,
}
}
#[inline]
pub(crate) fn get_temp<T: 'static>(&self) -> Option<&T> {
match self {
@@ -316,6 +327,41 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
use crate::Id;
/// The key used in [`IdTypeMap`], which is a combination of an [`Id`] and a [`TypeId`].
///
/// This key can be used to remove or access values in the [`IdTypeMap`] without
/// knowledge of the `TypeId` `T` that is required for other accessors.
///
/// [`RawKey`]s make no guarantees about layout or their ability to be persisted.
/// They only produce deterministic results if they are used with the map
/// they were initially obtained from. Using them on other instances of [`IdTypeMap`]
/// may produce unexpected behavior.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[repr(transparent)]
pub struct RawKey(u64);
impl nohash_hasher::IsEnabled for RawKey {}
impl RawKey {
/// Create a new key for the given type.
///
/// Note that two keys with the same id but different types
/// will be different keys.
///
/// ```
/// use egui::{Id, util::id_type_map::RawKey};
/// assert_ne!(
/// RawKey::new::<i32>(Id::NULL),
/// RawKey::new::<String>(Id::NULL),
/// );
/// ```
#[inline(always)]
pub fn new<T: 'static>(id: Id) -> Self {
let type_id = TypeId::of::<T>();
Self(type_id.value() ^ id.value())
}
}
// TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
/// Stores values identified by an [`Id`] AND the [`std::any::TypeId`] of the value.
///
@@ -358,7 +404,7 @@ use crate::Id;
#[derive(Clone, Debug)]
// We use `id XOR typeid` as a key, so we don't need to hash again!
pub struct IdTypeMap {
map: nohash_hasher::IntMap<u64, Element>,
map: nohash_hasher::IntMap<RawKey, Element>,
max_bytes_per_type: usize,
}
@@ -375,16 +421,23 @@ impl Default for IdTypeMap {
impl IdTypeMap {
/// Insert a value that will not be persisted.
#[inline]
pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id);
self.map.insert(hash, Element::new_temp(value));
pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(
&mut self,
id: Id,
value: T,
) -> RawKey {
let key = RawKey::new::<T>(id);
self.map.insert(key, Element::new_temp(value));
key
}
/// Insert a value that will be persisted next time you start the app.
#[inline]
pub fn insert_persisted<T: SerializableAny>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id);
self.map.insert(hash, Element::new_persisted(value));
let key = RawKey::new::<T>(id);
self.map.insert(key, Element::new_persisted(value));
// We don't yet return the key here, because currently all our `raw`
// methods are only for temporary values.
}
/// Read a value without trying to deserialize a persisted value.
@@ -392,8 +445,28 @@ impl IdTypeMap {
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_temp<T: 'static + Clone>(&self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
self.map.get(&hash).and_then(|x| x.get_temp()).cloned()
let key = RawKey::new::<T>(id);
self.map.get(&key).and_then(|x| x.get_temp()).cloned()
}
/// Gets a reference to a value for a given raw key.
///
/// Serialized values are ignored.
pub fn get_temp_raw(&self, raw: RawKey) -> Option<&(dyn Any + Send + Sync)> {
match self.map.get(&raw)? {
Element::Value { value, .. } => Some(value.as_ref()),
Element::Serialized(_) => None,
}
}
/// Gets a mutable reference to a value for a given raw key.
///
/// Serialized values are ignored.
pub fn get_temp_raw_mut(&mut self, raw: RawKey) -> Option<&mut (dyn Any + Send + Sync)> {
match self.map.get_mut(&raw)? {
Element::Value { value, .. } => Some(value.as_mut()),
Element::Serialized(_) => None,
}
}
/// Read a value, optionally deserializing it if available.
@@ -404,9 +477,9 @@ impl IdTypeMap {
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_persisted<T: SerializableAny>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
self.map
.get_mut(&hash)
.get_mut(&key)
.and_then(|x| x.get_mut_persisted())
.cloned()
}
@@ -443,9 +516,9 @@ impl IdTypeMap {
id: Id,
insert_with: impl FnOnce() -> T,
) -> &mut T {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
use std::collections::hash_map::Entry;
match self.map.entry(hash) {
match self.map.entry(key) {
Entry::Vacant(vacant) => {
// this unwrap will never panic, because we insert correct type right now
#[expect(clippy::unwrap_used)]
@@ -465,9 +538,9 @@ impl IdTypeMap {
id: Id,
insert_with: impl FnOnce() -> T,
) -> &mut T {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
use std::collections::hash_map::Entry;
match self.map.entry(hash) {
match self.map.entry(key) {
Entry::Vacant(vacant) => {
// this unwrap will never panic, because we insert correct type right now
#[expect(clippy::unwrap_used)]
@@ -486,7 +559,7 @@ impl IdTypeMap {
#[cfg(feature = "persistence")]
#[allow(clippy::allow_attributes, unused)]
fn get_generation<T: SerializableAny>(&self, id: Id) -> Option<usize> {
let element = self.map.get(&hash(TypeId::of::<T>(), id))?;
let element = self.map.get(&RawKey::new::<T>(id))?;
match element {
Element::Value { .. } => Some(0),
Element::Serialized(SerializedElement { generation, .. }) => Some(*generation),
@@ -496,18 +569,33 @@ impl IdTypeMap {
/// Remove the state of this type and id.
#[inline]
pub fn remove<T: 'static>(&mut self, id: Id) {
let hash = hash(TypeId::of::<T>(), id);
self.map.remove(&hash);
let key = RawKey::new::<T>(id);
self.map.remove(&key);
}
/// Remove and fetch the state of this type and id.
#[inline]
pub fn remove_temp<T: 'static + Default>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
let mut element = self.map.remove(&hash)?;
let key = RawKey::new::<T>(id);
let mut element = self.map.remove(&key)?;
Some(std::mem::take(element.get_mut_temp()?))
}
/// Remove a temporary value given a raw key.
///
/// Serialized values are ignored.
pub fn remove_temp_raw(&mut self, raw: RawKey) -> Option<Box<dyn Any + Send + Sync>> {
use std::collections::hash_map::Entry;
if let Entry::Occupied(e) = self.map.entry(raw)
&& e.get().is_temp()
&& let Element::Value { value, .. } = e.remove()
{
Some(value)
} else {
None
}
}
/// Note all state of the given type.
pub fn remove_by_type<T: 'static>(&mut self) {
let key = TypeId::of::<T>();
@@ -532,6 +620,18 @@ impl IdTypeMap {
self.map.len()
}
/// Returns all [`RawKey`]s to values in this map.
///
/// The returned keys can only be used with this map.
///
/// Serializable values will be ignored.
pub fn temp_keys(&self) -> impl Iterator<Item = RawKey> {
self.map
.iter()
.filter(|(_, v)| v.is_temp())
.map(|(k, _)| *k)
}
/// Count how many values are stored but not yet deserialized.
#[inline]
pub fn count_serialized(&self) -> usize {
@@ -576,11 +676,6 @@ impl IdTypeMap {
}
}
#[inline(always)]
fn hash(type_id: TypeId, id: Id) -> u64 {
type_id.value() ^ id.value()
}
// ----------------------------------------------------------------------------
/// How [`IdTypeMap`] is persisted.
@@ -613,13 +708,13 @@ impl PersistedMap {
{
profiling::scope!("gather");
for (hash, element) in &map.map {
for (key, element) in &map.map {
if let Some(element) = element.to_serialize() {
let stats = types_map.entry(element.type_id).or_default();
stats.num_bytes += element.ron.len();
let generation_stats = stats.generations.entry(element.generation).or_default();
generation_stats.num_bytes += element.ron.len();
generation_stats.elements.push((*hash, element));
generation_stats.elements.push((key.0, element));
} else {
// temporary value that shouldn't be serialized
}
@@ -659,7 +754,7 @@ impl PersistedMap {
.into_iter()
.map(
|(
hash,
raw,
SerializedElement {
type_id,
ron,
@@ -667,7 +762,7 @@ impl PersistedMap {
},
)| {
(
hash,
RawKey(raw),
Element::Serialized(SerializedElement {
type_id,
ron,

View File

@@ -8,7 +8,3 @@ pub use id_type_map::IdTypeMap;
pub use epaint::emath::History;
pub use epaint::util::{hash, hash_with};
/// Deprecated alias for [`crate::cache`].
#[deprecated = "Use egui::cache instead"]
pub use crate::cache;

View File

@@ -1201,7 +1201,7 @@ impl ViewportCommand {
/// Describes a viewport, i.e. a native window.
///
/// This is returned by [`crate::Context::run`] on each frame, and should be applied
/// This is returned by [`crate::Context::run_ui`] on each frame, and should be applied
/// by the integration.
#[derive(Clone)]
pub struct ViewportOutput {

View File

@@ -17,6 +17,12 @@ pub struct WidgetRect {
/// You can ensure globally unique ids using [`crate::Ui::push_id`].
pub id: Id,
/// The [`Id`] of the parent [`crate::Ui`] that hosts this widget.
///
/// Used by debug checks to distinguish true id-instability from
/// cascading id shifts caused by a parent Ui's auto-id changing.
pub parent_id: Id,
/// What layer the widget is on.
pub layer_id: LayerId,
@@ -46,6 +52,7 @@ impl WidgetRect {
pub fn transform(self, transform: emath::TSTransform) -> Self {
let Self {
id,
parent_id,
layer_id,
rect,
interact_rect,
@@ -54,6 +61,7 @@ impl WidgetRect {
} = self;
Self {
id,
parent_id,
layer_id,
rect: transform * rect,
interact_rect: transform * interact_rect,

View File

@@ -156,7 +156,7 @@ impl RichText {
/// Default: 0.0.
///
/// For even text it is recommended you round this to an even number of _pixels_,
/// e.g. using [`crate::Painter::round_to_pixel`].
/// e.g. using [`emath::GuiRounding`].
#[inline]
pub fn extra_letter_spacing(mut self, extra_letter_spacing: f32) -> Self {
self.extra_letter_spacing = extra_letter_spacing;
@@ -170,7 +170,7 @@ impl RichText {
/// If `None` (the default), the line height is determined by the font.
///
/// For even text it is recommended you round this to an even number of _pixels_,
/// e.g. using [`crate::Painter::round_to_pixel`].
/// e.g. using [`emath::GuiRounding`].
#[inline]
pub fn line_height(mut self, line_height: Option<f32>) -> Self {
self.line_height = line_height;

View File

@@ -202,12 +202,6 @@ impl<'a> Button<'a> {
self
}
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
/// If true, the tint of the image is multiplied by the widget text color.
///
/// This makes sense for images that are white, that should have the same color as the text color.
@@ -242,6 +236,18 @@ impl<'a> Button<'a> {
self
}
/// Show some text on the left side of the button.
#[inline]
pub fn left_text(mut self, left_text: impl IntoAtoms<'a>) -> Self {
self.layout.push_left(Atom::grow());
for atom in left_text.into_atoms() {
self.layout.push_left(atom);
}
self
}
/// Show some text on the right side of the button.
#[inline]
pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self {
@@ -364,6 +370,12 @@ impl<'a> Button<'a> {
AtomLayoutResponse::empty(prepared.response)
};
if let Some(cursor) = ui.visuals().interact_cursor
&& response.response.hovered()
{
ui.ctx().set_cursor_icon(cursor);
}
response.response.widget_info(|| {
if let Some(text) = &text {
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)

View File

@@ -91,16 +91,6 @@ impl<'a> DragValue<'a> {
self
}
/// Sets valid range for the value.
///
/// By default all values are clamped to this range, even when not interacted with.
/// You can change this behavior by passing `false` to [`Self::clamp_existing_to_range`].
#[deprecated = "Use `range` instead"]
#[inline]
pub fn clamp_range<Num: emath::Numeric>(self, range: RangeInclusive<Num>) -> Self {
self.range(range)
}
/// Sets valid range for dragging the value.
///
/// By default all values are clamped to this range, even when not interacted with.
@@ -157,12 +147,6 @@ impl<'a> DragValue<'a> {
self
}
#[inline]
#[deprecated = "Renamed clamp_existing_to_range"]
pub fn clamp_to_range(self, clamp_to_range: bool) -> Self {
self.clamp_existing_to_range(clamp_to_range)
}
/// Show a prefix before the number, e.g. "x: "
#[inline]
pub fn prefix(mut self, prefix: impl IntoAtoms<'a>) -> Self {
@@ -551,7 +535,7 @@ impl Widget for DragValue<'_> {
if let Some(value_text) = value_text {
// We were editing the value as text last frame, but lost focus.
// Make sure we applied the last text value:
let parsed_value = parse(&custom_parser, &value_text);
let parsed_value = parse(custom_parser.as_ref(), &value_text);
if let Some(mut parsed_value) = parsed_value {
// User edits always clamps:
parsed_value = clamp_value_to_range(parsed_value, range.clone());
@@ -591,7 +575,7 @@ impl Widget for DragValue<'_> {
response.lost_focus() && !ui.input(|i| i.key_pressed(Key::Escape))
};
if update {
let parsed_value = parse(&custom_parser, &value_text);
let parsed_value = parse(custom_parser.as_ref(), &value_text);
if let Some(mut parsed_value) = parsed_value {
// User edits always clamps:
parsed_value = clamp_value_to_range(parsed_value, range.clone());
@@ -733,8 +717,8 @@ impl Widget for DragValue<'_> {
}
}
fn parse(custom_parser: &Option<NumParser<'_>>, value_text: &str) -> Option<f64> {
match &custom_parser {
fn parse(custom_parser: Option<&NumParser<'_>>, value_text: &str) -> Option<f64> {
match custom_parser {
Some(parser) => parser(value_text),
None => default_parser(value_text),
}

View File

@@ -256,18 +256,6 @@ impl<'a> Image<'a> {
self
}
/// Round the corners of the image.
///
/// The default is no rounding ([`CornerRadius::ZERO`]).
///
/// Due to limitations in the current implementation,
/// this will turn off any rotation of the image.
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
/// Show a spinner when the image is loading.
///
/// By default this uses the value of [`crate::Visuals::image_loading_spinners`].

View File

@@ -1,164 +0,0 @@
use crate::{
Color32, CornerRadius, Image, Rect, Response, Sense, Ui, Vec2, Widget, WidgetInfo, WidgetType,
widgets,
};
/// A clickable image within a frame.
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
#[derive(Clone, Debug)]
#[deprecated(since = "0.33.0", note = "Use egui::Button::image instead")]
pub struct ImageButton<'a> {
pub(crate) image: Image<'a>,
sense: Sense,
frame: bool,
selected: bool,
alt_text: Option<String>,
}
#[expect(deprecated, reason = "Deprecated in egui 0.33.0")]
impl<'a> ImageButton<'a> {
pub fn new(image: impl Into<Image<'a>>) -> Self {
Self {
image: image.into(),
sense: Sense::click(),
frame: true,
selected: false,
alt_text: None,
}
}
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
#[inline]
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
self.image = self.image.uv(uv);
self
}
/// Multiply image color with this. Default is WHITE (no tint).
#[inline]
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
self.image = self.image.tint(tint);
self
}
/// If `true`, mark this button as "selected".
#[inline]
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
/// Turn off the frame
#[inline]
pub fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
self
}
/// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set rounding for the `ImageButton`.
///
/// If the underlying image already has rounding, this
/// will override that value.
#[inline]
pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
self.image = self.image.corner_radius(corner_radius.into());
self
}
/// Set rounding for the `ImageButton`.
///
/// If the underlying image already has rounding, this
/// will override that value.
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
}
#[expect(deprecated, reason = "Deprecated in egui 0.33.0")]
impl Widget for ImageButton<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let padding = if self.frame {
// so we can see that it is a button:
Vec2::splat(ui.spacing().button_padding.x)
} else {
Vec2::ZERO
};
let available_size_for_image = ui.available_size() - 2.0 * padding;
let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image);
let image_source_size = tlr.as_ref().ok().and_then(|t| t.size());
let image_size = self
.image
.calc_size(available_size_for_image, image_source_size);
let padded_size = image_size + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
response.widget_info(|| {
let mut info = WidgetInfo::new(WidgetType::Button);
info.label = self.alt_text.clone();
info
});
if ui.is_rect_visible(rect) {
let (expansion, rounding, fill, stroke) = if self.selected {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
self.image.image_options().corner_radius,
selection.bg_fill,
selection.stroke,
)
} else if self.frame {
let visuals = ui.style().interact(&response);
let expansion = Vec2::splat(visuals.expansion);
(
expansion,
self.image.image_options().corner_radius,
visuals.weak_bg_fill,
visuals.bg_stroke,
)
} else {
Default::default()
};
// Draw frame background (for transparent images):
ui.painter()
.rect_filled(rect.expand2(expansion), rounding, fill);
let image_rect = ui
.layout()
.align_size_within_rect(image_size, rect.shrink2(padding));
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
let image_options = self.image.image_options().clone();
widgets::image::paint_texture_load_result(
ui,
&tlr,
image_rect,
None,
&image_options,
self.alt_text.as_deref(),
);
// Draw frame outline:
ui.painter().rect_stroke(
rect.expand2(expansion),
rounding,
stroke,
epaint::StrokeKind::Inside,
);
}
widgets::image::texture_load_result_response(&self.image.source(ui.ctx()), &tlr, response)
}
}

View File

@@ -220,7 +220,7 @@ impl Label {
.rect_without_leading_space()
.translate(pos.to_vec2());
let mut response = ui.allocate_rect(rect, sense);
response.intrinsic_size = Some(galley.intrinsic_size());
response.set_intrinsic_size(galley.intrinsic_size());
for placed_row in galley.rows.iter().skip(1) {
let rect = placed_row.rect().translate(pos.to_vec2());
response |= ui.allocate_rect(rect, sense);
@@ -256,7 +256,7 @@ impl Label {
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense);
response.intrinsic_size = Some(galley.intrinsic_size());
response.set_intrinsic_size(galley.intrinsic_size());
let galley_pos = match galley.job.halign {
Align::LEFT => rect.left_top(),
Align::Center => rect.center_top(),

View File

@@ -4,7 +4,7 @@
//! * `ui.add(Label::new("Text").text_color(color::red));`
//! * `if ui.add(Button::new("Click me")).clicked() { … }`
use crate::{Response, Ui, epaint};
use crate::{Response, Ui};
mod button;
mod checkbox;
@@ -12,19 +12,14 @@ pub mod color_picker;
pub(crate) mod drag_value;
mod hyperlink;
mod image;
mod image_button;
mod label;
mod progress_bar;
mod radio_button;
mod selected_label;
mod separator;
mod slider;
mod spinner;
pub mod text_edit;
#[expect(deprecated)]
pub use self::selected_label::SelectableLabel;
#[expect(deprecated, reason = "Deprecated in egui 0.33.0")]
pub use self::{
button::Button,
checkbox::Checkbox,
@@ -34,7 +29,6 @@ pub use self::{
FrameDurations, Image, ImageFit, ImageOptions, ImageSize, ImageSource,
decode_animated_image_uri, has_gif_magic_header, has_webp_header, paint_texture_at,
},
image_button::ImageButton,
label::Label,
progress_bar::ProgressBar,
radio_button::RadioButton,
@@ -126,14 +120,6 @@ pub fn reset_button_with<T: PartialEq>(ui: &mut Ui, value: &mut T, text: &str, r
// ----------------------------------------------------------------------------
#[deprecated = "Use `ui.add(&mut stroke)` instead"]
pub fn stroke_ui(ui: &mut crate::Ui, stroke: &mut epaint::Stroke, text: &str) {
ui.horizontal(|ui| {
ui.label(text);
ui.add(stroke);
});
}
/// Show a small button to switch to/from dark/light mode (globally).
pub fn global_theme_preference_switch(ui: &mut Ui) {
if let Some(new_theme) = ui.ctx().theme().small_toggle_button(ui) {
@@ -147,15 +133,3 @@ pub fn global_theme_preference_buttons(ui: &mut Ui) {
theme_preference.radio_buttons(ui);
ui.ctx().set_theme(theme_preference);
}
/// Show a small button to switch to/from dark/light mode (globally).
#[deprecated = "Use global_theme_preference_switch instead"]
pub fn global_dark_light_mode_switch(ui: &mut Ui) {
global_theme_preference_switch(ui);
}
/// Show larger buttons for switching between light and dark mode (globally).
#[deprecated = "Use global_theme_preference_buttons instead"]
pub fn global_dark_light_mode_buttons(ui: &mut Ui) {
global_theme_preference_buttons(ui);
}

View File

@@ -94,12 +94,6 @@ impl ProgressBar {
self.corner_radius = Some(corner_radius.into());
self
}
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
}
impl Widget for ProgressBar {

View File

@@ -1,13 +0,0 @@
#![expect(deprecated, clippy::new_ret_no_self)]
use crate::WidgetText;
#[deprecated = "Use `Button::selectable()` instead"]
pub struct SelectableLabel {}
impl SelectableLabel {
#[deprecated = "Use `Button::selectable()` instead"]
pub fn new(selected: bool, text: impl Into<WidgetText>) -> super::Button<'static> {
crate::Button::selectable(selected, text)
}
}

View File

@@ -79,7 +79,7 @@ pub enum SliderClamping {
///
/// The slider range defines the values you get when pulling the slider to the far edges.
/// By default all values are clamped to this range, even when not interacted with.
/// You can change this behavior by passing `false` to [`Slider::clamp_to_range`].
/// You can change this behavior by passing `false` to [`Slider::clamping`].
///
/// The range can include any numbers, and go from low-to-high or from high-to-low.
///
@@ -288,16 +288,6 @@ impl<'a> Slider<'a> {
self
}
#[inline]
#[deprecated = "Use `slider.clamping(…) instead"]
pub fn clamp_to_range(self, clamp_to_range: bool) -> Self {
self.clamping(if clamp_to_range {
SliderClamping::Always
} else {
SliderClamping::Never
})
}
/// Turn smart aim on/off. Default is ON.
/// There is almost no point in turning this off.
#[inline]
@@ -314,7 +304,7 @@ impl<'a> Slider<'a> {
/// Default: `0.0` (disabled).
#[inline]
pub fn step_by(mut self, step: f64) -> Self {
self.step = if step != 0.0 { Some(step) } else { None };
self.step = if step == 0.0 { None } else { Some(step) };
self
}

View File

@@ -1,19 +1,20 @@
use std::sync::Arc;
use emath::{Rect, TSTransform};
use epaint::{
StrokeKind,
text::{Galley, LayoutJob, cursor::CCursor},
};
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
use crate::{
Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent,
Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint,
Align, Align2, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon, Event,
EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
os::OperatingSystem,
output::OutputEvent,
response, text_selection,
text_selection::{CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection},
response,
text_edit::state::TextEditCursorPurpose,
text_selection::{
self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection,
},
vec2,
};
@@ -67,15 +68,16 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct TextEdit<'t> {
text: &'t mut dyn TextBuffer,
hint_text: WidgetText,
hint_text_font: Option<FontSelection>,
prefix: Atoms<'static>,
suffix: Atoms<'static>,
hint_text: Atoms<'static>,
id: Option<Id>,
id_salt: Option<Id>,
font_selection: FontSelection,
text_color: Option<Color32>,
layouter: Option<LayouterFn<'t>>,
password: bool,
frame: bool,
frame: Option<Frame>,
margin: Margin,
multiline: bool,
interactive: bool,
@@ -120,15 +122,16 @@ impl<'t> TextEdit<'t> {
pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
Self {
text,
prefix: Default::default(),
suffix: Default::default(),
hint_text: Default::default(),
hint_text_font: None,
id: None,
id_salt: None,
font_selection: Default::default(),
text_color: None,
layouter: None,
password: false,
frame: true,
frame: None,
margin: Margin::symmetric(4, 2),
multiline: true,
interactive: true,
@@ -202,8 +205,22 @@ impl<'t> TextEdit<'t> {
/// # });
/// ```
#[inline]
pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
self.hint_text = hint_text.into();
pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self {
self.hint_text = hint_text.into_atoms();
self
}
/// Add a prefix to the text edit. This will always be shown before the editable text.
#[inline]
pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self {
self.prefix = prefix.into_atoms();
self
}
/// Add a suffix to the text edit. This will always be shown after the editable text.
#[inline]
pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self {
self.suffix = suffix.into_atoms();
self
}
@@ -215,13 +232,6 @@ impl<'t> TextEdit<'t> {
self
}
/// Set a specific style for the hint text.
#[inline]
pub fn hint_text_font(mut self, hint_text_font: impl Into<FontSelection>) -> Self {
self.hint_text_font = Some(hint_text_font.into());
self
}
/// If true, hide the letters from view and prevent copying from the field.
#[inline]
pub fn password(mut self, password: bool) -> Self {
@@ -290,10 +300,10 @@ impl<'t> TextEdit<'t> {
self
}
/// Default is `true`. If set to `false` there will be no frame showing that this is editable text!
/// Customize the [`Frame`] around the text edit.
#[inline]
pub fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
@@ -402,7 +412,7 @@ impl<'t> TextEdit<'t> {
impl Widget for TextEdit<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response
self.show(ui).response.response
}
}
@@ -423,63 +433,18 @@ impl TextEdit<'_> {
/// # });
/// ```
pub fn show(self, ui: &mut Ui) -> TextEditOutput {
let is_mutable = self.text.is_mutable();
let frame = self.frame;
let where_to_put_background = ui.painter().add(Shape::Noop);
let background_color = self
.background_color
.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
let output = self.show_content(ui);
if frame {
let visuals = ui.style().interact(&output.response);
let frame_rect = output.response.rect.expand(visuals.expansion);
let shape = if is_mutable {
if output.response.has_focus() {
epaint::RectShape::new(
frame_rect,
visuals.corner_radius,
background_color,
ui.visuals().selection.stroke,
StrokeKind::Inside,
)
} else {
epaint::RectShape::new(
frame_rect,
visuals.corner_radius,
background_color,
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
StrokeKind::Inside,
)
}
} else {
let visuals = &ui.style().visuals.widgets.inactive;
epaint::RectShape::stroke(
frame_rect,
visuals.corner_radius,
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
StrokeKind::Inside,
)
};
ui.painter().set(where_to_put_background, shape);
}
output
}
fn show_content(self, ui: &mut Ui) -> TextEditOutput {
let TextEdit {
text,
hint_text,
hint_text_font,
prefix,
suffix,
mut hint_text,
id,
id_salt,
font_selection,
text_color,
layouter,
password,
frame: _,
frame,
margin,
multiline,
interactive,
@@ -492,7 +457,7 @@ impl TextEdit<'_> {
clip_text,
char_limit,
return_key,
background_color: _,
background_color,
} = self;
let text_color = text_color
@@ -501,53 +466,45 @@ impl TextEdit<'_> {
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
let prev_text = text.as_str().to_owned();
let hint_text_str = hint_text.text().to_owned();
let hint_text_str = hint_text.text().unwrap_or_default().to_string();
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
let wrap_width = if ui.layout().horizontal_justify() {
available_width
} else {
desired_width.min(available_width)
};
let available_width = ui.available_width().at_least(MIN_WIDTH);
let desired_width = desired_width
.unwrap_or_else(|| ui.spacing().text_edit_width)
.at_least(min_size.x);
let allocate_width = desired_width.at_most(available_width);
let font_id_clone = font_id.clone();
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
let text = mask_if_password(password, text.as_str());
let layout_job = if multiline {
let mut layout_job = if multiline {
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
} else {
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
};
layout_job.halign = align.x();
// We want to keep the trailing whitespace, since hiding it feels really weird when typing
layout_job.keep_trailing_whitespace = true;
ui.fonts_mut(|f| f.layout_job(layout_job))
};
let layouter = layouter.unwrap_or(&mut default_layouter);
let mut galley = layouter(ui, text, wrap_width);
let desired_inner_width = if clip_text {
wrap_width // visual clipping with scroll in singleline input.
} else {
galley.size().x.max(wrap_width)
};
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height));
let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size);
let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
let rect = outer_rect - margin; // inner rect (excluding frame/margin).
let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height;
let id = id.unwrap_or_else(|| {
if let Some(id_salt) = id_salt {
ui.make_persistent_id(id_salt)
} else {
auto_id // Since we are only storing the cursor a persistent Id is not super important
// Since we are only storing the cursor a persistent Id is not super important
let id = ui.next_auto_id();
ui.skip_ahead_auto_ids(1);
id
}
});
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
// On touch screens (e.g. mobile in `eframe` web), should
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
@@ -565,12 +522,221 @@ impl TextEdit<'_> {
} else {
Sense::hover()
};
let mut response = ui.interact(outer_rect, id, sense);
response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y));
// Don't sent `OutputEvent::Clicked` when a user presses the space bar
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
let mut cursor_range = None;
let mut prev_cursor_range = None;
let mut text_changed = false;
let text_mutable = text.is_mutable();
let mut handle_events = |ui: &Ui, galley: &mut Arc<Galley>, layouter, wrap_width, text| {
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CCursorRange::one(galley.end())
} else {
CCursorRange::default()
};
prev_cursor_range = state.cursor.range(galley);
let (changed, new_cursor_range) = events(
ui,
&mut state,
text,
galley,
layouter,
id,
wrap_width,
multiline,
password,
default_cursor_range,
char_limit,
event_filter,
return_key,
);
if changed {
text_changed = true;
}
cursor_range = Some(new_cursor_range);
}
};
// We need to calculate the galley within the atom closure, so we can calculate it based on
// the available width (in case of wrapping multiline text edits). But we show it later,
// so we can clip it to the available size. Thus, extract it from the atom closure here.
let mut get_galley = None;
let inner_rect_id = Id::new("text_edit_rect");
let mut response = {
let any_shrink = hint_text.any_shrink();
// Ideally we could just do `let mut atoms = prefix` here, but that won't compile
// but due to servo/rust-smallvec#146 (also see the comment below).
let mut atoms: Atoms<'_> = Atoms::new(());
// TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
// smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
for atom in prefix {
atoms.push_right(atom);
}
if text.as_str().is_empty() && !hint_text.is_empty() {
// Add hint_text (if any):
let mut shrunk = any_shrink;
let mut first = true;
// Since we can't set a fallback color per atom, we have to override it here.
// Sucks, since it means users won't be able to override it.
hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color()));
for mut atom in hint_text {
if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
// elide the hint_text if needed
atom = atom.atom_shrink(true);
atom = atom.atom_grow(true);
shrunk = true;
}
if first {
// The first atom in the hint text gets inner_rect_id, so we can know
// where to paint the cursor
atom = atom.atom_id(inner_rect_id);
first = false;
}
// The hint text should be shown left top instead of centered (important for
// multi line text edits)
atoms.push_right(atom.atom_align(Align2::LEFT_TOP));
}
// Calculate the empty galley, so it can be read later. The available width is
// technically wrong, but doesn't matter since the galley is empty
let available_width = allocate_width - margin.sum().x;
let galley = layouter(ui, text, available_width);
// We can't update the galley immediately here, since it would show both hint text
// and the newly typed letter. So we pass a clone instead, and accept having a frame
// delay on the very first keystroke.
let mut galley_clone = Arc::clone(&galley);
handle_events(ui, &mut galley_clone, layouter, available_width, text);
get_galley = Some(galley);
} else {
// We need to shrink when clip_text, so that we don't exceed the available size
// and thus clip. We also need to shrink in multi line text edits, so text can
// wrap appropriately.
let should_shrink = clip_text || multiline;
// We need a closure here, so we can calculate the galley based on the available
// width (after adding suffix and prefix), for correct wrapping in multi line text
// edits
atoms.push_right(
AtomKind::closure(|ui, args| {
let mut galley = layouter(ui, text, args.available_size.x);
// Handling events here allows us to update the galley immediately on
// keystrokes, avoiding frame delays, and ensuring the scroll_to within
// ScrollAreas works correctly.
handle_events(ui, &mut galley, layouter, args.available_size.x, text);
let intrinsic_size = galley.intrinsic_size();
let mut size = galley.size();
size.y = size.y.at_least(min_inner_height);
if clip_text {
size.x = size.x.at_most(args.available_size.x);
}
// We paint the galley later, so we can do clipping and offsetting
get_galley = Some(galley);
IntoSizedResult {
intrinsic_size,
sized: SizedAtomKind::Empty { size: Some(size) },
}
})
.atom_grow(true)
.atom_align(self.align)
.atom_id(inner_rect_id)
.atom_shrink(should_shrink),
);
}
// TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
// smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
for atom in suffix {
atoms.push_right(atom);
}
let custom_frame = frame.is_some();
let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin));
let min_height = min_inner_height + frame.total_margin().sum().y;
// This wrap mode only affects the hint_text
let wrap_mode = if multiline {
TextWrapMode::Wrap
} else {
TextWrapMode::Truncate
};
let mut allocated = AtomLayout::new(atoms)
.id(id)
.min_size(Vec2::new(allocate_width, min_height))
.max_width(allocate_width)
.sense(sense)
.frame(frame)
.align2(align)
.wrap_mode(wrap_mode)
.allocate(ui);
allocated.frame = if custom_frame {
allocated.frame
} else {
let visuals = ui.style().interact(&allocated.response);
let background_color =
background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
let (corner_radius, background_color, stroke) = if text_mutable {
if allocated.response.has_focus() {
(
visuals.corner_radius,
background_color,
ui.visuals().selection.stroke,
)
} else {
(visuals.corner_radius, background_color, visuals.bg_stroke)
}
} else {
let visuals = &ui.style().visuals.widgets.inactive;
(
visuals.corner_radius,
Color32::TRANSPARENT,
visuals.bg_stroke,
)
};
allocated
.frame
.fill(background_color)
.corner_radius(corner_radius)
.inner_margin(
allocated.frame.inner_margin
+ Margin::same((visuals.expansion - stroke.width).round() as i8),
)
.outer_margin(Margin::same(-(visuals.expansion as i8)))
.stroke(stroke)
};
allocated.paint(ui)
};
let inner_rect = response.rect(inner_rect_id).unwrap_or(Rect::ZERO);
// Our atom closure was now called, so the galley should always be available here
let mut galley = get_galley.expect("Galley should be available here");
// Don't send `OutputEvent::Clicked` when a user presses the space bar
response.flags -= response::Flags::FAKE_PRIMARY_CLICKED;
let text_clip_rect = rect;
let text_clip_rect = inner_rect;
let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor
if interactive && let Some(pointer_pos) = response.interact_pointer_pos() {
@@ -580,20 +746,22 @@ impl TextEdit<'_> {
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
let cursor_at_pointer =
galley.cursor_from_pos(pointer_pos - rect.min + state.text_offset);
let cursor_at_pointer = galley.cursor_from_pos(
pointer_pos - inner_rect.min + state.text_offset + vec2(galley.rect.left(), 0.0),
);
if ui.visuals().text_cursor.preview
&& response.hovered()
&& ui.input(|i| i.pointer.is_moving())
{
// text cursor preview:
let cursor_rect = TSTransform::from_translation(rect.min.to_vec2())
* cursor_rect(&galley, &cursor_at_pointer, row_height);
let cursor_rect = TSTransform::from_translation(
inner_rect.min.to_vec2() - vec2(galley.rect.left(), 0.0),
) * cursor_rect(&galley, &cursor_at_pointer, row_height);
text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
}
let is_being_dragged = ui.ctx().is_being_dragged(response.id);
let is_being_dragged = ui.is_being_dragged(response.id);
let did_interact = state.cursor.pointer_interaction(
ui,
&response,
@@ -613,44 +781,15 @@ impl TextEdit<'_> {
ui.set_cursor_icon(CursorIcon::Text);
}
let mut cursor_range = None;
let prev_cursor_range = state.cursor.range(&galley);
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CCursorRange::one(galley.end())
} else {
CCursorRange::default()
};
let (changed, new_cursor_range) = events(
ui,
&mut state,
text,
&mut galley,
layouter,
id,
wrap_width,
multiline,
password,
default_cursor_range,
char_limit,
event_filter,
return_key,
);
if changed {
response.mark_changed();
}
cursor_range = Some(new_cursor_range);
if text_changed {
response.mark_changed();
}
let mut galley_pos = align
.align_size_within_rect(galley.size(), rect)
.intersect(rect) // limit pos to the response rect area
.align_size_within_rect(galley.size(), inner_rect)
.intersect(inner_rect) // limit pos to the response rect area
.min;
let align_offset = rect.left_top() - galley_pos;
let align_offset = inner_rect.left_top() - galley_pos;
// Visual clipping for singleline text editor with text larger than width
if clip_text && align_offset.x == 0.0 {
@@ -660,18 +799,18 @@ impl TextEdit<'_> {
};
let mut offset_x = state.text_offset.x;
let visible_range = offset_x..=offset_x + desired_inner_size.x;
let visible_range = offset_x..=offset_x + inner_rect.width();
if !visible_range.contains(&cursor_pos) {
if cursor_pos < *visible_range.start() {
offset_x = cursor_pos;
} else {
offset_x = cursor_pos - desired_inner_size.x;
offset_x = cursor_pos - inner_rect.width();
}
}
offset_x = offset_x
.at_most(galley.size().x - desired_inner_size.x)
.at_most(galley.size().x - inner_rect.width())
.at_least(0.0);
state.text_offset = vec2(offset_x, align_offset.y);
@@ -688,32 +827,7 @@ impl TextEdit<'_> {
false
};
if ui.is_rect_visible(rect) {
if text.as_str().is_empty() && !hint_text.is_empty() {
let hint_text_color = ui.visuals().weak_text_color();
let hint_text_font_id = hint_text_font.unwrap_or_else(|| font_id.into());
let galley = if multiline {
hint_text.into_galley(
ui,
Some(TextWrapMode::Wrap),
desired_inner_size.x,
hint_text_font_id,
)
} else {
hint_text.into_galley(
ui,
Some(TextWrapMode::Extend),
f32::INFINITY,
hint_text_font_id,
)
};
let galley_pos = align
.align_size_within_rect(galley.size(), rect)
.intersect(rect)
.min;
painter.galley(galley_pos, galley, hint_text_color);
}
if ui.is_rect_visible(inner_rect) {
let has_focus = ui.memory(|mem| mem.has_focus(id));
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
@@ -721,53 +835,19 @@ impl TextEdit<'_> {
paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
}
// Allocate additional space if edits were made this frame that changed the size. This is important so that,
// if there's a ScrollArea, it can properly scroll to the cursor.
// Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640)
if !clip_text
&& let extra_size = galley.size() - rect.size()
&& (extra_size.x > 0.0 || extra_size.y > 0.0)
{
match ui.layout().main_dir() {
crate::Direction::LeftToRight | crate::Direction::TopDown => {
ui.allocate_rect(
Rect::from_min_size(outer_rect.max, extra_size),
Sense::hover(),
);
}
crate::Direction::RightToLeft => {
ui.allocate_rect(
Rect::from_min_size(
emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y),
extra_size,
),
Sense::hover(),
);
}
crate::Direction::BottomUp => {
ui.allocate_rect(
Rect::from_min_size(
emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y),
extra_size,
),
Sense::hover(),
);
}
}
} else {
// Avoid an ID shift during this pass if the textedit grow
ui.skip_ahead_auto_ids(1);
}
painter.galley(galley_pos, Arc::clone(&galley), text_color);
painter.galley(
galley_pos - vec2(galley.rect.left(), 0.0),
Arc::clone(&galley),
text_color,
);
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height)
.translate(galley_pos.to_vec2());
.translate(galley_pos.to_vec2() - vec2(galley.rect.left(), 0.0));
if response.changed() || selection_changed {
// Scroll to keep primary cursor in view:
ui.scroll_to_rect(primary_cursor_rect + margin, None);
ui.scroll_to_rect(primary_cursor_rect, None);
}
if text.is_mutable() && interactive {
@@ -789,33 +869,24 @@ impl TextEdit<'_> {
now - state.last_interaction_time,
);
}
// Set IME output (in screen coords) when text is editable and visible
let to_global = ui
.ctx()
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
ui.ctx().output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
rect: to_global * rect,
cursor_rect: to_global * primary_cursor_rect,
if ui.memory(|mem| mem.owns_ime_events(id)) {
// Set IME output (in screen coords) when text is editable and visible
let to_global = ui
.ctx()
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
ui.output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
rect: to_global * inner_rect,
cursor_rect: to_global * primary_cursor_rect,
should_interrupt_composition: false,
});
});
});
}
}
}
}
// Ensures correct IME behavior when the text input area gains or loses focus.
if state.ime_enabled && (response.gained_focus() || response.lost_focus()) {
state.ime_enabled = false;
if let Some(mut ccursor_range) = state.cursor.char_range() {
ccursor_range.secondary.index = ccursor_range.primary.index;
state.cursor.set_char_range(Some(ccursor_range));
}
ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_))));
}
state.clone().store(ui.ctx(), id);
if response.changed() {
@@ -846,24 +917,22 @@ impl TextEdit<'_> {
});
}
{
let role = if password {
accesskit::Role::PasswordInput
} else if multiline {
accesskit::Role::MultilineTextInput
} else {
accesskit::Role::TextInput
};
let role = if password {
accesskit::Role::PasswordInput
} else if multiline {
accesskit::Role::MultilineTextInput
} else {
accesskit::Role::TextInput
};
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
ui.ctx(),
id,
cursor_range,
role,
TSTransform::from_translation(galley_pos.to_vec2()),
&galley,
);
}
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
ui.ctx(),
id,
cursor_range,
role,
TSTransform::from_translation(galley_pos.to_vec2()),
&galley,
);
TextEditOutput {
response,
@@ -911,7 +980,7 @@ fn events(
event_filter: EventFilter,
return_key: Option<KeyboardShortcut>,
) -> (bool, CCursorRange) {
let os = ui.ctx().os();
let os = ui.os();
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
@@ -930,12 +999,11 @@ fn events(
let mut any_change = false;
let mut events = ui.input(|i| i.filtered_events(&event_filter));
let events = ui.input(|i| i.filtered_events(&event_filter));
if state.ime_enabled {
remove_ime_incompatible_events(&mut events);
// Process IME events first:
events.sort_by_key(|e| !matches!(e, Event::Ime(_)));
let owns_ime_events = ui.memory(|mem| mem.owns_ime_events(id));
if !owns_ime_events {
state.cursor_purpose = TextEditCursorPurpose::Selection;
}
for event in &events {
@@ -958,7 +1026,9 @@ fn events(
}
}
Event::Paste(text_to_insert) => {
if !text_to_insert.is_empty() {
if text_to_insert.is_empty() {
None
} else {
let mut ccursor = text.delete_selected(&cursor_range);
if multiline {
text.insert_text_at(&mut ccursor, text_to_insert, char_limit);
@@ -968,8 +1038,6 @@ fn events(
}
Some(CCursorRange::one(ccursor))
} else {
None
}
}
Event::Text(text_to_insert) => {
@@ -1065,7 +1133,7 @@ fn events(
..
} => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
Event::Ime(ime_event) => {
Event::Ime(ime_event) if owns_ime_events => {
/// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")`
/// might be emitted from different integrations to signify that
/// the current IME composition should be cleared.
@@ -1099,46 +1167,58 @@ fn events(
}
match ime_event {
ImeEvent::Enabled => {
state.ime_enabled = true;
state.ime_cursor_range = cursor_range;
#[expect(deprecated)]
ImeEvent::Enabled | ImeEvent::Disabled => None,
// Ignore `Preedit`/`Commit` events with empty text when
// there is no active IME composition.
//
// Some integrations may emit these events when there is no
// active IME composition (e.g. when `set_ime_allowed` or
// `set_ime_cursor_area` is called on `winit`'s `Window` on
// Wayland). Without this guard, they would clear any
// selected text.
//
// TODO(umajho): Ideally this would be handled by the
// integration, but since this guard is harmless for well-
// behaved integrations and also fixes the issue described
// above, it is good enough for now.
ImeEvent::Preedit(composition_text) | ImeEvent::Commit(composition_text)
if composition_text.is_empty()
&& !matches!(
state.cursor_purpose,
TextEditCursorPurpose::ImeComposition
) =>
{
None
}
ImeEvent::Preedit(composition_text) | ImeEvent::Commit(composition_text)
if composition_text == "\n" || composition_text == "\r" =>
{
None
}
ImeEvent::Preedit(preedit_text) => {
if preedit_text == "\n" || preedit_text == "\r" {
None
state.cursor_purpose = if preedit_text.is_empty() {
TextEditCursorPurpose::Selection
} else {
let mut ccursor = clear_preedit_text(text, &cursor_range);
TextEditCursorPurpose::ImeComposition
};
let mut ccursor = clear_preedit_text(text, &cursor_range);
let start_cursor = ccursor;
if !preedit_text.is_empty() {
text.insert_text_at(&mut ccursor, preedit_text, char_limit);
}
state.ime_cursor_range = cursor_range;
Some(CCursorRange::two(start_cursor, ccursor))
let start_cursor = ccursor;
if !preedit_text.is_empty() {
text.insert_text_at(&mut ccursor, preedit_text, char_limit);
}
Some(CCursorRange::two(start_cursor, ccursor))
}
ImeEvent::Commit(commit_text) => {
if commit_text == "\n" || commit_text == "\r" {
None
} else {
state.ime_enabled = false;
state.cursor_purpose = TextEditCursorPurpose::Selection;
let mut ccursor = clear_preedit_text(text, &cursor_range);
let mut ccursor = clear_preedit_text(text, &cursor_range);
if !commit_text.is_empty()
&& cursor_range.secondary.index
== state.ime_cursor_range.secondary.index
{
text.insert_text_at(&mut ccursor, commit_text, char_limit);
}
Some(CCursorRange::one(ccursor))
if !commit_text.is_empty() {
text.insert_text_at(&mut ccursor, commit_text, char_limit);
}
}
ImeEvent::Disabled => {
state.ime_enabled = false;
None
Some(CCursorRange::one(ccursor))
}
}
}
@@ -1169,27 +1249,6 @@ fn events(
// ----------------------------------------------------------------------------
fn remove_ime_incompatible_events(events: &mut Vec<Event>) {
// Remove key events which cause problems while 'IME' is being used.
// See https://github.com/emilk/egui/pull/4509
events.retain(|event| {
!matches!(
event,
Event::Key { repeat: true, .. }
| Event::Key {
key: Key::Backspace
| Key::ArrowUp
| Key::ArrowDown
| Key::ArrowLeft
| Key::ArrowRight,
..
}
)
});
}
// ----------------------------------------------------------------------------
/// Returns `Some(new_cursor)` if we did mutate `text`.
fn check_for_mutating_key_press(
os: OperatingSystem,

View File

@@ -5,7 +5,7 @@ use crate::text::CCursorRange;
/// The output from a [`TextEdit`](crate::TextEdit).
pub struct TextEditOutput {
/// The interaction response.
pub response: crate::Response,
pub response: crate::AtomLayoutResponse,
/// How the text was displayed.
pub galley: Arc<crate::Galley>,

View File

@@ -37,18 +37,14 @@ pub struct TextEditState {
/// Controls the text selection.
pub cursor: TextCursorState,
/// The purpose of the cursor.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) cursor_purpose: TextEditCursorPurpose,
/// Wrapped in Arc for cheaper clones.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) undoer: Arc<Mutex<TextEditUndoer>>,
// If IME candidate window is shown on this text edit.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) ime_enabled: bool,
// cursor range for IME candidate.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) ime_cursor_range: CCursorRange,
// Text offset within the widget area.
// Used for sensing and singleline text clipping.
#[cfg_attr(feature = "serde", serde(skip))]
@@ -82,3 +78,13 @@ impl TextEditState {
self.set_undoer(TextEditUndoer::default());
}
}
#[derive(Clone, Default)]
pub(crate) enum TextEditCursorPurpose {
/// The cursor is used for text selection.
#[default]
Selection,
/// The cursor is used for IME composition.
ImeComposition,
}

View File

@@ -1,9 +1,9 @@
use std::{borrow::Cow, ops::Range};
use epaint::{
Galley,
text::{TAB_SIZE, cursor::CCursor},
};
use epaint::{Galley, text::cursor::CCursor};
/// One `\t` character is this many spaces wide (for indentation purposes).
const TAB_SIZE: usize = 4;
use crate::{
text::CCursorRange,

View File

@@ -8,8 +8,8 @@ rust-version.workspace = true
publish = false
default-run = "egui_demo_app"
[package.metadata.cargo-machete]
ignored = ["profiling"]
[package.metadata.cargo-shear]
ignored = ["image", "profiling", "wasm-bindgen-futures"]
[lints]
workspace = true
@@ -23,12 +23,12 @@ crate-type = ["cdylib", "rlib"]
[features]
default = ["wgpu", "persistence"]
default = ["wgpu", "persistence", "wayland", "x11"]
# image_viewer adds about 0.9 MB of WASM
web_app = ["http", "persistence"]
accessibility_inspector = ["dep:accesskit", "dep:accesskit_consumer", "eframe/accesskit"]
easymark = [] # easymark is off by default, because it a pretty shitty markup language
http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"]
image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"]
persistence = ["eframe/persistence", "egui_extras/serde", "egui/persistence", "serde"]
@@ -37,20 +37,20 @@ serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
syntect = ["egui_demo_lib/syntect"]
glow = ["eframe/glow"]
wgpu = ["eframe/wgpu", "bytemuck", "dep:wgpu"]
wgpu = ["eframe/wgpu", "bytemuck"]
wayland = ["eframe/wayland"]
x11 = ["eframe/x11"]
[dependencies]
chrono = { workspace = true, features = ["js-sys", "wasmbind"] }
eframe = { workspace = true, default-features = false, features = ["web_screen_reader"] }
egui = { workspace = true, features = ["callstack", "default"] }
egui_demo_lib = { workspace = true, features = ["default", "chrono"] }
egui_demo_lib = { workspace = true, features = ["default", "jiff"] }
egui_extras = { workspace = true, features = ["default", "image"] }
image = { workspace = true, default-features = false, features = [
# Ensure we can display the test images
"png",
] }
jiff = { workspace = true, features = ["std", "tz-system", "js"] }
log.workspace = true
profiling.workspace = true
@@ -60,9 +60,6 @@ accesskit_consumer = { workspace = true, optional = true }
bytemuck = { workspace = true, optional = true }
puffin = { workspace = true, optional = true }
puffin_http = { workspace = true, optional = true }
# Enable both WebGL & WebGPU when targeting the web (these features have no effect when not targeting wasm32)
# Also enable the default features so we have a supported backend for every platform.
wgpu = { workspace = true, features = ["default", "webgpu", "webgl"], optional = true }
# feature "http":

Some files were not shown because too many files have changed in this diff Show More