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:
19
.github/workflows/cargo_machete.yml
vendored
19
.github/workflows/cargo_machete.yml
vendored
@@ -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
25
.github/workflows/cargo_shear.yml
vendored
Normal 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
|
||||
10
.github/workflows/enforce_branch_name.yml
vendored
10
.github/workflows/enforce_branch_name.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
@@ -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
25
.github/workflows/taplo.yml
vendored
Normal 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
|
||||
140
CHANGELOG.md
140
CHANGELOG.md
@@ -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
1098
Cargo.lock
File diff suppressed because it is too large
Load Diff
70
Cargo.toml
70
Cargo.toml
@@ -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"
|
||||
|
||||
10
README.md
10
README.md
@@ -10,16 +10,14 @@
|
||||
[](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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
@@ -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).
|
||||
///
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
76
crates/eframe/src/native/macos.rs
Normal file
76
crates/eframe/src/native/macos.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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")))
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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 => {}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user