mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Merge branch 'master' into cache_galley_lines
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,6 +1,7 @@
|
||||
* text=auto eol=lf
|
||||
Cargo.lock linguist-generated=false
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Exclude some small files from LFS:
|
||||
crates/eframe/data/* !filter !diff !merge text=auto eol=lf
|
||||
|
||||
12
.github/workflows/cargo_machete.yml
vendored
12
.github/workflows/cargo_machete.yml
vendored
@@ -6,7 +6,13 @@ jobs:
|
||||
cargo-machete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.85
|
||||
- name: Machete install
|
||||
run: cargo install cargo-machete --locked
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Machete
|
||||
run: cargo install cargo-machete --locked && cargo machete
|
||||
uses: actions/checkout@v4
|
||||
- name: Machete Check
|
||||
run: cargo machete
|
||||
|
||||
|
||||
2
.github/workflows/deploy_web_demo.yml
vendored
2
.github/workflows/deploy_web_demo.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
with:
|
||||
profile: minimal
|
||||
target: wasm32-unknown-unknown
|
||||
toolchain: 1.80.0
|
||||
toolchain: 1.81.0
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
2
.github/workflows/png_only_on_lfs.yml
vendored
2
.github/workflows/png_only_on_lfs.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
exclude_pattern=$(printf "|^%s" "${exclude_paths[@]}" | sed 's/^|//')
|
||||
|
||||
if comm -23 <(git ls-files | grep -Ev "$exclude_pattern" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then
|
||||
echo "Error: Found binary file with extension .$ext not tracked by git LFS. See CONTRIBUTING.md"
|
||||
echo "Error: Found binary file with extension .$ext not tracked by git LFS. See https://github.com/emilk/egui/blob/master/CONTRIBUTING.md#working-with-git-lfs"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
7
.github/workflows/preview_build.yml
vendored
7
.github/workflows/preview_build.yml
vendored
@@ -16,8 +16,11 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: rustup toolchain install stable --profile minimal --target wasm32-unknown-unknown
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.81.0
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: "pr-preview-"
|
||||
|
||||
24
.github/workflows/rust.yml
vendored
24
.github/workflows/rust.yml
vendored
@@ -9,7 +9,7 @@ env:
|
||||
|
||||
jobs:
|
||||
fmt-crank-check-test:
|
||||
name: Format + check + test
|
||||
name: Format + check
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -18,11 +18,11 @@ jobs:
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.80.0
|
||||
toolchain: 1.81.0
|
||||
|
||||
- name: Install packages (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.2
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
|
||||
with:
|
||||
packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
|
||||
version: 1.0
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.80.0
|
||||
toolchain: 1.81.0
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
- name: wasm-bindgen
|
||||
uses: jetli/wasm-bindgen-action@v0.1.0
|
||||
with:
|
||||
version: "0.2.95"
|
||||
version: "0.2.97"
|
||||
|
||||
- run: ./scripts/wasm_bindgen_check.sh --skip-setup
|
||||
|
||||
@@ -153,9 +153,9 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
rust-version: "1.80.0"
|
||||
rust-version: "1.81.0"
|
||||
log-level: error
|
||||
command: check
|
||||
arguments: --target ${{ matrix.target }}
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.80.0
|
||||
toolchain: 1.81.0
|
||||
targets: aarch64-linux-android
|
||||
|
||||
- name: Set up cargo cache
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.80.0
|
||||
toolchain: 1.81.0
|
||||
targets: aarch64-apple-ios
|
||||
|
||||
- name: Set up cargo cache
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.80.0
|
||||
toolchain: 1.81.0
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
# We run the tests on macOS because it will run with a actual GPU
|
||||
# We run the tests on macOS because it will run with an actual GPU
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
lfs: true
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.80.0
|
||||
toolchain: 1.81.0
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
12
.github/workflows/spelling_and_links.yml
vendored
12
.github/workflows/spelling_and_links.yml
vendored
@@ -4,7 +4,7 @@ on: [pull_request]
|
||||
jobs:
|
||||
typos:
|
||||
# https://github.com/crate-ci/typos
|
||||
# Add exceptions to _typos.toml
|
||||
# Add exceptions to .typos.toml
|
||||
# install and run locally: cargo install typos-cli && typos
|
||||
name: typos
|
||||
runs-on: ubuntu-latest
|
||||
@@ -14,15 +14,7 @@ jobs:
|
||||
|
||||
- name: Check spelling of entire workspace
|
||||
uses: crate-ci/typos@master
|
||||
# Disabled: too many names of crates and user-names etc
|
||||
# spellcheck:
|
||||
# name: Spellcheck
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: streetsidesoftware/cspell-action@v2
|
||||
# with:
|
||||
# files: "**/*.md"
|
||||
|
||||
linkinator:
|
||||
name: linkinator
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
**/target_wasm
|
||||
**/tests/snapshots/**/*.diff.png
|
||||
**/tests/snapshots/**/*.new.png
|
||||
**/tests/snapshots/**/*.old.png
|
||||
/.*.json
|
||||
/.vscode
|
||||
/media/*
|
||||
|
||||
11
.typos.toml
11
.typos.toml
@@ -6,6 +6,17 @@
|
||||
ime = "ime" # Input Method Editor
|
||||
nknown = "nknown" # part of @55nknown username
|
||||
ro = "ro" # read-only, also part of the username @Phen-Ro
|
||||
typ = "typ" # Often used because `type` is a keyword in Rust
|
||||
|
||||
# I mistype these so often
|
||||
tesalator = "tessellator"
|
||||
teselator = "tessellator"
|
||||
tessalator = "tessellator"
|
||||
tesselator = "tessellator"
|
||||
tesalation = "tessellation"
|
||||
teselation = "tessellation"
|
||||
tessalation = "tessellation"
|
||||
tesselation = "tessellation"
|
||||
|
||||
[files]
|
||||
extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated
|
||||
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -33,7 +33,11 @@
|
||||
"--all-features",
|
||||
],
|
||||
"rust-analyzer.showUnlinkedFileNotification": false,
|
||||
|
||||
"rust-analyzer.cargo.extraEnv": {
|
||||
// rust-analyzer is only guaranteed to support the latest stable version of Rust. Use it instead of whatever is
|
||||
// specified in rust-toolchain.
|
||||
"RUSTUP_TOOLCHAIN": "stable"
|
||||
},
|
||||
// Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`.
|
||||
// Don't forget to put it in a comment again before committing.
|
||||
// "rust-analyzer.cargo.target": "wasm32-unknown-unknown",
|
||||
|
||||
87
CHANGELOG.md
87
CHANGELOG.md
@@ -14,6 +14,93 @@ 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.31.1 - 2025-03-05
|
||||
* Fix sizing bug in `TextEdit::singleline` [#5640](https://github.com/emilk/egui/pull/5640) by [@IaVashik](https://github.com/IaVashik)
|
||||
* Fix panic when rendering thin textured rectangles [#5692](https://github.com/emilk/egui/pull/5692) by [@PPakalns](https://github.com/PPakalns)
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04 - Scene container, improved rendering quality
|
||||
|
||||
### Highlights ✨
|
||||
|
||||
#### Scene container
|
||||
This release adds the `Scene` container to egui. It is a pannable, zoomable canvas that can contain `Widget`s and child `Ui`s.
|
||||
This will make it easier to e.g. implement a graph editor.
|
||||
|
||||

|
||||
|
||||
#### Clearer, pixel perfect rendering
|
||||
The tessellator has been updated for improved rendering quality and better performance. It will produce fewer vertices
|
||||
and shapes will have less overdraw. We've also defined what `CornerRadius` (previously `Rounding`) means.
|
||||
|
||||
We've also added a tessellator test to the [demo app](https://www.egui.rs/), where you can play around with different
|
||||
values to see what's produced:
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/adf55e3b-fb48-4df0-aaa2-150ee3163684
|
||||
|
||||
|
||||
Check the [PR](https://github.com/emilk/egui/pull/5669) for more details.
|
||||
|
||||
#### `CornerRadius`, `Margin`, `Shadow` size reduction
|
||||
In order to pave the path for more complex and customizable styling solutions, we've reduced the size of
|
||||
`CornerRadius`, `Margin` and `Shadow` values to `i8` and `u8`.
|
||||
|
||||
|
||||
|
||||
### Migration guide
|
||||
- Add a `StrokeKind` to all your `Painter::rect` calls [#5648](https://github.com/emilk/egui/pull/5648)
|
||||
- `StrokeKind::default` was removed, since the 'normal' value depends on the context [#5658](https://github.com/emilk/egui/pull/5658)
|
||||
- You probably want to use `StrokeKind::Inside` when drawing rectangles
|
||||
- You probably want to use `StrokeKind::Middle` when drawing open paths
|
||||
- Rename `Rounding` to `CornerRadius` [#5673](https://github.com/emilk/egui/pull/5673)
|
||||
- `CornerRadius`, `Margin` and `Shadow` have been updated to use `i8` and `u8` [#5563](https://github.com/emilk/egui/pull/5563), [#5567](https://github.com/emilk/egui/pull/5567), [#5568](https://github.com/emilk/egui/pull/5568)
|
||||
- Remove the .0 from your values
|
||||
- Cast dynamic values with `as i8` / `as u8` or `as _` if you want Rust to infer the type
|
||||
- Rust will do a 'saturating' cast, so if your `f32` value is bigger than `127` it will be clamped to `127`
|
||||
- `RectShape` parameters changed [#5565](https://github.com/emilk/egui/pull/5565)
|
||||
- Prefer to use the builder methods to create it instead of initializing it directly
|
||||
- `Frame` now takes the `Stroke` width into account for its sizing, so check all views of your app to make sure they still look right.
|
||||
Read the [PR](https://github.com/emilk/egui/pull/5575) for more info.
|
||||
|
||||
### ⭐ Added
|
||||
* Add `egui::Scene` for panning/zooming a `Ui` [#5505](https://github.com/emilk/egui/pull/5505) by [@grtlr](https://github.com/grtlr)
|
||||
* Animated WebP support [#5470](https://github.com/emilk/egui/pull/5470) by [@Aely0](https://github.com/Aely0)
|
||||
* Improve tessellation quality [#5669](https://github.com/emilk/egui/pull/5669) by [@emilk](https://github.com/emilk)
|
||||
* Add `OutputCommand` for copying text and opening URL:s [#5532](https://github.com/emilk/egui/pull/5532) by [@emilk](https://github.com/emilk)
|
||||
* Add `Context::copy_image` [#5533](https://github.com/emilk/egui/pull/5533) by [@emilk](https://github.com/emilk)
|
||||
* Add `WidgetType::Image` and `Image::alt_text` [#5534](https://github.com/emilk/egui/pull/5534) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Add `epaint::Brush` for controlling `RectShape` texturing [#5565](https://github.com/emilk/egui/pull/5565) by [@emilk](https://github.com/emilk)
|
||||
* Implement `nohash_hasher::IsEnabled` for `Id` [#5628](https://github.com/emilk/egui/pull/5628) by [@emilk](https://github.com/emilk)
|
||||
* Add keys for `!`, `{`, `}` [#5548](https://github.com/emilk/egui/pull/5548) by [@Its-Just-Nans](https://github.com/Its-Just-Nans)
|
||||
* Add `RectShape::stroke_kind ` to control if stroke is inside/outside/centered [#5647](https://github.com/emilk/egui/pull/5647) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🔧 Changed
|
||||
* ⚠️ `Frame` now includes stroke width as part of padding [#5575](https://github.com/emilk/egui/pull/5575) by [@emilk](https://github.com/emilk)
|
||||
* Rename `Rounding` to `CornerRadius` [#5673](https://github.com/emilk/egui/pull/5673) by [@emilk](https://github.com/emilk)
|
||||
* Require a `StrokeKind` when painting rectangles with strokes [#5648](https://github.com/emilk/egui/pull/5648) by [@emilk](https://github.com/emilk)
|
||||
* Round widget coordinates to even multiple of 1/32 [#5517](https://github.com/emilk/egui/pull/5517) by [@emilk](https://github.com/emilk)
|
||||
* Make all lines and rectangles crisp [#5518](https://github.com/emilk/egui/pull/5518) by [@emilk](https://github.com/emilk)
|
||||
* Tweak window resize handles [#5524](https://github.com/emilk/egui/pull/5524) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🔥 Removed
|
||||
* Remove `egui::special_emojis::TWITTER` [#5622](https://github.com/emilk/egui/pull/5622) by [@emilk](https://github.com/emilk)
|
||||
* Remove `StrokeKind::default` [#5658](https://github.com/emilk/egui/pull/5658) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Use correct minimum version of `profiling` crate [#5494](https://github.com/emilk/egui/pull/5494) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix interactive widgets sometimes being incorrectly marked as hovered [#5523](https://github.com/emilk/egui/pull/5523) by [@emilk](https://github.com/emilk)
|
||||
* Fix panic due to non-total ordering in `Area::compare_order()` [#5569](https://github.com/emilk/egui/pull/5569) by [@HactarCE](https://github.com/HactarCE)
|
||||
* Fix hovering through custom menu button [#5555](https://github.com/emilk/egui/pull/5555) by [@M4tthewDE](https://github.com/M4tthewDE)
|
||||
|
||||
### 🚀 Performance
|
||||
* Use `u8` in `CornerRadius`, and introduce `CornerRadiusF32` [#5563](https://github.com/emilk/egui/pull/5563) by [@emilk](https://github.com/emilk)
|
||||
* Store `Margin` using `i8` to reduce its size [#5567](https://github.com/emilk/egui/pull/5567) by [@emilk](https://github.com/emilk)
|
||||
* Shrink size of `Shadow` by using `i8/u8` instead of `f32` [#5568](https://github.com/emilk/egui/pull/5568) by [@emilk](https://github.com/emilk)
|
||||
* Avoid allocations for loader cache lookup [#5584](https://github.com/emilk/egui/pull/5584) by [@mineichen](https://github.com/mineichen)
|
||||
* Use bitfield instead of bools in `Response` and `Sense` [#5556](https://github.com/emilk/egui/pull/5556) by [@polwel](https://github.com/polwel)
|
||||
|
||||
|
||||
## 0.30.0 - 2024-12-16 - Modals and better layer support
|
||||
|
||||
### ✨ Highlights
|
||||
|
||||
@@ -34,14 +34,11 @@ Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pi
|
||||
You can test your code locally by running `./scripts/check.sh`.
|
||||
There are snapshots test that might need to be updated.
|
||||
Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them.
|
||||
If CI keeps complaining about snapshots (which could happen if you don't use macOS, snapshots in CI are currently
|
||||
rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from
|
||||
the last CI run of your PR (which will download the `test_results` artefact).
|
||||
For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md).
|
||||
|
||||
We use [git-lfs](https://git-lfs.com/) to store big files in the repository.
|
||||
Make sure you have it installed (running `git lfs ls-files` from the repository root should list some files).
|
||||
Don't forget to run `git lfs install` after installing the git-lfs binary.
|
||||
You need to add any .png images to `git lfs`.
|
||||
If the CI complains about this, make sure you run `git add --renormalize .`.
|
||||
|
||||
Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info.
|
||||
If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs.
|
||||
|
||||
When you have something that works, open a draft PR. You may get some helpful feedback early!
|
||||
@@ -51,6 +48,31 @@ Don't worry about having many small commits in the PR - they will be squashed to
|
||||
|
||||
Please keep pull requests small and focused. The smaller it is, the more likely it is to get merged.
|
||||
|
||||
## Working with git lfs
|
||||
|
||||
We use [git-lfs](https://git-lfs.com/) to store big files in the repository.
|
||||
Make sure you have it installed (running `git lfs ls-files` from the repository root should list some files).
|
||||
Don't forget to run `git lfs install` in this repo after installing the git-lfs binary.
|
||||
You need to add any .png images to `git lfs` (see the .gitattributes file for rules and exclusions).
|
||||
If the CI complains about lfs, try running `git add --renormalize .`.
|
||||
|
||||
Common git-lfs commands:
|
||||
```bash
|
||||
# Install git-lfs in the repo (installs git hooks)
|
||||
git lfs install
|
||||
|
||||
# Move a file to git lfs
|
||||
git lfs track "path/to/file/or/pattern" # OR manually edit .gitattributes
|
||||
git add --renormalize . # Moves already added files to lfs (according to .gitattributes)
|
||||
|
||||
# Move a file from lfs to regular git
|
||||
git lfs untrack "path/to/file/or/pattern" # OR manually edit .gitattributes
|
||||
git add --renormalize . # Moves already added files to regular git (according to .gitattributes)
|
||||
|
||||
# Push to a contributor remote (see https://github.com/cli/cli/discussions/8794#discussioncomment-8695076)
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## PR review
|
||||
|
||||
Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs!
|
||||
|
||||
1034
Cargo.lock
1034
Cargo.lock
File diff suppressed because it is too large
Load Diff
39
Cargo.toml
39
Cargo.toml
@@ -23,8 +23,8 @@ members = [
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.80"
|
||||
version = "0.30.0"
|
||||
rust-version = "1.81"
|
||||
version = "0.31.1"
|
||||
|
||||
|
||||
[profile.release]
|
||||
@@ -55,24 +55,27 @@ opt-level = 2
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
emath = { version = "0.30.0", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.30.0", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.30.0", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.30.0", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.30.0", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.30.0", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.30.0", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.30.0", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.30.0", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.30.0", path = "crates/egui_glow", default-features = false }
|
||||
egui_kittest = { version = "0.30.0", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.30.0", path = "crates/eframe", default-features = false }
|
||||
emath = { version = "0.31.1", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.31.1", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.31.1", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.31.1", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.31.1", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.31.1", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.31.1", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.31.1", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.31.1", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.31.1", path = "crates/egui_glow", default-features = false }
|
||||
egui_kittest = { version = "0.31.1", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.31.1", path = "crates/eframe", default-features = false }
|
||||
|
||||
accesskit = "0.18.0"
|
||||
accesskit_winit = "0.24"
|
||||
ahash = { version = "0.8.11", default-features = false, features = [
|
||||
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
||||
"std",
|
||||
] }
|
||||
backtrace = "0.3"
|
||||
bitflags = "2.6"
|
||||
bytemuck = "1.7.2"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
dify = { version = "0.7", default-features = false }
|
||||
@@ -82,7 +85,7 @@ glutin = { version = "0.32.0", default-features = false }
|
||||
glutin-winit = { version = "0.5.0", default-features = false }
|
||||
home = "0.5.9"
|
||||
image = { version = "0.25", default-features = false }
|
||||
kittest = { version = "0.1" }
|
||||
kittest = { version = "0.1.0", git = "https://github.com/rerun-io/kittest", branch = "main" }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
nohash-hasher = "0.2"
|
||||
parking_lot = "0.12"
|
||||
@@ -99,7 +102,7 @@ wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = "0.3.70"
|
||||
web-time = "1.1.0" # Timekeeping for native and web
|
||||
wgpu = { version = "23.0.0", default-features = false }
|
||||
wgpu = { version = "24.0.0", default-features = false }
|
||||
windows-sys = "0.59"
|
||||
winit = { version = "0.30.7", default-features = false }
|
||||
|
||||
@@ -113,6 +116,7 @@ rust_2018_idioms = { level = "warn", priority = -1 }
|
||||
rust_2021_prelude_collisions = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unexpected_cfgs = "warn"
|
||||
unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
@@ -202,8 +206,8 @@ match_same_arms = "warn"
|
||||
match_wild_err_arm = "warn"
|
||||
match_wildcard_for_single_variants = "warn"
|
||||
mem_forget = "warn"
|
||||
mismatched_target_os = "warn"
|
||||
mismatching_type_param_order = "warn"
|
||||
missing_assert_message = "warn"
|
||||
missing_enforced_import_renames = "warn"
|
||||
missing_errors_doc = "warn"
|
||||
missing_safety_doc = "warn"
|
||||
@@ -271,7 +275,6 @@ zero_sized_map_values = "warn"
|
||||
|
||||
# TODO(emilk): maybe enable more of these lints?
|
||||
iter_over_hash_type = "allow"
|
||||
missing_assert_message = "allow"
|
||||
should_panic_without_expect = "allow"
|
||||
too_many_lines = "allow"
|
||||
unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one
|
||||
|
||||
20
README.md
20
README.md
@@ -11,7 +11,7 @@
|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.rerun.io/"><img src="media/rerun_io_logo.png" width="250"></a>
|
||||
<a href="https://www.rerun.io/"><img src="https://github.com/user-attachments/assets/78e79463-4357-461b-bbd1-31aa5ef5e1a2" width="250"></a>
|
||||
|
||||
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
|
||||
an SDK for visualizing streams of multimodal data.
|
||||
@@ -46,7 +46,7 @@ ui.label(format!("Hello '{name}', age {age}"));
|
||||
ui.image(egui::include_image!("ferris.png"));
|
||||
```
|
||||
|
||||
<img alt="Dark mode" src="media/demo.gif"> <img alt="Light mode" src="media/demo_light_mode.png" height="278">
|
||||
<img alt="Dark mode" src="https://github.com/user-attachments/assets/3b446d29-99d8-4c82-86bb-4d8ef0516017"> <img alt="Light mode" src="https://github.com/user-attachments/assets/a5e7da93-89a8-4ba0-86b8-0fa2228a4f62" height="278">
|
||||
|
||||
## Sections:
|
||||
|
||||
@@ -133,26 +133,26 @@ Still, egui can be used to create professional looking applications, like [the R
|
||||
* Label text selection
|
||||
* And more!
|
||||
|
||||
Check out the [3rd party egui crates wiki](https://github.com/emilk/egui/wiki/3rd-party-egui-crates) for even more
|
||||
Check out the [3rd party egui crates wiki](https://github.com/emilk/egui/wiki/3rd-party-egui-crates) for even more
|
||||
widgets and features, maintained by the community.
|
||||
|
||||
<img src="media/widget_gallery_0.23.gif" width="50%">
|
||||
<img src="https://github.com/user-attachments/assets/13e73b76-e456-42bd-8ec9-220802834268" width="50%">
|
||||
|
||||
Light Theme:
|
||||
|
||||
<img src="media/widget_gallery_0.23_light.png" width="50%">
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/2e38972c-a444-4894-b32f-47a2719cf369" width="50%">
|
||||
|
||||
## Dependencies
|
||||
`egui` has a minimal set of default dependencies:
|
||||
|
||||
* [`ab_glyph`](https://crates.io/crates/ab_glyph)
|
||||
* [`ahash`](https://crates.io/crates/ahash)
|
||||
* [`bitflags`](https://crates.io/crates/bitflags)
|
||||
* [`nohash-hasher`](https://crates.io/crates/nohash-hasher)
|
||||
* [`parking_lot`](https://crates.io/crates/parking_lot)
|
||||
|
||||
Heavier dependencies are kept out of `egui`, even as opt-in.
|
||||
No code that isn't fully Wasm-friendly is part of `egui`.
|
||||
All code in `egui` is Wasm-friendly (even outside a browser).
|
||||
|
||||
To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/master/crates/egui_extras) crate.
|
||||
|
||||
@@ -190,7 +190,7 @@ These are the official egui integrations:
|
||||
|
||||
### 3rd party integrations
|
||||
|
||||
Check the wiki to find [3rd party integrations](https://github.com/emilk/egui/wiki/3rd-party-integrations)
|
||||
Check the wiki to find [3rd party integrations](https://github.com/emilk/egui/wiki/3rd-party-integrations)
|
||||
and [egui crates](https://github.com/emilk/egui/wiki/3rd-party-egui-crates).
|
||||
|
||||
### Writing your own egui integration
|
||||
@@ -267,7 +267,7 @@ This is not yet as powerful as say CSS, [but this is going to improve](https://g
|
||||
|
||||
Here is an example (from https://github.com/a-liashenko/TinyPomodoro):
|
||||
|
||||
<img src="media/pompodoro-skin.png" width="50%">
|
||||
<img src="https://github.com/user-attachments/assets/e6107237-2547-41d6-996b-9a20ae0345ab" width="50%">
|
||||
|
||||
### How do I use egui with `async`?
|
||||
If you call `.await` in your GUI code, the UI will freeze, which is very bad UX. Instead, keep the GUI thread non-blocking and communicate with any concurrent tasks (`async` tasks or other threads) with something like:
|
||||
@@ -375,7 +375,7 @@ Default fonts:
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.rerun.io/"><img src="media/rerun_io_logo.png" width="440"></a>
|
||||
<a href="https://www.rerun.io/"><img src="https://github.com/user-attachments/assets/78e79463-4357-461b-bbd1-31aa5ef5e1a2" width="440"></a>
|
||||
|
||||
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
|
||||
an SDK for visualizing streams of multimodal data.
|
||||
|
||||
24
RELEASES.md
24
RELEASES.md
@@ -36,23 +36,19 @@ We don't update the MSRV in a patch release, unless we really, really need to.
|
||||
|
||||
## Release testing
|
||||
* [ ] `cargo r -p egui_demo_app` and click around for while
|
||||
* [ ] `./scripts/build_demo_web.sh --release -g`
|
||||
- check frame-rate and wasm size
|
||||
- test on mobile
|
||||
- test on chromium
|
||||
- check the in-browser profiler
|
||||
* [ ] check the color test
|
||||
* [ ] update `eframe_template` and test
|
||||
* [ ] update `egui_plot` and test
|
||||
* [ ] update `egui_table` and test
|
||||
* [ ] update `egui_tiles` and test
|
||||
* [ ] test with Rerun
|
||||
* [ ] `./scripts/check.sh`
|
||||
* [ ] check that CI is green
|
||||
|
||||
## Preparation
|
||||
* [ ] make sure there are no important unmerged PRs
|
||||
* [ ] run `scripts/generate_example_screenshots.sh` if needed
|
||||
* [ ] write a short release note that fits in a tweet
|
||||
* [ ] record gif for `CHANGELOG.md` release note (and later twitter post)
|
||||
* [ ] write a short release note that fits in a bluesky post
|
||||
* [ ] record gif for `CHANGELOG.md` release note (and later bluesky post)
|
||||
* [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write`
|
||||
* [ ] bump version numbers in workspace `Cargo.toml`
|
||||
|
||||
@@ -60,9 +56,9 @@ We don't update the MSRV in a patch release, unless we really, really need to.
|
||||
I usually do this all on the `master` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `master` later.
|
||||
|
||||
* [ ] Run `typos`
|
||||
* [ ] `git commit -m 'Release 0.x.0 - summary'`
|
||||
* [ ] `git commit -m 'Release 0.x.0 - <release title>'`
|
||||
* [ ] `cargo publish` (see below)
|
||||
* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - summary'`
|
||||
* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - <release title>'`
|
||||
* [ ] `git pull --tags ; git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force ; git push --tags`
|
||||
* [ ] merge release PR or push to `master`
|
||||
* [ ] check that CI is green
|
||||
@@ -79,15 +75,17 @@ I usually do this all on the `master` branch, but doing it in a release branch i
|
||||
(cd crates/egui && cargo publish --quiet) && echo "✅ egui"
|
||||
(cd crates/egui-winit && cargo publish --quiet) && echo "✅ egui-winit"
|
||||
(cd crates/egui-wgpu && cargo publish --quiet) && echo "✅ egui-wgpu"
|
||||
(cd crates/eframe && cargo publish --quiet) && echo "✅ eframe"
|
||||
(cd crates/egui_kittest && cargo publish --quiet) && echo "✅ egui_kittest"
|
||||
(cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras"
|
||||
(cd crates/egui_demo_lib && cargo publish --quiet) && echo "✅ egui_demo_lib"
|
||||
(cd crates/egui_glow && cargo publish --quiet) && echo "✅ egui_glow"
|
||||
(cd crates/eframe && cargo publish --quiet) && echo "✅ eframe"
|
||||
```
|
||||
|
||||
\<continue with the checklist above\>
|
||||
|
||||
## Announcements
|
||||
* [ ] [twitter](https://x.com/ernerfeldt/status/1772665412225823105)
|
||||
* [ ] [Bluesky](https://bsky.app/profile/ernerfeldt.bsky.social)
|
||||
* [ ] egui discord
|
||||
* [ ] [r/rust](https://www.reddit.com/r/rust/comments/1bocr5s/announcing_egui_027_with_improved_menus_and/)
|
||||
* [ ] [r/programming](https://www.reddit.com/r/programming/comments/1bocsf6/announcing_egui_027_an_easytouse_crossplatform/)
|
||||
@@ -98,3 +96,5 @@ I usually do this all on the `master` branch, but doing it in a release branch i
|
||||
* [ ] publish new `egui_plot`
|
||||
* [ ] publish new `egui_table`
|
||||
* [ ] publish new `egui_tiles`
|
||||
* [ ] make a PR to `egui_commonmark`
|
||||
* [ ] make a PR to `rerun`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Section identical to scripts/clippy_wasm/clippy.toml:
|
||||
|
||||
msrv = "1.80"
|
||||
msrv = "1.81"
|
||||
|
||||
allow-unwrap-in-tests = true
|
||||
|
||||
|
||||
@@ -6,6 +6,14 @@ 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.31.1 - 2025-03-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04
|
||||
* Add `Color32::CYAN` and `Color32::MAGENTA` [#5663](https://github.com/emilk/egui/pull/5663) by [@juancampa](https://github.com/juancampa)
|
||||
|
||||
|
||||
## 0.30.0 - 2024-12-16
|
||||
* Use boxed slice for lookup table to avoid stack overflow [#5212](https://github.com/emilk/egui/pull/5212) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Add `Color32::mul` [#5437](https://github.com/emilk/egui/pull/5437) by [@emilk](https://github.com/emilk)
|
||||
|
||||
@@ -8,4 +8,6 @@
|
||||
|
||||
A simple color storage and conversion library.
|
||||
|
||||
Made for [`egui`](https://github.com/emilk/egui/).
|
||||
This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/).
|
||||
|
||||
If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead.
|
||||
|
||||
@@ -5,10 +5,24 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
|
||||
/// Instead of manipulating this directly it is often better
|
||||
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
|
||||
///
|
||||
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
|
||||
/// Alpha channel is in linear space.
|
||||
/// Internally this uses 0-255 gamma space `sRGBA` color with _premultiplied alpha_.
|
||||
///
|
||||
/// The special value of alpha=0 means the color is to be treated as an additive color.
|
||||
/// It's the non-linear ("gamma") values that are multiplied with the alpha.
|
||||
///
|
||||
/// Premultiplied alpha means that the color values have been pre-multiplied with the alpha (opacity).
|
||||
/// This is in contrast with "normal" RGBA, where the alpha is _separate_ (or "unmultiplied").
|
||||
/// Using premultiplied alpha has some advantages:
|
||||
/// * It allows encoding additive colors
|
||||
/// * It is the better way to blend colors, e.g. when filtering texture colors
|
||||
/// * Because the above, it is the better way to encode colors in a GPU texture
|
||||
///
|
||||
/// The color space is assumed to be [sRGB](https://en.wikipedia.org/wiki/SRGB).
|
||||
///
|
||||
/// All operations on `Color32` are done in "gamma space" (see <https://en.wikipedia.org/wiki/SRGB>).
|
||||
/// This is not physically correct, but it is fast and sometimes more perceptually even than linear space.
|
||||
/// If you instead want to perform these operations in linear-space color, use [`Rgba`].
|
||||
///
|
||||
/// An `alpha=0` means the color is to be treated as an additive color.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -16,6 +30,7 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
|
||||
pub struct Color32(pub(crate) [u8; 4]);
|
||||
|
||||
impl std::fmt::Debug for Color32 {
|
||||
/// Prints the contents with premultiplied alpha!
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let [r, g, b, a] = self.0;
|
||||
write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
|
||||
@@ -56,7 +71,10 @@ impl Color32 {
|
||||
pub const RED: Self = Self::from_rgb(255, 0, 0);
|
||||
pub const LIGHT_RED: Self = Self::from_rgb(255, 128, 128);
|
||||
|
||||
pub const CYAN: Self = Self::from_rgb(0, 255, 255);
|
||||
pub const MAGENTA: Self = Self::from_rgb(255, 0, 255);
|
||||
pub const YELLOW: Self = Self::from_rgb(255, 255, 0);
|
||||
|
||||
pub const ORANGE: Self = Self::from_rgb(255, 165, 0);
|
||||
pub const LIGHT_YELLOW: Self = Self::from_rgb(255, 255, 0xE0);
|
||||
pub const KHAKI: Self = Self::from_rgb(240, 230, 140);
|
||||
@@ -69,6 +87,8 @@ impl Color32 {
|
||||
pub const BLUE: Self = Self::from_rgb(0, 0, 255);
|
||||
pub const LIGHT_BLUE: Self = Self::from_rgb(0xAD, 0xD8, 0xE6);
|
||||
|
||||
pub const PURPLE: Self = Self::from_rgb(0x80, 0, 0x80);
|
||||
|
||||
pub const GOLD: Self = Self::from_rgb(255, 215, 0);
|
||||
|
||||
pub const DEBUG_COLOR: Self = Self::from_rgba_premultiplied(0, 200, 0, 128);
|
||||
@@ -85,41 +105,49 @@ impl Color32 {
|
||||
#[deprecated = "Renamed to PLACEHOLDER"]
|
||||
pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER;
|
||||
|
||||
/// From RGB with alpha of 255 (opaque).
|
||||
#[inline]
|
||||
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 255])
|
||||
}
|
||||
|
||||
/// From RGB into an additive color (will make everything it blend with brighter).
|
||||
#[inline]
|
||||
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 0])
|
||||
}
|
||||
|
||||
/// From `sRGBA` with premultiplied alpha.
|
||||
///
|
||||
/// You likely want to use [`Self::from_rgba_unmultiplied`] instead.
|
||||
#[inline]
|
||||
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
/// From `sRGBA` WITHOUT premultiplied alpha.
|
||||
/// From `sRGBA` with separate alpha.
|
||||
///
|
||||
/// This is a "normal" RGBA value that you would find in a color picker or a table somewhere.
|
||||
///
|
||||
/// You can use [`Self::to_srgba_unmultiplied`] to get back these values,
|
||||
/// but for transparent colors what you get back might be slightly different (rounding errors).
|
||||
#[inline]
|
||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
use std::sync::OnceLock;
|
||||
match a {
|
||||
// common-case optimization
|
||||
// common-case optimization:
|
||||
0 => Self::TRANSPARENT,
|
||||
// common-case optimization
|
||||
|
||||
// common-case optimization:
|
||||
255 => Self::from_rgb(r, g, b),
|
||||
|
||||
a => {
|
||||
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
|
||||
let lut = LOOKUP_TABLE.get_or_init(|| {
|
||||
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8};
|
||||
(0..=u16::MAX)
|
||||
.map(|i| {
|
||||
let [value, alpha] = i.to_ne_bytes();
|
||||
let value_lin = linear_f32_from_gamma_u8(value);
|
||||
let alpha_lin = linear_f32_from_linear_u8(alpha);
|
||||
gamma_u8_from_linear_f32(value_lin * alpha_lin)
|
||||
fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
@@ -131,22 +159,26 @@ impl Color32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque gray.
|
||||
#[doc(alias = "from_grey")]
|
||||
#[inline]
|
||||
pub const fn from_gray(l: u8) -> Self {
|
||||
Self([l, l, l, 255])
|
||||
}
|
||||
|
||||
/// Black with the given opacity.
|
||||
#[inline]
|
||||
pub const fn from_black_alpha(a: u8) -> Self {
|
||||
Self([0, 0, 0, a])
|
||||
}
|
||||
|
||||
/// White with the given opacity.
|
||||
#[inline]
|
||||
pub fn from_white_alpha(a: u8) -> Self {
|
||||
Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into()
|
||||
Self([a, a, a, a])
|
||||
}
|
||||
|
||||
/// Additive white.
|
||||
#[inline]
|
||||
pub const fn from_additive_luminance(l: u8) -> Self {
|
||||
Self([l, l, l, 0])
|
||||
@@ -157,21 +189,25 @@ impl Color32 {
|
||||
self.a() == 255
|
||||
}
|
||||
|
||||
/// Red component multiplied by alpha.
|
||||
#[inline]
|
||||
pub const fn r(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
/// Green component multiplied by alpha.
|
||||
#[inline]
|
||||
pub const fn g(&self) -> u8 {
|
||||
self.0[1]
|
||||
}
|
||||
|
||||
/// Blue component multiplied by alpha.
|
||||
#[inline]
|
||||
pub const fn b(&self) -> u8 {
|
||||
self.0[2]
|
||||
}
|
||||
|
||||
/// Alpha (opacity).
|
||||
#[inline]
|
||||
pub const fn a(&self) -> u8 {
|
||||
self.0[3]
|
||||
@@ -208,9 +244,26 @@ impl Color32 {
|
||||
(self.r(), self.g(), self.b(), self.a())
|
||||
}
|
||||
|
||||
/// Convert to a normal "unmultiplied" RGBA color (i.e. with separate alpha).
|
||||
///
|
||||
/// This will unmultiply the alpha.
|
||||
///
|
||||
/// This is the inverse of [`Self::from_rgba_unmultiplied`],
|
||||
/// but due to precision problems it may return slightly different values for transparent colors.
|
||||
#[inline]
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
Rgba::from(*self).to_srgba_unmultiplied()
|
||||
let [r, g, b, a] = self.to_array();
|
||||
match a {
|
||||
// Common-case optimization.
|
||||
0 | 255 => self.to_array(),
|
||||
a => {
|
||||
let factor = 255.0 / a as f32;
|
||||
let r = fast_round(factor * r as f32);
|
||||
let g = fast_round(factor * g as f32);
|
||||
let b = fast_round(factor * b as f32);
|
||||
[r, g, b, a]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiply with 0.5 to make color half as opaque, perceptually.
|
||||
@@ -220,7 +273,10 @@ impl Color32 {
|
||||
/// This is perceptually even, and faster that [`Self::linear_multiply`].
|
||||
#[inline]
|
||||
pub fn gamma_multiply(self, factor: f32) -> Self {
|
||||
debug_assert!(0.0 <= factor && factor.is_finite());
|
||||
debug_assert!(
|
||||
0.0 <= factor && factor.is_finite(),
|
||||
"factor should be finite, but was {factor}"
|
||||
);
|
||||
let Self([r, g, b, a]) = self;
|
||||
Self([
|
||||
(r as f32 * factor + 0.5) as u8,
|
||||
@@ -230,13 +286,33 @@ impl Color32 {
|
||||
])
|
||||
}
|
||||
|
||||
/// Multiply with 127 to make color half as opaque, perceptually.
|
||||
///
|
||||
/// Fast multiplication in gamma-space.
|
||||
///
|
||||
/// This is perceptually even, and faster that [`Self::linear_multiply`].
|
||||
#[inline]
|
||||
pub fn gamma_multiply_u8(self, factor: u8) -> Self {
|
||||
let Self([r, g, b, a]) = self;
|
||||
let factor = factor as u32;
|
||||
Self([
|
||||
((r as u32 * factor + 127) / 255) as u8,
|
||||
((g as u32 * factor + 127) / 255) as u8,
|
||||
((b as u32 * factor + 127) / 255) as u8,
|
||||
((a as u32 * factor + 127) / 255) as u8,
|
||||
])
|
||||
}
|
||||
|
||||
/// Multiply with 0.5 to make color half as opaque in linear space.
|
||||
///
|
||||
/// This is using linear space, which is not perceptually even.
|
||||
/// You likely want to use [`Self::gamma_multiply`] instead.
|
||||
#[inline]
|
||||
pub fn linear_multiply(self, factor: f32) -> Self {
|
||||
debug_assert!(0.0 <= factor && factor.is_finite());
|
||||
debug_assert!(
|
||||
0.0 <= factor && factor.is_finite(),
|
||||
"factor should be finite, but was {factor}"
|
||||
);
|
||||
// As an unfortunate side-effect of using premultiplied alpha
|
||||
// we need a somewhat expensive conversion to linear space and back.
|
||||
Rgba::from(self).multiply(factor).into()
|
||||
@@ -268,6 +344,19 @@ impl Color32 {
|
||||
fast_round(lerp((self[3] as f32)..=(other[3] as f32), t)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Blend two colors in gamma space, so that `self` is behind the argument.
|
||||
pub fn blend(self, on_top: Self) -> Self {
|
||||
self.gamma_multiply_u8(255 - on_top.a()) + on_top
|
||||
}
|
||||
|
||||
/// Intensity of the color.
|
||||
///
|
||||
/// Returns a value in the range 0-1.
|
||||
/// The brighter the color, the closer to 1.
|
||||
pub fn intensity(&self) -> f32 {
|
||||
(self.r() as f32 * 0.299 + self.g() as f32 * 0.587 + self.b() as f32 * 0.114) / 255.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for Color32 {
|
||||
@@ -284,3 +373,145 @@ impl std::ops::Mul for Color32 {
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Color32 {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self([
|
||||
self[0].saturating_add(other[0]),
|
||||
self[1].saturating_add(other[1]),
|
||||
self[2].saturating_add(other[2]),
|
||||
self[3].saturating_add(other[3]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn test_rgba() -> impl Iterator<Item = [u8; 4]> {
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 255],
|
||||
[10, 0, 30, 0],
|
||||
[10, 0, 30, 40],
|
||||
[10, 100, 200, 0],
|
||||
[10, 100, 200, 100],
|
||||
[10, 100, 200, 200],
|
||||
[10, 100, 200, 255],
|
||||
[10, 100, 200, 40],
|
||||
[10, 20, 0, 0],
|
||||
[10, 20, 0, 255],
|
||||
[10, 20, 30, 255],
|
||||
[10, 20, 30, 40],
|
||||
[255, 255, 255, 0],
|
||||
[255, 255, 255, 255],
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color32_additive() {
|
||||
let opaque = Color32::from_rgb(40, 50, 60);
|
||||
let additive = Color32::from_rgb(255, 127, 10).additive();
|
||||
assert_eq!(additive.blend(opaque), opaque, "opaque on top of additive");
|
||||
assert_eq!(
|
||||
opaque.blend(additive),
|
||||
Color32::from_rgb(255, 177, 70),
|
||||
"additive on top of opaque"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color32_blend_vs_gamma_blend() {
|
||||
let opaque = Color32::from_rgb(0x60, 0x60, 0x60);
|
||||
let transparent = Color32::from_rgba_unmultiplied(168, 65, 65, 79);
|
||||
assert_eq!(
|
||||
transparent.blend(opaque),
|
||||
opaque,
|
||||
"Opaque on top of transparent"
|
||||
);
|
||||
// Blending in gamma-space is the de-facto standard almost everywhere.
|
||||
// Browsers and most image editors do it, and so it is what users expect.
|
||||
assert_eq!(
|
||||
opaque.blend(transparent),
|
||||
Color32::from_rgb(
|
||||
blend(0x60, 168, 79),
|
||||
blend(0x60, 65, 79),
|
||||
blend(0x60, 65, 79)
|
||||
),
|
||||
"Transparent on top of opaque"
|
||||
);
|
||||
|
||||
fn blend(dest: u8, src: u8, alpha: u8) -> u8 {
|
||||
let src = src as f32 / 255.0;
|
||||
let dest = dest as f32 / 255.0;
|
||||
let alpha = alpha as f32 / 255.0;
|
||||
fast_round((src * alpha + dest * (1.0 - alpha)) * 255.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color32_unmultiplied_round_trip() {
|
||||
for in_rgba in test_rgba() {
|
||||
let [r, g, b, a] = in_rgba;
|
||||
if a == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let c = Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
let out_rgba = c.to_srgba_unmultiplied();
|
||||
|
||||
if a == 255 {
|
||||
assert_eq!(in_rgba, out_rgba);
|
||||
} else {
|
||||
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
||||
// because we multiply and then unmultiply the alpha.
|
||||
for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) {
|
||||
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_black_white_alpha() {
|
||||
for a in 0..=255 {
|
||||
assert_eq!(
|
||||
Color32::from_white_alpha(a),
|
||||
Color32::from_rgba_unmultiplied(255, 255, 255, a)
|
||||
);
|
||||
assert_eq!(
|
||||
Color32::from_white_alpha(a),
|
||||
Color32::WHITE.gamma_multiply_u8(a)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Color32::from_black_alpha(a),
|
||||
Color32::from_rgba_unmultiplied(0, 0, 0, a)
|
||||
);
|
||||
assert_eq!(
|
||||
Color32::from_black_alpha(a),
|
||||
Color32::BLACK.gamma_multiply_u8(a)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_from_rgba() {
|
||||
for [r, g, b, a] in test_rgba() {
|
||||
let original = Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
let rgba = Rgba::from(original);
|
||||
let back = Color32::from(rgba);
|
||||
assert_eq!(back, original);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Color32::from(Rgba::from_rgba_unmultiplied(1.0, 0.0, 0.0, 0.5)),
|
||||
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,14 +90,15 @@ impl HexColor {
|
||||
let [r, gb] = u16::from_str_radix(s, 16)
|
||||
.map_err(ParseHexColorError::InvalidInt)?
|
||||
.to_be_bytes();
|
||||
let [r, g, b] = [r, gb >> 4, gb & 0x0f].map(|u| u << 4 | u);
|
||||
let [r, g, b] = [r, gb >> 4, gb & 0x0f].map(|u| (u << 4) | u);
|
||||
Ok(Self::Hex3(Color32::from_rgb(r, g, b)))
|
||||
}
|
||||
4 => {
|
||||
let [r_g, b_a] = u16::from_str_radix(s, 16)
|
||||
.map_err(ParseHexColorError::InvalidInt)?
|
||||
.to_be_bytes();
|
||||
let [r, g, b, a] = [r_g >> 4, r_g & 0x0f, b_a >> 4, b_a & 0x0f].map(|u| u << 4 | u);
|
||||
let [r, g, b, a] =
|
||||
[r_g >> 4, r_g & 0x0f, b_a >> 4, b_a & 0x0f].map(|u| (u << 4) | u);
|
||||
Ok(Self::Hex4(Color32::from_rgba_unmultiplied(r, g, b, a)))
|
||||
}
|
||||
6 => {
|
||||
@@ -207,17 +208,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hex_string_round_trip() {
|
||||
use Color32 as C;
|
||||
let cases = [
|
||||
C::from_rgba_unmultiplied(10, 20, 30, 0),
|
||||
C::from_rgba_unmultiplied(10, 20, 30, 40),
|
||||
C::from_rgba_unmultiplied(10, 20, 30, 255),
|
||||
C::from_rgba_unmultiplied(0, 20, 30, 0),
|
||||
C::from_rgba_unmultiplied(10, 0, 30, 40),
|
||||
C::from_rgba_unmultiplied(10, 20, 0, 255),
|
||||
[0, 20, 30, 0],
|
||||
[10, 0, 30, 40],
|
||||
[10, 100, 200, 0],
|
||||
[10, 100, 200, 100],
|
||||
[10, 100, 200, 200],
|
||||
[10, 100, 200, 255],
|
||||
[10, 100, 200, 40],
|
||||
[10, 20, 0, 255],
|
||||
[10, 20, 30, 0],
|
||||
[10, 20, 30, 255],
|
||||
[10, 20, 30, 40],
|
||||
];
|
||||
for color in cases {
|
||||
assert_eq!(C::from_hex(color.to_hex().as_str()), Ok(color));
|
||||
for [r, g, b, a] in cases {
|
||||
let color = Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
assert_eq!(Color32::from_hex(color.to_hex().as_str()), Ok(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::{
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
||||
linear_u8_from_linear_f32, Color32, Rgba,
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32, Color32, Rgba,
|
||||
};
|
||||
|
||||
/// Hue, saturation, value, alpha. All in the range [0, 1].
|
||||
@@ -29,30 +28,20 @@ impl Hsva {
|
||||
/// From `sRGBA` with premultiplied alpha
|
||||
#[inline]
|
||||
pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self {
|
||||
Self::from_rgba_premultiplied(
|
||||
linear_f32_from_gamma_u8(r),
|
||||
linear_f32_from_gamma_u8(g),
|
||||
linear_f32_from_gamma_u8(b),
|
||||
linear_f32_from_linear_u8(a),
|
||||
)
|
||||
Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
|
||||
}
|
||||
|
||||
/// From `sRGBA` without premultiplied alpha
|
||||
#[inline]
|
||||
pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self {
|
||||
Self::from_rgba_unmultiplied(
|
||||
linear_f32_from_gamma_u8(r),
|
||||
linear_f32_from_gamma_u8(g),
|
||||
linear_f32_from_gamma_u8(b),
|
||||
linear_f32_from_linear_u8(a),
|
||||
)
|
||||
Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
|
||||
}
|
||||
|
||||
/// From linear RGBA with premultiplied alpha
|
||||
#[inline]
|
||||
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
if a == 0.0 {
|
||||
if a <= 0.0 {
|
||||
if r == 0.0 && b == 0.0 && a == 0.0 {
|
||||
Self::default()
|
||||
} else {
|
||||
@@ -152,13 +141,7 @@ impl Hsva {
|
||||
|
||||
#[inline]
|
||||
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_premultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a),
|
||||
]
|
||||
Color32::from(*self).to_array()
|
||||
}
|
||||
|
||||
/// To gamma-space 0-255.
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
//! Color conversions and types.
|
||||
//!
|
||||
//! This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/).
|
||||
//!
|
||||
//! If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead.
|
||||
//!
|
||||
//! If you want a compact color representation, use [`Color32`].
|
||||
//! If you want to manipulate RGBA colors use [`Rgba`].
|
||||
//! If you want to manipulate RGBA colors in linear space use [`Rgba`].
|
||||
//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
|
||||
//!
|
||||
//! ## Conventions
|
||||
//! The word "gamma" or "srgb" is used to refer to values in the non-linear space defined by
|
||||
//! [the sRGB transfer function](https://en.wikipedia.org/wiki/SRGB).
|
||||
//! We use `u8` for anything in the "gamma" space.
|
||||
//!
|
||||
//! We use `f32` in 0-1 range for anything in the linear space.
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
@@ -39,23 +50,46 @@ pub use hex_color_runtime::*;
|
||||
|
||||
impl From<Color32> for Rgba {
|
||||
fn from(srgba: Color32) -> Self {
|
||||
Self([
|
||||
linear_f32_from_gamma_u8(srgba.0[0]),
|
||||
linear_f32_from_gamma_u8(srgba.0[1]),
|
||||
linear_f32_from_gamma_u8(srgba.0[2]),
|
||||
linear_f32_from_linear_u8(srgba.0[3]),
|
||||
])
|
||||
let [r, g, b, a] = srgba.to_array();
|
||||
if a == 0 {
|
||||
// Additive, or completely transparent
|
||||
Self([
|
||||
linear_f32_from_gamma_u8(r),
|
||||
linear_f32_from_gamma_u8(g),
|
||||
linear_f32_from_gamma_u8(b),
|
||||
0.0,
|
||||
])
|
||||
} else {
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self([
|
||||
linear_from_gamma(r as f32 / (255.0 * a)) * a,
|
||||
linear_from_gamma(g as f32 / (255.0 * a)) * a,
|
||||
linear_from_gamma(b as f32 / (255.0 * a)) * a,
|
||||
a,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Color32 {
|
||||
fn from(rgba: Rgba) -> Self {
|
||||
Self([
|
||||
gamma_u8_from_linear_f32(rgba.0[0]),
|
||||
gamma_u8_from_linear_f32(rgba.0[1]),
|
||||
gamma_u8_from_linear_f32(rgba.0[2]),
|
||||
linear_u8_from_linear_f32(rgba.0[3]),
|
||||
])
|
||||
let [r, g, b, a] = rgba.to_array();
|
||||
if a == 0.0 {
|
||||
// Additive, or completely transparent
|
||||
Self([
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
0,
|
||||
])
|
||||
} else {
|
||||
Self([
|
||||
fast_round(gamma_u8_from_linear_f32(r / a) as f32 * a),
|
||||
fast_round(gamma_u8_from_linear_f32(g / a) as f32 * a),
|
||||
fast_round(gamma_u8_from_linear_f32(b / a) as f32 * a),
|
||||
linear_u8_from_linear_f32(a),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::{
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
||||
linear_u8_from_linear_f32,
|
||||
};
|
||||
use crate::Color32;
|
||||
|
||||
/// 0-1 linear space `RGBA` color with premultiplied alpha.
|
||||
///
|
||||
/// See [`crate::Color32`] for explanation of what "premultiplied alpha" means.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -70,20 +69,12 @@ impl Rgba {
|
||||
|
||||
#[inline]
|
||||
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
let r = linear_f32_from_gamma_u8(r);
|
||||
let g = linear_f32_from_gamma_u8(g);
|
||||
let b = linear_f32_from_gamma_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
let r = linear_f32_from_gamma_u8(r);
|
||||
let g = linear_f32_from_gamma_u8(g);
|
||||
let b = linear_f32_from_gamma_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self::from_rgba_premultiplied(r * a, g * a, b * a, a)
|
||||
Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -99,15 +90,24 @@ impl Rgba {
|
||||
|
||||
#[inline]
|
||||
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
|
||||
debug_assert!(0.0 <= l && l <= 1.0);
|
||||
debug_assert!(0.0 <= a && a <= 1.0);
|
||||
debug_assert!(
|
||||
0.0 <= l && l <= 1.0,
|
||||
"l should be in the range [0, 1], but was {l}"
|
||||
);
|
||||
debug_assert!(
|
||||
0.0 <= a && a <= 1.0,
|
||||
"a should be in the range [0, 1], but was {a}"
|
||||
);
|
||||
Self([l * a, l * a, l * a, a])
|
||||
}
|
||||
|
||||
/// Transparent black
|
||||
#[inline]
|
||||
pub fn from_black_alpha(a: f32) -> Self {
|
||||
debug_assert!(0.0 <= a && a <= 1.0);
|
||||
debug_assert!(
|
||||
0.0 <= a && a <= 1.0,
|
||||
"a should be in the range [0, 1], but was {a}"
|
||||
);
|
||||
Self([0.0, 0.0, 0.0, a])
|
||||
}
|
||||
|
||||
@@ -211,13 +211,12 @@ impl Rgba {
|
||||
/// unmultiply the alpha
|
||||
#[inline]
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a.abs()),
|
||||
]
|
||||
crate::Color32::from(*self).to_srgba_unmultiplied()
|
||||
}
|
||||
|
||||
/// Blend two colors in linear space, so that `self` is behind the argument.
|
||||
pub fn blend(self, on_top: Self) -> Self {
|
||||
self.multiply(1.0 - on_top.a()) + on_top
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,3 +275,72 @@ impl std::ops::Mul<Rgba> for f32 {
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_rgba() -> impl Iterator<Item = [u8; 4]> {
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 255],
|
||||
[10, 0, 30, 0],
|
||||
[10, 0, 30, 40],
|
||||
[10, 100, 200, 0],
|
||||
[10, 100, 200, 100],
|
||||
[10, 100, 200, 200],
|
||||
[10, 100, 200, 255],
|
||||
[10, 100, 200, 40],
|
||||
[10, 20, 0, 0],
|
||||
[10, 20, 0, 255],
|
||||
[10, 20, 30, 255],
|
||||
[10, 20, 30, 40],
|
||||
[255, 255, 255, 0],
|
||||
[255, 255, 255, 255],
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgba_blend() {
|
||||
let opaque = Rgba::from_rgb(0.4, 0.5, 0.6);
|
||||
let transparent = Rgba::from_rgb(1.0, 0.5, 0.0).multiply(0.3);
|
||||
assert_eq!(
|
||||
transparent.blend(opaque),
|
||||
opaque,
|
||||
"Opaque on top of transparent"
|
||||
);
|
||||
assert_eq!(
|
||||
opaque.blend(transparent),
|
||||
Rgba::from_rgb(
|
||||
0.7 * 0.4 + 0.3 * 1.0,
|
||||
0.7 * 0.5 + 0.3 * 0.5,
|
||||
0.7 * 0.6 + 0.3 * 0.0
|
||||
),
|
||||
"Transparent on top of opaque"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgba_roundtrip() {
|
||||
for in_rgba in test_rgba() {
|
||||
let [r, g, b, a] = in_rgba;
|
||||
if a == 0 {
|
||||
continue;
|
||||
}
|
||||
let rgba = Rgba::from_srgba_unmultiplied(r, g, b, a);
|
||||
let out_rgba = rgba.to_srgba_unmultiplied();
|
||||
|
||||
if a == 255 {
|
||||
assert_eq!(in_rgba, out_rgba);
|
||||
} else {
|
||||
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
||||
// because we multiply and then unmultiply the alpha.
|
||||
for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) {
|
||||
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
# Changelog for eframe
|
||||
All notable changes to the `eframe` crate.
|
||||
|
||||
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs!
|
||||
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs!
|
||||
|
||||
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.31.1 - 2025-03-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04
|
||||
* Web: Fix incorrect scale when moving to screen with new DPI [#5631](https://github.com/emilk/egui/pull/5631) by [@emilk](https://github.com/emilk)
|
||||
* Re-enable IME support on Linux [#5198](https://github.com/emilk/egui/pull/5198) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Serialize window maximized state in `WindowSettings` [#5554](https://github.com/emilk/egui/pull/5554) by [@landaire](https://github.com/landaire)
|
||||
* Save state on suspend on Android and iOS [#5601](https://github.com/emilk/egui/pull/5601) by [@Pandicon](https://github.com/Pandicon)
|
||||
* Eframe web: forward cmd-S/O to egui app (stop default browser action) [#5655](https://github.com/emilk/egui/pull/5655) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.30.0 - 2024-12-16 - Android support
|
||||
NOTE: you now need to enable the `wayland` or `x11` features to get Linux support, including getting it to work on most CI systems.
|
||||
|
||||
|
||||
@@ -177,12 +177,14 @@ wgpu = { workspace = true, optional = true, features = [
|
||||
# mac:
|
||||
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
||||
objc2 = "0.5.1"
|
||||
objc2-foundation = { version = "0.2.0", features = [
|
||||
objc2-foundation = { version = "0.2.0", default-features = false, features = [
|
||||
"std",
|
||||
"block2",
|
||||
"NSData",
|
||||
"NSString",
|
||||
] }
|
||||
objc2-app-kit = { version = "0.2.0", features = [
|
||||
objc2-app-kit = { version = "0.2.0", default-features = false, features = [
|
||||
"std",
|
||||
"NSApplication",
|
||||
"NSImage",
|
||||
"NSMenu",
|
||||
@@ -209,6 +211,7 @@ percent-encoding = "2.1"
|
||||
wasm-bindgen.workspace = true
|
||||
wasm-bindgen-futures.workspace = true
|
||||
web-sys = { workspace = true, features = [
|
||||
"AddEventListenerOptions",
|
||||
"BinaryType",
|
||||
"Blob",
|
||||
"BlobPropertyBag",
|
||||
@@ -250,6 +253,7 @@ web-sys = { workspace = true, features = [
|
||||
"ResizeObserverEntry",
|
||||
"ResizeObserverOptions",
|
||||
"ResizeObserverSize",
|
||||
"ShadowRoot",
|
||||
"Storage",
|
||||
"Touch",
|
||||
"TouchEvent",
|
||||
|
||||
@@ -878,20 +878,6 @@ pub trait Storage {
|
||||
fn flush(&mut self);
|
||||
}
|
||||
|
||||
/// Stores nothing.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct DummyStorage {}
|
||||
|
||||
impl Storage for DummyStorage {
|
||||
fn get_string(&self, _key: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_string(&mut self, _key: &str, _value: String) {}
|
||||
|
||||
fn flush(&mut self) {}
|
||||
}
|
||||
|
||||
/// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key.
|
||||
#[cfg(feature = "ron")]
|
||||
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
|
||||
|
||||
@@ -58,27 +58,34 @@ fn roaming_appdata() -> Option<PathBuf> {
|
||||
extern "C" {
|
||||
fn wcslen(buf: *const u16) -> usize;
|
||||
}
|
||||
unsafe {
|
||||
let mut path = ptr::null_mut();
|
||||
match SHGetKnownFolderPath(
|
||||
let mut path_raw = ptr::null_mut();
|
||||
|
||||
// SAFETY: SHGetKnownFolderPath allocates for us, we don't pass any pointers to it.
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
|
||||
let result = unsafe {
|
||||
SHGetKnownFolderPath(
|
||||
&FOLDERID_RoamingAppData,
|
||||
KF_FLAG_DONT_VERIFY as u32,
|
||||
std::ptr::null_mut(),
|
||||
&mut path,
|
||||
) {
|
||||
S_OK => {
|
||||
let path_slice = slice::from_raw_parts(path, wcslen(path));
|
||||
let s = OsString::from_wide(&path_slice);
|
||||
CoTaskMemFree(path.cast());
|
||||
Some(PathBuf::from(s))
|
||||
}
|
||||
_ => {
|
||||
// Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`.
|
||||
CoTaskMemFree(path.cast());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
&mut path_raw,
|
||||
)
|
||||
};
|
||||
|
||||
let path = if result == S_OK {
|
||||
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us.
|
||||
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
|
||||
Some(PathBuf::from(OsString::from_wide(path_slice)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// SAFETY:
|
||||
// This memory got allocated by SHGetKnownFolderPath, we didn't touch anything in the process.
|
||||
// A null ptr is a no-op for `CoTaskMemFree`, so in case this failed we're still good.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cotaskmemfree
|
||||
unsafe { CoTaskMemFree(path_raw.cast()) };
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
#[cfg(any(not(windows), target_vendor = "uwp"))]
|
||||
@@ -89,7 +96,7 @@ fn roaming_appdata() -> Option<PathBuf> {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk.
|
||||
/// Used to restore egui state, glium window position/size and app state.
|
||||
/// Used to restore egui state, glow window position/size and app state.
|
||||
pub struct FileStorage {
|
||||
ron_filepath: PathBuf,
|
||||
kv: HashMap<String, String>,
|
||||
|
||||
@@ -272,7 +272,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
..
|
||||
} = viewport
|
||||
{
|
||||
egui_winit.init_accesskit(window, event_loop_proxy);
|
||||
egui_winit.init_accesskit(event_loop, window, event_loop_proxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> WinitApp for GlowWinitApp<'app> {
|
||||
impl WinitApp for GlowWinitApp<'_> {
|
||||
fn egui_ctx(&self) -> Option<&egui::Context> {
|
||||
self.running.as_ref().map(|r| &r.integration.egui_ctx)
|
||||
}
|
||||
@@ -366,6 +366,20 @@ impl<'app> WinitApp for GlowWinitApp<'app> {
|
||||
.and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied())
|
||||
}
|
||||
|
||||
fn save(&mut self) {
|
||||
log::debug!("WinitApp::save called");
|
||||
if let Some(running) = self.running.as_mut() {
|
||||
profiling::function_scope!();
|
||||
|
||||
// This is used because of the "save on suspend" logic on Android. Once the application is suspended, there is no window associated to it, which was causing panics when `.window().expect()` was used.
|
||||
let window_opt = running.glutin.borrow().window_opt(ViewportId::ROOT);
|
||||
|
||||
running
|
||||
.integration
|
||||
.save(running.app.as_mut(), window_opt.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
fn save_and_destroy(&mut self) {
|
||||
if let Some(mut running) = self.running.take() {
|
||||
profiling::function_scope!();
|
||||
@@ -413,7 +427,7 @@ impl<'app> WinitApp for GlowWinitApp<'app> {
|
||||
if let Some(running) = &mut self.running {
|
||||
running.glutin.borrow_mut().on_suspend()?;
|
||||
}
|
||||
Ok(EventResult::Wait)
|
||||
Ok(EventResult::Save)
|
||||
}
|
||||
|
||||
fn device_event(
|
||||
@@ -479,7 +493,7 @@ impl<'app> WinitApp for GlowWinitApp<'app> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> GlowWinitRunning<'app> {
|
||||
impl GlowWinitRunning<'_> {
|
||||
fn run_ui_and_paint(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
@@ -1214,10 +1228,12 @@ impl GlutinWindowContext {
|
||||
.expect("viewport doesn't exist")
|
||||
}
|
||||
|
||||
fn window_opt(&self, viewport_id: ViewportId) -> Option<Arc<Window>> {
|
||||
self.viewport(viewport_id).window.clone()
|
||||
}
|
||||
|
||||
fn window(&self, viewport_id: ViewportId) -> Arc<Window> {
|
||||
self.viewport(viewport_id)
|
||||
.window
|
||||
.clone()
|
||||
self.window_opt(viewport_id)
|
||||
.expect("winit window doesn't exist")
|
||||
}
|
||||
|
||||
|
||||
@@ -89,47 +89,56 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
event_result: Result<EventResult>,
|
||||
) {
|
||||
let mut exit = false;
|
||||
let mut save = false;
|
||||
|
||||
log::trace!("event_result: {event_result:?}");
|
||||
|
||||
let combined_result = event_result.and_then(|event_result| {
|
||||
match event_result {
|
||||
EventResult::Wait => {
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::RepaintNow(window_id) => {
|
||||
log::trace!("RepaintNow of {window_id:?}",);
|
||||
let mut event_result = event_result;
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
||||
self.winit_app.run_ui_and_paint(event_loop, window_id)
|
||||
} else {
|
||||
// Fix for https://github.com/emilk/egui/issues/2425
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
Ok(event_result)
|
||||
}
|
||||
}
|
||||
EventResult::RepaintNext(window_id) => {
|
||||
log::trace!("RepaintNext of {window_id:?}",);
|
||||
if cfg!(target_os = "windows") {
|
||||
if let Ok(EventResult::RepaintNow(window_id)) = event_result {
|
||||
log::trace!("RepaintNow of {window_id:?}");
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
|
||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
||||
event_result = self.winit_app.run_ui_and_paint(event_loop, window_id);
|
||||
}
|
||||
}
|
||||
|
||||
let combined_result = event_result.map(|event_result| match event_result {
|
||||
EventResult::Wait => {
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintNow(window_id) => {
|
||||
log::trace!("RepaintNow of {window_id:?}",);
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintNext(window_id) => {
|
||||
log::trace!("RepaintNext of {window_id:?}",);
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintAt(window_id, repaint_time) => {
|
||||
self.windows_next_repaint_times.insert(
|
||||
window_id,
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::RepaintAt(window_id, repaint_time) => {
|
||||
self.windows_next_repaint_times.insert(
|
||||
window_id,
|
||||
self.windows_next_repaint_times
|
||||
.get(&window_id)
|
||||
.map_or(repaint_time, |last| (*last).min(repaint_time)),
|
||||
);
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::Exit => {
|
||||
exit = true;
|
||||
Ok(event_result)
|
||||
}
|
||||
.get(&window_id)
|
||||
.map_or(repaint_time, |last| (*last).min(repaint_time)),
|
||||
);
|
||||
event_result
|
||||
}
|
||||
EventResult::Save => {
|
||||
save = true;
|
||||
event_result
|
||||
}
|
||||
EventResult::Exit => {
|
||||
exit = true;
|
||||
event_result
|
||||
}
|
||||
});
|
||||
|
||||
@@ -139,6 +148,11 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
self.return_result = Err(err);
|
||||
};
|
||||
|
||||
if save {
|
||||
log::debug!("Received an EventResult::Save - saving app state");
|
||||
self.winit_app.save();
|
||||
}
|
||||
|
||||
if exit {
|
||||
if self.run_and_return {
|
||||
log::debug!("Asking to exit event loop…");
|
||||
|
||||
@@ -183,7 +183,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
|
||||
profiling::function_scope!();
|
||||
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
||||
let mut painter = egui_wgpu::winit::Painter::new(
|
||||
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
|
||||
egui_ctx.clone(),
|
||||
self.native_options.wgpu_options.clone(),
|
||||
self.native_options.multisampling.max(1) as _,
|
||||
@@ -193,7 +193,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
),
|
||||
self.native_options.viewport.transparent.unwrap_or(false),
|
||||
self.native_options.dithering,
|
||||
);
|
||||
));
|
||||
|
||||
let window = Arc::new(window);
|
||||
|
||||
@@ -249,7 +249,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
let event_loop_proxy = self.repaint_proxy.lock().clone();
|
||||
egui_winit.init_accesskit(&window, event_loop_proxy);
|
||||
egui_winit.init_accesskit(event_loop, &window, event_loop_proxy);
|
||||
}
|
||||
|
||||
let app_creator = std::mem::take(&mut self.app_creator)
|
||||
@@ -323,7 +323,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> WinitApp for WgpuWinitApp<'app> {
|
||||
impl WinitApp for WgpuWinitApp<'_> {
|
||||
fn egui_ctx(&self) -> Option<&egui::Context> {
|
||||
self.running.as_ref().map(|r| &r.integration.egui_ctx)
|
||||
}
|
||||
@@ -355,6 +355,13 @@ impl<'app> WinitApp for WgpuWinitApp<'app> {
|
||||
)
|
||||
}
|
||||
|
||||
fn save(&mut self) {
|
||||
log::debug!("WinitApp::save called");
|
||||
if let Some(running) = self.running.as_mut() {
|
||||
running.save();
|
||||
}
|
||||
}
|
||||
|
||||
fn save_and_destroy(&mut self) {
|
||||
if let Some(mut running) = self.running.take() {
|
||||
running.save_and_destroy();
|
||||
@@ -415,7 +422,7 @@ impl<'app> WinitApp for WgpuWinitApp<'app> {
|
||||
fn suspended(&mut self, _: &ActiveEventLoop) -> crate::Result<EventResult> {
|
||||
#[cfg(target_os = "android")]
|
||||
self.drop_window()?;
|
||||
Ok(EventResult::Wait)
|
||||
Ok(EventResult::Save)
|
||||
}
|
||||
|
||||
fn device_event(
|
||||
@@ -487,14 +494,24 @@ impl<'app> WinitApp for WgpuWinitApp<'app> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> WgpuWinitRunning<'app> {
|
||||
impl WgpuWinitRunning<'_> {
|
||||
/// Saves the application state
|
||||
fn save(&mut self) {
|
||||
let shared = self.shared.borrow();
|
||||
// This is done because of the "save on suspend" logic on Android. Once the application is suspended, there is no window associated to it.
|
||||
let window = if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT)
|
||||
{
|
||||
window.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.integration.save(self.app.as_mut(), window);
|
||||
}
|
||||
|
||||
fn save_and_destroy(&mut self) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let mut shared = self.shared.borrow_mut();
|
||||
if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) {
|
||||
self.integration.save(self.app.as_mut(), window.as_deref());
|
||||
}
|
||||
self.save();
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
self.app.on_exit(None);
|
||||
@@ -502,6 +519,7 @@ impl<'app> WgpuWinitRunning<'app> {
|
||||
#[cfg(not(feature = "glow"))]
|
||||
self.app.on_exit();
|
||||
|
||||
let mut shared = self.shared.borrow_mut();
|
||||
shared.painter.destroy();
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ pub trait WinitApp {
|
||||
|
||||
fn window_id_from_viewport_id(&self, id: ViewportId) -> Option<WindowId>;
|
||||
|
||||
fn save(&mut self);
|
||||
|
||||
fn save_and_destroy(&mut self);
|
||||
|
||||
fn run_ui_and_paint(
|
||||
@@ -119,6 +121,9 @@ pub enum EventResult {
|
||||
|
||||
RepaintAt(WindowId, Instant),
|
||||
|
||||
/// Causes a save of the client state when the persistence feature is enabled.
|
||||
Save,
|
||||
|
||||
Exit,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ impl Stopwatch {
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
assert!(self.start.is_none());
|
||||
assert!(self.start.is_none(), "Stopwatch already running");
|
||||
self.start = Some(Instant::now());
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Stopwatch {
|
||||
}
|
||||
|
||||
pub fn resume(&mut self) {
|
||||
assert!(self.start.is_none());
|
||||
assert!(self.start.is_none(), "Stopwatch still running");
|
||||
self.start = Some(Instant::now());
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ impl AppRunner {
|
||||
|
||||
egui_ctx.options_mut(|o| {
|
||||
// On web by default egui follows the zoom factor of the browser,
|
||||
// and lets the browser handle the zoom shortscuts.
|
||||
// and lets the browser handle the zoom shortcuts.
|
||||
// A user can still zoom egui separately by calling [`egui::Context::set_zoom_factor`].
|
||||
o.zoom_with_keyboard = false;
|
||||
o.zoom_factor = 1.0;
|
||||
@@ -216,6 +216,18 @@ impl AppRunner {
|
||||
let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx());
|
||||
let mut raw_input = self.input.new_frame(canvas_size);
|
||||
|
||||
if super::DEBUG_RESIZE {
|
||||
log::info!(
|
||||
"egui running at canvas size: {}x{}, DPR: {}, zoom_factor: {}. egui size: {}x{} points",
|
||||
self.canvas().width(),
|
||||
self.canvas().height(),
|
||||
super::native_pixels_per_point(),
|
||||
self.egui_ctx.zoom_factor(),
|
||||
canvas_size.x,
|
||||
canvas_size.y,
|
||||
);
|
||||
}
|
||||
|
||||
self.app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
||||
|
||||
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||
@@ -324,6 +336,9 @@ impl AppRunner {
|
||||
egui::OutputCommand::OpenUrl(open_url) => {
|
||||
super::open_url(&open_url.url, open_url.new_tab);
|
||||
}
|
||||
egui::OutputCommand::SetPointerPosition(_) => {
|
||||
// Not supported on the web.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use crate::web::string_from_js_value;
|
||||
|
||||
use super::{
|
||||
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
|
||||
modifiers_from_wheel_event, pos_from_mouse_event, prefers_color_scheme_dark, primary_touch_pos,
|
||||
push_touches, text_from_keyboard_event, theme_from_dark_mode, translate_key, AppRunner,
|
||||
Closure, JsCast, JsValue, WebRunner,
|
||||
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
|
||||
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
|
||||
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast, JsValue, WebRunner,
|
||||
DEBUG_RESIZE,
|
||||
};
|
||||
use web_sys::EventTarget;
|
||||
|
||||
use web_sys::{Document, EventTarget, ShadowRoot};
|
||||
|
||||
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
|
||||
// than what is probably needed.
|
||||
@@ -215,9 +219,16 @@ fn should_prevent_default_for_key(
|
||||
// * cmd-shift-C (debug tools)
|
||||
// * cmd/ctrl-c/v/x (lest we prevent copy/paste/cut events)
|
||||
|
||||
// Prevent ctrl-P from opening the print dialog. Users may want to use it for a command palette.
|
||||
if egui_key == egui::Key::P && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) {
|
||||
return true;
|
||||
// Prevent cmd/ctrl plus these keys from triggering the default browser action:
|
||||
let keys = [
|
||||
egui::Key::O, // open
|
||||
egui::Key::P, // print (cmd-P is common for command palette)
|
||||
egui::Key::S, // save
|
||||
];
|
||||
for key in keys {
|
||||
if egui_key == key && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if egui_key == egui::Key::Space && !runner.text_agent.has_focus() {
|
||||
@@ -363,10 +374,17 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result
|
||||
runner.save();
|
||||
})?;
|
||||
|
||||
// NOTE: resize is handled by `ResizeObserver` below
|
||||
// We want to handle the case of dragging the browser from one monitor to another,
|
||||
// which can cause the DPR to change without any resize event (e.g. Safari).
|
||||
install_dpr_change_event(runner_ref)?;
|
||||
|
||||
// No need to subscribe to "resize": we already subscribe to the canvas
|
||||
// size using a ResizeObserver, and we also subscribe to DPR changes of the monitor.
|
||||
for event_name in &["load", "pagehide", "pageshow"] {
|
||||
runner_ref.add_event_listener(window, event_name, move |_: web_sys::Event, runner| {
|
||||
// log::debug!("{event_name:?}");
|
||||
if DEBUG_RESIZE {
|
||||
log::debug!("{event_name:?}");
|
||||
}
|
||||
runner.needs_repaint.repaint_asap();
|
||||
})?;
|
||||
}
|
||||
@@ -380,6 +398,48 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_dpr_change_event(web_runner: &WebRunner) -> Result<(), JsValue> {
|
||||
let original_dpr = native_pixels_per_point();
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let Some(media_query_list) =
|
||||
window.match_media(&format!("(resolution: {original_dpr}dppx)"))?
|
||||
else {
|
||||
log::error!(
|
||||
"Failed to create MediaQueryList: eframe won't be able to detect changes in DPR"
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let closure = move |_: web_sys::Event, app_runner: &mut AppRunner, web_runner: &WebRunner| {
|
||||
let new_dpr = native_pixels_per_point();
|
||||
log::debug!("Device Pixel Ratio changed from {original_dpr} to {new_dpr}");
|
||||
|
||||
if true {
|
||||
// Explicitly resize canvas to match the new DPR.
|
||||
// This is a bit ugly, but I haven't found a better way to do it.
|
||||
let canvas = app_runner.canvas();
|
||||
canvas.set_width((canvas.width() as f32 * new_dpr / original_dpr).round() as _);
|
||||
canvas.set_height((canvas.height() as f32 * new_dpr / original_dpr).round() as _);
|
||||
log::debug!("Resized canvas to {}x{}", canvas.width(), canvas.height());
|
||||
}
|
||||
|
||||
// It may be tempting to call `resize_observer.observe(&canvas)` here,
|
||||
// but unfortunately this has no effect.
|
||||
|
||||
if let Err(err) = install_dpr_change_event(web_runner) {
|
||||
log::error!(
|
||||
"Failed to install DPR change event: {}",
|
||||
string_from_js_value(&err)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let options = web_sys::AddEventListenerOptions::default();
|
||||
options.set_once(true);
|
||||
web_runner.add_event_listener_ex(&media_query_list, "change", &options, closure)
|
||||
}
|
||||
|
||||
fn install_color_scheme_change_event(
|
||||
runner_ref: &WebRunner,
|
||||
window: &web_sys::Window,
|
||||
@@ -510,10 +570,17 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
/// Returns true if the cursor is above the canvas, or if we're dragging something.
|
||||
/// Pass in the position in browser viewport coordinates (usually event.clientX/Y).
|
||||
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let is_hovering_canvas = document
|
||||
.element_from_point(pos.x, pos.y)
|
||||
.is_some_and(|element| element.eq(runner.canvas()));
|
||||
let root_node = runner.canvas().get_root_node();
|
||||
|
||||
let element_at_point = if let Some(document) = root_node.dyn_ref::<Document>() {
|
||||
document.element_from_point(pos.x, pos.y)
|
||||
} else if let Some(shadow) = root_node.dyn_ref::<ShadowRoot>() {
|
||||
shadow.element_from_point(pos.x, pos.y)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let is_hovering_canvas = element_at_point.is_some_and(|element| element.eq(runner.canvas()));
|
||||
let is_pointer_down = runner
|
||||
.egui_ctx()
|
||||
.input(|i| i.pointer.any_down() || i.any_touches());
|
||||
@@ -813,53 +880,81 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a `ResizeObserver` to observe changes to the size of the canvas.
|
||||
///
|
||||
/// This is the only way to ensure a canvas size change without an associated window `resize` event
|
||||
/// actually results in a resize of the canvas.
|
||||
/// A `ResizeObserver` is used to observe changes to the size of the canvas.
|
||||
///
|
||||
/// The resize observer is called the by the browser at `observe` time, instead of just on the first actual resize.
|
||||
/// We use that to trigger the first `request_animation_frame` _after_ updating the size of the canvas to the correct dimensions,
|
||||
/// to avoid [#4622](https://github.com/emilk/egui/issues/4622).
|
||||
pub(crate) fn install_resize_observer(runner_ref: &WebRunner) -> Result<(), JsValue> {
|
||||
let closure = Closure::wrap(Box::new({
|
||||
let runner_ref = runner_ref.clone();
|
||||
move |entries: js_sys::Array| {
|
||||
// Only call the wrapped closure if the egui code has not panicked
|
||||
if let Some(mut runner_lock) = runner_ref.try_lock() {
|
||||
let canvas = runner_lock.canvas();
|
||||
let (width, height) = match get_display_size(&entries) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("{}", super::string_from_js_value(&err));
|
||||
return;
|
||||
pub struct ResizeObserverContext {
|
||||
observer: web_sys::ResizeObserver,
|
||||
|
||||
// Kept so it is not dropped until we are done with it.
|
||||
_closure: Closure<dyn FnMut(js_sys::Array)>,
|
||||
}
|
||||
|
||||
impl Drop for ResizeObserverContext {
|
||||
fn drop(&mut self) {
|
||||
self.observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
impl ResizeObserverContext {
|
||||
pub fn new(runner_ref: &WebRunner) -> Result<Self, JsValue> {
|
||||
let closure = Closure::wrap(Box::new({
|
||||
let runner_ref = runner_ref.clone();
|
||||
move |entries: js_sys::Array| {
|
||||
if DEBUG_RESIZE {
|
||||
log::info!("ResizeObserverContext callback");
|
||||
}
|
||||
// Only call the wrapped closure if the egui code has not panicked
|
||||
if let Some(mut runner_lock) = runner_ref.try_lock() {
|
||||
let canvas = runner_lock.canvas();
|
||||
let (width, height) = match get_display_size(&entries) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("{}", super::string_from_js_value(&err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if DEBUG_RESIZE {
|
||||
log::info!(
|
||||
"ResizeObserver: new canvas size: {width}x{height}, DPR: {}",
|
||||
web_sys::window().unwrap().device_pixel_ratio()
|
||||
);
|
||||
}
|
||||
};
|
||||
canvas.set_width(width);
|
||||
canvas.set_height(height);
|
||||
canvas.set_width(width);
|
||||
canvas.set_height(height);
|
||||
|
||||
// force an immediate repaint
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
paint_if_needed(&mut runner_lock);
|
||||
drop(runner_lock);
|
||||
// we rely on the resize observer to trigger the first `request_animation_frame`:
|
||||
if let Err(err) = runner_ref.request_animation_frame() {
|
||||
log::error!("{}", super::string_from_js_value(&err));
|
||||
};
|
||||
// force an immediate repaint
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
paint_if_needed(&mut runner_lock);
|
||||
drop(runner_lock);
|
||||
// we rely on the resize observer to trigger the first `request_animation_frame`:
|
||||
if let Err(err) = runner_ref.request_animation_frame() {
|
||||
log::error!("{}", super::string_from_js_value(&err));
|
||||
};
|
||||
} else {
|
||||
log::warn!("ResizeObserverContext callback: failed to lock runner");
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut(js_sys::Array)>);
|
||||
}) as Box<dyn FnMut(js_sys::Array)>);
|
||||
|
||||
let observer = web_sys::ResizeObserver::new(closure.as_ref().unchecked_ref())?;
|
||||
let options = web_sys::ResizeObserverOptions::new();
|
||||
options.set_box(web_sys::ResizeObserverBoxOptions::ContentBox);
|
||||
if let Some(runner_lock) = runner_ref.try_lock() {
|
||||
observer.observe_with_options(runner_lock.canvas(), &options);
|
||||
drop(runner_lock);
|
||||
runner_ref.set_resize_observer(observer, closure);
|
||||
let observer = web_sys::ResizeObserver::new(closure.as_ref().unchecked_ref())?;
|
||||
|
||||
Ok(Self {
|
||||
observer,
|
||||
_closure: closure,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub fn observe(&self, canvas: &web_sys::HtmlCanvasElement) {
|
||||
if DEBUG_RESIZE {
|
||||
log::info!("Calling observe on canvas…");
|
||||
}
|
||||
let options = web_sys::ResizeObserverOptions::new();
|
||||
options.set_box(web_sys::ResizeObserverBoxOptions::ContentBox);
|
||||
self.observer.observe_with_options(canvas, &options);
|
||||
}
|
||||
}
|
||||
|
||||
// Code ported to Rust from:
|
||||
@@ -878,6 +973,10 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32
|
||||
width = size.inline_size();
|
||||
height = size.block_size();
|
||||
dpr = 1.0; // no need to apply
|
||||
|
||||
if DEBUG_RESIZE {
|
||||
// log::info!("devicePixelContentBoxSize {width}x{height}");
|
||||
}
|
||||
} 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);
|
||||
@@ -892,6 +991,9 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32
|
||||
width = size.inline_size();
|
||||
height = size.block_size();
|
||||
}
|
||||
if DEBUG_RESIZE {
|
||||
log::info!("contentBoxSize {width}x{height}");
|
||||
}
|
||||
} else {
|
||||
// legacy
|
||||
let content_rect = entry.content_rect();
|
||||
|
||||
@@ -41,7 +41,7 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
||||
pub use backend::*;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::MediaQueryList;
|
||||
use web_sys::{Document, MediaQueryList, Node};
|
||||
|
||||
use input::{
|
||||
button_from_mouse_event, modifiers_from_kb_event, modifiers_from_mouse_event,
|
||||
@@ -51,6 +51,9 @@ use input::{
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Debug browser resizing?
|
||||
const DEBUG_RESIZE: bool = false;
|
||||
|
||||
pub(crate) fn string_from_js_value(value: &JsValue) -> String {
|
||||
value.as_string().unwrap_or_else(|| format!("{value:#?}"))
|
||||
}
|
||||
@@ -61,18 +64,22 @@ pub(crate) fn string_from_js_value(value: &JsValue) -> String {
|
||||
/// - `<a>`/`<area>` with an `href` attribute
|
||||
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
|
||||
/// - any other element with a `tabindex` attribute
|
||||
pub(crate) fn focused_element() -> Option<web_sys::Element> {
|
||||
web_sys::window()?
|
||||
.document()?
|
||||
.active_element()?
|
||||
.dyn_into()
|
||||
.ok()
|
||||
pub(crate) fn focused_element(root: &Node) -> Option<web_sys::Element> {
|
||||
if let Some(document) = root.dyn_ref::<Document>() {
|
||||
document.active_element()
|
||||
} else if let Some(shadow) = root.dyn_ref::<web_sys::ShadowRoot>() {
|
||||
shadow.active_element()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
|
||||
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
|
||||
let element = element.dyn_ref::<web_sys::Element>()?;
|
||||
let focused_element = focused_element()?;
|
||||
let root = element.get_root_node();
|
||||
|
||||
let focused_element = focused_element(&root)?;
|
||||
Some(element == &focused_element)
|
||||
}
|
||||
try_has_focus(element).unwrap_or(false)
|
||||
@@ -152,7 +159,10 @@ fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
|
||||
}
|
||||
|
||||
fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, ctx: &egui::Context) -> egui::Vec2 {
|
||||
let pixels_per_point = ctx.pixels_per_point();
|
||||
// ctx.pixels_per_point can be outdated
|
||||
|
||||
let pixels_per_point = ctx.zoom_factor() * native_pixels_per_point();
|
||||
|
||||
egui::vec2(
|
||||
canvas.width() as f32 / pixels_per_point,
|
||||
canvas.height() as f32 / pixels_per_point,
|
||||
@@ -352,3 +362,8 @@ pub fn percent_decode(s: &str) -> String {
|
||||
.decode_utf8_lossy()
|
||||
.to_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")))
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ struct PanicHandlerInner {
|
||||
|
||||
/// Contains a summary about a panics.
|
||||
///
|
||||
/// This is basically a human-readable version of [`std::panic::PanicInfo`]
|
||||
/// This is basically a human-readable version of [`std::panic::PanicHookInfo`]
|
||||
/// with an added callstack.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PanicSummary {
|
||||
@@ -66,7 +66,7 @@ pub struct PanicSummary {
|
||||
|
||||
impl PanicSummary {
|
||||
/// Construct a summary from a panic.
|
||||
pub fn new(info: &std::panic::PanicInfo<'_>) -> Self {
|
||||
pub fn new(info: &std::panic::PanicHookInfo<'_>) -> Self {
|
||||
let message = info.to_string();
|
||||
let callstack = Error::new().stack();
|
||||
Self { message, callstack }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Document, Node};
|
||||
|
||||
use super::{AppRunner, WebRunner};
|
||||
|
||||
@@ -14,15 +15,16 @@ pub struct TextAgent {
|
||||
|
||||
impl TextAgent {
|
||||
/// Attach the agent to the document.
|
||||
pub fn attach(runner_ref: &WebRunner) -> Result<Self, JsValue> {
|
||||
pub fn attach(runner_ref: &WebRunner, root: Node) -> Result<Self, JsValue> {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
// create an `<input>` element
|
||||
let input = document
|
||||
.create_element("input")?
|
||||
.dyn_into::<web_sys::HtmlInputElement>()?;
|
||||
.dyn_into::<web_sys::HtmlElement>()?;
|
||||
input.set_autofocus(true)?;
|
||||
let input = input.dyn_into::<web_sys::HtmlInputElement>()?;
|
||||
input.set_type("text");
|
||||
input.set_autofocus(true);
|
||||
input.set_attribute("autocapitalize", "off")?;
|
||||
|
||||
// append it to `<body>` and hide it outside of the viewport
|
||||
@@ -36,7 +38,17 @@ impl TextAgent {
|
||||
style.set_property("position", "absolute")?;
|
||||
style.set_property("top", "0")?;
|
||||
style.set_property("left", "0")?;
|
||||
document.body().unwrap().append_child(&input)?;
|
||||
|
||||
if root.has_type::<Document>() {
|
||||
// root object is a document, append to its body
|
||||
root.dyn_into::<Document>()?
|
||||
.body()
|
||||
.unwrap()
|
||||
.append_child(&input)?;
|
||||
} else {
|
||||
// append input into root directly
|
||||
root.append_child(&input)?;
|
||||
}
|
||||
|
||||
// attach event listeners
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::web_painter::WebPainter;
|
||||
use crate::WebOptions;
|
||||
use egui::{Event, UserData, ViewportId};
|
||||
use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
|
||||
use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup};
|
||||
use egui_wgpu::{RenderState, SurfaceErrorAction};
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
@@ -63,49 +63,7 @@ impl WebPainterWgpu {
|
||||
) -> Result<Self, String> {
|
||||
log::debug!("Creating wgpu painter");
|
||||
|
||||
let instance = match &options.wgpu_options.wgpu_setup {
|
||||
WgpuSetup::CreateNew {
|
||||
supported_backends: backends,
|
||||
power_preference,
|
||||
..
|
||||
} => {
|
||||
let mut backends = *backends;
|
||||
|
||||
// Don't try WebGPU if we're not in a secure context.
|
||||
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
|
||||
let is_secure_context =
|
||||
web_sys::window().map_or(false, |w| w.is_secure_context());
|
||||
if !is_secure_context {
|
||||
log::info!(
|
||||
"WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost."
|
||||
);
|
||||
|
||||
// Don't try WebGPU since we established now that it will fail.
|
||||
backends.remove(wgpu::Backends::BROWSER_WEBGPU);
|
||||
|
||||
if backends.is_empty() {
|
||||
return Err("No available supported graphics backends.".to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Creating wgpu instance with backends {:?}", backends);
|
||||
|
||||
let instance =
|
||||
wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor {
|
||||
backends,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
|
||||
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
Arc::new(instance)
|
||||
}
|
||||
WgpuSetup::Existing { instance, .. } => instance.clone(),
|
||||
};
|
||||
|
||||
let instance = options.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}"))?;
|
||||
|
||||
@@ -4,7 +4,11 @@ use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{epi, App};
|
||||
|
||||
use super::{events, text_agent::TextAgent, AppRunner, PanicHandler};
|
||||
use super::{
|
||||
events::{self, ResizeObserverContext},
|
||||
text_agent::TextAgent,
|
||||
AppRunner, PanicHandler,
|
||||
};
|
||||
|
||||
/// This is how `eframe` runs your web application
|
||||
///
|
||||
@@ -18,7 +22,7 @@ pub struct WebRunner {
|
||||
|
||||
/// If we ever panic during running, this `RefCell` is poisoned.
|
||||
/// So before we use it, we need to check [`Self::panic_handler`].
|
||||
runner: Rc<RefCell<Option<AppRunner>>>,
|
||||
app_runner: Rc<RefCell<Option<AppRunner>>>,
|
||||
|
||||
/// In case of a panic, unsubscribe these.
|
||||
/// They have to be in a separate `Rc` so that we don't need to pass them to
|
||||
@@ -39,7 +43,7 @@ impl WebRunner {
|
||||
|
||||
Self {
|
||||
panic_handler,
|
||||
runner: Rc::new(RefCell::new(None)),
|
||||
app_runner: Rc::new(RefCell::new(None)),
|
||||
events_to_unsubscribe: Rc::new(RefCell::new(Default::default())),
|
||||
frame: Default::default(),
|
||||
resize_observer: Default::default(),
|
||||
@@ -58,28 +62,36 @@ impl WebRunner {
|
||||
) -> Result<(), JsValue> {
|
||||
self.destroy();
|
||||
|
||||
let text_agent = TextAgent::attach(self)?;
|
||||
|
||||
let runner = AppRunner::new(canvas, web_options, app_creator, text_agent).await?;
|
||||
|
||||
{
|
||||
// Make sure the canvas can be given focus.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex
|
||||
runner.canvas().set_tab_index(0);
|
||||
canvas.set_tab_index(0);
|
||||
|
||||
// Don't outline the canvas when it has focus:
|
||||
runner.canvas().style().set_property("outline", "none")?;
|
||||
canvas.style().set_property("outline", "none")?;
|
||||
}
|
||||
|
||||
self.runner.replace(Some(runner));
|
||||
|
||||
{
|
||||
events::install_event_handlers(self)?;
|
||||
|
||||
// The resize observer handles calling `request_animation_frame` to start the render loop.
|
||||
events::install_resize_observer(self)?;
|
||||
// First set up the app runner:
|
||||
let text_agent = TextAgent::attach(self, canvas.get_root_node())?;
|
||||
let app_runner =
|
||||
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
|
||||
self.app_runner.replace(Some(app_runner));
|
||||
}
|
||||
|
||||
{
|
||||
let resize_observer = events::ResizeObserverContext::new(self)?;
|
||||
|
||||
// Properly size the canvas. Will also call `self.request_animation_frame()` (eventually)
|
||||
resize_observer.observe(&canvas);
|
||||
|
||||
self.resize_observer.replace(Some(resize_observer));
|
||||
}
|
||||
|
||||
events::install_event_handlers(self)?;
|
||||
|
||||
log::info!("event handlers installed.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -109,10 +121,7 @@ impl WebRunner {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(context) = self.resize_observer.take() {
|
||||
context.resize_observer.disconnect();
|
||||
drop(context.closure);
|
||||
}
|
||||
self.resize_observer.replace(None);
|
||||
}
|
||||
|
||||
/// Shut down eframe and clean up resources.
|
||||
@@ -124,7 +133,7 @@ impl WebRunner {
|
||||
window.cancel_animation_frame(frame.id).ok();
|
||||
}
|
||||
|
||||
if let Some(runner) = self.runner.replace(None) {
|
||||
if let Some(runner) = self.app_runner.replace(None) {
|
||||
runner.destroy();
|
||||
}
|
||||
}
|
||||
@@ -138,7 +147,7 @@ impl WebRunner {
|
||||
self.unsubscribe_from_all_events();
|
||||
None
|
||||
} else {
|
||||
let lock = self.runner.try_borrow_mut().ok()?;
|
||||
let lock = self.app_runner.try_borrow_mut().ok()?;
|
||||
std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
|
||||
.ok()
|
||||
}
|
||||
@@ -166,20 +175,45 @@ impl WebRunner {
|
||||
event_name: &'static str,
|
||||
mut closure: impl FnMut(E, &mut AppRunner) + 'static,
|
||||
) -> Result<(), wasm_bindgen::JsValue> {
|
||||
let runner_ref = self.clone();
|
||||
let options = web_sys::AddEventListenerOptions::default();
|
||||
self.add_event_listener_ex(
|
||||
target,
|
||||
event_name,
|
||||
&options,
|
||||
move |event, app_runner, _web_runner| closure(event, app_runner),
|
||||
)
|
||||
}
|
||||
|
||||
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
||||
/// are dealt with in the same way.
|
||||
///
|
||||
/// All events added with this method will automatically be unsubscribed on panic,
|
||||
/// or when [`Self::destroy`] is called.
|
||||
pub fn add_event_listener_ex<E: wasm_bindgen::JsCast>(
|
||||
&self,
|
||||
target: &web_sys::EventTarget,
|
||||
event_name: &'static str,
|
||||
options: &web_sys::AddEventListenerOptions,
|
||||
mut closure: impl FnMut(E, &mut AppRunner, &Self) + 'static,
|
||||
) -> Result<(), wasm_bindgen::JsValue> {
|
||||
let web_runner = self.clone();
|
||||
|
||||
// Create a JS closure based on the FnMut provided
|
||||
let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
||||
// Only call the wrapped closure if the egui code has not panicked
|
||||
if let Some(mut runner_lock) = runner_ref.try_lock() {
|
||||
if let Some(mut runner_lock) = web_runner.try_lock() {
|
||||
// Cast the event to the expected event type
|
||||
let event = event.unchecked_into::<E>();
|
||||
closure(event, &mut runner_lock);
|
||||
closure(event, &mut runner_lock, &web_runner);
|
||||
}
|
||||
}) as Box<dyn FnMut(web_sys::Event)>);
|
||||
|
||||
// Add the event listener to the target
|
||||
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
||||
target.add_event_listener_with_callback_and_add_event_listener_options(
|
||||
event_name,
|
||||
closure.as_ref().unchecked_ref(),
|
||||
options,
|
||||
)?;
|
||||
|
||||
let handle = TargetEvent {
|
||||
target: target.clone(),
|
||||
@@ -208,13 +242,13 @@ impl WebRunner {
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let closure = Closure::once({
|
||||
let runner_ref = self.clone();
|
||||
let web_runner = self.clone();
|
||||
move || {
|
||||
// We can paint now, so clear the animation frame.
|
||||
// This drops the `closure` and allows another
|
||||
// animation frame to be scheduled
|
||||
let _ = runner_ref.frame.take();
|
||||
events::paint_and_schedule(&runner_ref)
|
||||
let _ = web_runner.frame.take();
|
||||
events::paint_and_schedule(&web_runner)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -226,19 +260,6 @@ impl WebRunner {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_resize_observer(
|
||||
&self,
|
||||
resize_observer: web_sys::ResizeObserver,
|
||||
closure: Closure<dyn FnMut(js_sys::Array)>,
|
||||
) {
|
||||
self.resize_observer
|
||||
.borrow_mut()
|
||||
.replace(ResizeObserverContext {
|
||||
resize_observer,
|
||||
closure,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -253,11 +274,6 @@ struct AnimationFrameRequest {
|
||||
_closure: Closure<dyn FnMut() -> Result<(), JsValue>>,
|
||||
}
|
||||
|
||||
struct ResizeObserverContext {
|
||||
resize_observer: web_sys::ResizeObserver,
|
||||
closure: Closure<dyn FnMut(js_sys::Array)>,
|
||||
}
|
||||
|
||||
struct TargetEvent {
|
||||
target: web_sys::EventTarget,
|
||||
event_name: String,
|
||||
|
||||
@@ -6,6 +6,16 @@ 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.31.1 - 2025-03-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04
|
||||
* Upgrade to wgpu 24 [#5610](https://github.com/emilk/egui/pull/5610) by [@torokati44](https://github.com/torokati44)
|
||||
* Extend `WgpuSetup`, `egui_kittest` now prefers software rasterizers for testing [#5506](https://github.com/emilk/egui/pull/5506) by [@Wumpf](https://github.com/Wumpf)
|
||||
* Wgpu resources are no longer wrapped in `Arc` (since they are now `Clone`) [#5612](https://github.com/emilk/egui/pull/5612) by [@Wumpf](https://github.com/Wumpf)
|
||||
|
||||
|
||||
## 0.30.0 - 2024-12-16
|
||||
* Fix docs.rs build [#5204](https://github.com/emilk/egui/pull/5204) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Free textures after submitting queue instead of before with wgpu renderer [#5226](https://github.com/emilk/egui/pull/5226) by [@Rusty-Cube](https://github.com/Rusty-Cube)
|
||||
|
||||
@@ -139,9 +139,9 @@ impl CaptureState {
|
||||
|
||||
encoder.copy_texture_to_buffer(
|
||||
tex.as_image_copy(),
|
||||
wgpu::ImageCopyBuffer {
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::ImageDataLayout {
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padding.padded_bytes_per_row),
|
||||
rows_per_image: None,
|
||||
|
||||
@@ -23,8 +23,10 @@ pub use wgpu;
|
||||
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
|
||||
mod renderer;
|
||||
|
||||
mod setup;
|
||||
|
||||
pub use renderer::*;
|
||||
use wgpu::{Adapter, Device, Instance, Queue, TextureFormat};
|
||||
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
|
||||
|
||||
/// Helpers for capturing screenshots of the UI.
|
||||
pub mod capture;
|
||||
@@ -40,8 +42,8 @@ use epaint::mutex::RwLock;
|
||||
/// An error produced by egui-wgpu.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum WgpuError {
|
||||
#[error("Failed to create wgpu adapter, no suitable adapter found.")]
|
||||
NoSuitableAdapterFound,
|
||||
#[error("Failed to create wgpu adapter, no suitable adapter found: {0}")]
|
||||
NoSuitableAdapterFound(String),
|
||||
|
||||
#[error("There was no valid format for the surface at all.")]
|
||||
NoSurfaceFormatsAvailable,
|
||||
@@ -61,20 +63,20 @@ pub enum WgpuError {
|
||||
#[derive(Clone)]
|
||||
pub struct RenderState {
|
||||
/// Wgpu adapter used for rendering.
|
||||
pub adapter: Arc<wgpu::Adapter>,
|
||||
pub adapter: wgpu::Adapter,
|
||||
|
||||
/// All the available adapters.
|
||||
///
|
||||
/// This is not available on web.
|
||||
/// On web, we always select WebGPU is available, then fall back to WebGL if not.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub available_adapters: Arc<[wgpu::Adapter]>,
|
||||
pub available_adapters: Vec<wgpu::Adapter>,
|
||||
|
||||
/// Wgpu device used for rendering, created from the adapter.
|
||||
pub device: Arc<wgpu::Device>,
|
||||
pub device: wgpu::Device,
|
||||
|
||||
/// Wgpu queue used for rendering, created from the adapter.
|
||||
pub queue: Arc<wgpu::Queue>,
|
||||
pub queue: wgpu::Queue,
|
||||
|
||||
/// The target texture format used for presenting to the window.
|
||||
pub target_format: wgpu::TextureFormat,
|
||||
@@ -83,6 +85,72 @@ pub struct RenderState {
|
||||
pub renderer: Arc<RwLock<Renderer>>,
|
||||
}
|
||||
|
||||
async fn request_adapter(
|
||||
instance: &wgpu::Instance,
|
||||
power_preference: wgpu::PowerPreference,
|
||||
compatible_surface: Option<&wgpu::Surface<'_>>,
|
||||
_available_adapters: &[wgpu::Adapter],
|
||||
) -> Result<wgpu::Adapter, WgpuError> {
|
||||
profiling::function_scope!();
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference,
|
||||
compatible_surface,
|
||||
// We don't expose this as an option right now since it's fairly rarely useful:
|
||||
// * only has an effect on native
|
||||
// * fails if there's no software rasterizer available
|
||||
// * can achieve the same with `native_adapter_selector`
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if _available_adapters.is_empty() {
|
||||
log::info!("No wgpu adapters found");
|
||||
} else if _available_adapters.len() == 1 {
|
||||
log::info!(
|
||||
"The only available wgpu adapter was not suitable: {}",
|
||||
adapter_info_summary(&_available_adapters[0].get_info())
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"No suitable wgpu adapter found out of the {} available ones: {}",
|
||||
_available_adapters.len(),
|
||||
describe_adapters(_available_adapters)
|
||||
);
|
||||
}
|
||||
|
||||
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
|
||||
})?;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
log::debug!(
|
||||
"Picked wgpu adapter: {}",
|
||||
adapter_info_summary(&adapter.get_info())
|
||||
);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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)
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
/// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu.
|
||||
///
|
||||
@@ -100,96 +168,65 @@ impl RenderState {
|
||||
|
||||
// This is always an empty list on web.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let available_adapters = instance.enumerate_adapters(wgpu::Backends::all());
|
||||
let available_adapters = {
|
||||
let backends = if let WgpuSetup::CreateNew(create_new) = &config.wgpu_setup {
|
||||
create_new.instance_descriptor.backends
|
||||
} else {
|
||||
wgpu::Backends::all()
|
||||
};
|
||||
|
||||
instance.enumerate_adapters(backends)
|
||||
};
|
||||
|
||||
let (adapter, device, queue) = match config.wgpu_setup.clone() {
|
||||
WgpuSetup::CreateNew {
|
||||
supported_backends: _,
|
||||
WgpuSetup::CreateNew(WgpuSetupCreateNew {
|
||||
instance_descriptor: _,
|
||||
power_preference,
|
||||
native_adapter_selector: _native_adapter_selector,
|
||||
device_descriptor,
|
||||
} => {
|
||||
trace_path,
|
||||
}) => {
|
||||
let adapter = {
|
||||
profiling::scope!("request_adapter");
|
||||
instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
request_adapter(instance, power_preference, compatible_surface, &[]).await
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(native_adapter_selector) = _native_adapter_selector {
|
||||
native_adapter_selector(&available_adapters, compatible_surface)
|
||||
.map_err(WgpuError::NoSuitableAdapterFound)
|
||||
} else {
|
||||
request_adapter(
|
||||
instance,
|
||||
power_preference,
|
||||
compatible_surface,
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
&available_adapters,
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if available_adapters.is_empty() {
|
||||
log::info!("No wgpu adapters found");
|
||||
} else if available_adapters.len() == 1 {
|
||||
log::info!(
|
||||
"The only available wgpu adapter was not suitable: {}",
|
||||
adapter_info_summary(&available_adapters[0].get_info())
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"No suitable wgpu adapter found out of the {} available ones: {}",
|
||||
available_adapters.len(),
|
||||
describe_adapters(&available_adapters)
|
||||
);
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
WgpuError::NoSuitableAdapterFound
|
||||
})?
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
log::debug!(
|
||||
"Picked wgpu adapter: {}",
|
||||
adapter_info_summary(&adapter.get_info())
|
||||
);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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())
|
||||
);
|
||||
}
|
||||
|
||||
let trace_path = std::env::var("WGPU_TRACE");
|
||||
let (device, queue) = {
|
||||
profiling::scope!("request_device");
|
||||
adapter
|
||||
.request_device(
|
||||
&(*device_descriptor)(&adapter),
|
||||
trace_path.ok().as_ref().map(std::path::Path::new),
|
||||
)
|
||||
.request_device(&(*device_descriptor)(&adapter), trace_path.as_deref())
|
||||
.await?
|
||||
};
|
||||
|
||||
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
|
||||
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
(Arc::new(adapter), Arc::new(device), Arc::new(queue))
|
||||
(adapter, device, queue)
|
||||
}
|
||||
WgpuSetup::Existing {
|
||||
WgpuSetup::Existing(WgpuSetupExisting {
|
||||
instance: _,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
} => (adapter, device, queue),
|
||||
}) => (adapter, device, queue),
|
||||
};
|
||||
|
||||
let surface_formats = {
|
||||
profiling::scope!("get_capabilities");
|
||||
compatible_surface.map_or_else(
|
||||
|| vec![TextureFormat::Rgba8Unorm],
|
||||
|| vec![wgpu::TextureFormat::Rgba8Unorm],
|
||||
|s| s.get_capabilities(&adapter).formats,
|
||||
)
|
||||
};
|
||||
@@ -209,7 +246,7 @@ impl RenderState {
|
||||
Ok(Self {
|
||||
adapter,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
available_adapters: available_adapters.into(),
|
||||
available_adapters,
|
||||
device,
|
||||
queue,
|
||||
target_format,
|
||||
@@ -225,14 +262,11 @@ fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
|
||||
} else if adapters.len() == 1 {
|
||||
adapter_info_summary(&adapters[0].get_info())
|
||||
} else {
|
||||
let mut list_string = String::new();
|
||||
for adapter in adapters {
|
||||
if !list_string.is_empty() {
|
||||
list_string += ", ";
|
||||
}
|
||||
list_string += &format!("{{{}}}", adapter_info_summary(&adapter.get_info()));
|
||||
}
|
||||
list_string
|
||||
adapters
|
||||
.iter()
|
||||
.map(|a| format!("{{{}}}", adapter_info_summary(&a.get_info())))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,62 +279,6 @@ pub enum SurfaceErrorAction {
|
||||
RecreateSurface,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WgpuSetup {
|
||||
/// Construct a wgpu setup using some predefined settings & heuristics.
|
||||
/// This is the default option. You can customize most behaviours overriding the
|
||||
/// supported backends, power preferences, and device description.
|
||||
///
|
||||
/// This can also be configured with the environment variables:
|
||||
/// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu`
|
||||
/// * `WGPU_POWER_PREF`: `low`, `high` or `none`
|
||||
CreateNew {
|
||||
/// Backends that should be supported (wgpu will pick one of these).
|
||||
///
|
||||
/// For instance, 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`.
|
||||
supported_backends: wgpu::Backends,
|
||||
|
||||
/// Power preference for the adapter.
|
||||
power_preference: wgpu::PowerPreference,
|
||||
|
||||
/// Configuration passed on device request, given an adapter
|
||||
device_descriptor:
|
||||
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
|
||||
},
|
||||
|
||||
/// Run on an existing wgpu setup.
|
||||
Existing {
|
||||
instance: Arc<Instance>,
|
||||
adapter: Arc<Adapter>,
|
||||
device: Arc<Device>,
|
||||
queue: Arc<Queue>,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WgpuSetup {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CreateNew {
|
||||
supported_backends,
|
||||
power_preference,
|
||||
device_descriptor: _,
|
||||
} => f
|
||||
.debug_struct("AdapterSelection::Standard")
|
||||
.field("supported_backends", &supported_backends)
|
||||
.field("power_preference", &power_preference)
|
||||
.finish(),
|
||||
Self::Existing { .. } => f
|
||||
.debug_struct("AdapterSelection::Existing")
|
||||
.finish_non_exhaustive(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
|
||||
#[derive(Clone)]
|
||||
pub struct WgpuConfiguration {
|
||||
@@ -352,42 +330,8 @@ impl Default for WgpuConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
|
||||
desired_maximum_frame_latency: None,
|
||||
|
||||
// By default, create a new wgpu setup. This will create a new instance, adapter, device and queue.
|
||||
// This will create an instance for the supported backends (which can be configured by
|
||||
// `WGPU_BACKEND`), and will pick an adapter by iterating adapters based on their power preference. The power
|
||||
// preference can also be configured by `WGPU_POWER_PREF`.
|
||||
wgpu_setup: WgpuSetup::CreateNew {
|
||||
// Add GL backend, primarily because WebGPU is not stable enough yet.
|
||||
// (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl")
|
||||
supported_backends: wgpu::util::backend_bits_from_env()
|
||||
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
|
||||
|
||||
power_preference: wgpu::util::power_preference_from_env()
|
||||
.unwrap_or(wgpu::PowerPreference::HighPerformance),
|
||||
device_descriptor: Arc::new(|adapter| {
|
||||
let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
|
||||
wgpu::Limits::downlevel_webgl2_defaults()
|
||||
} else {
|
||||
wgpu::Limits::default()
|
||||
};
|
||||
|
||||
wgpu::DeviceDescriptor {
|
||||
label: Some("egui wgpu device"),
|
||||
required_features: wgpu::Features::default(),
|
||||
required_limits: wgpu::Limits {
|
||||
// When using a depth buffer, we have to be able to create a texture
|
||||
// large enough for the entire surface, and we want to support 4k+ displays.
|
||||
max_texture_dimension_2d: 8192,
|
||||
..base_limits
|
||||
},
|
||||
memory_hints: wgpu::MemoryHints::default(),
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
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.
|
||||
@@ -468,8 +412,14 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
|
||||
summary += &format!(", driver_info: {driver_info:?}");
|
||||
}
|
||||
if *vendor != 0 {
|
||||
// TODO(emilk): decode using https://github.com/gfx-rs/wgpu/blob/767ac03245ee937d3dc552edc13fe7ab0a860eec/wgpu-hal/src/auxil/mod.rs#L7
|
||||
summary += &format!(", vendor: 0x{vendor:04X}");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
summary += &format!(", vendor: {} (0x{vendor:04X})", parse_vendor_id(*vendor));
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
summary += &format!(", vendor: 0x{vendor:04X}");
|
||||
}
|
||||
}
|
||||
if *device != 0 {
|
||||
summary += &format!(", device: 0x{device:02X}");
|
||||
@@ -477,3 +427,20 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
|
||||
|
||||
summary
|
||||
}
|
||||
|
||||
/// Tries to parse the adapter's vendor ID to a human-readable string.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn parse_vendor_id(vendor_id: u32) -> &'static str {
|
||||
match vendor_id {
|
||||
wgpu::hal::auxil::db::amd::VENDOR => "AMD",
|
||||
wgpu::hal::auxil::db::apple::VENDOR => "Apple",
|
||||
wgpu::hal::auxil::db::arm::VENDOR => "ARM",
|
||||
wgpu::hal::auxil::db::broadcom::VENDOR => "Broadcom",
|
||||
wgpu::hal::auxil::db::imgtec::VENDOR => "Imagination Technologies",
|
||||
wgpu::hal::auxil::db::intel::VENDOR => "Intel",
|
||||
wgpu::hal::auxil::db::mesa::VENDOR => "Mesa",
|
||||
wgpu::hal::auxil::db::nvidia::VENDOR => "NVIDIA",
|
||||
wgpu::hal::auxil::db::qualcomm::VENDOR => "Qualcomm",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,14 +579,14 @@ impl Renderer {
|
||||
let queue_write_data_to_texture = |texture, origin| {
|
||||
profiling::scope!("write_texture");
|
||||
queue.write_texture(
|
||||
wgpu::ImageCopyTexture {
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture,
|
||||
mip_level: 0,
|
||||
origin,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
data_bytes,
|
||||
wgpu::ImageDataLayout {
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * width),
|
||||
rows_per_image: Some(height),
|
||||
|
||||
217
crates/egui-wgpu/src/setup.rs
Normal file
217
crates/egui-wgpu/src/setup.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WgpuSetup {
|
||||
/// Construct a wgpu setup using some predefined settings & heuristics.
|
||||
/// This is the default option. You can customize most behaviours overriding the
|
||||
/// supported backends, power preferences, and device description.
|
||||
///
|
||||
/// By default can also be configured with various environment variables:
|
||||
/// * `WGPU_BACKEND`: `vulkan`, `dx12`, `metal`, `opengl`, `webgpu`
|
||||
/// * `WGPU_POWER_PREF`: `low`, `high` or `none`
|
||||
/// * `WGPU_TRACE`: Path to a file to output a wgpu trace file.
|
||||
///
|
||||
/// Each instance flag also comes with an environment variable (for details see [`wgpu::InstanceFlags`]):
|
||||
/// * `WGPU_VALIDATION`: Enables validation (enabled by default in debug builds).
|
||||
/// * `WGPU_DEBUG`: Generate debug information in shaders and objects (enabled by default in debug builds).
|
||||
/// * `WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER`: Whether wgpu should expose adapters that run on top of non-compliant adapters.
|
||||
/// * `WGPU_GPU_BASED_VALIDATION`: Enable GPU-based validation.
|
||||
CreateNew(WgpuSetupCreateNew),
|
||||
|
||||
/// Run on an existing wgpu setup.
|
||||
Existing(WgpuSetupExisting),
|
||||
}
|
||||
|
||||
impl Default for WgpuSetup {
|
||||
fn default() -> Self {
|
||||
Self::CreateNew(WgpuSetupCreateNew::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WgpuSetup {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CreateNew(create_new) => f
|
||||
.debug_tuple("WgpuSetup::CreateNew")
|
||||
.field(create_new)
|
||||
.finish(),
|
||||
Self::Existing { .. } => f.debug_tuple("WgpuSetup::Existing").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WgpuSetup {
|
||||
/// Creates a new [`wgpu::Instance`] or clones the existing one.
|
||||
///
|
||||
/// Does *not* store the wgpu instance, so calling this repeatedly may
|
||||
/// create a new instance every time!
|
||||
pub async fn new_instance(&self) -> wgpu::Instance {
|
||||
match self {
|
||||
Self::CreateNew(create_new) => {
|
||||
#[allow(unused_mut)]
|
||||
let mut backends = create_new.instance_descriptor.backends;
|
||||
|
||||
// Don't try WebGPU if we're not in a secure context.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
|
||||
let is_secure_context =
|
||||
wgpu::web_sys::window().is_some_and(|w| w.is_secure_context());
|
||||
if !is_secure_context {
|
||||
log::info!(
|
||||
"WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost."
|
||||
);
|
||||
backends.remove(wgpu::Backends::BROWSER_WEBGPU);
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Creating wgpu instance with backends {:?}", backends);
|
||||
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
|
||||
.await
|
||||
}
|
||||
Self::Existing(existing) => existing.instance.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WgpuSetupCreateNew> for WgpuSetup {
|
||||
fn from(create_new: WgpuSetupCreateNew) -> Self {
|
||||
Self::CreateNew(create_new)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WgpuSetupExisting> for WgpuSetup {
|
||||
fn from(existing: WgpuSetupExisting) -> Self {
|
||||
Self::Existing(existing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Method for selecting an adapter on native.
|
||||
///
|
||||
/// This can be used for fully custom adapter selection.
|
||||
/// If available, `wgpu::Surface` is passed to allow checking for surface compatibility.
|
||||
pub type NativeAdapterSelectorMethod = Arc<
|
||||
dyn Fn(&[wgpu::Adapter], Option<&wgpu::Surface<'_>>) -> Result<wgpu::Adapter, String>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>;
|
||||
|
||||
/// Configuration for creating a new wgpu setup.
|
||||
///
|
||||
/// Used for [`WgpuSetup::CreateNew`].
|
||||
pub struct WgpuSetupCreateNew {
|
||||
/// Instance descriptor for creating a 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`.
|
||||
pub instance_descriptor: wgpu::InstanceDescriptor,
|
||||
|
||||
/// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web.
|
||||
pub power_preference: wgpu::PowerPreference,
|
||||
|
||||
/// Optional selector for native adapters.
|
||||
///
|
||||
/// This field has no effect when targeting web!
|
||||
/// Otherwise, if set [`Self::power_preference`] is ignored and the adapter is instead selected by this method.
|
||||
/// Note that [`Self::instance_descriptor`]'s [`wgpu::InstanceDescriptor::backends`]
|
||||
/// are still used to filter the adapter enumeration in the first place.
|
||||
///
|
||||
/// Defaults to `None`.
|
||||
pub native_adapter_selector: Option<NativeAdapterSelectorMethod>,
|
||||
|
||||
/// Configuration passed on device request, given an adapter
|
||||
pub device_descriptor:
|
||||
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
|
||||
|
||||
/// Option path to output a wgpu trace file.
|
||||
///
|
||||
/// This only works if this feature is enabled in `wgpu-core`.
|
||||
/// Does not work when running with WebGPU.
|
||||
/// Defaults to the path set in the `WGPU_TRACE` environment variable.
|
||||
pub trace_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl Clone for WgpuSetupCreateNew {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
instance_descriptor: self.instance_descriptor.clone(),
|
||||
power_preference: self.power_preference,
|
||||
native_adapter_selector: self.native_adapter_selector.clone(),
|
||||
device_descriptor: self.device_descriptor.clone(),
|
||||
trace_path: self.trace_path.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
)
|
||||
.field("trace_path", &self.trace_path)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WgpuSetupCreateNew {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
instance_descriptor: wgpu::InstanceDescriptor {
|
||||
// Add GL backend, primarily because WebGPU is not stable enough yet.
|
||||
// (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl")
|
||||
backends: wgpu::Backends::from_env()
|
||||
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
|
||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||
backend_options: wgpu::BackendOptions::from_env_or_default(),
|
||||
},
|
||||
|
||||
power_preference: wgpu::PowerPreference::from_env()
|
||||
.unwrap_or(wgpu::PowerPreference::HighPerformance),
|
||||
|
||||
native_adapter_selector: None,
|
||||
|
||||
device_descriptor: Arc::new(|adapter| {
|
||||
let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
|
||||
wgpu::Limits::downlevel_webgl2_defaults()
|
||||
} else {
|
||||
wgpu::Limits::default()
|
||||
};
|
||||
|
||||
wgpu::DeviceDescriptor {
|
||||
label: Some("egui wgpu device"),
|
||||
required_features: wgpu::Features::default(),
|
||||
required_limits: wgpu::Limits {
|
||||
// When using a depth buffer, we have to be able to create a texture
|
||||
// large enough for the entire surface, and we want to support 4k+ displays.
|
||||
max_texture_dimension_2d: 8192,
|
||||
..base_limits
|
||||
},
|
||||
memory_hints: wgpu::MemoryHints::default(),
|
||||
}
|
||||
}),
|
||||
|
||||
trace_path: std::env::var("WGPU_TRACE")
|
||||
.ok()
|
||||
.map(std::path::PathBuf::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for using an existing wgpu setup.
|
||||
///
|
||||
/// Used for [`WgpuSetup::Existing`].
|
||||
#[derive(Clone)]
|
||||
pub struct WgpuSetupExisting {
|
||||
pub instance: wgpu::Instance,
|
||||
pub adapter: wgpu::Adapter,
|
||||
pub device: wgpu::Device,
|
||||
pub queue: wgpu::Queue,
|
||||
}
|
||||
@@ -27,7 +27,7 @@ pub struct Painter {
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
screen_capture_state: Option<CaptureState>,
|
||||
|
||||
instance: Arc<wgpu::Instance>,
|
||||
instance: wgpu::Instance,
|
||||
render_state: Option<RenderState>,
|
||||
|
||||
// Per viewport/window:
|
||||
@@ -51,7 +51,7 @@ impl Painter {
|
||||
/// [`set_window()`](Self::set_window) once you have
|
||||
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
|
||||
/// associated.
|
||||
pub fn new(
|
||||
pub async fn new(
|
||||
context: Context,
|
||||
configuration: WgpuConfiguration,
|
||||
msaa_samples: u32,
|
||||
@@ -59,17 +59,8 @@ impl Painter {
|
||||
support_transparent_backbuffer: bool,
|
||||
dithering: bool,
|
||||
) -> Self {
|
||||
let instance = match &configuration.wgpu_setup {
|
||||
crate::WgpuSetup::CreateNew {
|
||||
supported_backends, ..
|
||||
} => Arc::new(wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends: *supported_backends,
|
||||
..Default::default()
|
||||
})),
|
||||
crate::WgpuSetup::Existing { instance, .. } => instance.clone(),
|
||||
};
|
||||
|
||||
let (capture_tx, capture_rx) = capture_channel();
|
||||
let instance = configuration.wgpu_setup.new_instance().await;
|
||||
|
||||
Self {
|
||||
context,
|
||||
|
||||
@@ -5,6 +5,15 @@ 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.31.1 - 2025-03-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04
|
||||
* Re-enable IME support on Linux [#5198](https://github.com/emilk/egui/pull/5198) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Update to winit 0.30.7 [#5516](https://github.com/emilk/egui/pull/5516) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.30.0 - 2024-12-16
|
||||
* iOS: Support putting UI next to the dynamic island [#5211](https://github.com/emilk/egui/pull/5211) by [@frederik-uni](https://github.com/frederik-uni)
|
||||
* Remove implicit `accesskit_winit` feature [#5316](https://github.com/emilk/egui/pull/5316) by [@waywardmonkeys](https://github.com/waywardmonkeys)
|
||||
|
||||
@@ -67,7 +67,7 @@ winit = { workspace = true, default-features = false }
|
||||
#! ### Optional dependencies
|
||||
|
||||
# feature accesskit
|
||||
accesskit_winit = { version = "0.23", optional = true }
|
||||
accesskit_winit = { workspace = true, optional = true }
|
||||
|
||||
bytemuck = { workspace = true, optional = true }
|
||||
|
||||
|
||||
@@ -166,12 +166,14 @@ impl State {
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn init_accesskit<T: From<accesskit_winit::Event> + Send>(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window: &Window,
|
||||
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy(
|
||||
event_loop,
|
||||
window,
|
||||
event_loop_proxy,
|
||||
));
|
||||
@@ -727,7 +729,7 @@ impl State {
|
||||
// When telling users "Press Ctrl-F to find", this is where we should
|
||||
// look for the "F" key, because they may have a dvorak layout on
|
||||
// a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position.
|
||||
logical_key,
|
||||
logical_key: winit_logical_key,
|
||||
|
||||
text,
|
||||
|
||||
@@ -746,7 +748,7 @@ impl State {
|
||||
None
|
||||
};
|
||||
|
||||
let logical_key = key_from_winit_key(logical_key);
|
||||
let logical_key = key_from_winit_key(winit_logical_key);
|
||||
|
||||
// Helpful logging to enable when adding new key support
|
||||
log::trace!(
|
||||
@@ -789,7 +791,11 @@ impl State {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(text) = &text {
|
||||
if let Some(text) = text
|
||||
.as_ref()
|
||||
.map(|t| t.as_str())
|
||||
.or_else(|| winit_logical_key.to_text())
|
||||
{
|
||||
// Make sure there is text, and that it is not control characters
|
||||
// (e.g. delete is sent as "\u{f728}" on macOS).
|
||||
if !text.is_empty() && text.chars().all(is_printable_char) {
|
||||
@@ -803,7 +809,7 @@ impl State {
|
||||
if pressed && !is_cmd {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Text(text.to_string()));
|
||||
.push(egui::Event::Text(text.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,6 +856,9 @@ impl State {
|
||||
egui::OutputCommand::OpenUrl(open_url) => {
|
||||
open_url_in_browser(&open_url.url);
|
||||
}
|
||||
egui::OutputCommand::SetPointerPosition(egui::Pos2 { x, y }) => {
|
||||
let _ = window.set_cursor_position(winit::dpi::LogicalPosition { x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1610,6 +1619,7 @@ pub fn create_winit_window_attributes(
|
||||
|
||||
// macOS:
|
||||
fullsize_content_view: _fullsize_content_view,
|
||||
movable_by_window_background: _movable_by_window_background,
|
||||
title_shown: _title_shown,
|
||||
titlebar_buttons_shown: _titlebar_buttons_shown,
|
||||
titlebar_shown: _titlebar_shown,
|
||||
@@ -1756,7 +1766,8 @@ pub fn create_winit_window_attributes(
|
||||
.with_title_hidden(!_title_shown.unwrap_or(true))
|
||||
.with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
|
||||
.with_titlebar_transparent(!_titlebar_shown.unwrap_or(true))
|
||||
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false));
|
||||
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false))
|
||||
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false));
|
||||
}
|
||||
|
||||
window_attributes
|
||||
@@ -1824,6 +1835,9 @@ pub fn apply_viewport_builder_to_window(
|
||||
let pos = PhysicalPosition::new(pixels_per_point * pos.x, pixels_per_point * pos.y);
|
||||
window.set_outer_position(pos);
|
||||
}
|
||||
if let Some(maximized) = builder.maximized {
|
||||
window.set_maximized(maximized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ pub struct WindowSettings {
|
||||
|
||||
fullscreen: bool,
|
||||
|
||||
maximized: bool,
|
||||
|
||||
/// Inner size of window in logical pixels
|
||||
inner_size_points: Option<egui::Vec2>,
|
||||
}
|
||||
@@ -38,6 +40,7 @@ impl WindowSettings {
|
||||
outer_position_pixels,
|
||||
|
||||
fullscreen: window.fullscreen().is_some(),
|
||||
maximized: window.is_maximized(),
|
||||
|
||||
inner_size_points: Some(egui::vec2(
|
||||
inner_size_points.width,
|
||||
@@ -80,7 +83,8 @@ impl WindowSettings {
|
||||
if let Some(inner_size_points) = self.inner_size_points {
|
||||
viewport_builder = viewport_builder
|
||||
.with_inner_size(inner_size_points)
|
||||
.with_fullscreen(self.fullscreen);
|
||||
.with_fullscreen(self.fullscreen)
|
||||
.with_maximized(self.maximized);
|
||||
}
|
||||
|
||||
viewport_builder
|
||||
|
||||
@@ -84,11 +84,12 @@ emath = { workspace = true, default-features = false }
|
||||
epaint = { workspace = true, default-features = false }
|
||||
|
||||
ahash.workspace = true
|
||||
bitflags.workspace = true
|
||||
nohash-hasher.workspace = true
|
||||
profiling.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
accesskit = { version = "0.17.0", optional = true }
|
||||
accesskit = { workspace = true, optional = true }
|
||||
|
||||
backtrace = { workspace = true, optional = true }
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
use emath::GuiRounding as _;
|
||||
|
||||
use crate::{
|
||||
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
|
||||
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
|
||||
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect,
|
||||
Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
|
||||
};
|
||||
|
||||
/// State of an [`Area`] that is persisted between frames.
|
||||
@@ -103,10 +103,10 @@ impl AreaState {
|
||||
///
|
||||
/// The previous rectangle used by this area can be obtained through [`crate::Memory::area_rect()`].
|
||||
#[must_use = "You should call .show()"]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Area {
|
||||
pub(crate) id: Id,
|
||||
kind: UiKind,
|
||||
info: UiStackInfo,
|
||||
sense: Option<Sense>,
|
||||
movable: bool,
|
||||
interactable: bool,
|
||||
@@ -120,6 +120,7 @@ pub struct Area {
|
||||
anchor: Option<(Align2, Vec2)>,
|
||||
new_pos: Option<Pos2>,
|
||||
fade_in: bool,
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl WidgetWithState for Area {
|
||||
@@ -131,7 +132,7 @@ impl Area {
|
||||
pub fn new(id: Id) -> Self {
|
||||
Self {
|
||||
id,
|
||||
kind: UiKind::GenericArea,
|
||||
info: UiStackInfo::new(UiKind::GenericArea),
|
||||
sense: None,
|
||||
movable: true,
|
||||
interactable: true,
|
||||
@@ -145,6 +146,7 @@ impl Area {
|
||||
pivot: Align2::LEFT_TOP,
|
||||
anchor: None,
|
||||
fade_in: true,
|
||||
layout: Layout::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +164,16 @@ impl Area {
|
||||
/// Default to [`UiKind::GenericArea`].
|
||||
#[inline]
|
||||
pub fn kind(mut self, kind: UiKind) -> Self {
|
||||
self.kind = kind;
|
||||
self.info = UiStackInfo::new(kind);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`UiStackInfo`] of the area's [`Ui`].
|
||||
///
|
||||
/// Default to [`UiStackInfo::new(UiKind::GenericArea)`].
|
||||
#[inline]
|
||||
pub fn info(mut self, info: UiStackInfo) -> Self {
|
||||
self.info = info;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -339,10 +350,17 @@ impl Area {
|
||||
self.fade_in = fade_in;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the layout for the child Ui.
|
||||
#[inline]
|
||||
pub fn layout(mut self, layout: Layout) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Prepared {
|
||||
kind: UiKind,
|
||||
info: Option<UiStackInfo>,
|
||||
layer_id: LayerId,
|
||||
state: AreaState,
|
||||
move_response: Response,
|
||||
@@ -358,6 +376,7 @@ pub(crate) struct Prepared {
|
||||
sizing_pass: bool,
|
||||
|
||||
fade_in: bool,
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
@@ -366,7 +385,7 @@ impl Area {
|
||||
ctx: &Context,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let prepared = self.begin(ctx);
|
||||
let mut prepared = self.begin(ctx);
|
||||
let mut content_ui = prepared.content_ui(ctx);
|
||||
let inner = add_contents(&mut content_ui);
|
||||
let response = prepared.end(ctx, content_ui);
|
||||
@@ -376,7 +395,7 @@ impl Area {
|
||||
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
|
||||
let Self {
|
||||
id,
|
||||
kind,
|
||||
info,
|
||||
sense,
|
||||
movable,
|
||||
order,
|
||||
@@ -390,6 +409,7 @@ impl Area {
|
||||
constrain,
|
||||
constrain_rect,
|
||||
fade_in,
|
||||
layout,
|
||||
} = self;
|
||||
|
||||
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
|
||||
@@ -507,7 +527,7 @@ impl Area {
|
||||
move_response.interact_rect = state.rect();
|
||||
|
||||
Prepared {
|
||||
kind,
|
||||
info: Some(info),
|
||||
layer_id,
|
||||
state,
|
||||
move_response,
|
||||
@@ -516,6 +536,7 @@ impl Area {
|
||||
constrain_rect,
|
||||
sizing_pass,
|
||||
fade_in,
|
||||
layout,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,13 +558,15 @@ impl Prepared {
|
||||
self.constrain_rect
|
||||
}
|
||||
|
||||
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
|
||||
pub(crate) fn content_ui(&mut self, ctx: &Context) -> Ui {
|
||||
let max_rect = self.state.rect();
|
||||
|
||||
let mut ui_builder = UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(self.kind))
|
||||
.ui_stack_info(self.info.take().unwrap_or_default())
|
||||
.layer_id(self.layer_id)
|
||||
.max_rect(max_rect);
|
||||
.max_rect(max_rect)
|
||||
.layout(self.layout)
|
||||
.closable();
|
||||
|
||||
if !self.enabled {
|
||||
ui_builder = ui_builder.disabled();
|
||||
@@ -582,7 +605,7 @@ impl Prepared {
|
||||
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
|
||||
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
|
||||
let Self {
|
||||
kind: _,
|
||||
info: _,
|
||||
layer_id,
|
||||
mut state,
|
||||
move_response: mut response,
|
||||
@@ -598,6 +621,12 @@ impl Prepared {
|
||||
response.rect = final_rect;
|
||||
response.interact_rect = final_rect;
|
||||
|
||||
// TODO(lucasmerlin): Can the area response be based on Ui::response? Then this won't be needed
|
||||
// Bubble up the close event
|
||||
if content_ui.should_close() {
|
||||
response.set_close();
|
||||
}
|
||||
|
||||
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
|
||||
|
||||
if sizing_pass {
|
||||
|
||||
20
crates/egui/src/containers/close_tag.rs
Normal file
20
crates/egui/src/containers/close_tag.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ClosableTag {
|
||||
pub close: AtomicBool,
|
||||
}
|
||||
|
||||
impl ClosableTag {
|
||||
pub const NAME: &'static str = "egui_close_tag";
|
||||
|
||||
/// Set close to `true`
|
||||
pub fn set_close(&self) {
|
||||
self.close.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Returns `true` if [`ClosableTag::set_close`] has been called.
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.close.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ use std::hash::Hash;
|
||||
|
||||
use crate::{
|
||||
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
|
||||
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
|
||||
WidgetInfo, WidgetText, WidgetType,
|
||||
};
|
||||
use epaint::Shape;
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::{Shape, StrokeKind};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -202,11 +204,16 @@ impl CollapsingState {
|
||||
add_body: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let openness = self.openness(ui.ctx());
|
||||
|
||||
let builder = UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(UiKind::Collapsible))
|
||||
.closable();
|
||||
|
||||
if openness <= 0.0 {
|
||||
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
|
||||
None
|
||||
} else if openness < 1.0 {
|
||||
Some(ui.scope(|child_ui| {
|
||||
Some(ui.scope_builder(builder, |child_ui| {
|
||||
let max_height = if self.state.open && self.state.open_height.is_none() {
|
||||
// First frame of expansion.
|
||||
// We don't know full height yet, but we will next frame.
|
||||
@@ -214,7 +221,7 @@ impl CollapsingState {
|
||||
10.0
|
||||
} else {
|
||||
let full_height = self.state.open_height.unwrap_or_default();
|
||||
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
|
||||
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height).round_ui()
|
||||
};
|
||||
|
||||
let mut clip_rect = child_ui.clip_rect();
|
||||
@@ -225,6 +232,9 @@ impl CollapsingState {
|
||||
|
||||
let mut min_rect = child_ui.min_rect();
|
||||
self.state.open_height = Some(min_rect.height());
|
||||
if child_ui.should_close() {
|
||||
self.state.open = false;
|
||||
}
|
||||
self.store(child_ui.ctx()); // remember the height
|
||||
|
||||
// Pretend children took up at most `max_height` space:
|
||||
@@ -233,7 +243,10 @@ impl CollapsingState {
|
||||
ret
|
||||
}))
|
||||
} else {
|
||||
let ret_response = ui.scope(add_body);
|
||||
let ret_response = ui.scope_builder(builder, add_body);
|
||||
if ret_response.response.should_close() {
|
||||
self.state.open = false;
|
||||
}
|
||||
let full_size = ret_response.response.rect.size();
|
||||
self.state.open_height = Some(full_size.y);
|
||||
self.store(ui.ctx()); // remember the height
|
||||
@@ -282,7 +295,7 @@ pub struct HeaderResponse<'ui, HeaderRet> {
|
||||
header_response: InnerResponse<HeaderRet>,
|
||||
}
|
||||
|
||||
impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
|
||||
impl<HeaderRet> HeaderResponse<'_, HeaderRet> {
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.state.is_open()
|
||||
}
|
||||
@@ -572,9 +585,10 @@ impl CollapsingHeader {
|
||||
if ui.visuals().collapsing_header_frame || show_background {
|
||||
ui.painter().add(epaint::RectShape::new(
|
||||
header_response.rect.expand(visuals.expansion),
|
||||
visuals.rounding,
|
||||
visuals.corner_radius,
|
||||
visuals.weak_bg_fill,
|
||||
visuals.bg_stroke,
|
||||
StrokeKind::Inside,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -582,8 +596,13 @@ impl CollapsingHeader {
|
||||
{
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
|
||||
ui.painter()
|
||||
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
|
||||
ui.painter().rect(
|
||||
rect,
|
||||
visuals.corner_radius,
|
||||
visuals.bg_fill,
|
||||
visuals.bg_stroke,
|
||||
StrokeKind::Inside,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
use epaint::Shape;
|
||||
|
||||
use crate::{
|
||||
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter,
|
||||
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
|
||||
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
|
||||
NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
|
||||
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)] // Documentation
|
||||
use crate::style::Spacing;
|
||||
|
||||
/// Indicate whether a popup will be shown above or below the box.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum AboveOrBelow {
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// A function that paints the [`ComboBox`] icon
|
||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
|
||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
|
||||
|
||||
/// A drop-down selection menu with a descriptive label.
|
||||
///
|
||||
@@ -135,7 +128,6 @@ impl ComboBox {
|
||||
/// rect: egui::Rect,
|
||||
/// visuals: &egui::style::WidgetVisuals,
|
||||
/// _is_open: bool,
|
||||
/// _above_or_below: egui::AboveOrBelow,
|
||||
/// ) {
|
||||
/// let rect = egui::Rect::from_center_size(
|
||||
/// rect.center(),
|
||||
@@ -154,10 +146,8 @@ impl ComboBox {
|
||||
/// .show_ui(ui, |_ui| {});
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn icon(
|
||||
mut self,
|
||||
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
|
||||
) -> Self {
|
||||
#[inline]
|
||||
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self {
|
||||
self.icon = Some(Box::new(icon_fn));
|
||||
self
|
||||
}
|
||||
@@ -322,22 +312,6 @@ fn combo_box_dyn<'c, R>(
|
||||
|
||||
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
|
||||
|
||||
let popup_height = ui.memory(|m| {
|
||||
m.areas()
|
||||
.get(popup_id)
|
||||
.and_then(|state| state.size)
|
||||
.map_or(100.0, |size| size.y)
|
||||
});
|
||||
|
||||
let above_or_below =
|
||||
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
|
||||
< ui.ctx().screen_rect().bottom()
|
||||
{
|
||||
AboveOrBelow::Below
|
||||
} else {
|
||||
AboveOrBelow::Above
|
||||
};
|
||||
|
||||
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
|
||||
|
||||
let close_behavior = close_behavior.unwrap_or(PopupCloseBehavior::CloseOnClick);
|
||||
@@ -385,15 +359,9 @@ fn combo_box_dyn<'c, R>(
|
||||
icon_rect.expand(visuals.expansion),
|
||||
visuals,
|
||||
is_popup_open,
|
||||
above_or_below,
|
||||
);
|
||||
} else {
|
||||
paint_default_icon(
|
||||
ui.painter(),
|
||||
icon_rect.expand(visuals.expansion),
|
||||
visuals,
|
||||
above_or_below,
|
||||
);
|
||||
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
||||
}
|
||||
|
||||
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
||||
@@ -402,19 +370,16 @@ fn combo_box_dyn<'c, R>(
|
||||
}
|
||||
});
|
||||
|
||||
if button_response.clicked() {
|
||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
}
|
||||
|
||||
let height = height.unwrap_or_else(|| ui.spacing().combo_height);
|
||||
|
||||
let inner = crate::popup::popup_above_or_below_widget(
|
||||
ui,
|
||||
popup_id,
|
||||
&button_response,
|
||||
above_or_below,
|
||||
close_behavior,
|
||||
|ui| {
|
||||
let inner = Popup::menu(&button_response)
|
||||
.id(popup_id)
|
||||
.style(StyleModifier::default())
|
||||
.width(button_response.rect.width())
|
||||
.close_behavior(close_behavior)
|
||||
.show(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
|
||||
ScrollArea::vertical()
|
||||
.max_height(height)
|
||||
.show(ui, |ui| {
|
||||
@@ -427,8 +392,8 @@ fn combo_box_dyn<'c, R>(
|
||||
menu_contents(ui)
|
||||
})
|
||||
.inner
|
||||
},
|
||||
);
|
||||
})
|
||||
.map(|r| r.inner);
|
||||
|
||||
InnerResponse {
|
||||
inner,
|
||||
@@ -471,9 +436,10 @@ fn button_frame(
|
||||
where_to_put_background,
|
||||
epaint::RectShape::new(
|
||||
outer_rect.expand(visuals.expansion),
|
||||
visuals.rounding,
|
||||
visuals.corner_radius,
|
||||
visuals.weak_bg_fill,
|
||||
visuals.bg_stroke,
|
||||
epaint::StrokeKind::Inside,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -483,33 +449,19 @@ fn button_frame(
|
||||
response
|
||||
}
|
||||
|
||||
fn paint_default_icon(
|
||||
painter: &Painter,
|
||||
rect: Rect,
|
||||
visuals: &WidgetVisuals,
|
||||
above_or_below: AboveOrBelow,
|
||||
) {
|
||||
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
|
||||
let rect = Rect::from_center_size(
|
||||
rect.center(),
|
||||
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
||||
);
|
||||
|
||||
match above_or_below {
|
||||
AboveOrBelow::Above => {
|
||||
// Upward pointing triangle
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
AboveOrBelow::Below => {
|
||||
// Downward pointing triangle
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
}
|
||||
// Downward pointing triangle
|
||||
// Previously, we would show an up arrow when we expected the popup to open upwards
|
||||
// (due to lack of space below the button), but this could look weird in edge cases, so this
|
||||
// feature was removed. (See https://github.com/emilk/egui/pull/5713#issuecomment-2654420245)
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4,9 +4,45 @@ use crate::{
|
||||
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
|
||||
UiStackInfo,
|
||||
};
|
||||
use epaint::{Color32, Margin, Marginf, Rect, Rounding, Shadow, Shape, Stroke};
|
||||
use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stroke};
|
||||
|
||||
/// Add a background, frame and/or margin to a rectangular background of a [`Ui`].
|
||||
/// A frame around some content, including margin, colors, etc.
|
||||
///
|
||||
/// ## Definitions
|
||||
/// The total (outer) size of a frame is
|
||||
/// `content_size + inner_margin + 2 * stroke.width + outer_margin`.
|
||||
///
|
||||
/// Everything within the stroke is filled with the fill color (if any).
|
||||
///
|
||||
/// ```text
|
||||
/// +-----------------^-------------------------------------- -+
|
||||
/// | | outer_margin |
|
||||
/// | +------------v----^------------------------------+ |
|
||||
/// | | | stroke width | |
|
||||
/// | | +------------v---^---------------------+ | |
|
||||
/// | | | | inner_margin | | |
|
||||
/// | | | +-----------v----------------+ | | |
|
||||
/// | | | | ^ | | | |
|
||||
/// | | | | | | | | |
|
||||
/// | | | |<------ content_size ------>| | | |
|
||||
/// | | | | | | | | |
|
||||
/// | | | | v | | | |
|
||||
/// | | | +------- content_rect -------+ | | |
|
||||
/// | | | | | |
|
||||
/// | | +-------------fill_rect ---------------+ | |
|
||||
/// | | | |
|
||||
/// | +----------------- widget_rect ------------------+ |
|
||||
/// | |
|
||||
/// +---------------------- outer_rect ------------------------+
|
||||
/// ```
|
||||
///
|
||||
/// The four rectangles, from inside to outside, are:
|
||||
/// * `content_rect`: the rectangle that is made available to the inner [`Ui`] or widget.
|
||||
/// * `fill_rect`: the rectangle that is filled with the fill color (inside the stroke, if any).
|
||||
/// * `widget_rect`: is the interactive part of the widget (what sense clicks etc).
|
||||
/// * `outer_rect`: what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`].
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
@@ -58,19 +94,50 @@ use epaint::{Color32, Margin, Marginf, Rect, Rounding, Shadow, Shape, Stroke};
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[must_use = "You should call .show()"]
|
||||
pub struct Frame {
|
||||
// Fields are ordered inside-out.
|
||||
// TODO(emilk): add `min_content_size: Vec2`
|
||||
//
|
||||
/// Margin within the painted frame.
|
||||
///
|
||||
/// Known as `padding` in CSS.
|
||||
#[doc(alias = "padding")]
|
||||
pub inner_margin: Margin,
|
||||
|
||||
/// Margin outside the painted frame.
|
||||
pub outer_margin: Margin,
|
||||
|
||||
pub rounding: Rounding,
|
||||
|
||||
pub shadow: Shadow,
|
||||
|
||||
/// The background fill color of the frame, within the [`Self::stroke`].
|
||||
///
|
||||
/// Known as `background` in CSS.
|
||||
#[doc(alias = "background")]
|
||||
pub fill: Color32,
|
||||
|
||||
/// The width and color of the outline around the frame.
|
||||
///
|
||||
/// The width of the stroke is part of the total margin/padding of the frame.
|
||||
#[doc(alias = "border")]
|
||||
pub stroke: Stroke,
|
||||
|
||||
/// 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_.
|
||||
pub corner_radius: CornerRadius,
|
||||
|
||||
/// Margin outside the painted frame.
|
||||
///
|
||||
/// Similar to what is called `margin` in CSS.
|
||||
/// However, egui does NOT do "Margin Collapse" like in CSS,
|
||||
/// i.e. when placing two frames next to each other,
|
||||
/// the distance between their borders is the SUM
|
||||
/// of their other margins.
|
||||
/// In CSS the distance would be the MAX of their outer margins.
|
||||
/// Supporting margin collapse is difficult, and would
|
||||
/// requires complicating the already complicated egui layout code.
|
||||
///
|
||||
/// Consider using [`crate::Spacing::item_spacing`]
|
||||
/// for adding space between widgets.
|
||||
pub outer_margin: Margin,
|
||||
|
||||
/// Optional drop-shadow behind the frame.
|
||||
pub shadow: Shadow,
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -85,68 +152,75 @@ fn frame_size() {
|
||||
);
|
||||
}
|
||||
|
||||
/// ## Constructors
|
||||
impl Frame {
|
||||
pub fn none() -> Self {
|
||||
Self::default()
|
||||
/// No colors, no margins, no border.
|
||||
///
|
||||
/// This is also the default.
|
||||
pub const NONE: Self = Self {
|
||||
inner_margin: Margin::ZERO,
|
||||
stroke: Stroke::NONE,
|
||||
fill: Color32::TRANSPARENT,
|
||||
corner_radius: CornerRadius::ZERO,
|
||||
outer_margin: Margin::ZERO,
|
||||
shadow: Shadow::NONE,
|
||||
};
|
||||
|
||||
/// No colors, no margins, no border.
|
||||
///
|
||||
/// Same as [`Frame::NONE`].
|
||||
pub const fn new() -> Self {
|
||||
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 {
|
||||
inner_margin: Margin::same(6), // same and symmetric looks best in corners when nesting groups
|
||||
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||
stroke: style.visuals.widgets.noninteractive.bg_stroke,
|
||||
..Default::default()
|
||||
}
|
||||
Self::new()
|
||||
.inner_margin(6)
|
||||
.corner_radius(style.visuals.widgets.noninteractive.corner_radius)
|
||||
.stroke(style.visuals.widgets.noninteractive.bg_stroke)
|
||||
}
|
||||
|
||||
pub fn side_top_panel(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: Margin::symmetric(8, 2),
|
||||
fill: style.visuals.panel_fill,
|
||||
..Default::default()
|
||||
}
|
||||
Self::new()
|
||||
.inner_margin(Margin::symmetric(8, 2))
|
||||
.fill(style.visuals.panel_fill)
|
||||
}
|
||||
|
||||
pub fn central_panel(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: Margin::same(8),
|
||||
fill: style.visuals.panel_fill,
|
||||
..Default::default()
|
||||
}
|
||||
Self::new().inner_margin(8).fill(style.visuals.panel_fill)
|
||||
}
|
||||
|
||||
pub fn window(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: style.spacing.window_margin,
|
||||
rounding: style.visuals.window_rounding,
|
||||
shadow: style.visuals.window_shadow,
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: style.visuals.window_stroke(),
|
||||
..Default::default()
|
||||
}
|
||||
Self::new()
|
||||
.inner_margin(style.spacing.window_margin)
|
||||
.corner_radius(style.visuals.window_corner_radius)
|
||||
.shadow(style.visuals.window_shadow)
|
||||
.fill(style.visuals.window_fill())
|
||||
.stroke(style.visuals.window_stroke())
|
||||
}
|
||||
|
||||
pub fn menu(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: style.spacing.menu_margin,
|
||||
rounding: style.visuals.menu_rounding,
|
||||
shadow: style.visuals.popup_shadow,
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: style.visuals.window_stroke(),
|
||||
..Default::default()
|
||||
}
|
||||
Self::new()
|
||||
.inner_margin(style.spacing.menu_margin)
|
||||
.corner_radius(style.visuals.menu_corner_radius)
|
||||
.shadow(style.visuals.popup_shadow)
|
||||
.fill(style.visuals.window_fill())
|
||||
.stroke(style.visuals.window_stroke())
|
||||
}
|
||||
|
||||
pub fn popup(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: style.spacing.menu_margin,
|
||||
rounding: style.visuals.menu_rounding,
|
||||
shadow: style.visuals.popup_shadow,
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: style.visuals.window_stroke(),
|
||||
..Default::default()
|
||||
}
|
||||
Self::new()
|
||||
.inner_margin(style.spacing.menu_margin)
|
||||
.corner_radius(style.visuals.menu_corner_radius)
|
||||
.shadow(style.visuals.popup_shadow)
|
||||
.fill(style.visuals.window_fill())
|
||||
.stroke(style.visuals.window_stroke())
|
||||
}
|
||||
|
||||
/// A canvas to draw on.
|
||||
@@ -154,57 +228,90 @@ impl Frame {
|
||||
/// In bright mode this will be very bright,
|
||||
/// and in dark mode this will be very dark.
|
||||
pub fn canvas(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: Margin::same(2),
|
||||
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||
fill: style.visuals.extreme_bg_color,
|
||||
stroke: style.visuals.window_stroke(),
|
||||
..Default::default()
|
||||
}
|
||||
Self::new()
|
||||
.inner_margin(2)
|
||||
.corner_radius(style.visuals.widgets.noninteractive.corner_radius)
|
||||
.fill(style.visuals.extreme_bg_color)
|
||||
.stroke(style.visuals.window_stroke())
|
||||
}
|
||||
|
||||
/// A dark canvas to draw on.
|
||||
pub fn dark_canvas(style: &Style) -> Self {
|
||||
Self {
|
||||
fill: Color32::from_black_alpha(250),
|
||||
..Self::canvas(style)
|
||||
}
|
||||
Self::canvas(style).fill(Color32::from_black_alpha(250))
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Builders
|
||||
impl Frame {
|
||||
#[inline]
|
||||
pub fn fill(mut self, fill: Color32) -> Self {
|
||||
self.fill = fill;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = stroke.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||
self.rounding = rounding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Margin within the painted frame.
|
||||
///
|
||||
/// Known as `padding` in CSS.
|
||||
#[doc(alias = "padding")]
|
||||
#[inline]
|
||||
pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
|
||||
self.inner_margin = inner_margin.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// The background fill color of the frame, within the [`Self::stroke`].
|
||||
///
|
||||
/// Known as `background` in CSS.
|
||||
#[doc(alias = "background")]
|
||||
#[inline]
|
||||
pub fn fill(mut self, fill: Color32) -> Self {
|
||||
self.fill = fill;
|
||||
self
|
||||
}
|
||||
|
||||
/// The width and color of the outline around the frame.
|
||||
///
|
||||
/// The width of the stroke is part of the total margin/padding of the frame.
|
||||
#[inline]
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = stroke.into();
|
||||
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]
|
||||
pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
|
||||
self.corner_radius = corner_radius.into();
|
||||
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.
|
||||
/// However, egui does NOT do "Margin Collapse" like in CSS,
|
||||
/// i.e. when placing two frames next to each other,
|
||||
/// the distance between their borders is the SUM
|
||||
/// of their other margins.
|
||||
/// In CSS the distance would be the MAX of their outer margins.
|
||||
/// Supporting margin collapse is difficult, and would
|
||||
/// requires complicating the already complicated egui layout code.
|
||||
///
|
||||
/// Consider using [`crate::Spacing::item_spacing`]
|
||||
/// for adding space between widgets.
|
||||
#[inline]
|
||||
pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
|
||||
self.outer_margin = outer_margin.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Optional drop-shadow behind the frame.
|
||||
#[inline]
|
||||
pub fn shadow(mut self, shadow: Shadow) -> Self {
|
||||
self.shadow = shadow;
|
||||
@@ -224,11 +331,37 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Inspectors
|
||||
impl Frame {
|
||||
/// Inner margin plus outer margin.
|
||||
/// How much extra space the frame uses up compared to the content.
|
||||
///
|
||||
/// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`].
|
||||
#[inline]
|
||||
pub fn total_margin(&self) -> Marginf {
|
||||
Marginf::from(self.inner_margin) + Marginf::from(self.outer_margin)
|
||||
pub fn total_margin(&self) -> MarginF32 {
|
||||
MarginF32::from(self.inner_margin)
|
||||
+ MarginF32::from(self.stroke.width)
|
||||
+ MarginF32::from(self.outer_margin)
|
||||
}
|
||||
|
||||
/// Calculate the `fill_rect` from the `content_rect`.
|
||||
///
|
||||
/// This is the rectangle that is filled with the fill color (inside the stroke, if any).
|
||||
pub fn fill_rect(&self, content_rect: Rect) -> Rect {
|
||||
content_rect + self.inner_margin
|
||||
}
|
||||
|
||||
/// Calculate the `widget_rect` from the `content_rect`.
|
||||
///
|
||||
/// This is the visible and interactive rectangle.
|
||||
pub fn widget_rect(&self, content_rect: Rect) -> Rect {
|
||||
content_rect + self.inner_margin + MarginF32::from(self.stroke.width)
|
||||
}
|
||||
|
||||
/// Calculate the `outer_rect` from the `content_rect`.
|
||||
///
|
||||
/// This is what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`].
|
||||
pub fn outer_rect(&self, content_rect: Rect) -> Rect {
|
||||
content_rect + self.inner_margin + MarginF32::from(self.stroke.width) + self.outer_margin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,20 +392,18 @@ impl Frame {
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let outer_rect_bounds = ui.available_rect_before_wrap();
|
||||
|
||||
let mut inner_rect = outer_rect_bounds - self.outer_margin - self.inner_margin;
|
||||
let mut max_content_rect = outer_rect_bounds - self.total_margin();
|
||||
|
||||
// Make sure we don't shrink to the negative:
|
||||
inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
|
||||
inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y);
|
||||
max_content_rect.max.x = max_content_rect.max.x.max(max_content_rect.min.x);
|
||||
max_content_rect.max.y = max_content_rect.max.y.max(max_content_rect.min.y);
|
||||
|
||||
let content_ui = ui.new_child(
|
||||
UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(UiKind::Frame).with_frame(self))
|
||||
.max_rect(inner_rect),
|
||||
.max_rect(max_content_rect),
|
||||
);
|
||||
|
||||
// content_ui.set_clip_rect(outer_rect_bounds.shrink(self.stroke.width * 0.5)); // Can't do this since we don't know final size yet
|
||||
|
||||
Prepared {
|
||||
frame: self,
|
||||
where_to_put_background,
|
||||
@@ -298,32 +429,42 @@ impl Frame {
|
||||
}
|
||||
|
||||
/// Paint this frame as a shape.
|
||||
///
|
||||
/// The margin is ignored.
|
||||
pub fn paint(&self, outer_rect: Rect) -> Shape {
|
||||
pub fn paint(&self, content_rect: Rect) -> Shape {
|
||||
let Self {
|
||||
inner_margin: _,
|
||||
outer_margin: _,
|
||||
rounding,
|
||||
shadow,
|
||||
fill,
|
||||
stroke,
|
||||
corner_radius,
|
||||
outer_margin: _,
|
||||
shadow,
|
||||
} = *self;
|
||||
|
||||
let frame_shape = Shape::Rect(epaint::RectShape::new(outer_rect, rounding, fill, stroke));
|
||||
let widget_rect = self.widget_rect(content_rect);
|
||||
|
||||
let frame_shape = Shape::Rect(epaint::RectShape::new(
|
||||
widget_rect,
|
||||
corner_radius,
|
||||
fill,
|
||||
stroke,
|
||||
epaint::StrokeKind::Inside,
|
||||
));
|
||||
|
||||
if shadow == Default::default() {
|
||||
frame_shape
|
||||
} else {
|
||||
let shadow = shadow.as_shape(outer_rect, rounding);
|
||||
let shadow = shadow.as_shape(widget_rect, corner_radius);
|
||||
Shape::Vec(vec![Shape::from(shadow), frame_shape])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
fn content_with_margin(&self) -> Rect {
|
||||
self.content_ui.min_rect() + self.frame.inner_margin + self.frame.outer_margin
|
||||
fn outer_rect(&self) -> Rect {
|
||||
let content_rect = self.content_ui.min_rect();
|
||||
content_rect
|
||||
+ self.frame.inner_margin
|
||||
+ MarginF32::from(self.frame.stroke.width)
|
||||
+ self.frame.outer_margin
|
||||
}
|
||||
|
||||
/// Allocate the space that was used by [`Self::content_ui`].
|
||||
@@ -332,22 +473,25 @@ impl Prepared {
|
||||
///
|
||||
/// This can be called before or after [`Self::paint`].
|
||||
pub fn allocate_space(&self, ui: &mut Ui) -> Response {
|
||||
ui.allocate_rect(self.content_with_margin(), Sense::hover())
|
||||
ui.allocate_rect(self.outer_rect(), Sense::hover())
|
||||
}
|
||||
|
||||
/// Paint the frame.
|
||||
///
|
||||
/// This can be called before or after [`Self::allocate_space`].
|
||||
pub fn paint(&self, ui: &Ui) {
|
||||
let paint_rect = self.content_ui.min_rect() + self.frame.inner_margin;
|
||||
let content_rect = self.content_ui.min_rect();
|
||||
let widget_rect = self.frame.widget_rect(content_rect);
|
||||
|
||||
if ui.is_rect_visible(paint_rect) {
|
||||
let shape = self.frame.paint(paint_rect);
|
||||
if ui.is_rect_visible(widget_rect) {
|
||||
let shape = self.frame.paint(content_rect);
|
||||
ui.painter().set(self.where_to_put_background, shape);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience for calling [`Self::allocate_space`] and [`Self::paint`].
|
||||
///
|
||||
/// Returns the outer rect, i.e. including the outer margin.
|
||||
pub fn end(self, ui: &mut Ui) -> Response {
|
||||
self.paint(ui);
|
||||
self.allocate_space(ui)
|
||||
|
||||
527
crates/egui/src/containers/menu.rs
Normal file
527
crates/egui/src/containers/menu.rs
Normal file
@@ -0,0 +1,527 @@
|
||||
use crate::style::StyleModifier;
|
||||
use crate::{
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior,
|
||||
Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, WidgetText,
|
||||
};
|
||||
use emath::{vec2, Align, RectAlign, Vec2};
|
||||
use epaint::Stroke;
|
||||
|
||||
/// Apply a menu style to the [`Style`].
|
||||
///
|
||||
/// Mainly removes the background stroke and the inactive background fill.
|
||||
pub fn menu_style(style: &mut Style) {
|
||||
style.spacing.button_padding = vec2(2.0, 0.0);
|
||||
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
|
||||
style.visuals.widgets.open.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;
|
||||
}
|
||||
|
||||
/// Find the root [`UiStack`] of the menu.
|
||||
pub fn find_menu_root(ui: &Ui) -> &UiStack {
|
||||
ui.stack()
|
||||
.iter()
|
||||
.find(|stack| {
|
||||
stack.is_root_ui()
|
||||
|| [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind())
|
||||
|| stack.info.tags.contains(MenuConfig::MENU_CONFIG_TAG)
|
||||
})
|
||||
.expect("We should always find the root")
|
||||
}
|
||||
|
||||
/// Is this Ui part of a menu?
|
||||
///
|
||||
/// Returns `false` if this is a menu bar.
|
||||
/// Should be used to determine if we should show a menu button or submenu button.
|
||||
pub fn is_in_menu(ui: &Ui) -> bool {
|
||||
for stack in ui.stack().iter() {
|
||||
if let Some(config) = stack
|
||||
.info
|
||||
.tags
|
||||
.get_downcast::<MenuConfig>(MenuConfig::MENU_CONFIG_TAG)
|
||||
{
|
||||
return !config.bar;
|
||||
}
|
||||
if [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MenuConfig {
|
||||
/// Is this a menu bar?
|
||||
bar: bool,
|
||||
|
||||
/// If the user clicks, should we close the menu?
|
||||
pub close_behavior: PopupCloseBehavior,
|
||||
|
||||
/// Override the menu style.
|
||||
///
|
||||
/// Default is [`menu_style`].
|
||||
pub style: StyleModifier,
|
||||
}
|
||||
|
||||
impl Default for MenuConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
close_behavior: PopupCloseBehavior::default(),
|
||||
bar: false,
|
||||
style: menu_style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MenuConfig {
|
||||
/// The tag used to store the menu config in the [`UiStack`].
|
||||
pub const MENU_CONFIG_TAG: &'static str = "egui_menu_config";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// If the user clicks, should we close the menu?
|
||||
#[inline]
|
||||
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
|
||||
self.close_behavior = close_behavior;
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the menu style.
|
||||
///
|
||||
/// Default is [`menu_style`].
|
||||
#[inline]
|
||||
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn from_stack(stack: &UiStack) -> Self {
|
||||
stack
|
||||
.info
|
||||
.tags
|
||||
.get_downcast(Self::MENU_CONFIG_TAG)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Find the config for the current menu.
|
||||
///
|
||||
/// Returns the default config if no config is found.
|
||||
pub fn find(ui: &Ui) -> Self {
|
||||
find_menu_root(ui)
|
||||
.info
|
||||
.tags
|
||||
.get_downcast(Self::MENU_CONFIG_TAG)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MenuState {
|
||||
pub open_item: Option<Id>,
|
||||
last_visible_pass: u64,
|
||||
}
|
||||
|
||||
impl MenuState {
|
||||
pub const ID: &'static str = "menu_state";
|
||||
|
||||
/// Find the root of the menu and get the state
|
||||
pub fn from_ui<R>(ui: &Ui, f: impl FnOnce(&mut Self, &UiStack) -> R) -> R {
|
||||
let stack = find_menu_root(ui);
|
||||
Self::from_id(ui.ctx(), stack.id, |state| f(state, stack))
|
||||
}
|
||||
|
||||
/// Get the state via the menus root [`Ui`] id
|
||||
pub fn from_id<R>(ctx: &Context, id: Id, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
let pass_nr = ctx.cumulative_pass_nr();
|
||||
ctx.data_mut(|data| {
|
||||
let state = data.get_temp_mut_or_insert_with(id.with(Self::ID), || Self {
|
||||
open_item: None,
|
||||
last_visible_pass: pass_nr,
|
||||
});
|
||||
// If the menu was closed for at least a frame, reset the open item
|
||||
if state.last_visible_pass + 1 < pass_nr {
|
||||
state.open_item = None;
|
||||
}
|
||||
state.last_visible_pass = pass_nr;
|
||||
f(state)
|
||||
})
|
||||
}
|
||||
|
||||
/// Is the menu with this id the deepest sub menu? (-> no child sub menu is open)
|
||||
pub fn is_deepest_sub_menu(ctx: &Context, id: Id) -> bool {
|
||||
Self::from_id(ctx, id, |state| state.open_item.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal menu bar where you can add [`MenuButton`]s.
|
||||
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
|
||||
/// but can also be placed in a [`crate::Window`].
|
||||
/// In the latter case you may want to wrap it in [`Frame`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Bar {
|
||||
config: MenuConfig,
|
||||
style: StyleModifier,
|
||||
}
|
||||
|
||||
impl Default for Bar {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: MenuConfig::default(),
|
||||
style: menu_style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Bar {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the style for buttons in the menu bar.
|
||||
///
|
||||
/// Doesn't affect the style of submenus, use [`MenuConfig::style`] for that.
|
||||
/// Default is [`menu_style`].
|
||||
#[inline]
|
||||
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the config for submenus.
|
||||
///
|
||||
/// Note: The config will only be passed when using [`MenuButton`], not via [`Popup::menu`].
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the menu bar.
|
||||
#[inline]
|
||||
pub fn ui<R>(self, ui: &mut Ui, content: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
let Self { mut config, style } = self;
|
||||
config.bar = true;
|
||||
// TODO(lucasmerlin): It'd be nice if we had a ui.horizontal_builder or something
|
||||
// So we don't need the nested scope here
|
||||
ui.horizontal(|ui| {
|
||||
ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.layout(Layout::left_to_right(Align::Center))
|
||||
.ui_stack_info(
|
||||
UiStackInfo::new(UiKind::Menu)
|
||||
.with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
|
||||
),
|
||||
|ui| {
|
||||
style.apply(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));
|
||||
|
||||
content(ui)
|
||||
},
|
||||
)
|
||||
.inner
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A thin wrapper around a [`Button`] that shows a [`Popup::menu`] when clicked.
|
||||
///
|
||||
/// The only thing this does is search for the current menu config (if set via [`Bar`]).
|
||||
/// If your menu button is not in a [`Bar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
|
||||
/// directly.
|
||||
pub struct MenuButton<'a> {
|
||||
pub button: Button<'a>,
|
||||
pub config: Option<MenuConfig>,
|
||||
}
|
||||
|
||||
impl<'a> MenuButton<'a> {
|
||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
||||
Self::from_button(Button::new(text))
|
||||
}
|
||||
|
||||
/// Set the config for the menu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new menu button from a [`Button`].
|
||||
#[inline]
|
||||
pub fn from_button(button: Button<'a>) -> Self {
|
||||
Self {
|
||||
button,
|
||||
config: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the menu button.
|
||||
pub fn ui<R>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
content: impl FnOnce(&mut Ui) -> R,
|
||||
) -> (Response, Option<InnerResponse<R>>) {
|
||||
let response = self.button.ui(ui);
|
||||
let mut config = self.config.unwrap_or_else(|| MenuConfig::find(ui));
|
||||
config.bar = false;
|
||||
let inner = Popup::menu(&response)
|
||||
.close_behavior(config.close_behavior)
|
||||
.style(config.style.clone())
|
||||
.info(
|
||||
UiStackInfo::new(UiKind::Menu).with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
|
||||
)
|
||||
.show(content);
|
||||
(response, inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A submenu button that shows a [`SubMenu`] if a [`Button`] is hovered.
|
||||
pub struct SubMenuButton<'a> {
|
||||
pub button: Button<'a>,
|
||||
pub sub_menu: SubMenu,
|
||||
}
|
||||
|
||||
impl<'a> SubMenuButton<'a> {
|
||||
/// The default right arrow symbol: `"⏵"`
|
||||
pub const RIGHT_ARROW: &'static str = "⏵";
|
||||
|
||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
||||
Self::from_button(Button::new(text).right_text("⏵"))
|
||||
}
|
||||
|
||||
/// Create a new submenu button from a [`Button`].
|
||||
///
|
||||
/// Use [`Button::right_text`] and [`SubMenuButton::RIGHT_ARROW`] to add the default right
|
||||
/// arrow symbol.
|
||||
pub fn from_button(button: Button<'a>) -> Self {
|
||||
Self {
|
||||
button,
|
||||
sub_menu: SubMenu::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the config for the submenu.
|
||||
///
|
||||
/// The close behavior will not affect the current button, but the buttons in the submenu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.sub_menu.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the submenu button.
|
||||
pub fn ui<R>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
content: impl FnOnce(&mut Ui) -> R,
|
||||
) -> (Response, Option<InnerResponse<R>>) {
|
||||
let my_id = ui.next_auto_id();
|
||||
let open = MenuState::from_ui(ui, |state, _| {
|
||||
state.open_item == Some(SubMenu::id_from_widget_id(my_id))
|
||||
});
|
||||
let inactive = ui.style().visuals.widgets.inactive;
|
||||
// TODO(lucasmerlin) add `open` function to `Button`
|
||||
if open {
|
||||
ui.style_mut().visuals.widgets.inactive = ui.style().visuals.widgets.open;
|
||||
}
|
||||
let response = self.button.ui(ui);
|
||||
ui.style_mut().visuals.widgets.inactive = inactive;
|
||||
|
||||
let popup_response = self.sub_menu.show(ui, &response, content);
|
||||
|
||||
(response, popup_response)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SubMenu {
|
||||
config: Option<MenuConfig>,
|
||||
}
|
||||
|
||||
impl SubMenu {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the config for the submenu.
|
||||
///
|
||||
/// The close behavior will not affect the current button, but the buttons in the submenu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the id for the submenu from the widget/response id.
|
||||
pub fn id_from_widget_id(widget_id: Id) -> Id {
|
||||
widget_id.with("submenu")
|
||||
}
|
||||
|
||||
/// Show the submenu.
|
||||
pub fn show<R>(
|
||||
self,
|
||||
ui: &Ui,
|
||||
button_response: &Response,
|
||||
content: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let frame = Frame::menu(ui.style());
|
||||
|
||||
let id = Self::id_from_widget_id(button_response.id);
|
||||
|
||||
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
|
||||
(state.open_item, stack.id, MenuConfig::from_stack(stack))
|
||||
});
|
||||
|
||||
let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone());
|
||||
menu_config.bar = false;
|
||||
|
||||
let menu_root_response = ui
|
||||
.ctx()
|
||||
.read_response(menu_id)
|
||||
// Since we are a child of that ui, this should always exist
|
||||
.unwrap();
|
||||
|
||||
let hover_pos = ui.ctx().pointer_hover_pos();
|
||||
|
||||
// We don't care if the user is hovering over the border
|
||||
let menu_rect = menu_root_response.rect - frame.total_margin();
|
||||
let is_hovering_menu = hover_pos.is_some_and(|pos| {
|
||||
ui.ctx().layer_id_at(pos) == Some(menu_root_response.layer_id)
|
||||
&& menu_rect.contains(pos)
|
||||
});
|
||||
|
||||
let is_any_open = open_item.is_some();
|
||||
let mut is_open = open_item == Some(id);
|
||||
let mut set_open = None;
|
||||
|
||||
// We expand the button rect so there is no empty space where no menu is shown
|
||||
// TODO(lucasmerlin): Instead, maybe make item_spacing.y 0.0?
|
||||
let button_rect = button_response
|
||||
.rect
|
||||
.expand2(ui.style().spacing.item_spacing / 2.0);
|
||||
|
||||
// In theory some other widget could cover the button and this check would still pass
|
||||
// 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));
|
||||
|
||||
// The clicked handler is there for accessibility (keyboard navigation)
|
||||
if (!is_any_open && is_hovered) || button_response.clicked() {
|
||||
set_open = Some(true);
|
||||
is_open = true;
|
||||
// Ensure that all other sub menus are closed when we open the menu
|
||||
MenuState::from_id(ui.ctx(), id, |state| {
|
||||
state.open_item = None;
|
||||
});
|
||||
}
|
||||
|
||||
let gap = frame.total_margin().sum().x / 2.0 + 2.0;
|
||||
|
||||
let mut response = button_response.clone();
|
||||
// Expand the button rect so that the button and the first item in the submenu are aligned
|
||||
let expand = Vec2::new(0.0, frame.total_margin().sum().y / 2.0);
|
||||
response.interact_rect = response.interact_rect.expand2(expand);
|
||||
|
||||
let popup_response = Popup::from_response(&response)
|
||||
.id(id)
|
||||
.open(is_open)
|
||||
.align(RectAlign::RIGHT_START)
|
||||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.gap(gap)
|
||||
.style(menu_config.style.clone())
|
||||
.frame(frame)
|
||||
// The close behavior is handled by the menu (see below)
|
||||
.close_behavior(PopupCloseBehavior::IgnoreClicks)
|
||||
.info(
|
||||
UiStackInfo::new(UiKind::Menu)
|
||||
.with_tag_value(MenuConfig::MENU_CONFIG_TAG, menu_config.clone()),
|
||||
)
|
||||
.show(|ui| {
|
||||
// Ensure our layer stays on top when the button is clicked
|
||||
if button_response.clicked() || button_response.is_pointer_button_down_on() {
|
||||
ui.ctx().move_to_top(ui.layer_id());
|
||||
}
|
||||
content(ui)
|
||||
});
|
||||
|
||||
if let Some(popup_response) = &popup_response {
|
||||
// If no child sub menu is open means we must be the deepest child sub menu.
|
||||
let is_deepest_submenu = MenuState::is_deepest_sub_menu(ui.ctx(), id);
|
||||
|
||||
// If the user clicks and the cursor is not hovering over our menu rect, it's
|
||||
// safe to assume they clicked outside the menu, so we close everything.
|
||||
// If they were to hover some other parent submenu we wouldn't be open.
|
||||
// Only edge case is the user hovering this submenu's button, so we also check
|
||||
// if we clicked outside the parent menu (which we luckily have access to here).
|
||||
let clicked_outside = is_deepest_submenu
|
||||
&& popup_response.response.clicked_elsewhere()
|
||||
&& menu_root_response.clicked_elsewhere();
|
||||
|
||||
// We never automatically close when a submenu button is clicked, (so menus work
|
||||
// on touch devices)
|
||||
// Luckily we will always be the deepest submenu when a submenu button is clicked,
|
||||
// so the following check is enough.
|
||||
let submenu_button_clicked = button_response.clicked();
|
||||
|
||||
let clicked_inside = is_deepest_submenu
|
||||
&& !submenu_button_clicked
|
||||
&& response.ctx.input(|i| i.pointer.any_click())
|
||||
&& hover_pos.is_some_and(|pos| popup_response.response.interact_rect.contains(pos));
|
||||
|
||||
let click_close = match menu_config.close_behavior {
|
||||
PopupCloseBehavior::CloseOnClick => clicked_outside || clicked_inside,
|
||||
PopupCloseBehavior::CloseOnClickOutside => clicked_outside,
|
||||
PopupCloseBehavior::IgnoreClicks => false,
|
||||
};
|
||||
|
||||
if click_close {
|
||||
set_open = Some(false);
|
||||
ui.close();
|
||||
}
|
||||
|
||||
let is_moving_towards_rect = ui.input(|i| {
|
||||
i.pointer
|
||||
.is_moving_towards_rect(&popup_response.response.rect)
|
||||
});
|
||||
if is_moving_towards_rect {
|
||||
// We need to repaint while this is true, so we can detect when
|
||||
// the pointer is no longer moving towards the rect
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
let hovering_other_menu_entry = is_open
|
||||
&& !is_hovered
|
||||
&& !popup_response.response.contains_pointer()
|
||||
&& !is_moving_towards_rect
|
||||
&& is_hovering_menu;
|
||||
|
||||
let close_called = popup_response.response.should_close();
|
||||
|
||||
// Close the parent ui to e.g. close the popup from where the submenu was opened
|
||||
if close_called {
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if hovering_other_menu_entry {
|
||||
set_open = Some(false);
|
||||
}
|
||||
|
||||
if ui.will_parent_close() {
|
||||
ui.data_mut(|data| data.remove_by_type::<MenuState>());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(set_open) = set_open {
|
||||
MenuState::from_id(ui.ctx(), menu_id, |state| {
|
||||
state.open_item = set_open.then_some(id);
|
||||
});
|
||||
}
|
||||
|
||||
popup_response
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,20 @@
|
||||
//! For instance, a [`Frame`] adds a frame and background to some contained UI.
|
||||
|
||||
pub(crate) mod area;
|
||||
pub mod close_tag;
|
||||
pub mod collapsing_header;
|
||||
mod combo_box;
|
||||
pub mod frame;
|
||||
pub mod menu;
|
||||
pub mod modal;
|
||||
pub mod old_popup;
|
||||
pub mod panel;
|
||||
pub mod popup;
|
||||
mod popup;
|
||||
pub(crate) mod resize;
|
||||
mod scene;
|
||||
pub mod scroll_area;
|
||||
mod sides;
|
||||
mod tooltip;
|
||||
pub(crate) mod window;
|
||||
|
||||
pub use {
|
||||
@@ -20,10 +25,13 @@ pub use {
|
||||
combo_box::*,
|
||||
frame::Frame,
|
||||
modal::{Modal, ModalResponse},
|
||||
old_popup::*,
|
||||
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
||||
popup::*,
|
||||
resize::Resize,
|
||||
scene::Scene,
|
||||
scroll_area::ScrollArea,
|
||||
sides::Sides,
|
||||
tooltip::*,
|
||||
window::Window,
|
||||
};
|
||||
|
||||
@@ -4,11 +4,14 @@ use crate::{
|
||||
use emath::{Align2, Vec2};
|
||||
|
||||
/// A modal dialog.
|
||||
///
|
||||
/// Similar to a [`crate::Window`] but centered and with a backdrop that
|
||||
/// blocks input to the rest of the UI.
|
||||
///
|
||||
/// You can show multiple modals on top of each other. The topmost modal will always be
|
||||
/// the most recently shown one.
|
||||
/// If multiple modals are newly shown in the same frame, the order of the modals not undefined
|
||||
/// (either first or second could be top).
|
||||
pub struct Modal {
|
||||
pub area: Area,
|
||||
pub backdrop_color: Color32,
|
||||
@@ -16,7 +19,9 @@ pub struct Modal {
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
/// Create a new Modal. The id is passed to the area.
|
||||
/// Create a new Modal.
|
||||
///
|
||||
/// The id is passed to the area.
|
||||
pub fn new(id: Id) -> Self {
|
||||
Self {
|
||||
area: Self::default_area(id),
|
||||
@@ -26,6 +31,7 @@ impl Modal {
|
||||
}
|
||||
|
||||
/// Returns an area customized for a modal.
|
||||
///
|
||||
/// Makes these changes to the default area:
|
||||
/// - sense: hover
|
||||
/// - anchor: center
|
||||
@@ -86,11 +92,7 @@ impl Modal {
|
||||
response,
|
||||
} = area.show(ctx, |ui| {
|
||||
let bg_rect = ui.ctx().screen_rect();
|
||||
let bg_sense = Sense {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: false,
|
||||
};
|
||||
let bg_sense = Sense::CLICK | Sense::DRAG;
|
||||
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
|
||||
backdrop.set_min_size(bg_rect.size());
|
||||
ui.painter().rect_filled(bg_rect, 0.0, backdrop_color);
|
||||
@@ -101,14 +103,9 @@ impl Modal {
|
||||
// We need the extra scope with the sense since frame can't have a sense and since we
|
||||
// need to prevent the clicks from passing through to the backdrop.
|
||||
let inner = ui
|
||||
.scope_builder(
|
||||
UiBuilder::new().sense(Sense {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: false,
|
||||
}),
|
||||
|ui| frame.show(ui, content).inner,
|
||||
)
|
||||
.scope_builder(UiBuilder::new().sense(Sense::CLICK | Sense::DRAG), |ui| {
|
||||
frame.show(ui, content).inner
|
||||
})
|
||||
.inner;
|
||||
|
||||
(inner, backdrop_response)
|
||||
@@ -159,7 +156,10 @@ impl<T> ModalResponse<T> {
|
||||
let escape_clicked =
|
||||
|| ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
|
||||
|
||||
let ui_close_called = self.response.should_close();
|
||||
|
||||
self.backdrop_response.clicked()
|
||||
|| ui_close_called
|
||||
|| (self.is_top_modal && !self.any_popup_open && escape_clicked())
|
||||
}
|
||||
}
|
||||
|
||||
212
crates/egui/src/containers/old_popup.rs
Normal file
212
crates/egui/src/containers/old_popup.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
//! Old and deprecated API for popups. Use [`Popup`] instead.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use crate::containers::tooltip::Tooltip;
|
||||
use crate::{
|
||||
Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect,
|
||||
Response, Ui, Widget, 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| {
|
||||
/// # #[allow(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::new(widget_id, ctx.clone(), PopupAnchor::Pointer, parent_layer)
|
||||
.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::new(widget_id, ctx.clone(), *widget_rect, parent_layer)
|
||||
.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::new(widget_id, ctx.clone(), suggested_position, parent_layer)
|
||||
.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;
|
||||
/// # #[allow(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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -356,6 +356,7 @@ impl Resize {
|
||||
rect,
|
||||
3.0,
|
||||
ui.visuals().widgets.noninteractive.bg_stroke,
|
||||
epaint::StrokeKind::Inside,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
221
crates/egui/src/containers/scene.rs
Normal file
221
crates/egui/src/containers/scene.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use core::f32;
|
||||
|
||||
use emath::{GuiRounding, Pos2};
|
||||
|
||||
use crate::{
|
||||
emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
|
||||
};
|
||||
|
||||
/// Creates a transformation that fits a given scene rectangle into the available screen size.
|
||||
///
|
||||
/// The resulting visual scene bounds can be larger, due to letterboxing.
|
||||
///
|
||||
/// Returns the transformation from `scene` to `global` coordinates.
|
||||
fn fit_to_rect_in_scene(
|
||||
rect_in_global: Rect,
|
||||
rect_in_scene: Rect,
|
||||
zoom_range: Rangef,
|
||||
) -> TSTransform {
|
||||
// Compute the scale factor to fit the bounding rectangle into the available screen size:
|
||||
let scale = rect_in_global.size() / rect_in_scene.size();
|
||||
|
||||
// Use the smaller of the two scales to ensure the whole rectangle fits on the screen:
|
||||
let scale = scale.min_elem();
|
||||
|
||||
// Clamp scale to what is allowed
|
||||
let scale = zoom_range.clamp(scale);
|
||||
|
||||
// Compute the translation to center the bounding rect in the screen:
|
||||
let center_in_global = rect_in_global.center().to_vec2();
|
||||
let center_scene = rect_in_scene.center().to_vec2();
|
||||
|
||||
// Set the transformation to scale and then translate to center.
|
||||
TSTransform::from_translation(center_in_global - scale * center_scene)
|
||||
* TSTransform::from_scaling(scale)
|
||||
}
|
||||
|
||||
/// A container that allows you to zoom and pan.
|
||||
///
|
||||
/// This is similar to [`crate::ScrollArea`] but:
|
||||
/// * Supports zooming
|
||||
/// * Has no scroll bars
|
||||
/// * Has no limits on the scrolling
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use = "You should call .show()"]
|
||||
pub struct Scene {
|
||||
zoom_range: Rangef,
|
||||
max_inner_size: Vec2,
|
||||
}
|
||||
|
||||
impl Default for Scene {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
zoom_range: Rangef::new(f32::EPSILON, 1.0),
|
||||
max_inner_size: Vec2::splat(1000.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Set the allowed zoom range.
|
||||
///
|
||||
/// The default zoom range is `0.0..=1.0`,
|
||||
/// which mean you zan make things arbitrarily small, but you cannot zoom in past a `1:1` ratio.
|
||||
///
|
||||
/// If you want to allow zooming in, you can set the zoom range to `0.0..=f32::INFINITY`.
|
||||
/// Note that text rendering becomes blurry when you zoom in: <https://github.com/emilk/egui/issues/4813>.
|
||||
#[inline]
|
||||
pub fn zoom_range(mut self, zoom_range: impl Into<Rangef>) -> Self {
|
||||
self.zoom_range = zoom_range.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum size of the inner [`Ui`] that will be created.
|
||||
#[inline]
|
||||
pub fn max_inner_size(mut self, max_inner_size: impl Into<Vec2>) -> Self {
|
||||
self.max_inner_size = max_inner_size.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// `scene_rect` contains the view bounds of the inner [`Ui`].
|
||||
///
|
||||
/// `scene_rect` will be mutated by any panning/zooming done by the user.
|
||||
/// If `scene_rect` is somehow invalid (e.g. `Rect::ZERO`),
|
||||
/// then it will be reset to the inner rect of the inner ui.
|
||||
///
|
||||
/// You need to store the `scene_rect` in your state between frames.
|
||||
pub fn show<R>(
|
||||
&self,
|
||||
parent_ui: &mut Ui,
|
||||
scene_rect: &mut Rect,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let (outer_rect, _outer_response) =
|
||||
parent_ui.allocate_exact_size(parent_ui.available_size_before_wrap(), Sense::hover());
|
||||
|
||||
let mut to_global = fit_to_rect_in_scene(outer_rect, *scene_rect, self.zoom_range);
|
||||
|
||||
let scene_rect_was_good =
|
||||
to_global.is_valid() && scene_rect.is_finite() && scene_rect.size() != Vec2::ZERO;
|
||||
|
||||
let mut inner_rect = *scene_rect;
|
||||
|
||||
let ret = self.show_global_transform(parent_ui, outer_rect, &mut to_global, |ui| {
|
||||
let r = add_contents(ui);
|
||||
inner_rect = ui.min_rect();
|
||||
r
|
||||
});
|
||||
|
||||
if ret.response.changed() {
|
||||
// Only update if changed, both to avoid numeric drift,
|
||||
// and to avoid expanding the scene rect unnecessarily.
|
||||
*scene_rect = to_global.inverse() * outer_rect;
|
||||
}
|
||||
|
||||
if !scene_rect_was_good {
|
||||
// Auto-reset if the transformation goes bad somehow (or started bad).
|
||||
// Recalculates transform based on inner_rect, resulting in a rect that's the full size of outer_rect but centered on inner_rect.
|
||||
let to_global = fit_to_rect_in_scene(outer_rect, inner_rect, self.zoom_range);
|
||||
*scene_rect = to_global.inverse() * outer_rect;
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn show_global_transform<R>(
|
||||
&self,
|
||||
parent_ui: &mut Ui,
|
||||
outer_rect: Rect,
|
||||
to_global: &mut TSTransform,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
// Create a new egui paint layer, where we can draw our contents:
|
||||
let scene_layer_id = LayerId::new(
|
||||
parent_ui.layer_id().order,
|
||||
parent_ui.id().with("scene_area"),
|
||||
);
|
||||
|
||||
// Put the layer directly on-top of the main layer of the ui:
|
||||
parent_ui
|
||||
.ctx()
|
||||
.set_sublayer(parent_ui.layer_id(), scene_layer_id);
|
||||
|
||||
let mut local_ui = parent_ui.new_child(
|
||||
UiBuilder::new()
|
||||
.layer_id(scene_layer_id)
|
||||
.max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size))
|
||||
.sense(Sense::click_and_drag()),
|
||||
);
|
||||
|
||||
let mut pan_response = local_ui.response();
|
||||
|
||||
// Update the `to_global` transform based on use interaction:
|
||||
self.register_pan_and_zoom(&local_ui, &mut pan_response, to_global);
|
||||
|
||||
// Set a correct global clip rect:
|
||||
local_ui.set_clip_rect(to_global.inverse() * outer_rect);
|
||||
|
||||
// Add the actual contents to the area:
|
||||
let ret = add_contents(&mut local_ui);
|
||||
|
||||
// This ensures we catch clicks/drags/pans anywhere on the background.
|
||||
local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui());
|
||||
|
||||
// Tell egui to apply the transform on the layer:
|
||||
local_ui
|
||||
.ctx()
|
||||
.set_transform_layer(scene_layer_id, *to_global);
|
||||
|
||||
InnerResponse {
|
||||
response: pan_response,
|
||||
inner: ret,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to handle pan and zoom interactions on a response.
|
||||
pub fn register_pan_and_zoom(&self, ui: &Ui, resp: &mut Response, to_global: &mut TSTransform) {
|
||||
if resp.dragged() {
|
||||
to_global.translation += to_global.scaling * resp.drag_delta();
|
||||
resp.mark_changed();
|
||||
}
|
||||
|
||||
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos()) {
|
||||
if resp.contains_pointer() {
|
||||
let pointer_in_scene = to_global.inverse() * mouse_pos;
|
||||
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
|
||||
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
|
||||
|
||||
// Most of the time we can return early. This is also important to
|
||||
// avoid `ui_from_scene` to change slightly due to floating point errors.
|
||||
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
|
||||
return;
|
||||
}
|
||||
|
||||
if zoom_delta != 1.0 {
|
||||
// Zoom in on pointer, but only if we are not zoomed in or out too far.
|
||||
let zoom_delta = zoom_delta.clamp(
|
||||
self.zoom_range.min / to_global.scaling,
|
||||
self.zoom_range.max / to_global.scaling,
|
||||
);
|
||||
|
||||
*to_global = *to_global
|
||||
* TSTransform::from_translation(pointer_in_scene.to_vec2())
|
||||
* TSTransform::from_scaling(zoom_delta)
|
||||
* TSTransform::from_translation(-pointer_in_scene.to_vec2());
|
||||
|
||||
// Clamp to exact zoom range.
|
||||
to_global.scaling = self.zoom_range.clamp(to_global.scaling);
|
||||
}
|
||||
|
||||
// Pan:
|
||||
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
|
||||
resp.mark_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
use crate::{
|
||||
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2,
|
||||
Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
|
||||
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt, Pos2, Rangef,
|
||||
Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -161,6 +161,9 @@ impl ScrollBarVisibility {
|
||||
/// ```
|
||||
///
|
||||
/// You can scroll to an element using [`crate::Response::scroll_to_me`], [`Ui::scroll_to_cursor`] and [`Ui::scroll_to_rect`].
|
||||
///
|
||||
/// ## See also
|
||||
/// If you want to allow zooming, use [`crate::Scene`].
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use = "You should call .show()"]
|
||||
pub struct ScrollArea {
|
||||
@@ -773,7 +776,7 @@ impl ScrollArea {
|
||||
|
||||
let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
|
||||
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |viewport_ui| {
|
||||
ui.scope_builder(UiBuilder::new().max_rect(rect), |viewport_ui| {
|
||||
viewport_ui.skip_ahead_auto_ids(min_row); // Make sure we get consistent IDs.
|
||||
add_contents(viewport_ui, min_row..max_row)
|
||||
})
|
||||
@@ -1087,21 +1090,35 @@ impl Prepared {
|
||||
)
|
||||
};
|
||||
|
||||
let handle_rect = if d == 0 {
|
||||
Rect::from_min_max(
|
||||
pos2(from_content(state.offset.x), cross.min),
|
||||
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
|
||||
)
|
||||
} else {
|
||||
Rect::from_min_max(
|
||||
pos2(cross.min, from_content(state.offset.y)),
|
||||
pos2(
|
||||
cross.max,
|
||||
from_content(state.offset.y + inner_rect.height()),
|
||||
),
|
||||
)
|
||||
let calculate_handle_rect = |d, offset: &Vec2| {
|
||||
let handle_size = if d == 0 {
|
||||
from_content(offset.x + inner_rect.width()) - from_content(offset.x)
|
||||
} else {
|
||||
from_content(offset.y + inner_rect.height()) - from_content(offset.y)
|
||||
}
|
||||
.max(scroll_style.handle_min_length);
|
||||
|
||||
let handle_start_point = remap_clamp(
|
||||
offset[d],
|
||||
0.0..=max_offset[d],
|
||||
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
|
||||
);
|
||||
|
||||
if d == 0 {
|
||||
Rect::from_min_max(
|
||||
pos2(handle_start_point, cross.min),
|
||||
pos2(handle_start_point + handle_size, cross.max),
|
||||
)
|
||||
} else {
|
||||
Rect::from_min_max(
|
||||
pos2(cross.min, handle_start_point),
|
||||
pos2(cross.max, handle_start_point + handle_size),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let handle_rect = calculate_handle_rect(d, &state.offset);
|
||||
|
||||
let interact_id = id.with(d);
|
||||
let sense = if self.scrolling_enabled {
|
||||
Sense::click_and_drag()
|
||||
@@ -1130,8 +1147,8 @@ impl Prepared {
|
||||
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
|
||||
state.offset[d] = remap(
|
||||
new_handle_top,
|
||||
scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
|
||||
0.0..=content_size[d],
|
||||
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]),
|
||||
0.0..=max_offset[d],
|
||||
);
|
||||
|
||||
// some manual action taken, scroll not stuck
|
||||
@@ -1151,31 +1168,7 @@ impl Prepared {
|
||||
|
||||
if ui.is_rect_visible(outer_scroll_bar_rect) {
|
||||
// Avoid frame-delay by calculating a new handle rect:
|
||||
let mut handle_rect = if d == 0 {
|
||||
Rect::from_min_max(
|
||||
pos2(from_content(state.offset.x), cross.min),
|
||||
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
|
||||
)
|
||||
} else {
|
||||
Rect::from_min_max(
|
||||
pos2(cross.min, from_content(state.offset.y)),
|
||||
pos2(
|
||||
cross.max,
|
||||
from_content(state.offset.y + inner_rect.height()),
|
||||
),
|
||||
)
|
||||
};
|
||||
let min_handle_size = scroll_style.handle_min_length;
|
||||
if handle_rect.size()[d] < min_handle_size {
|
||||
handle_rect = Rect::from_center_size(
|
||||
handle_rect.center(),
|
||||
if d == 0 {
|
||||
vec2(min_handle_size, handle_rect.size().y)
|
||||
} else {
|
||||
vec2(handle_rect.size().x, min_handle_size)
|
||||
},
|
||||
);
|
||||
}
|
||||
let handle_rect = calculate_handle_rect(d, &state.offset);
|
||||
|
||||
let visuals = if scrolling_enabled {
|
||||
// Pick visuals based on interaction with the handle.
|
||||
@@ -1184,7 +1177,7 @@ impl Prepared {
|
||||
&& ui.input(|i| {
|
||||
i.pointer
|
||||
.latest_pos()
|
||||
.map_or(false, |p| handle_rect.contains(p))
|
||||
.is_some_and(|p| handle_rect.contains(p))
|
||||
});
|
||||
let visuals = ui.visuals();
|
||||
if response.is_pointer_button_down_on() {
|
||||
@@ -1237,7 +1230,7 @@ impl Prepared {
|
||||
// Background:
|
||||
ui.painter().add(epaint::Shape::rect_filled(
|
||||
outer_scroll_bar_rect,
|
||||
visuals.rounding,
|
||||
visuals.corner_radius,
|
||||
ui.visuals()
|
||||
.extreme_bg_color
|
||||
.gamma_multiply(background_opacity),
|
||||
@@ -1246,7 +1239,7 @@ impl Prepared {
|
||||
// Handle:
|
||||
ui.painter().add(epaint::Shape::rect_filled(
|
||||
handle_rect,
|
||||
visuals.rounding,
|
||||
visuals.corner_radius,
|
||||
handle_color.gamma_multiply(handle_opacity),
|
||||
));
|
||||
}
|
||||
|
||||
376
crates/egui/src/containers/tooltip.rs
Normal file
376
crates/egui/src/containers/tooltip.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
use crate::pass_state::PerWidgetTooltipState;
|
||||
use crate::{
|
||||
AreaState, Context, Id, InnerResponse, LayerId, Layout, Order, Popup, PopupAnchor, PopupKind,
|
||||
Response, Sense,
|
||||
};
|
||||
use emath::Vec2;
|
||||
|
||||
pub struct Tooltip<'a> {
|
||||
pub popup: Popup<'a>,
|
||||
layer_id: LayerId,
|
||||
widget_id: Id,
|
||||
}
|
||||
|
||||
impl Tooltip<'_> {
|
||||
/// Show a tooltip that is always open
|
||||
pub fn new(
|
||||
widget_id: Id,
|
||||
ctx: Context,
|
||||
anchor: impl Into<PopupAnchor>,
|
||||
layer_id: LayerId,
|
||||
) -> Self {
|
||||
Self {
|
||||
// TODO(lucasmerlin): Set width somehow (we're missing context here)
|
||||
popup: Popup::new(widget_id, ctx, anchor.into(), layer_id)
|
||||
.kind(PopupKind::Tooltip)
|
||||
.gap(4.0)
|
||||
.sense(Sense::hover()),
|
||||
layer_id,
|
||||
widget_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a tooltip for a widget. Always open (as long as this function is called).
|
||||
pub fn for_widget(response: &Response) -> Self {
|
||||
let popup = Popup::from_response(response)
|
||||
.kind(PopupKind::Tooltip)
|
||||
.gap(4.0)
|
||||
.width(response.ctx.style().spacing.tooltip_width)
|
||||
.sense(Sense::hover());
|
||||
Self {
|
||||
popup,
|
||||
layer_id: response.layer_id,
|
||||
widget_id: response.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a tooltip when hovering an enabled widget.
|
||||
pub fn for_enabled(response: &Response) -> Self {
|
||||
let mut tooltip = Self::for_widget(response);
|
||||
tooltip.popup = tooltip
|
||||
.popup
|
||||
.open(response.enabled() && Self::should_show_tooltip(response));
|
||||
tooltip
|
||||
}
|
||||
|
||||
/// Show a tooltip when hovering a disabled widget.
|
||||
pub fn for_disabled(response: &Response) -> Self {
|
||||
let mut tooltip = Self::for_widget(response);
|
||||
tooltip.popup = tooltip
|
||||
.popup
|
||||
.open(!response.enabled() && Self::should_show_tooltip(response));
|
||||
tooltip
|
||||
}
|
||||
|
||||
/// Show the tooltip at the pointer position.
|
||||
#[inline]
|
||||
pub fn at_pointer(mut self) -> Self {
|
||||
self.popup = self.popup.at_pointer();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the gap between the tooltip and the anchor
|
||||
///
|
||||
/// Default: 5.0
|
||||
#[inline]
|
||||
pub fn gap(mut self, gap: f32) -> Self {
|
||||
self.popup = self.popup.gap(gap);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the layout of the tooltip
|
||||
#[inline]
|
||||
pub fn layout(mut self, layout: Layout) -> Self {
|
||||
self.popup = self.popup.layout(layout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the width of the tooltip
|
||||
#[inline]
|
||||
pub fn width(mut self, width: f32) -> Self {
|
||||
self.popup = self.popup.width(width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the tooltip
|
||||
pub fn show<R>(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option<InnerResponse<R>> {
|
||||
let Self {
|
||||
mut popup,
|
||||
layer_id: parent_layer,
|
||||
widget_id,
|
||||
} = self;
|
||||
|
||||
if !popup.is_open() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rect = popup.get_anchor_rect()?;
|
||||
|
||||
let mut state = popup.ctx().pass_state_mut(|fs| {
|
||||
// Remember that this is the widget showing the tooltip:
|
||||
fs.layers
|
||||
.entry(parent_layer)
|
||||
.or_default()
|
||||
.widget_with_tooltip = Some(widget_id);
|
||||
|
||||
fs.tooltips
|
||||
.widget_tooltips
|
||||
.get(&widget_id)
|
||||
.copied()
|
||||
.unwrap_or(PerWidgetTooltipState {
|
||||
bounding_rect: rect,
|
||||
tooltip_count: 0,
|
||||
})
|
||||
});
|
||||
|
||||
let tooltip_area_id = Self::tooltip_id(widget_id, state.tooltip_count);
|
||||
popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
|
||||
|
||||
let response = popup.show(|ui| {
|
||||
// By default, the text in tooltips aren't selectable.
|
||||
// This means that most tooltips aren't interactable,
|
||||
// which also mean they won't stick around so you can click them.
|
||||
// Only tooltips that have actual interactive stuff (buttons, links, …)
|
||||
// will stick around when you try to click them.
|
||||
ui.style_mut().interaction.selectable_labels = false;
|
||||
|
||||
content(ui)
|
||||
});
|
||||
|
||||
// The popup might not be shown on at_pointer if there is no pointer.
|
||||
if let Some(response) = &response {
|
||||
state.tooltip_count += 1;
|
||||
state.bounding_rect = state.bounding_rect.union(response.response.rect);
|
||||
response
|
||||
.response
|
||||
.ctx
|
||||
.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
|
||||
Self::remember_that_tooltip_was_shown(&response.response.ctx);
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
fn when_was_a_toolip_last_shown_id() -> Id {
|
||||
Id::new("when_was_a_toolip_last_shown")
|
||||
}
|
||||
|
||||
pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 {
|
||||
let when_was_a_toolip_last_shown =
|
||||
ctx.data(|d| d.get_temp::<f64>(Self::when_was_a_toolip_last_shown_id()));
|
||||
|
||||
if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown {
|
||||
let now = ctx.input(|i| i.time);
|
||||
(now - when_was_a_toolip_last_shown) as f32
|
||||
} else {
|
||||
f32::INFINITY
|
||||
}
|
||||
}
|
||||
|
||||
fn remember_that_tooltip_was_shown(ctx: &Context) {
|
||||
let now = ctx.input(|i| i.time);
|
||||
ctx.data_mut(|data| data.insert_temp::<f64>(Self::when_was_a_toolip_last_shown_id(), now));
|
||||
}
|
||||
|
||||
/// What is the id of the next tooltip for this widget?
|
||||
pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
|
||||
let tooltip_count = ctx.pass_state(|fs| {
|
||||
fs.tooltips
|
||||
.widget_tooltips
|
||||
.get(&widget_id)
|
||||
.map_or(0, |state| state.tooltip_count)
|
||||
});
|
||||
Self::tooltip_id(widget_id, tooltip_count)
|
||||
}
|
||||
|
||||
pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
|
||||
widget_id.with(tooltip_count)
|
||||
}
|
||||
|
||||
/// Should we show a tooltip for this response?
|
||||
pub fn should_show_tooltip(response: &Response) -> bool {
|
||||
if response.ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let any_open_popups = response.ctx.prev_pass_state(|fs| {
|
||||
fs.layers
|
||||
.get(&response.layer_id)
|
||||
.is_some_and(|layer| !layer.open_popups.is_empty())
|
||||
});
|
||||
if any_open_popups {
|
||||
// Hide tooltips if the user opens a popup (menu, combo-box, etc.) in the same layer.
|
||||
return false;
|
||||
}
|
||||
|
||||
let style = response.ctx.style();
|
||||
|
||||
let tooltip_delay = style.interaction.tooltip_delay;
|
||||
let tooltip_grace_time = style.interaction.tooltip_grace_time;
|
||||
|
||||
let (
|
||||
time_since_last_scroll,
|
||||
time_since_last_click,
|
||||
time_since_last_pointer_movement,
|
||||
pointer_pos,
|
||||
pointer_dir,
|
||||
) = response.ctx.input(|i| {
|
||||
(
|
||||
i.time_since_last_scroll(),
|
||||
i.pointer.time_since_last_click(),
|
||||
i.pointer.time_since_last_movement(),
|
||||
i.pointer.hover_pos(),
|
||||
i.pointer.direction(),
|
||||
)
|
||||
});
|
||||
|
||||
if time_since_last_scroll < tooltip_delay {
|
||||
// See https://github.com/emilk/egui/issues/4781
|
||||
// Note that this means we cannot have `ScrollArea`s in a tooltip.
|
||||
response
|
||||
.ctx
|
||||
.request_repaint_after_secs(tooltip_delay - time_since_last_scroll);
|
||||
return false;
|
||||
}
|
||||
|
||||
let is_our_tooltip_open = response.is_tooltip_open();
|
||||
|
||||
if is_our_tooltip_open {
|
||||
// Check if we should automatically stay open:
|
||||
|
||||
let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
|
||||
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
||||
|
||||
let tooltip_has_interactive_widget = response.ctx.viewport(|vp| {
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.get_layer(tooltip_layer_id)
|
||||
.any(|w| w.enabled && w.sense.interactive())
|
||||
});
|
||||
|
||||
if tooltip_has_interactive_widget {
|
||||
// We keep the tooltip open if hovered,
|
||||
// or if the pointer is on its way to it,
|
||||
// so that the user can interact with the tooltip
|
||||
// (i.e. click links that are in it).
|
||||
if let Some(area) = AreaState::load(&response.ctx, tooltip_id) {
|
||||
let rect = area.rect();
|
||||
|
||||
if let Some(pos) = pointer_pos {
|
||||
if rect.contains(pos) {
|
||||
return true; // hovering interactive tooltip
|
||||
}
|
||||
if pointer_dir != Vec2::ZERO
|
||||
&& rect.intersects_ray(pos, pointer_dir.normalized())
|
||||
{
|
||||
return true; // on the way to interactive tooltip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let clicked_more_recently_than_moved =
|
||||
time_since_last_click < time_since_last_pointer_movement + 0.1;
|
||||
if clicked_more_recently_than_moved {
|
||||
// It is common to click a widget and then rest the mouse there.
|
||||
// It would be annoying to then see a tooltip for it immediately.
|
||||
// Similarly, clicking should hide the existing tooltip.
|
||||
// Only hovering should lead to a tooltip, not clicking.
|
||||
// The offset is only to allow small movement just right after the click.
|
||||
return false;
|
||||
}
|
||||
|
||||
if is_our_tooltip_open {
|
||||
// Check if we should automatically stay open:
|
||||
|
||||
if pointer_pos.is_some_and(|pointer_pos| response.rect.contains(pointer_pos)) {
|
||||
// Handle the case of a big tooltip that covers the widget:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let is_other_tooltip_open = response.ctx.prev_pass_state(|fs| {
|
||||
if let Some(already_open_tooltip) = fs
|
||||
.layers
|
||||
.get(&response.layer_id)
|
||||
.and_then(|layer| layer.widget_with_tooltip)
|
||||
{
|
||||
already_open_tooltip != response.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if is_other_tooltip_open {
|
||||
// We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast early-outs:
|
||||
if response.enabled() {
|
||||
if !response.hovered() || !response.ctx.input(|i| i.pointer.has_pointer()) {
|
||||
return false;
|
||||
}
|
||||
} else if !response
|
||||
.ctx
|
||||
.rect_contains_pointer(response.layer_id, response.rect)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// There is a tooltip_delay before showing the first tooltip,
|
||||
// but once one tooltip is show, moving the mouse cursor to
|
||||
// another widget should show the tooltip for that widget right away.
|
||||
|
||||
// Let the user quickly move over some dead space to hover the next thing
|
||||
let tooltip_was_recently_shown =
|
||||
Self::seconds_since_last_tooltip(&response.ctx) < tooltip_grace_time;
|
||||
|
||||
if !tooltip_was_recently_shown && !is_our_tooltip_open {
|
||||
if style.interaction.show_tooltips_only_when_still {
|
||||
// We only show the tooltip when the mouse pointer is still.
|
||||
if !response
|
||||
.ctx
|
||||
.input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
|
||||
{
|
||||
// wait for mouse to stop
|
||||
response.ctx.request_repaint();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let time_since_last_interaction = time_since_last_scroll
|
||||
.min(time_since_last_pointer_movement)
|
||||
.min(time_since_last_click);
|
||||
let time_til_tooltip = tooltip_delay - time_since_last_interaction;
|
||||
|
||||
if 0.0 < time_til_tooltip {
|
||||
// Wait until the mouse has been still for a while
|
||||
response.ctx.request_repaint_after_secs(time_til_tooltip);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't want tooltips of things while we are dragging them,
|
||||
// but we do want tooltips while holding down on an item on a touch screen.
|
||||
if response
|
||||
.ctx
|
||||
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// All checks passed: show the tooltip!
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Was this tooltip visible last frame?
|
||||
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
|
||||
let primary_tooltip_area_id = Self::tooltip_id(widget_id, 0);
|
||||
ctx.memory(|mem| {
|
||||
mem.areas()
|
||||
.visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,11 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::collapsing_header::CollapsingState;
|
||||
use crate::{
|
||||
Align, Align2, Context, CursorIcon, Id, InnerResponse, LayerId, NumExt, Order, Response, Sense,
|
||||
TextStyle, Ui, UiKind, Vec2b, WidgetInfo, WidgetRect, WidgetText, WidgetType,
|
||||
};
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::{
|
||||
emath, pos2, vec2, Galley, Pos2, Rect, RectShape, Rounding, Roundingf, Shape, Stroke, Vec2,
|
||||
};
|
||||
use epaint::{CornerRadiusF32, RectShape};
|
||||
|
||||
use crate::collapsing_header::CollapsingState;
|
||||
use crate::*;
|
||||
|
||||
use super::scroll_area::ScrollBarVisibility;
|
||||
use super::{area, resize, Area, Frame, Resize, ScrollArea};
|
||||
@@ -419,7 +415,7 @@ impl<'open> Window<'open> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'open> Window<'open> {
|
||||
impl Window<'_> {
|
||||
/// Returns `None` if the window is not open (if [`Window::open`] was called with `&mut false`).
|
||||
/// Returns `Some(InnerResponse { inner: None })` if the window is collapsed.
|
||||
#[inline]
|
||||
@@ -438,7 +434,7 @@ impl<'open> Window<'open> {
|
||||
) -> Option<InnerResponse<Option<R>>> {
|
||||
let Window {
|
||||
title,
|
||||
open,
|
||||
mut open,
|
||||
area,
|
||||
frame,
|
||||
resize,
|
||||
@@ -452,8 +448,6 @@ impl<'open> Window<'open> {
|
||||
let header_color =
|
||||
frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill);
|
||||
let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
|
||||
// Keep the original inner margin for later use
|
||||
let window_margin = window_frame.inner_margin;
|
||||
|
||||
let is_explicitly_closed = matches!(open, Some(false));
|
||||
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
|
||||
@@ -483,15 +477,23 @@ impl<'open> Window<'open> {
|
||||
|
||||
area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text()));
|
||||
|
||||
// Calculate roughly how much larger the window size is compared to the inner rect
|
||||
let (title_bar_height, title_content_spacing) = if with_title_bar {
|
||||
// Calculate roughly how much larger the full window inner size is compared to the content rect
|
||||
let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar {
|
||||
let style = ctx.style();
|
||||
let spacing = window_margin.sum().y;
|
||||
let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing;
|
||||
let half_height = (height / 2.0).round() as _;
|
||||
window_frame.rounding.ne = window_frame.rounding.ne.clamp(0, half_height);
|
||||
window_frame.rounding.nw = window_frame.rounding.nw.clamp(0, half_height);
|
||||
(height, spacing)
|
||||
let title_bar_inner_height = ctx
|
||||
.fonts(|fonts| title.font_height(fonts, &style))
|
||||
.at_least(style.spacing.interact_size.y);
|
||||
let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y;
|
||||
let half_height = (title_bar_inner_height / 2.0).round() as _;
|
||||
window_frame.corner_radius.ne = window_frame.corner_radius.ne.clamp(0, half_height);
|
||||
window_frame.corner_radius.nw = window_frame.corner_radius.nw.clamp(0, half_height);
|
||||
|
||||
let title_content_spacing = if is_collapsed {
|
||||
0.0
|
||||
} else {
|
||||
window_frame.stroke.width
|
||||
};
|
||||
(title_bar_inner_height, title_content_spacing)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
@@ -500,7 +502,8 @@ impl<'open> Window<'open> {
|
||||
// Prevent window from becoming larger than the constrain rect.
|
||||
let constrain_rect = area.constrain_rect();
|
||||
let max_width = constrain_rect.width();
|
||||
let max_height = constrain_rect.height() - title_bar_height;
|
||||
let max_height =
|
||||
constrain_rect.height() - title_bar_height_with_margin - title_content_spacing;
|
||||
resize.max_size.x = resize.max_size.x.min(max_width);
|
||||
resize.max_size.y = resize.max_size.y.min(max_height);
|
||||
}
|
||||
@@ -508,21 +511,28 @@ impl<'open> Window<'open> {
|
||||
// First check for resize to avoid frame delay:
|
||||
let last_frame_outer_rect = area.state().rect();
|
||||
let resize_interaction = ctx.with_accessibility_parent(area.id(), || {
|
||||
resize_interaction(ctx, possible, area_layer_id, last_frame_outer_rect)
|
||||
resize_interaction(
|
||||
ctx,
|
||||
possible,
|
||||
area_layer_id,
|
||||
last_frame_outer_rect,
|
||||
window_frame,
|
||||
)
|
||||
});
|
||||
|
||||
let margins = window_frame.outer_margin.sum()
|
||||
+ window_frame.inner_margin.sum()
|
||||
+ vec2(0.0, title_bar_height);
|
||||
{
|
||||
let margins = window_frame.total_margin().sum()
|
||||
+ vec2(0.0, title_bar_height_with_margin + title_content_spacing);
|
||||
|
||||
resize_response(
|
||||
resize_interaction,
|
||||
ctx,
|
||||
margins,
|
||||
area_layer_id,
|
||||
&mut area,
|
||||
resize_id,
|
||||
);
|
||||
resize_response(
|
||||
resize_interaction,
|
||||
ctx,
|
||||
margins,
|
||||
area_layer_id,
|
||||
&mut area,
|
||||
resize_id,
|
||||
);
|
||||
}
|
||||
|
||||
let mut area_content_ui = area.content_ui(ctx);
|
||||
if is_open {
|
||||
@@ -535,40 +545,43 @@ impl<'open> Window<'open> {
|
||||
let content_inner = {
|
||||
ctx.with_accessibility_parent(area.id(), || {
|
||||
// BEGIN FRAME --------------------------------
|
||||
let frame_stroke = window_frame.stroke;
|
||||
let mut frame = window_frame.begin(&mut area_content_ui);
|
||||
|
||||
let show_close_button = open.is_some();
|
||||
|
||||
let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
|
||||
|
||||
// Backup item spacing before the title bar
|
||||
let item_spacing = frame.content_ui.spacing().item_spacing;
|
||||
// Use title bar spacing as the item spacing before the content
|
||||
frame.content_ui.spacing_mut().item_spacing.y = title_content_spacing;
|
||||
|
||||
let title_bar = if with_title_bar {
|
||||
let title_bar = TitleBar::new(
|
||||
&mut frame.content_ui,
|
||||
&frame.content_ui,
|
||||
title,
|
||||
show_close_button,
|
||||
&mut collapsing,
|
||||
collapsible,
|
||||
window_frame,
|
||||
title_bar_height_with_margin,
|
||||
);
|
||||
resize.min_size.x = resize.min_size.x.at_least(title_bar.rect.width()); // Prevent making window smaller than title bar width
|
||||
resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width
|
||||
|
||||
frame.content_ui.set_min_size(title_bar.inner_rect.size());
|
||||
|
||||
// Skip the title bar (and separator):
|
||||
if is_collapsed {
|
||||
frame.content_ui.add_space(title_bar.inner_rect.height());
|
||||
} else {
|
||||
frame.content_ui.add_space(
|
||||
title_bar.inner_rect.height()
|
||||
+ title_content_spacing
|
||||
+ window_frame.inner_margin.sum().y,
|
||||
);
|
||||
}
|
||||
|
||||
Some(title_bar)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Remove item spacing after the title bar
|
||||
frame.content_ui.spacing_mut().item_spacing.y = 0.0;
|
||||
|
||||
let (content_inner, mut content_response) = collapsing
|
||||
let (content_inner, content_response) = collapsing
|
||||
.show_body_unindented(&mut frame.content_ui, |ui| {
|
||||
// Restore item spacing for the content
|
||||
ui.spacing_mut().item_spacing.y = item_spacing.y;
|
||||
|
||||
resize.show(ui, |ui| {
|
||||
if scroll.is_any_scroll_enabled() {
|
||||
scroll.show(ui, add_contents).inner
|
||||
@@ -584,26 +597,20 @@ impl<'open> Window<'open> {
|
||||
&area_content_ui,
|
||||
&possible,
|
||||
outer_rect,
|
||||
frame_stroke,
|
||||
window_frame.rounding,
|
||||
&window_frame,
|
||||
resize_interaction,
|
||||
);
|
||||
|
||||
// END FRAME --------------------------------
|
||||
|
||||
if let Some(title_bar) = title_bar {
|
||||
let mut title_rect = Rect::from_min_size(
|
||||
outer_rect.min,
|
||||
Vec2 {
|
||||
x: outer_rect.size().x,
|
||||
y: title_bar_height,
|
||||
},
|
||||
);
|
||||
|
||||
title_rect = title_rect.round_to_pixels(area_content_ui.pixels_per_point());
|
||||
if let Some(mut title_bar) = title_bar {
|
||||
title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width);
|
||||
title_bar.inner_rect.max.y =
|
||||
title_bar.inner_rect.min.y + title_bar_height_with_margin;
|
||||
|
||||
if on_top && area_content_ui.visuals().window_highlight_topmost {
|
||||
let mut round = window_frame.rounding;
|
||||
let mut round =
|
||||
window_frame.corner_radius - window_frame.stroke.width.round() as u8;
|
||||
|
||||
if !is_collapsed {
|
||||
round.se = 0;
|
||||
@@ -612,20 +619,22 @@ impl<'open> Window<'open> {
|
||||
|
||||
area_content_ui.painter().set(
|
||||
*where_to_put_header_background,
|
||||
RectShape::filled(title_rect, round, header_color),
|
||||
RectShape::filled(title_bar.inner_rect, round, header_color),
|
||||
);
|
||||
};
|
||||
|
||||
// Fix title bar separator line position
|
||||
if let Some(response) = &mut content_response {
|
||||
response.rect.min.y = outer_rect.min.y + title_bar_height;
|
||||
if false {
|
||||
ctx.debug_painter().debug_rect(
|
||||
title_bar.inner_rect,
|
||||
Color32::LIGHT_BLUE,
|
||||
"title_bar.rect",
|
||||
);
|
||||
}
|
||||
|
||||
title_bar.ui(
|
||||
&mut area_content_ui,
|
||||
title_rect,
|
||||
&content_response,
|
||||
open,
|
||||
open.as_deref_mut(),
|
||||
&mut collapsing,
|
||||
collapsible,
|
||||
);
|
||||
@@ -641,6 +650,12 @@ impl<'open> Window<'open> {
|
||||
|
||||
let full_response = area.end(ctx, area_content_ui);
|
||||
|
||||
if full_response.should_close() {
|
||||
if let Some(open) = open {
|
||||
*open = false;
|
||||
}
|
||||
}
|
||||
|
||||
let inner_response = InnerResponse {
|
||||
inner: content_inner,
|
||||
response: full_response,
|
||||
@@ -653,32 +668,31 @@ fn paint_resize_corner(
|
||||
ui: &Ui,
|
||||
possible: &PossibleInteractions,
|
||||
outer_rect: Rect,
|
||||
stroke: impl Into<Stroke>,
|
||||
rounding: impl Into<Rounding>,
|
||||
window_frame: &Frame,
|
||||
i: ResizeInteraction,
|
||||
) {
|
||||
let inactive_stroke = stroke.into();
|
||||
let rounding = rounding.into();
|
||||
let cr = window_frame.corner_radius;
|
||||
|
||||
let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
|
||||
(Align2::RIGHT_BOTTOM, rounding.se, i.right & i.bottom)
|
||||
(Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
|
||||
} else if possible.resize_left && possible.resize_bottom {
|
||||
(Align2::LEFT_BOTTOM, rounding.sw, i.left & i.bottom)
|
||||
(Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
|
||||
} else if possible.resize_left && possible.resize_top {
|
||||
(Align2::LEFT_TOP, rounding.nw, i.left & i.top)
|
||||
(Align2::LEFT_TOP, cr.nw, i.left & i.top)
|
||||
} else if possible.resize_right && possible.resize_top {
|
||||
(Align2::RIGHT_TOP, rounding.ne, i.right & i.top)
|
||||
(Align2::RIGHT_TOP, cr.ne, i.right & i.top)
|
||||
} else {
|
||||
// We're not in two directions, but it is still nice to tell the user
|
||||
// we're resizable by painting the resize corner in the expected place
|
||||
// (i.e. for windows only resizable in one direction):
|
||||
if possible.resize_right || possible.resize_bottom {
|
||||
(Align2::RIGHT_BOTTOM, rounding.se, i.right & i.bottom)
|
||||
(Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
|
||||
} else if possible.resize_left || possible.resize_bottom {
|
||||
(Align2::LEFT_BOTTOM, rounding.sw, i.left & i.bottom)
|
||||
(Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
|
||||
} else if possible.resize_left || possible.resize_top {
|
||||
(Align2::LEFT_TOP, rounding.nw, i.left & i.top)
|
||||
(Align2::LEFT_TOP, cr.nw, i.left & i.top)
|
||||
} else if possible.resize_right || possible.resize_top {
|
||||
(Align2::RIGHT_TOP, rounding.ne, i.right & i.top)
|
||||
(Align2::RIGHT_TOP, cr.ne, i.right & i.top)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -694,11 +708,12 @@ fn paint_resize_corner(
|
||||
} else if corner_response.hover {
|
||||
ui.visuals().widgets.hovered.fg_stroke
|
||||
} else {
|
||||
inactive_stroke
|
||||
window_frame.stroke
|
||||
};
|
||||
|
||||
let fill_rect = outer_rect.shrink(window_frame.stroke.width);
|
||||
let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
|
||||
let corner_rect = corner.align_size_within_rect(corner_size, outer_rect);
|
||||
let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
|
||||
let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner
|
||||
crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
|
||||
}
|
||||
@@ -738,7 +753,11 @@ impl PossibleInteractions {
|
||||
/// Resizing the window edges.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct ResizeInteraction {
|
||||
start_rect: Rect,
|
||||
/// Outer rect (outside the stroke)
|
||||
outer_rect: Rect,
|
||||
|
||||
window_frame: Frame,
|
||||
|
||||
left: SideResponse,
|
||||
right: SideResponse,
|
||||
top: SideResponse,
|
||||
@@ -835,13 +854,17 @@ fn resize_response(
|
||||
ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
|
||||
}
|
||||
|
||||
/// Acts on outer rect (outside the stroke)
|
||||
fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option<Rect> {
|
||||
if !interaction.any_dragged() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
|
||||
let mut rect = interaction.start_rect; // prevent drift
|
||||
let mut rect = interaction.outer_rect; // prevent drift
|
||||
|
||||
// Put the rect in the center of the stroke:
|
||||
rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
|
||||
|
||||
if interaction.left.drag {
|
||||
rect.min.x = pointer_pos.x;
|
||||
@@ -855,6 +878,9 @@ fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Opt
|
||||
rect.max.y = pointer_pos.y;
|
||||
}
|
||||
|
||||
// Return to having the rect outside the stroke:
|
||||
rect = rect.expand(interaction.window_frame.stroke.width / 2.0);
|
||||
|
||||
Some(rect.round_ui())
|
||||
}
|
||||
|
||||
@@ -862,11 +888,13 @@ fn resize_interaction(
|
||||
ctx: &Context,
|
||||
possible: PossibleInteractions,
|
||||
layer_id: LayerId,
|
||||
rect: Rect,
|
||||
outer_rect: Rect,
|
||||
window_frame: Frame,
|
||||
) -> ResizeInteraction {
|
||||
if !possible.resizable() {
|
||||
return ResizeInteraction {
|
||||
start_rect: rect,
|
||||
outer_rect,
|
||||
window_frame,
|
||||
left: Default::default(),
|
||||
right: Default::default(),
|
||||
top: Default::default(),
|
||||
@@ -874,6 +902,9 @@ fn resize_interaction(
|
||||
};
|
||||
}
|
||||
|
||||
// The rect that is in the middle of the stroke:
|
||||
let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
|
||||
|
||||
let side_response = |rect, id| {
|
||||
let response = ctx.create_widget(
|
||||
WidgetRect {
|
||||
@@ -990,7 +1021,8 @@ fn resize_interaction(
|
||||
}
|
||||
|
||||
let interaction = ResizeInteraction {
|
||||
start_rect: rect,
|
||||
outer_rect,
|
||||
window_frame,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
@@ -1026,137 +1058,123 @@ fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction)
|
||||
bottom = interaction.bottom.hover;
|
||||
}
|
||||
|
||||
let rounding = Roundingf::from(ui.visuals().window_rounding);
|
||||
let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius);
|
||||
|
||||
// Put the rect in the center of the fixed window stroke:
|
||||
let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
|
||||
|
||||
// Make sure the inner part of the stroke is at a pixel boundary:
|
||||
let stroke = visuals.bg_stroke;
|
||||
let half_stroke = stroke.width / 2.0;
|
||||
let rect = rect
|
||||
.shrink(half_stroke)
|
||||
.round_to_pixels(ui.pixels_per_point())
|
||||
.expand(half_stroke);
|
||||
|
||||
let Rect { min, max } = rect;
|
||||
|
||||
let mut points = Vec::new();
|
||||
|
||||
if right && !bottom && !top {
|
||||
points.push(pos2(max.x, min.y + rounding.ne));
|
||||
points.push(pos2(max.x, max.y - rounding.se));
|
||||
points.push(pos2(max.x, min.y + cr.ne));
|
||||
points.push(pos2(max.x, max.y - cr.se));
|
||||
}
|
||||
if right && bottom {
|
||||
points.push(pos2(max.x, min.y + rounding.ne));
|
||||
points.push(pos2(max.x, max.y - rounding.se));
|
||||
add_circle_quadrant(
|
||||
&mut points,
|
||||
pos2(max.x - rounding.se, max.y - rounding.se),
|
||||
rounding.se,
|
||||
0.0,
|
||||
);
|
||||
points.push(pos2(max.x, min.y + cr.ne));
|
||||
points.push(pos2(max.x, max.y - cr.se));
|
||||
add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0);
|
||||
}
|
||||
if bottom {
|
||||
points.push(pos2(max.x - rounding.se, max.y));
|
||||
points.push(pos2(min.x + rounding.sw, max.y));
|
||||
points.push(pos2(max.x - cr.se, max.y));
|
||||
points.push(pos2(min.x + cr.sw, max.y));
|
||||
}
|
||||
if left && bottom {
|
||||
add_circle_quadrant(
|
||||
&mut points,
|
||||
pos2(min.x + rounding.sw, max.y - rounding.sw),
|
||||
rounding.sw,
|
||||
1.0,
|
||||
);
|
||||
add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0);
|
||||
}
|
||||
if left {
|
||||
points.push(pos2(min.x, max.y - rounding.sw));
|
||||
points.push(pos2(min.x, min.y + rounding.nw));
|
||||
points.push(pos2(min.x, max.y - cr.sw));
|
||||
points.push(pos2(min.x, min.y + cr.nw));
|
||||
}
|
||||
if left && top {
|
||||
add_circle_quadrant(
|
||||
&mut points,
|
||||
pos2(min.x + rounding.nw, min.y + rounding.nw),
|
||||
rounding.nw,
|
||||
2.0,
|
||||
);
|
||||
add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0);
|
||||
}
|
||||
if top {
|
||||
points.push(pos2(min.x + rounding.nw, min.y));
|
||||
points.push(pos2(max.x - rounding.ne, min.y));
|
||||
points.push(pos2(min.x + cr.nw, min.y));
|
||||
points.push(pos2(max.x - cr.ne, min.y));
|
||||
}
|
||||
if right && top {
|
||||
add_circle_quadrant(
|
||||
&mut points,
|
||||
pos2(max.x - rounding.ne, min.y + rounding.ne),
|
||||
rounding.ne,
|
||||
3.0,
|
||||
);
|
||||
points.push(pos2(max.x, min.y + rounding.ne));
|
||||
points.push(pos2(max.x, max.y - rounding.se));
|
||||
add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0);
|
||||
points.push(pos2(max.x, min.y + cr.ne));
|
||||
points.push(pos2(max.x, max.y - cr.se));
|
||||
}
|
||||
ui.painter().add(Shape::line(points, visuals.bg_stroke));
|
||||
|
||||
ui.painter().add(Shape::line(points, stroke));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct TitleBar {
|
||||
/// A title Id used for dragging windows
|
||||
id: Id,
|
||||
window_frame: Frame,
|
||||
|
||||
/// Prepared text in the title
|
||||
title_galley: Arc<Galley>,
|
||||
|
||||
/// Size of the title bar in a collapsed state (if window is collapsible),
|
||||
/// which includes all necessary space for showing the expand button, the
|
||||
/// title and the close button.
|
||||
min_rect: Rect,
|
||||
|
||||
/// Size of the title bar in an expanded state. This size become known only
|
||||
/// after expanding window and painting its content
|
||||
rect: Rect,
|
||||
/// after expanding window and painting its content.
|
||||
///
|
||||
/// Does not include the stroke, nor the separator line between the title bar and the window contents.
|
||||
inner_rect: Rect,
|
||||
}
|
||||
|
||||
impl TitleBar {
|
||||
fn new(
|
||||
ui: &mut Ui,
|
||||
ui: &Ui,
|
||||
title: WidgetText,
|
||||
show_close_button: bool,
|
||||
collapsing: &mut CollapsingState,
|
||||
collapsible: bool,
|
||||
window_frame: Frame,
|
||||
title_bar_height_with_margin: f32,
|
||||
) -> Self {
|
||||
let inner_response = ui.horizontal(|ui| {
|
||||
let height = ui
|
||||
.fonts(|fonts| title.font_height(fonts, ui.style()))
|
||||
.max(ui.spacing().interact_size.y);
|
||||
ui.set_min_height(height);
|
||||
if false {
|
||||
ui.ctx()
|
||||
.debug_painter()
|
||||
.debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect");
|
||||
}
|
||||
|
||||
let item_spacing = ui.spacing().item_spacing;
|
||||
let button_size = Vec2::splat(ui.spacing().icon_width);
|
||||
let inner_height = title_bar_height_with_margin - window_frame.inner_margin.sum().y;
|
||||
|
||||
let pad = ((height - button_size.y) / 2.0).round_ui(); // calculated so that the icon is on the diagonal (if window padding is symmetrical)
|
||||
let item_spacing = ui.spacing().item_spacing;
|
||||
let button_size = Vec2::splat(ui.spacing().icon_width.at_most(inner_height));
|
||||
|
||||
if collapsible {
|
||||
ui.add_space(pad);
|
||||
collapsing.show_default_button_with_size(ui, button_size);
|
||||
}
|
||||
let left_pad = ((inner_height - button_size.y) / 2.0).round_ui(); // calculated so that the icon is on the diagonal (if window padding is symmetrical)
|
||||
|
||||
let title_galley = title.into_galley(
|
||||
ui,
|
||||
Some(crate::TextWrapMode::Extend),
|
||||
f32::INFINITY,
|
||||
TextStyle::Heading,
|
||||
);
|
||||
let title_galley = title.into_galley(
|
||||
ui,
|
||||
Some(crate::TextWrapMode::Extend),
|
||||
f32::INFINITY,
|
||||
TextStyle::Heading,
|
||||
);
|
||||
|
||||
let minimum_width = if collapsible || show_close_button {
|
||||
// If at least one button is shown we make room for both buttons (since title is centered):
|
||||
2.0 * (pad + button_size.x + item_spacing.x) + title_galley.size().x
|
||||
} else {
|
||||
pad + title_galley.size().x + pad
|
||||
};
|
||||
let min_rect = Rect::from_min_size(ui.min_rect().min, vec2(minimum_width, height));
|
||||
let id = ui.advance_cursor_after_rect(min_rect);
|
||||
let minimum_width = if collapsible || show_close_button {
|
||||
// If at least one button is shown we make room for both buttons (since title should be centered):
|
||||
2.0 * (left_pad + button_size.x + item_spacing.x) + title_galley.size().x
|
||||
} else {
|
||||
left_pad + title_galley.size().x + left_pad
|
||||
};
|
||||
let min_inner_size = vec2(minimum_width, inner_height);
|
||||
let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size);
|
||||
|
||||
Self {
|
||||
id,
|
||||
title_galley,
|
||||
min_rect,
|
||||
rect: Rect::NAN, // Will be filled in later
|
||||
}
|
||||
});
|
||||
if false {
|
||||
ui.ctx()
|
||||
.debug_painter()
|
||||
.debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect");
|
||||
}
|
||||
|
||||
let title_bar = inner_response.inner;
|
||||
let rect = inner_response.response.rect;
|
||||
|
||||
Self { rect, ..title_bar }
|
||||
Self {
|
||||
window_frame,
|
||||
title_galley,
|
||||
inner_rect: min_rect, // First estimate - will be refined later
|
||||
}
|
||||
}
|
||||
|
||||
/// Finishes painting of the title bar when the window content size already known.
|
||||
@@ -1174,17 +1192,34 @@ impl TitleBar {
|
||||
/// - `collapsible`: if `true`, double click on the title bar will be handled for a change
|
||||
/// of `collapsing` state
|
||||
fn ui(
|
||||
mut self,
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
outer_rect: Rect,
|
||||
content_response: &Option<Response>,
|
||||
open: Option<&mut bool>,
|
||||
collapsing: &mut CollapsingState,
|
||||
collapsible: bool,
|
||||
) {
|
||||
if let Some(content_response) = &content_response {
|
||||
// Now we know how large we got to be:
|
||||
self.rect.max.x = self.rect.max.x.max(content_response.rect.max.x);
|
||||
let window_frame = self.window_frame;
|
||||
let title_inner_rect = self.inner_rect;
|
||||
|
||||
if false {
|
||||
ui.ctx()
|
||||
.debug_painter()
|
||||
.debug_rect(self.inner_rect, Color32::RED, "TitleBar");
|
||||
}
|
||||
|
||||
if collapsible {
|
||||
// Show collapse-button:
|
||||
let button_center = Align2::LEFT_CENTER
|
||||
.align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
|
||||
.center();
|
||||
let button_size = Vec2::splat(ui.spacing().icon_width);
|
||||
let button_rect = Rect::from_center_size(button_center, button_size);
|
||||
let button_rect = button_rect.round_ui();
|
||||
|
||||
ui.scope_builder(UiBuilder::new().max_rect(button_rect), |ui| {
|
||||
collapsing.show_default_button_with_size(ui, button_size);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(open) = open {
|
||||
@@ -1194,9 +1229,9 @@ impl TitleBar {
|
||||
}
|
||||
}
|
||||
|
||||
let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range());
|
||||
let text_pos =
|
||||
emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top();
|
||||
emath::align::center_size_in_rect(self.title_galley.size(), title_inner_rect)
|
||||
.left_top();
|
||||
let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
|
||||
ui.painter().galley(
|
||||
text_pos,
|
||||
@@ -1205,22 +1240,35 @@ impl TitleBar {
|
||||
);
|
||||
|
||||
if let Some(content_response) = &content_response {
|
||||
// paint separator between title and content:
|
||||
let y = content_response.rect.top();
|
||||
// let y = lerp(self.rect.bottom()..=content_response.rect.top(), 0.5);
|
||||
let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
|
||||
// Workaround: To prevent border infringement,
|
||||
// the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels
|
||||
// or we could support selectively disabling feathering on line caps
|
||||
let x_range = outer_rect.x_range().shrink(0.1);
|
||||
ui.painter().hline(x_range, y, stroke);
|
||||
// Paint separator between title and content:
|
||||
let content_rect = content_response.rect;
|
||||
if false {
|
||||
ui.ctx()
|
||||
.debug_painter()
|
||||
.debug_rect(content_rect, Color32::RED, "content_rect");
|
||||
}
|
||||
let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0;
|
||||
|
||||
// To verify the sanity of this, use a very wide window stroke
|
||||
ui.painter()
|
||||
.hline(title_inner_rect.x_range(), y, window_frame.stroke);
|
||||
}
|
||||
|
||||
// Don't cover the close- and collapse buttons:
|
||||
let double_click_rect = self.rect.shrink2(vec2(32.0, 0.0));
|
||||
let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0));
|
||||
|
||||
if false {
|
||||
ui.ctx().debug_painter().debug_rect(
|
||||
double_click_rect,
|
||||
Color32::GREEN,
|
||||
"double_click_rect",
|
||||
);
|
||||
}
|
||||
|
||||
let id = ui.unique_id().with("__window_title_bar");
|
||||
|
||||
if ui
|
||||
.interact(double_click_rect, self.id, Sense::click())
|
||||
.interact(double_click_rect, id, Sense::click())
|
||||
.double_clicked()
|
||||
&& collapsible
|
||||
{
|
||||
@@ -1234,16 +1282,12 @@ impl TitleBar {
|
||||
/// The button is square and its size is determined by the
|
||||
/// [`crate::style::Spacing::icon_width`] setting.
|
||||
fn close_button_ui(&self, ui: &mut Ui) -> Response {
|
||||
let button_center = Align2::RIGHT_CENTER
|
||||
.align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
|
||||
.center();
|
||||
let button_size = Vec2::splat(ui.spacing().icon_width);
|
||||
let pad = (self.rect.height() - button_size.y) / 2.0; // calculated so that the icon is on the diagonal (if window padding is symmetrical)
|
||||
let button_rect = Rect::from_min_size(
|
||||
pos2(
|
||||
self.rect.right() - pad - button_size.x,
|
||||
self.rect.center().y - 0.5 * button_size.y,
|
||||
),
|
||||
button_size,
|
||||
);
|
||||
|
||||
let button_rect = Rect::from_center_size(button_center, button_size);
|
||||
let button_rect = button_rect.round_to_pixels(ui.pixels_per_point());
|
||||
close_button(ui, button_rect)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use epaint::{
|
||||
tessellator,
|
||||
text::{FontInsert, FontPriority, Fonts},
|
||||
util::OrderedFloat,
|
||||
vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect,
|
||||
vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
|
||||
TessellationOptions, TextureAtlas, TextureId, Vec2,
|
||||
};
|
||||
|
||||
@@ -26,11 +26,10 @@ use crate::{
|
||||
load,
|
||||
load::{Bytes, Loaders, SizedTexture},
|
||||
memory::{Options, Theme},
|
||||
menu,
|
||||
os::OperatingSystem,
|
||||
output::FullOutput,
|
||||
pass_state::PassState,
|
||||
resize, scroll_area,
|
||||
resize, response, scroll_area,
|
||||
util::IdTypeMap,
|
||||
viewport::ViewportClass,
|
||||
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
|
||||
@@ -83,7 +82,11 @@ impl Default for WrappedTextureManager {
|
||||
epaint::FontImage::new([0, 0]).into(),
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(font_id, TextureId::default());
|
||||
assert_eq!(
|
||||
font_id,
|
||||
TextureId::default(),
|
||||
"font id should be equal to TextureId::default(), but was {font_id:?}",
|
||||
);
|
||||
|
||||
Self(Arc::new(RwLock::new(tex_mngr)))
|
||||
}
|
||||
@@ -211,14 +214,14 @@ impl ContextImpl {
|
||||
fn requested_immediate_repaint_prev_pass(&self, viewport_id: &ViewportId) -> bool {
|
||||
self.viewports
|
||||
.get(viewport_id)
|
||||
.map_or(false, |v| v.repaint.requested_immediate_repaint_prev_pass())
|
||||
.is_some_and(|v| v.repaint.requested_immediate_repaint_prev_pass())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn has_requested_repaint(&self, viewport_id: &ViewportId) -> bool {
|
||||
self.viewports.get(viewport_id).map_or(false, |v| {
|
||||
0 < v.repaint.outstanding || v.repaint.repaint_delay < Duration::MAX
|
||||
})
|
||||
self.viewports
|
||||
.get(viewport_id)
|
||||
.is_some_and(|v| 0 < v.repaint.outstanding || v.repaint.repaint_delay < Duration::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,7 +778,7 @@ impl Context {
|
||||
writer(&mut self.0.write())
|
||||
}
|
||||
|
||||
/// Run the ui code for one 1.
|
||||
/// 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.
|
||||
@@ -805,7 +808,11 @@ impl Context {
|
||||
let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get());
|
||||
|
||||
let mut output = FullOutput::default();
|
||||
debug_assert_eq!(output.platform_output.num_completed_passes, 0);
|
||||
debug_assert_eq!(
|
||||
output.platform_output.num_completed_passes, 0,
|
||||
"output must be fresh, but had {} passes",
|
||||
output.platform_output.num_completed_passes
|
||||
);
|
||||
|
||||
loop {
|
||||
profiling::scope!(
|
||||
@@ -829,7 +836,11 @@ impl Context {
|
||||
self.begin_pass(new_input.take());
|
||||
run_ui(self);
|
||||
output.append(self.end_pass());
|
||||
debug_assert!(0 < output.platform_output.num_completed_passes);
|
||||
debug_assert!(
|
||||
0 < output.platform_output.num_completed_passes,
|
||||
"Completed passes was lower than 0, was {}",
|
||||
output.platform_output.num_completed_passes
|
||||
);
|
||||
|
||||
if !output.platform_output.requested_discard() {
|
||||
break; // no need for another pass
|
||||
@@ -1087,7 +1098,7 @@ impl Context {
|
||||
let text = format!("🔥 {text}");
|
||||
let color = self.style().visuals.error_fg_color;
|
||||
let painter = self.debug_painter();
|
||||
painter.rect_stroke(widget_rect, 0.0, (1.0, color));
|
||||
painter.rect_stroke(widget_rect, 0.0, (1.0, color), StrokeKind::Outside);
|
||||
|
||||
let below = widget_rect.bottom() + 32.0 < screen_rect.bottom();
|
||||
|
||||
@@ -1151,8 +1162,9 @@ impl Context {
|
||||
/// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response {
|
||||
let interested_in_focus =
|
||||
w.enabled && w.sense.focusable && self.memory(|mem| mem.allows_interaction(w.layer_id));
|
||||
let interested_in_focus = w.enabled
|
||||
&& w.sense.is_focusable()
|
||||
&& self.memory(|mem| mem.allows_interaction(w.layer_id));
|
||||
|
||||
// Remember this widget
|
||||
self.write(|ctx| {
|
||||
@@ -1173,7 +1185,7 @@ impl Context {
|
||||
self.memory_mut(|mem| mem.surrender_focus(w.id));
|
||||
}
|
||||
|
||||
if w.sense.interactive() || w.sense.focusable {
|
||||
if w.sense.interactive() || w.sense.is_focusable() {
|
||||
self.check_for_id_clash(w.id, w.rect, "widget");
|
||||
}
|
||||
|
||||
@@ -1181,7 +1193,7 @@ impl Context {
|
||||
let res = self.get_response(w);
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if allow_focus && w.sense.focusable {
|
||||
if allow_focus && w.sense.is_focusable() {
|
||||
// Make sure anything that can receive focus has an AccessKit node.
|
||||
// TODO(mwcampbell): For nodes that are filled from widget info,
|
||||
// some information is written to the node twice.
|
||||
@@ -1196,15 +1208,27 @@ impl Context {
|
||||
/// This is because widget interaction happens at the start of the pass, using the widget rects from the previous pass.
|
||||
///
|
||||
/// If the widget was not visible the previous pass (or this pass), this will return `None`.
|
||||
///
|
||||
/// If you try to read a [`Ui`]'s response, while still inside, this will return the [`Rect`] from the previous frame.
|
||||
pub fn read_response(&self, id: Id) -> Option<Response> {
|
||||
self.write(|ctx| {
|
||||
let viewport = ctx.viewport();
|
||||
viewport
|
||||
let widget_rect = viewport
|
||||
.this_pass
|
||||
.widgets
|
||||
.get(id)
|
||||
.or_else(|| viewport.prev_pass.widgets.get(id))
|
||||
.copied()
|
||||
.copied();
|
||||
widget_rect.map(|mut rect| {
|
||||
// If the Rect is invalid the Ui hasn't registered its final Rect yet.
|
||||
// We return the Rect from last frame instead.
|
||||
if !(rect.rect.is_positive() && rect.rect.is_finite()) {
|
||||
if let Some(prev_rect) = viewport.prev_pass.widgets.get(id) {
|
||||
rect.rect = prev_rect.rect;
|
||||
}
|
||||
}
|
||||
rect
|
||||
})
|
||||
})
|
||||
.map(|widget_rect| self.get_response(widget_rect))
|
||||
}
|
||||
@@ -1213,11 +1237,13 @@ impl Context {
|
||||
#[deprecated = "Use Response.contains_pointer or Context::read_response instead"]
|
||||
pub fn widget_contains_pointer(&self, id: Id) -> bool {
|
||||
self.read_response(id)
|
||||
.map_or(false, |response| response.contains_pointer)
|
||||
.is_some_and(|response| response.contains_pointer())
|
||||
}
|
||||
|
||||
/// Do all interaction for an existing widget, without (re-)registering it.
|
||||
pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response {
|
||||
use response::Flags;
|
||||
|
||||
let WidgetRect {
|
||||
id,
|
||||
layer_id,
|
||||
@@ -1237,61 +1263,72 @@ impl Context {
|
||||
rect,
|
||||
interact_rect,
|
||||
sense,
|
||||
enabled,
|
||||
contains_pointer: false,
|
||||
hovered: false,
|
||||
highlighted,
|
||||
clicked: false,
|
||||
fake_primary_click: false,
|
||||
long_touched: false,
|
||||
drag_started: false,
|
||||
dragged: false,
|
||||
drag_stopped: false,
|
||||
is_pointer_button_down_on: false,
|
||||
flags: Flags::empty(),
|
||||
interact_pointer_pos: None,
|
||||
changed: false,
|
||||
intrinsic_size: None,
|
||||
};
|
||||
|
||||
res.flags.set(Flags::ENABLED, enabled);
|
||||
res.flags.set(Flags::HIGHLIGHTED, highlighted);
|
||||
|
||||
self.write(|ctx| {
|
||||
let viewport = ctx.viewports.entry(ctx.viewport_id()).or_default();
|
||||
|
||||
res.contains_pointer = viewport.interact_widgets.contains_pointer.contains(&id);
|
||||
res.flags.set(
|
||||
Flags::CONTAINS_POINTER,
|
||||
viewport.interact_widgets.contains_pointer.contains(&id),
|
||||
);
|
||||
|
||||
let input = &viewport.input;
|
||||
let memory = &mut ctx.memory;
|
||||
|
||||
if enabled
|
||||
&& sense.click
|
||||
&& sense.senses_click()
|
||||
&& memory.has_focus(id)
|
||||
&& (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter))
|
||||
{
|
||||
// Space/enter works like a primary click for e.g. selected buttons
|
||||
res.fake_primary_click = true;
|
||||
res.flags.set(Flags::FAKE_PRIMARY_CLICKED, true);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if enabled
|
||||
&& sense.click
|
||||
&& sense.senses_click()
|
||||
&& input.has_accesskit_action_request(id, accesskit::Action::Click)
|
||||
{
|
||||
res.fake_primary_click = true;
|
||||
res.flags.set(Flags::FAKE_PRIMARY_CLICKED, true);
|
||||
}
|
||||
|
||||
if enabled && sense.click && Some(id) == viewport.interact_widgets.long_touched {
|
||||
res.long_touched = true;
|
||||
if enabled && sense.senses_click() && Some(id) == viewport.interact_widgets.long_touched
|
||||
{
|
||||
res.flags.set(Flags::LONG_TOUCHED, true);
|
||||
}
|
||||
|
||||
let interaction = memory.interaction();
|
||||
|
||||
res.is_pointer_button_down_on = interaction.potential_click_id == Some(id)
|
||||
|| interaction.potential_drag_id == Some(id);
|
||||
res.flags.set(
|
||||
Flags::IS_POINTER_BUTTON_DOWN_ON,
|
||||
interaction.potential_click_id == Some(id)
|
||||
|| interaction.potential_drag_id == Some(id),
|
||||
);
|
||||
|
||||
if res.enabled {
|
||||
res.hovered = viewport.interact_widgets.hovered.contains(&id);
|
||||
res.dragged = Some(id) == viewport.interact_widgets.dragged;
|
||||
res.drag_started = Some(id) == viewport.interact_widgets.drag_started;
|
||||
res.drag_stopped = Some(id) == viewport.interact_widgets.drag_stopped;
|
||||
if res.enabled() {
|
||||
res.flags.set(
|
||||
Flags::HOVERED,
|
||||
viewport.interact_widgets.hovered.contains(&id),
|
||||
);
|
||||
res.flags.set(
|
||||
Flags::DRAGGED,
|
||||
Some(id) == viewport.interact_widgets.dragged,
|
||||
);
|
||||
res.flags.set(
|
||||
Flags::DRAG_STARTED,
|
||||
Some(id) == viewport.interact_widgets.drag_started,
|
||||
);
|
||||
res.flags.set(
|
||||
Flags::DRAG_STOPPED,
|
||||
Some(id) == viewport.interact_widgets.drag_stopped,
|
||||
);
|
||||
}
|
||||
|
||||
let clicked = Some(id) == viewport.interact_widgets.clicked;
|
||||
@@ -1304,20 +1341,22 @@ impl Context {
|
||||
any_press = true;
|
||||
}
|
||||
PointerEvent::Released { click, .. } => {
|
||||
if enabled && sense.click && clicked && click.is_some() {
|
||||
res.clicked = true;
|
||||
if enabled && sense.senses_click() && clicked && click.is_some() {
|
||||
res.flags.set(Flags::CLICKED, true);
|
||||
}
|
||||
|
||||
res.is_pointer_button_down_on = false;
|
||||
res.dragged = false;
|
||||
res.flags.set(Flags::IS_POINTER_BUTTON_DOWN_ON, false);
|
||||
res.flags.set(Flags::DRAGGED, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is_pointer_button_down_on is false when released, but we want interact_pointer_pos
|
||||
// to still work.
|
||||
let is_interacted_with =
|
||||
res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped;
|
||||
let is_interacted_with = res.is_pointer_button_down_on()
|
||||
|| 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)) = (
|
||||
@@ -1330,10 +1369,10 @@ impl Context {
|
||||
|
||||
if input.pointer.any_down() && !is_interacted_with {
|
||||
// We don't hover widgets while interacting with *other* widgets:
|
||||
res.hovered = false;
|
||||
res.flags.set(Flags::HOVERED, false);
|
||||
}
|
||||
|
||||
let pointer_pressed_elsewhere = any_press && !res.hovered;
|
||||
let pointer_pressed_elsewhere = any_press && !res.hovered();
|
||||
if pointer_pressed_elsewhere && memory.has_focus(id) {
|
||||
memory.surrender_focus(id);
|
||||
}
|
||||
@@ -1455,6 +1494,11 @@ impl Context {
|
||||
self.send_cmd(crate::OutputCommand::CopyImage(image));
|
||||
}
|
||||
|
||||
/// Set the mouse cursor position (if the platform supports it).
|
||||
pub fn set_pointer_position(&self, position: Pos2) {
|
||||
self.send_cmd(crate::OutputCommand::SetPointerPosition(position));
|
||||
}
|
||||
|
||||
/// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`).
|
||||
///
|
||||
/// Can be used to get the text for [`crate::Button::shortcut_text`].
|
||||
@@ -2152,11 +2196,12 @@ impl Context {
|
||||
let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING);
|
||||
for rect in rects {
|
||||
if rect.sense.interactive() {
|
||||
let (color, text) = if rect.sense.click && rect.sense.drag {
|
||||
let (color, text) = if rect.sense.senses_click() && rect.sense.senses_drag()
|
||||
{
|
||||
(Color32::from_rgb(0x88, 0, 0x88), "click+drag")
|
||||
} else if rect.sense.click {
|
||||
} else if rect.sense.senses_click() {
|
||||
(Color32::from_rgb(0x88, 0, 0), "click")
|
||||
} else if rect.sense.drag {
|
||||
} else if rect.sense.senses_drag() {
|
||||
(Color32::from_rgb(0, 0, 0x88), "drag")
|
||||
} else {
|
||||
// unreachable since we only show interactive
|
||||
@@ -2532,10 +2577,7 @@ impl Context {
|
||||
self.input(|i| i.screen_rect()).round_ui()
|
||||
}
|
||||
|
||||
/// How much space is still available after panels has been added.
|
||||
///
|
||||
/// This is the "background" area, what egui doesn't cover with panels (but may cover with windows).
|
||||
/// This is also the area to which windows are constrained.
|
||||
/// How much space is still available after panels have been added.
|
||||
pub fn available_rect(&self) -> Rect {
|
||||
self.pass_state(|s| s.available_rect()).round_ui()
|
||||
}
|
||||
@@ -2612,10 +2654,25 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Is an egui context menu open?
|
||||
///
|
||||
/// This only works with the old, deprecated [`crate::menu`] API.
|
||||
#[allow(deprecated)]
|
||||
#[deprecated = "Use `is_popup_open` instead"]
|
||||
pub fn is_context_menu_open(&self) -> bool {
|
||||
self.data(|d| {
|
||||
d.get_temp::<crate::menu::BarState>(menu::CONTEXT_MENU_ID_STR.into())
|
||||
.map_or(false, |state| state.has_root())
|
||||
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).
|
||||
pub fn is_popup_open(&self) -> bool {
|
||||
self.pass_state_mut(|fs| {
|
||||
fs.layers
|
||||
.values()
|
||||
.any(|layer| !layer.open_popups.is_empty())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2662,7 +2719,8 @@ impl Context {
|
||||
///
|
||||
/// Can be used to implement pan and zoom (see relevant demo).
|
||||
///
|
||||
/// For a temporary transform, use [`Self::transform_layer_shapes`] instead.
|
||||
/// For a temporary transform, use [`Self::transform_layer_shapes`] or
|
||||
/// [`Ui::with_visual_transform`].
|
||||
pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) {
|
||||
self.memory_mut(|m| {
|
||||
if transform == TSTransform::IDENTITY {
|
||||
@@ -3131,7 +3189,7 @@ impl Context {
|
||||
// TODO(emilk): `Sense::hover_highlight()`
|
||||
let response =
|
||||
ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click()));
|
||||
if response.hovered && is_visible {
|
||||
if response.hovered() && is_visible {
|
||||
ui.ctx()
|
||||
.debug_painter()
|
||||
.debug_rect(area.rect(), Color32::RED, "");
|
||||
@@ -3152,13 +3210,14 @@ impl Context {
|
||||
}
|
||||
});
|
||||
|
||||
#[allow(deprecated)]
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!(
|
||||
"{} menu bars",
|
||||
self.data(|d| d.count::<menu::BarState>())
|
||||
self.data(|d| d.count::<crate::menu::BarState>())
|
||||
));
|
||||
if ui.button("Reset").clicked() {
|
||||
self.data_mut(|d| d.remove_by_type::<menu::BarState>());
|
||||
self.data_mut(|d| d.remove_by_type::<crate::menu::BarState>());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3225,7 +3284,11 @@ impl Context {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.pass_state_mut(|fs| {
|
||||
if let Some(state) = fs.accesskit_state.as_mut() {
|
||||
assert_eq!(state.parent_stack.pop(), Some(_id));
|
||||
assert_eq!(
|
||||
state.parent_stack.pop(),
|
||||
Some(_id),
|
||||
"Mismatched push/pop in with_accessibility_parent"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3488,7 +3551,6 @@ impl Context {
|
||||
|
||||
/// The loaders of bytes, images, and textures.
|
||||
pub fn loaders(&self) -> Arc<Loaders> {
|
||||
profiling::function_scope!();
|
||||
self.read(|this| this.loaders.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,11 +214,21 @@ pub struct ViewportInfo {
|
||||
/// The inner rectangle of the native window, in monitor space and ui points scale.
|
||||
///
|
||||
/// This is the content rectangle of the viewport.
|
||||
///
|
||||
/// **`eframe` notes**:
|
||||
///
|
||||
/// On Android / Wayland, this will always be `None` since getting the
|
||||
/// position of the window is not possible.
|
||||
pub inner_rect: Option<Rect>,
|
||||
|
||||
/// The outer rectangle of the native window, in monitor space and ui points scale.
|
||||
///
|
||||
/// This is the content rectangle plus decoration chrome.
|
||||
///
|
||||
/// **`eframe` notes**:
|
||||
///
|
||||
/// On Android / Wayland, this will always be `None` since getting the
|
||||
/// position of the window is not possible.
|
||||
pub outer_rect: Option<Rect>,
|
||||
|
||||
/// Are we minimized?
|
||||
@@ -943,6 +953,13 @@ impl std::ops::BitOr for Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOrAssign for Modifiers {
|
||||
#[inline]
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
*self = *self | rhs;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Names of different modifier keys.
|
||||
@@ -986,7 +1003,7 @@ impl ModifierNames<'static> {
|
||||
};
|
||||
}
|
||||
|
||||
impl<'a> ModifierNames<'a> {
|
||||
impl ModifierNames<'_> {
|
||||
pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String {
|
||||
let mut s = String::new();
|
||||
|
||||
@@ -1084,7 +1101,7 @@ impl RawInput {
|
||||
system_theme,
|
||||
} = self;
|
||||
|
||||
ui.label(format!("Active viwport: {viewport_id:?}"));
|
||||
ui.label(format!("Active viewport: {viewport_id:?}"));
|
||||
for (id, viewport) in viewports {
|
||||
ui.group(|ui| {
|
||||
ui.label(format!("Viewport {id:?}"));
|
||||
|
||||
@@ -49,12 +49,21 @@ pub enum Key {
|
||||
/// `?`
|
||||
Questionmark,
|
||||
|
||||
// '!'
|
||||
Exclamationmark,
|
||||
|
||||
// `[`
|
||||
OpenBracket,
|
||||
|
||||
// `]`
|
||||
CloseBracket,
|
||||
|
||||
// `{`
|
||||
OpenCurlyBracket,
|
||||
|
||||
// `}`
|
||||
CloseCurlyBracket,
|
||||
|
||||
/// Also known as "backquote" or "grave"
|
||||
Backtick,
|
||||
|
||||
@@ -215,11 +224,14 @@ impl Key {
|
||||
Self::Semicolon,
|
||||
Self::OpenBracket,
|
||||
Self::CloseBracket,
|
||||
Self::OpenCurlyBracket,
|
||||
Self::CloseCurlyBracket,
|
||||
Self::Backtick,
|
||||
Self::Backslash,
|
||||
Self::Slash,
|
||||
Self::Pipe,
|
||||
Self::Questionmark,
|
||||
Self::Exclamationmark,
|
||||
Self::Quote,
|
||||
// Digits:
|
||||
Self::Num0,
|
||||
@@ -341,8 +353,11 @@ impl Key {
|
||||
"/" | "Slash" => Self::Slash,
|
||||
"|" | "Pipe" => Self::Pipe,
|
||||
"?" | "Questionmark" => Self::Questionmark,
|
||||
"!" | "Exclamationmark" => Self::Exclamationmark,
|
||||
"[" | "OpenBracket" => Self::OpenBracket,
|
||||
"]" | "CloseBracket" => Self::CloseBracket,
|
||||
"{" | "OpenCurlyBracket" => Self::OpenCurlyBracket,
|
||||
"}" | "CloseCurlyBracket" => Self::CloseCurlyBracket,
|
||||
"`" | "Backtick" | "Backquote" | "Grave" => Self::Backtick,
|
||||
"'" | "Quote" => Self::Quote,
|
||||
|
||||
@@ -446,8 +461,11 @@ impl Key {
|
||||
Self::Slash => "/",
|
||||
Self::Pipe => "|",
|
||||
Self::Questionmark => "?",
|
||||
Self::Exclamationmark => "!",
|
||||
Self::OpenBracket => "[",
|
||||
Self::CloseBracket => "]",
|
||||
Self::OpenCurlyBracket => "{",
|
||||
Self::CloseCurlyBracket => "}",
|
||||
Self::Backtick => "`",
|
||||
|
||||
_ => self.name(),
|
||||
@@ -490,8 +508,11 @@ impl Key {
|
||||
Self::Slash => "Slash",
|
||||
Self::Pipe => "Pipe",
|
||||
Self::Questionmark => "Questionmark",
|
||||
Self::Exclamationmark => "Exclamationmark",
|
||||
Self::OpenBracket => "OpenBracket",
|
||||
Self::CloseBracket => "CloseBracket",
|
||||
Self::OpenCurlyBracket => "OpenCurlyBracket",
|
||||
Self::CloseCurlyBracket => "CloseCurlyBracket",
|
||||
Self::Backtick => "Backtick",
|
||||
Self::Quote => "Quote",
|
||||
|
||||
|
||||
@@ -95,6 +95,9 @@ pub enum OutputCommand {
|
||||
|
||||
/// Open this url in a browser.
|
||||
OpenUrl(OpenUrl),
|
||||
|
||||
/// Set the mouse cursor position (if the platform supports it).
|
||||
SetPointerPosition(emath::Pos2),
|
||||
}
|
||||
|
||||
/// The non-rendering part of what egui emits each frame.
|
||||
@@ -112,7 +115,7 @@ pub struct PlatformOutput {
|
||||
pub cursor_icon: CursorIcon,
|
||||
|
||||
/// If set, open this url.
|
||||
#[deprecated = "Use `Context::open_url` instead"]
|
||||
#[deprecated = "Use `Context::open_url` or `PlatformOutput::commands` instead"]
|
||||
pub open_url: Option<OpenUrl>,
|
||||
|
||||
/// If set, put this text in the system clipboard. Ignore if empty.
|
||||
@@ -126,7 +129,7 @@ pub struct PlatformOutput {
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[deprecated = "Use `Context::copy_text` instead"]
|
||||
#[deprecated = "Use `Context::copy_text` or `PlatformOutput::commands` instead"]
|
||||
pub copied_text: String,
|
||||
|
||||
/// Events that may be useful to e.g. a screen reader.
|
||||
@@ -539,6 +542,9 @@ pub struct WidgetInfo {
|
||||
|
||||
/// Selected range of characters in [`Self::current_text_value`].
|
||||
pub text_selection: Option<std::ops::RangeInclusive<usize>>,
|
||||
|
||||
/// The hint text for text edit fields.
|
||||
pub hint_text: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WidgetInfo {
|
||||
@@ -552,6 +558,7 @@ impl std::fmt::Debug for WidgetInfo {
|
||||
selected,
|
||||
value,
|
||||
text_selection,
|
||||
hint_text,
|
||||
} = self;
|
||||
|
||||
let mut s = f.debug_struct("WidgetInfo");
|
||||
@@ -580,6 +587,9 @@ impl std::fmt::Debug for WidgetInfo {
|
||||
if let Some(text_selection) = text_selection {
|
||||
s.field("text_selection", text_selection);
|
||||
}
|
||||
if let Some(hint_text) = hint_text {
|
||||
s.field("hint_text", hint_text);
|
||||
}
|
||||
|
||||
s.finish()
|
||||
}
|
||||
@@ -596,6 +606,7 @@ impl WidgetInfo {
|
||||
selected: None,
|
||||
value: None,
|
||||
text_selection: None,
|
||||
hint_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,9 +654,11 @@ impl WidgetInfo {
|
||||
enabled: bool,
|
||||
prev_text_value: impl ToString,
|
||||
text_value: impl ToString,
|
||||
hint_text: impl ToString,
|
||||
) -> Self {
|
||||
let text_value = text_value.to_string();
|
||||
let prev_text_value = prev_text_value.to_string();
|
||||
let hint_text = hint_text.to_string();
|
||||
let prev_text_value = if text_value == prev_text_value {
|
||||
None
|
||||
} else {
|
||||
@@ -655,6 +668,7 @@ impl WidgetInfo {
|
||||
enabled,
|
||||
current_text_value: Some(text_value),
|
||||
prev_text_value,
|
||||
hint_text: Some(hint_text),
|
||||
..Self::new(WidgetType::TextEdit)
|
||||
}
|
||||
}
|
||||
@@ -684,6 +698,7 @@ impl WidgetInfo {
|
||||
selected,
|
||||
value,
|
||||
text_selection: _,
|
||||
hint_text: _,
|
||||
} = self;
|
||||
|
||||
// TODO(emilk): localization
|
||||
|
||||
@@ -54,7 +54,7 @@ impl<'de> serde::Deserialize<'de> for UserData {
|
||||
{
|
||||
struct UserDataVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for UserDataVisitor {
|
||||
impl serde::de::Visitor<'_> for UserDataVisitor {
|
||||
type Value = UserData;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
||||
@@ -141,7 +141,7 @@ impl DragAndDrop {
|
||||
pub fn has_any_payload(ctx: &Context) -> bool {
|
||||
ctx.data(|data| {
|
||||
let state = data.get_temp::<Self>(Id::NULL);
|
||||
state.map_or(false, |state| state.payload.is_some())
|
||||
state.is_some_and(|state| state.payload.is_some())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,12 @@ impl GridLayout {
|
||||
|
||||
if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
|
||||
let painter = self.ctx.debug_painter();
|
||||
painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE));
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
(1.0, Color32::LIGHT_BLUE),
|
||||
crate::StrokeKind::Inside,
|
||||
);
|
||||
|
||||
let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
|
||||
let paint_line_seg = |a, b| painter.line_segment([a, b], stroke);
|
||||
@@ -451,7 +456,7 @@ impl Grid {
|
||||
ui_builder = ui_builder.sizing_pass().invisible();
|
||||
}
|
||||
|
||||
ui.allocate_new_ui(ui_builder, |ui| {
|
||||
ui.scope_builder(ui_builder, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let is_color = color_picker.is_some();
|
||||
let grid = GridLayout {
|
||||
|
||||
@@ -88,7 +88,6 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
|
||||
.clicked()
|
||||
{
|
||||
zoom_in(ui.ctx());
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui
|
||||
@@ -99,7 +98,6 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
|
||||
.clicked()
|
||||
{
|
||||
zoom_out(ui.ctx());
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui
|
||||
@@ -110,6 +108,5 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().set_zoom_factor(1.0);
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use ahash::HashMap;
|
||||
|
||||
use emath::TSTransform;
|
||||
|
||||
use crate::{ahash, emath, LayerId, Pos2, Rect, WidgetRect, WidgetRects};
|
||||
use crate::{ahash, emath, id::IdSet, LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects};
|
||||
|
||||
/// Result of a hit-test against [`WidgetRects`].
|
||||
///
|
||||
@@ -128,11 +128,28 @@ pub fn hit_test(
|
||||
// the `enabled` flag everywhere:
|
||||
for w in &mut close {
|
||||
if !w.enabled {
|
||||
w.sense.click = false;
|
||||
w.sense.drag = false;
|
||||
w.sense -= Sense::CLICK;
|
||||
w.sense -= Sense::DRAG;
|
||||
}
|
||||
}
|
||||
|
||||
// Find widgets which are hidden behind another widget and discard them.
|
||||
// This is the case when a widget fully contains another widget and is on a different layer.
|
||||
// It prevents "hovering through" widgets when there is a clickable widget behind.
|
||||
|
||||
let mut hidden = IdSet::default();
|
||||
for (i, current) in close.iter().enumerate().rev() {
|
||||
for next in &close[i + 1..] {
|
||||
if next.interact_rect.contains_rect(current.interact_rect)
|
||||
&& current.layer_id != next.layer_id
|
||||
{
|
||||
hidden.insert(current.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close.retain(|c| !hidden.contains(&c.id));
|
||||
|
||||
let mut hits = hit_test_on_close(&close, pos);
|
||||
|
||||
hits.contains_pointer = close
|
||||
@@ -158,11 +175,17 @@ pub fn hit_test(
|
||||
restore_widget_rect(wr);
|
||||
}
|
||||
if let Some(wr) = &mut hits.drag {
|
||||
debug_assert!(wr.sense.drag);
|
||||
debug_assert!(
|
||||
wr.sense.senses_drag(),
|
||||
"We should only return drag hits if they sense drag"
|
||||
);
|
||||
restore_widget_rect(wr);
|
||||
}
|
||||
if let Some(wr) = &mut hits.click {
|
||||
debug_assert!(wr.sense.click);
|
||||
debug_assert!(
|
||||
wr.sense.senses_click(),
|
||||
"We should only return click hits if they sense click"
|
||||
);
|
||||
restore_widget_rect(wr);
|
||||
}
|
||||
}
|
||||
@@ -179,8 +202,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
|
||||
#![allow(clippy::collapsible_else_if)]
|
||||
|
||||
// First find the best direct hits:
|
||||
let hit_click = find_closest_within(close.iter().copied().filter(|w| w.sense.click), pos, 0.0);
|
||||
let hit_drag = find_closest_within(close.iter().copied().filter(|w| w.sense.drag), pos, 0.0);
|
||||
let hit_click = find_closest_within(
|
||||
close.iter().copied().filter(|w| w.sense.senses_click()),
|
||||
pos,
|
||||
0.0,
|
||||
);
|
||||
let hit_drag = find_closest_within(
|
||||
close.iter().copied().filter(|w| w.sense.senses_drag()),
|
||||
pos,
|
||||
0.0,
|
||||
);
|
||||
|
||||
match (hit_click, hit_drag) {
|
||||
(None, None) => {
|
||||
@@ -190,14 +221,14 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
|
||||
close
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|w| w.sense.click || w.sense.drag),
|
||||
.filter(|w| w.sense.senses_click() || w.sense.senses_drag()),
|
||||
pos,
|
||||
);
|
||||
|
||||
if let Some(closest) = closest {
|
||||
WidgetHits {
|
||||
click: closest.sense.click.then_some(closest),
|
||||
drag: closest.sense.drag.then_some(closest),
|
||||
click: closest.sense.senses_click().then_some(closest),
|
||||
drag: closest.sense.senses_drag().then_some(closest),
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
@@ -218,9 +249,12 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
|
||||
// or a moveable window.
|
||||
// It could also be something small, like a slider, or panel resize handle.
|
||||
|
||||
let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos);
|
||||
let closest_click = find_closest(
|
||||
close.iter().copied().filter(|w| w.sense.senses_click()),
|
||||
pos,
|
||||
);
|
||||
if let Some(closest_click) = closest_click {
|
||||
if closest_click.sense.drag {
|
||||
if closest_click.sense.senses_drag() {
|
||||
// We have something close that sense both clicks and drag.
|
||||
// Should we use it over the direct drag-hit?
|
||||
if hit_drag
|
||||
@@ -244,7 +278,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// These is a close pure-click widget.
|
||||
// This is a close pure-click widget.
|
||||
// However, we should be careful to only return two different widgets
|
||||
// when it is absolutely not going to confuse the user.
|
||||
if hit_drag
|
||||
@@ -277,7 +311,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
|
||||
close
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|w| w.sense.drag && w.id != hit_drag.id),
|
||||
.filter(|w| w.sense.senses_drag() && w.id != hit_drag.id),
|
||||
pos,
|
||||
);
|
||||
|
||||
@@ -331,7 +365,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
|
||||
|
||||
let click_is_on_top_of_drag = drag_idx < click_idx;
|
||||
if click_is_on_top_of_drag {
|
||||
if hit_click.sense.drag {
|
||||
if hit_click.sense.senses_drag() {
|
||||
// The top thing senses both clicks and drags.
|
||||
WidgetHits {
|
||||
click: Some(hit_click),
|
||||
@@ -349,7 +383,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if hit_drag.sense.click {
|
||||
if hit_drag.sense.senses_click() {
|
||||
// The top thing senses both clicks and drags.
|
||||
WidgetHits {
|
||||
click: Some(hit_drag),
|
||||
@@ -393,7 +427,7 @@ fn find_closest_within(
|
||||
if dist_sq == closest_dist_sq {
|
||||
// It's a tie! Pick the thin candidate over the thick one.
|
||||
// This makes it easier to hit a thin resize-handle, for instance:
|
||||
if should_prioritizie_hits_on_back(closest.interact_rect, widget.interact_rect) {
|
||||
if should_prioritize_hits_on_back(closest.interact_rect, widget.interact_rect) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -409,12 +443,12 @@ fn find_closest_within(
|
||||
closest
|
||||
}
|
||||
|
||||
/// Should we prioritizie hits on `back` over those on `front`?
|
||||
/// Should we prioritize hits on `back` over those on `front`?
|
||||
///
|
||||
/// `back` should be behind the `front` widget.
|
||||
///
|
||||
/// Returns true if `back` is a small hit-target and `front` is not.
|
||||
fn should_prioritizie_hits_on_back(back: Rect, front: Rect) -> bool {
|
||||
fn should_prioritize_hits_on_back(back: Rect, front: Rect) -> bool {
|
||||
if front.contains_rect(back) {
|
||||
return false; // back widget is fully occluded; no way to hit it
|
||||
}
|
||||
@@ -484,7 +518,7 @@ mod tests {
|
||||
assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag"));
|
||||
assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag"));
|
||||
|
||||
// Close hit - should still ignore the drag-background so as not to confuse the userr:
|
||||
// Close hit - should still ignore the drag-background so as not to confuse the user:
|
||||
let hits = hit_test_on_close(&widgets, pos2(105.0, 5.0));
|
||||
assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag"));
|
||||
assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag"));
|
||||
|
||||
@@ -33,6 +33,8 @@ use std::num::NonZeroU64;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Id(NonZeroU64);
|
||||
|
||||
impl nohash_hasher::IsEnabled for Id {}
|
||||
|
||||
impl Id {
|
||||
/// A special [`Id`], in particular as a key to [`crate::Memory::data`]
|
||||
/// for when there is no particular widget to attach the data.
|
||||
@@ -112,77 +114,8 @@ fn id_size() {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Idea taken from the `nohash_hasher` crate.
|
||||
#[derive(Default)]
|
||||
pub struct IdHasher(u64);
|
||||
|
||||
impl std::hash::Hasher for IdHasher {
|
||||
fn write(&mut self, _: &[u8]) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_u8(&mut self, _n: u8) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_u16(&mut self, _n: u16) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_u32(&mut self, _n: u32) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn write_u64(&mut self, n: u64) {
|
||||
self.0 = n;
|
||||
}
|
||||
|
||||
fn write_usize(&mut self, _n: usize) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_i8(&mut self, _n: i8) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_i16(&mut self, _n: i16) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_i32(&mut self, _n: i32) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_i64(&mut self, _n: i64) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
fn write_isize(&mut self, _n: isize) {
|
||||
unreachable!("Invalid use of IdHasher");
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct BuildIdHasher {}
|
||||
|
||||
impl std::hash::BuildHasher for BuildIdHasher {
|
||||
type Hasher = IdHasher;
|
||||
|
||||
#[inline(always)]
|
||||
fn build_hasher(&self) -> IdHasher {
|
||||
IdHasher::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
pub type IdSet = std::collections::HashSet<Id, BuildIdHasher>;
|
||||
pub type IdSet = nohash_hasher::IntSet<Id>;
|
||||
|
||||
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
pub type IdMap<V> = std::collections::HashMap<Id, V, BuildIdHasher>;
|
||||
pub type IdMap<V> = nohash_hasher::IntMap<Id, V>;
|
||||
|
||||
@@ -362,6 +362,14 @@ impl InputState {
|
||||
Event::Zoom(factor) => {
|
||||
zoom_factor_delta *= *factor;
|
||||
}
|
||||
Event::WindowFocused(false) => {
|
||||
// Example: pressing `Cmd+S` brings up a save-dialog (e.g. using rfd),
|
||||
// but we get no key-up event for the `S` key (in winit).
|
||||
// This leads to `S` being mistakenly marked as down when we switch back to the app.
|
||||
// So we take the safe route and just clear all the keys and modifiers when
|
||||
// the app loses focus.
|
||||
keys_down.clear();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -919,6 +927,7 @@ impl PointerState {
|
||||
self.motion = Some(Vec2::ZERO);
|
||||
}
|
||||
|
||||
let mut clear_history_after_velocity_calculation = false;
|
||||
for event in &new.events {
|
||||
match event {
|
||||
Event::PointerMoved(pos) => {
|
||||
@@ -1005,7 +1014,10 @@ impl PointerState {
|
||||
// When dragging a slider and the mouse leaves the viewport, we still want the drag to work,
|
||||
// so we don't treat this as a `PointerEvent::Released`.
|
||||
// NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame.
|
||||
self.pos_history.clear();
|
||||
|
||||
// Delay the clearing until after the final velocity calculation, so we can
|
||||
// get the final velocity when `drag_stopped` is true.
|
||||
clear_history_after_velocity_calculation = true;
|
||||
}
|
||||
Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta,
|
||||
_ => {}
|
||||
@@ -1036,6 +1048,9 @@ impl PointerState {
|
||||
if self.velocity != Vec2::ZERO {
|
||||
self.last_move_time = time;
|
||||
}
|
||||
if clear_history_after_velocity_calculation {
|
||||
self.pos_history.clear();
|
||||
}
|
||||
|
||||
self.direction = self.pos_history.velocity().unwrap_or_default().normalized();
|
||||
|
||||
@@ -1303,7 +1318,7 @@ impl PointerState {
|
||||
self.started_decidedly_dragging
|
||||
&& !self.has_moved_too_much_for_a_click
|
||||
&& self.button_down(PointerButton::Primary)
|
||||
&& self.press_start_time.map_or(false, |press_start_time| {
|
||||
&& self.press_start_time.is_some_and(|press_start_time| {
|
||||
self.time - press_start_time > self.input_options.max_click_duration
|
||||
})
|
||||
}
|
||||
@@ -1325,6 +1340,18 @@ impl PointerState {
|
||||
pub fn middle_down(&self) -> bool {
|
||||
self.button_down(PointerButton::Middle)
|
||||
}
|
||||
|
||||
/// Is the mouse moving in the direction of the given rect?
|
||||
pub fn is_moving_towards_rect(&self, rect: &Rect) -> bool {
|
||||
if self.is_still() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(pos) = self.hover_pos() {
|
||||
return rect.intersects_ray(pos, self.direction());
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
|
||||
@@ -192,14 +192,14 @@ pub(crate) fn interact(
|
||||
// Check if we started dragging something new:
|
||||
if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) {
|
||||
if widget.enabled {
|
||||
let is_dragged = if widget.sense.click && widget.sense.drag {
|
||||
let is_dragged = if widget.sense.senses_click() && widget.sense.senses_drag() {
|
||||
// This widget is sensitive to both clicks and drags.
|
||||
// When the mouse first is pressed, it could be either,
|
||||
// so we postpone the decision until we know.
|
||||
input.pointer.is_decidedly_dragging()
|
||||
} else {
|
||||
// This widget is just sensitive to drags, so we can mark it as dragged right away:
|
||||
widget.sense.drag
|
||||
widget.sense.senses_drag()
|
||||
};
|
||||
|
||||
if is_dragged {
|
||||
@@ -271,7 +271,7 @@ pub(crate) fn interact(
|
||||
let mut hovered: IdSet = hits.click.iter().chain(&hits.drag).map(|w| w.id).collect();
|
||||
|
||||
for w in &hits.contains_pointer {
|
||||
let is_interactive = w.sense.click || w.sense.drag;
|
||||
let is_interactive = w.sense.senses_click() || w.sense.senses_drag();
|
||||
if is_interactive {
|
||||
// The only interactive widgets we mark as hovered are the ones
|
||||
// in `hits.click` and `hits.drag`!
|
||||
|
||||
@@ -160,7 +160,7 @@ impl Widget for &mut epaint::TessellationOptions {
|
||||
.on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain.");
|
||||
|
||||
if *feathering {
|
||||
ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.1).suffix(" px"));
|
||||
ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.025).suffix(" px"));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -71,9 +71,17 @@ impl Region {
|
||||
}
|
||||
|
||||
pub fn sanity_check(&self) {
|
||||
debug_assert!(!self.min_rect.any_nan());
|
||||
debug_assert!(!self.max_rect.any_nan());
|
||||
debug_assert!(!self.cursor.any_nan());
|
||||
debug_assert!(
|
||||
!self.min_rect.any_nan(),
|
||||
"min rect has Nan: {:?}",
|
||||
self.min_rect
|
||||
);
|
||||
debug_assert!(
|
||||
!self.max_rect.any_nan(),
|
||||
"max rect has Nan: {:?}",
|
||||
self.max_rect
|
||||
);
|
||||
debug_assert!(!self.cursor.any_nan(), "cursor has Nan: {:?}", self.cursor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,8 +402,8 @@ impl Layout {
|
||||
/// ## Doing layout
|
||||
impl Layout {
|
||||
pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect {
|
||||
debug_assert!(size.x >= 0.0 && size.y >= 0.0);
|
||||
debug_assert!(!outer.is_negative());
|
||||
debug_assert!(size.x >= 0.0 && size.y >= 0.0, "Negative size: {size:?}");
|
||||
debug_assert!(!outer.is_negative(), "Negative outer: {outer:?}");
|
||||
self.align2().align_size_within_rect(size, outer).round_ui()
|
||||
}
|
||||
|
||||
@@ -421,7 +429,7 @@ impl Layout {
|
||||
}
|
||||
|
||||
pub(crate) fn region_from_max_rect(&self, max_rect: Rect) -> Region {
|
||||
debug_assert!(!max_rect.any_nan());
|
||||
debug_assert!(!max_rect.any_nan(), "max_rect is not NaN: {max_rect:?}");
|
||||
let mut region = Region {
|
||||
min_rect: Rect::NOTHING, // temporary
|
||||
max_rect,
|
||||
@@ -454,8 +462,8 @@ impl Layout {
|
||||
/// Given the cursor in the region, how much space is available
|
||||
/// for the next widget?
|
||||
fn available_from_cursor_max_rect(&self, cursor: Rect, max_rect: Rect) -> Rect {
|
||||
debug_assert!(!cursor.any_nan());
|
||||
debug_assert!(!max_rect.any_nan());
|
||||
debug_assert!(!cursor.any_nan(), "cursor is NaN: {cursor:?}");
|
||||
debug_assert!(!max_rect.any_nan(), "max_rect is NaN: {max_rect:?}");
|
||||
|
||||
// NOTE: in normal top-down layout the cursor has moved below the current max_rect,
|
||||
// but the available shouldn't be negative.
|
||||
@@ -509,7 +517,7 @@ impl Layout {
|
||||
avail.max.y = y;
|
||||
}
|
||||
|
||||
debug_assert!(!avail.any_nan());
|
||||
debug_assert!(!avail.any_nan(), "avail is NaN: {avail:?}");
|
||||
|
||||
avail
|
||||
}
|
||||
@@ -520,7 +528,10 @@ impl Layout {
|
||||
/// Use `justify_and_align` to get the inner `widget_rect`.
|
||||
pub(crate) fn next_frame(&self, region: &Region, child_size: Vec2, spacing: Vec2) -> Rect {
|
||||
region.sanity_check();
|
||||
debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
|
||||
debug_assert!(
|
||||
child_size.x >= 0.0 && child_size.y >= 0.0,
|
||||
"Negative size: {child_size:?}"
|
||||
);
|
||||
|
||||
if self.main_wrap {
|
||||
let available_size = self.available_rect_before_wrap(region).size();
|
||||
@@ -600,7 +611,10 @@ impl Layout {
|
||||
|
||||
fn next_frame_ignore_wrap(&self, region: &Region, child_size: Vec2) -> Rect {
|
||||
region.sanity_check();
|
||||
debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
|
||||
debug_assert!(
|
||||
child_size.x >= 0.0 && child_size.y >= 0.0,
|
||||
"Negative size: {child_size:?}"
|
||||
);
|
||||
|
||||
let available_rect = self.available_rect_before_wrap(region);
|
||||
|
||||
@@ -633,16 +647,19 @@ impl Layout {
|
||||
frame_rect = frame_rect.translate(Vec2::Y * (region.cursor.top() - frame_rect.top()));
|
||||
}
|
||||
|
||||
debug_assert!(!frame_rect.any_nan());
|
||||
debug_assert!(!frame_rect.is_negative());
|
||||
debug_assert!(!frame_rect.any_nan(), "frame_rect is NaN: {frame_rect:?}");
|
||||
debug_assert!(!frame_rect.is_negative(), "frame_rect is negative");
|
||||
|
||||
frame_rect.round_ui()
|
||||
}
|
||||
|
||||
/// Apply justify (fill width/height) and/or alignment after calling `next_space`.
|
||||
pub(crate) fn justify_and_align(&self, frame: Rect, mut child_size: Vec2) -> Rect {
|
||||
debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
|
||||
debug_assert!(!frame.is_negative());
|
||||
debug_assert!(
|
||||
child_size.x >= 0.0 && child_size.y >= 0.0,
|
||||
"Negative size: {child_size:?}"
|
||||
);
|
||||
debug_assert!(!frame.is_negative(), "frame is negative");
|
||||
|
||||
if self.horizontal_justify() {
|
||||
child_size.x = child_size.x.at_least(frame.width()); // fill full width
|
||||
@@ -660,8 +677,8 @@ impl Layout {
|
||||
) -> Rect {
|
||||
let frame = self.next_frame_ignore_wrap(region, size);
|
||||
let rect = self.align_size_within_rect(size, frame);
|
||||
debug_assert!(!rect.any_nan());
|
||||
debug_assert!(!rect.is_negative());
|
||||
debug_assert!(!rect.any_nan(), "rect is NaN: {rect:?}");
|
||||
debug_assert!(!rect.is_negative(), "rect is negative: {rect:?}");
|
||||
rect
|
||||
}
|
||||
|
||||
@@ -704,7 +721,7 @@ impl Layout {
|
||||
widget_rect: Rect,
|
||||
item_spacing: Vec2,
|
||||
) {
|
||||
debug_assert!(!cursor.any_nan());
|
||||
debug_assert!(!cursor.any_nan(), "cursor is NaN: {cursor:?}");
|
||||
if self.main_wrap {
|
||||
if cursor.intersects(frame_rect.shrink(1.0)) {
|
||||
// make row/column larger if necessary
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
|
||||
//!
|
||||
//! `egui` is in heavy development, with each new version having breaking changes.
|
||||
//! You need to have rust 1.80.0 or later to use `egui`.
|
||||
//! You need to have rust 1.81.0 or later to use `egui`.
|
||||
//!
|
||||
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
|
||||
//! which uses [`eframe`](https://docs.rs/eframe).
|
||||
@@ -422,12 +422,13 @@ 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;
|
||||
pub(crate) mod placer;
|
||||
mod response;
|
||||
pub mod response;
|
||||
mod sense;
|
||||
pub mod style;
|
||||
pub mod text_selection;
|
||||
@@ -458,18 +459,19 @@ pub use epaint::emath;
|
||||
pub use ecolor::hex_color;
|
||||
pub use ecolor::{Color32, Rgba};
|
||||
pub use emath::{
|
||||
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, Vec2, Vec2b,
|
||||
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign,
|
||||
Vec2, Vec2b,
|
||||
};
|
||||
pub use epaint::{
|
||||
mutex,
|
||||
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
|
||||
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
|
||||
ClippedPrimitive, ColorImage, FontImage, ImageData, Margin, Mesh, PaintCallback,
|
||||
PaintCallbackInfo, Rounding, Shadow, Shape, Stroke, TextureHandle, TextureId,
|
||||
ClippedPrimitive, ColorImage, CornerRadius, FontImage, ImageData, Margin, Mesh, PaintCallback,
|
||||
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId,
|
||||
};
|
||||
|
||||
pub mod text {
|
||||
pub use crate::text_selection::{CCursorRange, CursorRange};
|
||||
pub use crate::text_selection::CCursorRange;
|
||||
pub use epaint::text::{
|
||||
cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob,
|
||||
LayoutSection, TextFormat, TextWrapping, TAB_SIZE,
|
||||
@@ -510,6 +512,9 @@ pub use self::{
|
||||
widgets::*,
|
||||
};
|
||||
|
||||
#[deprecated = "Renamed to CornerRadius"]
|
||||
pub type Rounding = CornerRadius;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Helper function that adds a label when compiling with debug assertions enabled.
|
||||
@@ -538,7 +543,7 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
|
||||
/// ui.add(
|
||||
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
|
||||
/// .max_width(200.0)
|
||||
/// .rounding(10.0),
|
||||
/// .corner_radius(10),
|
||||
/// );
|
||||
///
|
||||
/// let image_source: egui::ImageSource = egui::include_image!("../assets/ferris.png");
|
||||
@@ -625,9 +630,6 @@ pub mod special_emojis {
|
||||
/// The Github logo.
|
||||
pub const GITHUB: char = '';
|
||||
|
||||
/// The Twitter bird.
|
||||
pub const TWITTER: char = '';
|
||||
|
||||
/// The word `git`.
|
||||
pub const GIT: char = '';
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{
|
||||
BytesLoader, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle,
|
||||
TextureLoadResult, TextureLoader, TextureOptions, TexturePoll,
|
||||
@@ -5,7 +7,7 @@ use super::{
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DefaultTextureLoader {
|
||||
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
|
||||
cache: Mutex<HashMap<(Cow<'static, str>, TextureOptions), TextureHandle>>,
|
||||
}
|
||||
|
||||
impl TextureLoader for DefaultTextureLoader {
|
||||
@@ -21,7 +23,7 @@ impl TextureLoader for DefaultTextureLoader {
|
||||
size_hint: SizeHint,
|
||||
) -> TextureLoadResult {
|
||||
let mut cache = self.cache.lock();
|
||||
if let Some(handle) = cache.get(&(uri.into(), texture_options)) {
|
||||
if let Some(handle) = cache.get(&(Cow::Borrowed(uri), texture_options)) {
|
||||
let texture = SizedTexture::from_handle(handle);
|
||||
Ok(TexturePoll::Ready { texture })
|
||||
} else {
|
||||
@@ -30,7 +32,7 @@ impl TextureLoader for DefaultTextureLoader {
|
||||
ImagePoll::Ready { image } => {
|
||||
let handle = ctx.load_texture(uri, image, texture_options);
|
||||
let texture = SizedTexture::from_handle(&handle);
|
||||
cache.insert((uri.into(), texture_options), handle);
|
||||
cache.insert((Cow::Owned(uri.to_owned()), texture_options), handle);
|
||||
let reduce_texture_memory = ctx.options(|o| o.reduce_texture_memory);
|
||||
if reduce_texture_memory {
|
||||
let loaders = ctx.loaders();
|
||||
|
||||
@@ -89,8 +89,12 @@ pub struct Memory {
|
||||
|
||||
/// Which popup-window is open (if any)?
|
||||
/// Could be a combo box, color picker, menu, etc.
|
||||
/// Optionally stores the position of the popup (usually this would be the position where
|
||||
/// the user clicked).
|
||||
/// If position is [`None`], the popup position will be calculated based on some configuration
|
||||
/// (e.g. relative to some other widget).
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
popup: Option<Id>,
|
||||
popup: Option<OpenPopup>,
|
||||
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
everything_is_visible: bool,
|
||||
@@ -318,7 +322,7 @@ impl Default for Options {
|
||||
Self {
|
||||
dark_style: std::sync::Arc::new(Theme::Dark.default_style()),
|
||||
light_style: std::sync::Arc::new(Theme::Light.default_style()),
|
||||
theme_preference: ThemePreference::System,
|
||||
theme_preference: Default::default(),
|
||||
fallback_theme: Theme::Dark,
|
||||
system_theme: None,
|
||||
zoom_factor: 1.0,
|
||||
@@ -803,6 +807,15 @@ impl Memory {
|
||||
self.caches.update();
|
||||
self.areas_mut().end_pass();
|
||||
self.focus_mut().end_pass(used_ids);
|
||||
|
||||
// Clean up abandoned popups.
|
||||
if let Some(popup) = &mut self.popup {
|
||||
if popup.open_this_frame {
|
||||
popup.open_this_frame = false;
|
||||
} else {
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) {
|
||||
@@ -1064,13 +1077,37 @@ impl Memory {
|
||||
}
|
||||
}
|
||||
|
||||
/// State of an open popup.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct OpenPopup {
|
||||
/// Id of the popup.
|
||||
id: Id,
|
||||
|
||||
/// Optional position of the popup.
|
||||
pos: Option<Pos2>,
|
||||
|
||||
/// Whether this popup was still open this frame. Otherwise it's considered abandoned and `Memory::popup` will be cleared.
|
||||
open_this_frame: bool,
|
||||
}
|
||||
|
||||
impl OpenPopup {
|
||||
/// Create a new `OpenPopup`.
|
||||
fn new(id: Id, pos: Option<Pos2>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
pos,
|
||||
open_this_frame: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Popups
|
||||
/// Popups are things like combo-boxes, color pickers, menus etc.
|
||||
/// Only one can be open at a time.
|
||||
impl Memory {
|
||||
/// Is the given popup open?
|
||||
pub fn is_popup_open(&self, popup_id: Id) -> bool {
|
||||
self.popup == Some(popup_id) || self.everything_is_visible()
|
||||
self.popup.is_some_and(|state| state.id == popup_id) || self.everything_is_visible()
|
||||
}
|
||||
|
||||
/// Is any popup open?
|
||||
@@ -1079,21 +1116,56 @@ impl Memory {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn open_popup(&mut self, popup_id: Id) {
|
||||
self.popup = Some(popup_id);
|
||||
self.popup = Some(OpenPopup::new(popup_id, None));
|
||||
}
|
||||
|
||||
/// Close the open popup, if any.
|
||||
pub fn close_popup(&mut self) {
|
||||
/// 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.
|
||||
pub fn keep_popup_open(&mut self, popup_id: Id) {
|
||||
if let Some(state) = self.popup.as_mut() {
|
||||
if state.id == popup_id {
|
||||
state.open_this_frame = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the popup and remember its position.
|
||||
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
|
||||
self.popup = Some(OpenPopup::new(popup_id, pos.into()));
|
||||
}
|
||||
|
||||
/// Get the position for this popup.
|
||||
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
|
||||
self.popup
|
||||
.and_then(|state| if state.id == id { state.pos } else { None })
|
||||
}
|
||||
|
||||
/// Close any currently open popup.
|
||||
pub fn close_all_popups(&mut self) {
|
||||
self.popup = None;
|
||||
}
|
||||
|
||||
/// Close the given popup, if it is open.
|
||||
///
|
||||
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
|
||||
pub fn close_popup(&mut self, popup_id: Id) {
|
||||
if self.is_popup_open(popup_id) {
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the given popup between closed and open.
|
||||
///
|
||||
/// Note: At most, only one popup can be open at a time.
|
||||
pub fn toggle_popup(&mut self, popup_id: Id) {
|
||||
if self.is_popup_open(popup_id) {
|
||||
self.close_popup();
|
||||
self.close_popup(popup_id);
|
||||
} else {
|
||||
self.open_popup(popup_id);
|
||||
}
|
||||
@@ -1175,17 +1247,19 @@ impl Areas {
|
||||
///
|
||||
/// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list.
|
||||
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering {
|
||||
if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) {
|
||||
a.cmp(b)
|
||||
} else {
|
||||
a.order.cmp(&b.order)
|
||||
// Sort by layer `order` first and use `order_map` to resolve disputes.
|
||||
// If `order_map` only contains one layer ID, then the other one will be
|
||||
// lower because `None < Some(x)`.
|
||||
match a.order.cmp(&b.order) {
|
||||
std::cmp::Ordering::Equal => self.order_map.get(&a).cmp(&self.order_map.get(&b)),
|
||||
cmp => cmp,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) {
|
||||
self.visible_areas_current_frame.insert(layer_id);
|
||||
self.areas.insert(layer_id.id, state);
|
||||
if !self.order.iter().any(|x| *x == layer_id) {
|
||||
if !self.order.contains(&layer_id) {
|
||||
self.order.push(layer_id);
|
||||
}
|
||||
}
|
||||
@@ -1351,3 +1425,47 @@ fn memory_impl_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Memory>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_map_total_ordering() {
|
||||
let mut layers = [
|
||||
LayerId::new(Order::Tooltip, Id::new("a")),
|
||||
LayerId::new(Order::Background, Id::new("b")),
|
||||
LayerId::new(Order::Background, Id::new("c")),
|
||||
LayerId::new(Order::Tooltip, Id::new("d")),
|
||||
LayerId::new(Order::Background, Id::new("e")),
|
||||
LayerId::new(Order::Background, Id::new("f")),
|
||||
LayerId::new(Order::Tooltip, Id::new("g")),
|
||||
];
|
||||
let mut areas = Areas::default();
|
||||
|
||||
// skip some of the layers
|
||||
for &layer in &layers[3..] {
|
||||
areas.set_state(layer, crate::AreaState::default());
|
||||
}
|
||||
areas.end_pass(); // sort layers
|
||||
|
||||
// Sort layers
|
||||
layers.sort_by(|&a, &b| areas.compare_order(a, b));
|
||||
|
||||
// Assert that `areas.compare_order()` forms a total ordering
|
||||
let mut equivalence_classes = vec![0];
|
||||
let mut i = 0;
|
||||
for l in layers.windows(2) {
|
||||
assert!(l[0].order <= l[1].order, "does not follow LayerId.order");
|
||||
if areas.compare_order(l[0], l[1]) != std::cmp::Ordering::Equal {
|
||||
i += 1;
|
||||
}
|
||||
equivalence_classes.push(i);
|
||||
}
|
||||
assert_eq!(layers.len(), equivalence_classes.len());
|
||||
for (&l1, c1) in std::iter::zip(&layers, &equivalence_classes) {
|
||||
for (&l2, c2) in std::iter::zip(&layers, &equivalence_classes) {
|
||||
assert_eq!(
|
||||
c1.cmp(c2),
|
||||
areas.compare_order(l1, l2),
|
||||
"not a total ordering",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ impl Theme {
|
||||
}
|
||||
|
||||
/// The user's theme preference.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum ThemePreference {
|
||||
/// Dark mode: light text on a dark background.
|
||||
@@ -76,6 +76,7 @@ pub enum ThemePreference {
|
||||
Light,
|
||||
|
||||
/// Follow the system's theme preference.
|
||||
#[default]
|
||||
System,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(deprecated)]
|
||||
//! Menu bar functionality (very basic so far).
|
||||
//!
|
||||
//! Usage:
|
||||
@@ -75,11 +76,13 @@ impl std::ops::DerefMut for BarState {
|
||||
}
|
||||
|
||||
fn set_menu_style(style: &mut 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;
|
||||
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::TopBottomPanel::top`],
|
||||
@@ -146,7 +149,7 @@ pub fn menu_image_button<R>(
|
||||
/// Opens on hover.
|
||||
///
|
||||
/// Returns `None` if the menu is not open.
|
||||
pub(crate) fn submenu_button<R>(
|
||||
pub fn submenu_button<R>(
|
||||
ui: &mut Ui,
|
||||
parent_state: Arc<RwLock<MenuState>>,
|
||||
title: impl Into<WidgetText>,
|
||||
@@ -267,7 +270,7 @@ fn stationary_menu_button_impl<'c, R>(
|
||||
pub(crate) const CONTEXT_MENU_ID_STR: &str = "__egui::context_menu";
|
||||
|
||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||
pub(crate) fn context_menu(
|
||||
pub fn context_menu(
|
||||
response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) -> Option<InnerResponse<()>> {
|
||||
@@ -282,7 +285,7 @@ pub(crate) fn context_menu(
|
||||
}
|
||||
|
||||
/// Returns `true` if the context menu is opened for this widget.
|
||||
pub(crate) fn context_menu_opened(response: &Response) -> bool {
|
||||
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)
|
||||
@@ -364,7 +367,10 @@ impl MenuRoot {
|
||||
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 {
|
||||
if menu_state.response.is_close()
|
||||
|| escape_pressed
|
||||
|| inner_response.response.should_close()
|
||||
{
|
||||
return (MenuResponse::Close, Some(inner_response));
|
||||
}
|
||||
}
|
||||
@@ -580,7 +586,7 @@ impl SubMenuButton {
|
||||
if ui.visuals().button_frame {
|
||||
ui.painter().rect_filled(
|
||||
rect.expand(visuals.expansion),
|
||||
visuals.rounding,
|
||||
visuals.corner_radius,
|
||||
visuals.weak_bg_fill,
|
||||
);
|
||||
}
|
||||
@@ -667,6 +673,9 @@ impl MenuState {
|
||||
) -> 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);
|
||||
@@ -679,7 +688,7 @@ impl MenuState {
|
||||
|| self
|
||||
.sub_menu
|
||||
.as_ref()
|
||||
.map_or(false, |(_, sub)| sub.read().area_contains(pos))
|
||||
.is_some_and(|(_, sub)| sub.read().area_contains(pos))
|
||||
}
|
||||
|
||||
fn next_entry_index(&mut self) -> usize {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::{
|
||||
text::{Fonts, Galley, LayoutJob},
|
||||
CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke,
|
||||
CircleShape, ClippedShape, CornerRadius, PathStroke, RectShape, Shape, Stroke, StrokeKind,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -301,6 +301,7 @@ impl Painter {
|
||||
0.0,
|
||||
color.additive().linear_multiply(0.015),
|
||||
(1.0, color),
|
||||
StrokeKind::Outside,
|
||||
);
|
||||
self.text(
|
||||
rect.min,
|
||||
@@ -407,34 +408,41 @@ impl Painter {
|
||||
})
|
||||
}
|
||||
|
||||
/// The stroke extends _outside_ the [`Rect`].
|
||||
/// See also [`Self::rect_filled`] and [`Self::rect_stroke`].
|
||||
pub fn rect(
|
||||
&self,
|
||||
rect: Rect,
|
||||
rounding: impl Into<Rounding>,
|
||||
corner_radius: impl Into<CornerRadius>,
|
||||
fill_color: impl Into<Color32>,
|
||||
stroke: impl Into<Stroke>,
|
||||
stroke_kind: StrokeKind,
|
||||
) -> ShapeIdx {
|
||||
self.add(RectShape::new(rect, rounding, fill_color, stroke))
|
||||
self.add(RectShape::new(
|
||||
rect,
|
||||
corner_radius,
|
||||
fill_color,
|
||||
stroke,
|
||||
stroke_kind,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn rect_filled(
|
||||
&self,
|
||||
rect: Rect,
|
||||
rounding: impl Into<Rounding>,
|
||||
corner_radius: impl Into<CornerRadius>,
|
||||
fill_color: impl Into<Color32>,
|
||||
) -> ShapeIdx {
|
||||
self.add(RectShape::filled(rect, rounding, fill_color))
|
||||
self.add(RectShape::filled(rect, corner_radius, fill_color))
|
||||
}
|
||||
|
||||
/// The stroke extends _outside_ the [`Rect`].
|
||||
pub fn rect_stroke(
|
||||
&self,
|
||||
rect: Rect,
|
||||
rounding: impl Into<Rounding>,
|
||||
corner_radius: impl Into<CornerRadius>,
|
||||
stroke: impl Into<Stroke>,
|
||||
stroke_kind: StrokeKind,
|
||||
) -> ShapeIdx {
|
||||
self.add(RectShape::stroke(rect, rounding, stroke))
|
||||
self.add(RectShape::stroke(rect, corner_radius, stroke, stroke_kind))
|
||||
}
|
||||
|
||||
/// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
|
||||
@@ -463,7 +471,7 @@ impl Painter {
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0));
|
||||
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
|
||||
/// .rounding(5.0)
|
||||
/// .corner_radius(5)
|
||||
/// .tint(egui::Color32::LIGHT_BLUE)
|
||||
/// .paint_at(ui, rect);
|
||||
/// # });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ahash::{HashMap, HashSet};
|
||||
use ahash::HashMap;
|
||||
|
||||
use crate::{id::IdSet, style, Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects};
|
||||
|
||||
@@ -34,7 +34,7 @@ pub struct PerLayerState {
|
||||
/// Is there any open popup (menus, combo-boxes, etc)?
|
||||
///
|
||||
/// Does NOT include tooltips.
|
||||
pub open_popups: HashSet<Id>,
|
||||
pub open_popups: IdSet,
|
||||
|
||||
/// Which widget is showing a tooltip (if any)?
|
||||
///
|
||||
@@ -121,7 +121,13 @@ impl DebugRect {
|
||||
Color32::LIGHT_BLUE
|
||||
};
|
||||
let rect_bg_color = Color32::BLUE.gamma_multiply(0.5);
|
||||
painter.rect(rect, 0.0, rect_bg_color, (1.0, rect_fg_color));
|
||||
painter.rect(
|
||||
rect,
|
||||
0.0,
|
||||
rect_bg_color,
|
||||
(1.0, rect_fg_color),
|
||||
crate::StrokeKind::Outside,
|
||||
);
|
||||
}
|
||||
|
||||
if !callstack.is_empty() {
|
||||
@@ -157,7 +163,13 @@ impl DebugRect {
|
||||
text_bg_color
|
||||
};
|
||||
let text_rect = Rect::from_min_size(text_pos, galley.size());
|
||||
painter.rect(text_rect, 0.0, text_bg_color, (1.0, text_rect_stroke_color));
|
||||
painter.rect(
|
||||
text_rect,
|
||||
0.0,
|
||||
text_bg_color,
|
||||
(1.0, text_rect_stroke_color),
|
||||
crate::StrokeKind::Middle,
|
||||
);
|
||||
painter.galley(text_pos, galley, text_color);
|
||||
|
||||
if is_clicking {
|
||||
@@ -190,7 +202,6 @@ pub struct PassState {
|
||||
|
||||
/// Starts off as the `screen_rect`, shrinks as panels are added.
|
||||
/// The [`crate::CentralPanel`] does not change this.
|
||||
/// This is the area available to Window's.
|
||||
pub available_rect: Rect,
|
||||
|
||||
/// Starts off as the `screen_rect`, shrinks as panels are added.
|
||||
@@ -291,8 +302,6 @@ impl PassState {
|
||||
}
|
||||
|
||||
/// How much space is still available after panels has been added.
|
||||
/// This is the "background" area, what egui doesn't cover with panels (but may cover with windows).
|
||||
/// This is also the area to which windows are constrained.
|
||||
pub(crate) fn available_rect(&self) -> Rect {
|
||||
debug_assert!(
|
||||
self.available_rect.is_finite(),
|
||||
|
||||
@@ -133,8 +133,8 @@ impl Placer {
|
||||
|
||||
/// Apply justify or alignment after calling `next_space`.
|
||||
pub(crate) fn justify_and_align(&self, rect: Rect, child_size: Vec2) -> Rect {
|
||||
debug_assert!(!rect.any_nan());
|
||||
debug_assert!(!child_size.any_nan());
|
||||
debug_assert!(!rect.any_nan(), "rect: {rect:?}");
|
||||
debug_assert!(!child_size.any_nan(), "child_size is NaN: {child_size:?}");
|
||||
|
||||
if let Some(grid) = &self.grid {
|
||||
grid.justify_and_align(rect, child_size)
|
||||
@@ -164,8 +164,11 @@ impl Placer {
|
||||
widget_rect: Rect,
|
||||
item_spacing: Vec2,
|
||||
) {
|
||||
debug_assert!(!frame_rect.any_nan());
|
||||
debug_assert!(!widget_rect.any_nan());
|
||||
debug_assert!(!frame_rect.any_nan(), "frame_rect: {frame_rect:?}");
|
||||
debug_assert!(
|
||||
!widget_rect.any_nan(),
|
||||
"widget_rect is NaN: {widget_rect:?}"
|
||||
);
|
||||
self.region.sanity_check();
|
||||
|
||||
if let Some(grid) = &mut self.grid {
|
||||
@@ -281,7 +284,7 @@ impl Placer {
|
||||
|
||||
if let Some(grid) = &self.grid {
|
||||
let rect = grid.next_cell(self.cursor(), Vec2::splat(0.0));
|
||||
painter.rect_stroke(rect, 1.0, stroke);
|
||||
painter.rect_stroke(rect, 1.0, stroke, epaint::StrokeKind::Inside);
|
||||
let align = Align2::CENTER_CENTER;
|
||||
painter.debug_text(align.pos_in_rect(&rect), align, stroke.color, text);
|
||||
} else {
|
||||
|
||||
@@ -2,21 +2,21 @@ use std::{any::Any, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
emath::{Align, Pos2, Rect, Vec2},
|
||||
menu, pass_state, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui,
|
||||
WidgetRect, WidgetText,
|
||||
pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip,
|
||||
Ui, WidgetRect, WidgetText,
|
||||
};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The result of adding a widget to a [`Ui`].
|
||||
///
|
||||
/// A [`Response`] lets you know whether or not a widget is being hovered, clicked or dragged.
|
||||
/// A [`Response`] lets you know whether a widget is being hovered, clicked or dragged.
|
||||
/// It also lets you easily show a tooltip on hover.
|
||||
///
|
||||
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
|
||||
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
|
||||
///
|
||||
/// ⚠️ The `Response` contains a clone of [`Context`], and many methods lock the `Context`.
|
||||
/// It can therefor be a deadlock to use `Context` from within a context-locking closures,
|
||||
/// It can therefore be a deadlock to use `Context` from within a context-locking closures,
|
||||
/// such as [`Context::input`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Response {
|
||||
@@ -50,78 +50,12 @@ pub struct Response {
|
||||
/// (that is handled by the `Painter` directly).
|
||||
pub sense: Sense,
|
||||
|
||||
/// Was the widget enabled?
|
||||
/// If `false`, there was no interaction attempted (not even hover).
|
||||
#[doc(hidden)]
|
||||
pub enabled: bool,
|
||||
|
||||
// OUT:
|
||||
/// The pointer is above this widget with no other blocking it.
|
||||
#[doc(hidden)]
|
||||
pub contains_pointer: bool,
|
||||
|
||||
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
|
||||
#[doc(hidden)]
|
||||
pub hovered: bool,
|
||||
|
||||
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
|
||||
#[doc(hidden)]
|
||||
pub highlighted: bool,
|
||||
|
||||
/// This widget was clicked this frame.
|
||||
///
|
||||
/// Which pointer and how many times we don't know,
|
||||
/// and ask [`crate::InputState`] about at runtime.
|
||||
///
|
||||
/// This is only set to true if the widget was clicked
|
||||
/// by an actual mouse.
|
||||
#[doc(hidden)]
|
||||
pub clicked: bool,
|
||||
|
||||
/// This widget should act as if clicked due
|
||||
/// to something else than a click.
|
||||
///
|
||||
/// This is set to true if the widget has keyboard focus and
|
||||
/// the user hit the Space or Enter key.
|
||||
#[doc(hidden)]
|
||||
pub fake_primary_click: bool,
|
||||
|
||||
/// This widget was long-pressed on a touch screen to simulate a secondary click.
|
||||
#[doc(hidden)]
|
||||
pub long_touched: bool,
|
||||
|
||||
/// The widget started being dragged this frame.
|
||||
#[doc(hidden)]
|
||||
pub drag_started: bool,
|
||||
|
||||
/// The widget is being dragged.
|
||||
#[doc(hidden)]
|
||||
pub dragged: bool,
|
||||
|
||||
/// The widget was being dragged, but now it has been released.
|
||||
#[doc(hidden)]
|
||||
pub drag_stopped: bool,
|
||||
|
||||
/// Is the pointer button currently down on this widget?
|
||||
/// This is true if the pointer is pressing down or dragging a widget
|
||||
#[doc(hidden)]
|
||||
pub is_pointer_button_down_on: bool,
|
||||
|
||||
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
|
||||
/// 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>,
|
||||
|
||||
/// Was the underlying data changed?
|
||||
///
|
||||
/// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc.
|
||||
/// Always `false` for something like a [`Button`](crate::Button).
|
||||
///
|
||||
/// Note that this can be `true` even if the user did not interact with the widget,
|
||||
/// for instance if an existing slider value was clamped to the given range.
|
||||
#[doc(hidden)]
|
||||
pub changed: bool,
|
||||
|
||||
/// The intrinsic / desired size of the widget.
|
||||
///
|
||||
/// For a button, this will be the size of the label + the frames padding,
|
||||
@@ -133,6 +67,76 @@ pub struct Response {
|
||||
/// 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 flags: Flags,
|
||||
}
|
||||
|
||||
/// A bit set for various boolean properties of `Response`.
|
||||
#[doc(hidden)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Flags(u16);
|
||||
|
||||
bitflags::bitflags! {
|
||||
impl Flags: u16 {
|
||||
/// Was the widget enabled?
|
||||
/// If `false`, there was no interaction attempted (not even hover).
|
||||
const ENABLED = 1<<0;
|
||||
|
||||
/// The pointer is above this widget with no other blocking it.
|
||||
const CONTAINS_POINTER = 1<<1;
|
||||
|
||||
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
|
||||
const HOVERED = 1<<2;
|
||||
|
||||
/// The widget is highlighted via a call to [`Response::highlight`] or
|
||||
/// [`Context::highlight_widget`].
|
||||
const HIGHLIGHTED = 1<<3;
|
||||
|
||||
/// This widget was clicked this frame.
|
||||
///
|
||||
/// Which pointer and how many times we don't know,
|
||||
/// and ask [`crate::InputState`] about at runtime.
|
||||
///
|
||||
/// This is only set to true if the widget was clicked
|
||||
/// by an actual mouse.
|
||||
const CLICKED = 1<<4;
|
||||
|
||||
/// This widget should act as if clicked due
|
||||
/// to something else than a click.
|
||||
///
|
||||
/// This is set to true if the widget has keyboard focus and
|
||||
/// the user hit the Space or Enter key.
|
||||
const FAKE_PRIMARY_CLICKED = 1<<5;
|
||||
|
||||
/// This widget was long-pressed on a touch screen to simulate a secondary click.
|
||||
const LONG_TOUCHED = 1<<6;
|
||||
|
||||
/// The widget started being dragged this frame.
|
||||
const DRAG_STARTED = 1<<7;
|
||||
|
||||
/// The widget is being dragged.
|
||||
const DRAGGED = 1<<8;
|
||||
|
||||
/// The widget was being dragged, but now it has been released.
|
||||
const DRAG_STOPPED = 1<<9;
|
||||
|
||||
/// Is the pointer button currently down on this widget?
|
||||
/// This is true if the pointer is pressing down or dragging a widget
|
||||
const IS_POINTER_BUTTON_DOWN_ON = 1<<10;
|
||||
|
||||
/// Was the underlying data changed?
|
||||
///
|
||||
/// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc.
|
||||
/// Always `false` for something like a [`Button`](crate::Button).
|
||||
///
|
||||
/// Note that this can be `true` even if the user did not interact with the widget,
|
||||
/// for instance if an existing slider value was clamped to the given range.
|
||||
const CHANGED = 1<<11;
|
||||
|
||||
/// Should this container be closed?
|
||||
const CLOSE = 1<<12;
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
@@ -150,7 +154,7 @@ impl Response {
|
||||
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
|
||||
#[inline(always)]
|
||||
pub fn clicked(&self) -> bool {
|
||||
self.fake_primary_click || self.clicked_by(PointerButton::Primary)
|
||||
self.flags.contains(Flags::FAKE_PRIMARY_CLICKED) || self.clicked_by(PointerButton::Primary)
|
||||
}
|
||||
|
||||
/// Returns true if this widget was clicked this frame by the given mouse button.
|
||||
@@ -163,7 +167,7 @@ impl Response {
|
||||
/// Use [`Self::secondary_clicked`] instead to also detect that.
|
||||
#[inline]
|
||||
pub fn clicked_by(&self, button: PointerButton) -> bool {
|
||||
self.clicked && self.ctx.input(|i| i.pointer.button_clicked(button))
|
||||
self.flags.contains(Flags::CLICKED) && self.ctx.input(|i| i.pointer.button_clicked(button))
|
||||
}
|
||||
|
||||
/// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button).
|
||||
@@ -171,7 +175,7 @@ impl Response {
|
||||
/// This also returns true if the widget was pressed-and-held on a touch screen.
|
||||
#[inline]
|
||||
pub fn secondary_clicked(&self) -> bool {
|
||||
self.long_touched || self.clicked_by(PointerButton::Secondary)
|
||||
self.flags.contains(Flags::LONG_TOUCHED) || self.clicked_by(PointerButton::Secondary)
|
||||
}
|
||||
|
||||
/// Was this long-pressed on a touch screen?
|
||||
@@ -179,7 +183,7 @@ impl Response {
|
||||
/// Usually you want to check [`Self::secondary_clicked`] instead.
|
||||
#[inline]
|
||||
pub fn long_touched(&self) -> bool {
|
||||
self.long_touched
|
||||
self.flags.contains(Flags::LONG_TOUCHED)
|
||||
}
|
||||
|
||||
/// Returns true if this widget was clicked this frame by the middle mouse button.
|
||||
@@ -203,38 +207,47 @@ impl Response {
|
||||
/// Returns true if this widget was double-clicked this frame by the given button.
|
||||
#[inline]
|
||||
pub fn double_clicked_by(&self, button: PointerButton) -> bool {
|
||||
self.clicked && self.ctx.input(|i| i.pointer.button_double_clicked(button))
|
||||
self.flags.contains(Flags::CLICKED)
|
||||
&& self.ctx.input(|i| i.pointer.button_double_clicked(button))
|
||||
}
|
||||
|
||||
/// Returns true if this widget was triple-clicked this frame by the given button.
|
||||
#[inline]
|
||||
pub fn triple_clicked_by(&self, button: PointerButton) -> bool {
|
||||
self.clicked && self.ctx.input(|i| i.pointer.button_triple_clicked(button))
|
||||
self.flags.contains(Flags::CLICKED)
|
||||
&& self.ctx.input(|i| i.pointer.button_triple_clicked(button))
|
||||
}
|
||||
|
||||
/// `true` if there was a click *outside* the rect of this widget.
|
||||
///
|
||||
/// Clicks on widgets contained in this one counts as clicks inside this widget,
|
||||
/// so that clicking a button in an area will not be considered as clicking "elsewhere" from the area.
|
||||
///
|
||||
/// Clicks on other layers above this widget *will* be considered as clicking elsewhere.
|
||||
pub fn clicked_elsewhere(&self) -> bool {
|
||||
let (pointer_interact_pos, any_click) = self
|
||||
.ctx
|
||||
.input(|i| (i.pointer.interact_pos(), i.pointer.any_click()));
|
||||
|
||||
// We do not use self.clicked(), because we want to catch all clicks within our frame,
|
||||
// even if we aren't clickable (or even enabled).
|
||||
// This is important for windows and such that should close then the user clicks elsewhere.
|
||||
self.ctx.input(|i| {
|
||||
let pointer = &i.pointer;
|
||||
|
||||
if pointer.any_click() {
|
||||
if self.contains_pointer || self.hovered {
|
||||
false
|
||||
} else if let Some(pos) = pointer.interact_pos() {
|
||||
!self.interact_rect.contains(pos)
|
||||
if any_click {
|
||||
if self.contains_pointer() || self.hovered() {
|
||||
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 {
|
||||
false // clicked without a pointer, weird
|
||||
!self.interact_rect.contains(pos)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
false // clicked without a pointer, weird
|
||||
}
|
||||
})
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Was the widget enabled?
|
||||
@@ -242,7 +255,7 @@ impl Response {
|
||||
/// and the widget should be drawn in a gray disabled look.
|
||||
#[inline(always)]
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled
|
||||
self.flags.contains(Flags::ENABLED)
|
||||
}
|
||||
|
||||
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
|
||||
@@ -251,7 +264,7 @@ impl Response {
|
||||
/// `hovered` is always `false` for disabled widgets.
|
||||
#[inline(always)]
|
||||
pub fn hovered(&self) -> bool {
|
||||
self.hovered
|
||||
self.flags.contains(Flags::HOVERED)
|
||||
}
|
||||
|
||||
/// Returns true if the pointer is contained by the response rect, and no other widget is covering it.
|
||||
@@ -264,14 +277,14 @@ impl Response {
|
||||
/// [`Self::contains_pointer`] also checks that no other widget is covering this response rectangle.
|
||||
#[inline(always)]
|
||||
pub fn contains_pointer(&self) -> bool {
|
||||
self.contains_pointer
|
||||
self.flags.contains(Flags::CONTAINS_POINTER)
|
||||
}
|
||||
|
||||
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
pub fn highlighted(&self) -> bool {
|
||||
self.highlighted
|
||||
self.flags.contains(Flags::HIGHLIGHTED)
|
||||
}
|
||||
|
||||
/// This widget has the keyboard focus (i.e. is receiving key presses).
|
||||
@@ -316,7 +329,7 @@ impl Response {
|
||||
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
|
||||
}
|
||||
|
||||
/// Did a drag on this widgets begin this frame?
|
||||
/// Did a drag on this widget begin this frame?
|
||||
///
|
||||
/// This is only true if the widget sense drags.
|
||||
/// If the widget also senses clicks, this will only become true if the pointer has moved a bit.
|
||||
@@ -324,10 +337,10 @@ impl Response {
|
||||
/// This will only be true for a single frame.
|
||||
#[inline]
|
||||
pub fn drag_started(&self) -> bool {
|
||||
self.drag_started
|
||||
self.flags.contains(Flags::DRAG_STARTED)
|
||||
}
|
||||
|
||||
/// Did a drag on this widgets by the button begin this frame?
|
||||
/// Did a drag on this widget by the button begin this frame?
|
||||
///
|
||||
/// This is only true if the widget sense drags.
|
||||
/// If the widget also senses clicks, this will only become true if the pointer has moved a bit.
|
||||
@@ -354,7 +367,7 @@ impl Response {
|
||||
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
|
||||
#[inline(always)]
|
||||
pub fn dragged(&self) -> bool {
|
||||
self.dragged
|
||||
self.flags.contains(Flags::DRAGGED)
|
||||
}
|
||||
|
||||
/// See [`Self::dragged`].
|
||||
@@ -366,7 +379,7 @@ impl Response {
|
||||
/// The widget was being dragged, but now it has been released.
|
||||
#[inline]
|
||||
pub fn drag_stopped(&self) -> bool {
|
||||
self.drag_stopped
|
||||
self.flags.contains(Flags::DRAG_STOPPED)
|
||||
}
|
||||
|
||||
/// The widget was being dragged by the button, but now it has been released.
|
||||
@@ -378,7 +391,7 @@ impl Response {
|
||||
#[inline]
|
||||
#[deprecated = "Renamed 'drag_stopped'"]
|
||||
pub fn drag_released(&self) -> bool {
|
||||
self.drag_stopped
|
||||
self.drag_stopped()
|
||||
}
|
||||
|
||||
/// The widget was being dragged by the button, but now it has been released.
|
||||
@@ -422,7 +435,7 @@ impl Response {
|
||||
crate::DragAndDrop::set_payload(&self.ctx, payload);
|
||||
}
|
||||
|
||||
if self.hovered() && !self.sense.click {
|
||||
if self.hovered() && !self.sense.senses_click() {
|
||||
// Things that can be drag-dropped should use the Grab cursor icon,
|
||||
// but if the thing is _also_ clickable, that can be annoying.
|
||||
self.ctx.set_cursor_icon(CursorIcon::Grab);
|
||||
@@ -460,7 +473,7 @@ impl Response {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
|
||||
/// Where the pointer (mouse/touch) were when this widget was clicked or dragged.
|
||||
///
|
||||
/// `None` if the widget is not being interacted with.
|
||||
#[inline]
|
||||
@@ -492,7 +505,7 @@ impl Response {
|
||||
/// This could also be thought of as "is this widget being interacted with?".
|
||||
#[inline(always)]
|
||||
pub fn is_pointer_button_down_on(&self) -> bool {
|
||||
self.is_pointer_button_down_on
|
||||
self.flags.contains(Flags::IS_POINTER_BUTTON_DOWN_ON)
|
||||
}
|
||||
|
||||
/// Was the underlying data changed?
|
||||
@@ -510,7 +523,7 @@ impl Response {
|
||||
/// for instance if an existing slider value was clamped to the given range.
|
||||
#[inline(always)]
|
||||
pub fn changed(&self) -> bool {
|
||||
self.changed
|
||||
self.flags.contains(Flags::CHANGED)
|
||||
}
|
||||
|
||||
/// Report the data shown by this widget changed.
|
||||
@@ -519,10 +532,25 @@ impl Response {
|
||||
/// e.g. checkboxes, sliders etc.
|
||||
///
|
||||
/// This should be called when the *content* changes, but not when the view does.
|
||||
/// So we call this when the text of a [`crate::TextEdit`], but not when the cursors changes.
|
||||
/// So we call this when the text of a [`crate::TextEdit`], but not when the cursor changes.
|
||||
#[inline(always)]
|
||||
pub fn mark_changed(&mut self) {
|
||||
self.changed = true;
|
||||
self.flags.set(Flags::CHANGED, true);
|
||||
}
|
||||
|
||||
/// Should the container be closed?
|
||||
///
|
||||
/// Will e.g. be set by calling [`Ui::close`] in a child [`Ui`] or by calling
|
||||
/// [`Self::set_close`].
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.flags.contains(Flags::CLOSE)
|
||||
}
|
||||
|
||||
/// Set the [`Flags::CLOSE`] flag.
|
||||
///
|
||||
/// Can be used to e.g. signal that a container should be closed.
|
||||
pub fn set_close(&mut self) {
|
||||
self.flags.set(Flags::CLOSE, true);
|
||||
}
|
||||
|
||||
/// Show this UI if the widget was hovered (i.e. a tooltip).
|
||||
@@ -547,36 +575,22 @@ impl Response {
|
||||
/// ```
|
||||
#[doc(alias = "tooltip")]
|
||||
pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
||||
if self.enabled && self.should_show_hover_ui() {
|
||||
self.show_tooltip_ui(add_contents);
|
||||
}
|
||||
Tooltip::for_enabled(&self).show(add_contents);
|
||||
self
|
||||
}
|
||||
|
||||
/// Show this UI when hovering if the widget is disabled.
|
||||
pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
||||
if !self.enabled && self.should_show_hover_ui() {
|
||||
crate::containers::show_tooltip_for(
|
||||
&self.ctx,
|
||||
self.layer_id,
|
||||
self.id,
|
||||
&self.rect,
|
||||
add_contents,
|
||||
);
|
||||
}
|
||||
Tooltip::for_disabled(&self).show(add_contents);
|
||||
self
|
||||
}
|
||||
|
||||
/// Like `on_hover_ui`, but show the ui next to cursor.
|
||||
pub fn on_hover_ui_at_pointer(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
||||
if self.enabled && self.should_show_hover_ui() {
|
||||
crate::containers::show_tooltip_at_pointer(
|
||||
&self.ctx,
|
||||
self.layer_id,
|
||||
self.id,
|
||||
add_contents,
|
||||
);
|
||||
}
|
||||
Tooltip::for_enabled(&self)
|
||||
.at_pointer()
|
||||
.gap(12.0)
|
||||
.show(add_contents);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -584,13 +598,9 @@ impl Response {
|
||||
///
|
||||
/// This can be used to give attention to a widget during a tutorial.
|
||||
pub fn show_tooltip_ui(&self, add_contents: impl FnOnce(&mut Ui)) {
|
||||
crate::containers::show_tooltip_for(
|
||||
&self.ctx,
|
||||
self.layer_id,
|
||||
self.id,
|
||||
&self.rect,
|
||||
add_contents,
|
||||
);
|
||||
Popup::from_response(self)
|
||||
.kind(PopupKind::Tooltip)
|
||||
.show(add_contents);
|
||||
}
|
||||
|
||||
/// Always show this tooltip, even if disabled and the user isn't hovering it.
|
||||
@@ -604,180 +614,7 @@ impl Response {
|
||||
|
||||
/// Was the tooltip open last frame?
|
||||
pub fn is_tooltip_open(&self) -> bool {
|
||||
crate::popup::was_tooltip_open_last_frame(&self.ctx, self.id)
|
||||
}
|
||||
|
||||
fn should_show_hover_ui(&self) -> bool {
|
||||
if self.ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let any_open_popups = self.ctx.prev_pass_state(|fs| {
|
||||
fs.layers
|
||||
.get(&self.layer_id)
|
||||
.map_or(false, |layer| !layer.open_popups.is_empty())
|
||||
});
|
||||
if any_open_popups {
|
||||
// Hide tooltips if the user opens a popup (menu, combo-box, etc) in the same layer.
|
||||
return false;
|
||||
}
|
||||
|
||||
let style = self.ctx.style();
|
||||
|
||||
let tooltip_delay = style.interaction.tooltip_delay;
|
||||
let tooltip_grace_time = style.interaction.tooltip_grace_time;
|
||||
|
||||
let (
|
||||
time_since_last_scroll,
|
||||
time_since_last_click,
|
||||
time_since_last_pointer_movement,
|
||||
pointer_pos,
|
||||
pointer_dir,
|
||||
) = self.ctx.input(|i| {
|
||||
(
|
||||
i.time_since_last_scroll(),
|
||||
i.pointer.time_since_last_click(),
|
||||
i.pointer.time_since_last_movement(),
|
||||
i.pointer.hover_pos(),
|
||||
i.pointer.direction(),
|
||||
)
|
||||
});
|
||||
|
||||
if time_since_last_scroll < tooltip_delay {
|
||||
// See https://github.com/emilk/egui/issues/4781
|
||||
// Note that this means we cannot have `ScrollArea`s in a tooltip.
|
||||
self.ctx
|
||||
.request_repaint_after_secs(tooltip_delay - time_since_last_scroll);
|
||||
return false;
|
||||
}
|
||||
|
||||
let is_our_tooltip_open = self.is_tooltip_open();
|
||||
|
||||
if is_our_tooltip_open {
|
||||
// Check if we should automatically stay open:
|
||||
|
||||
let tooltip_id = crate::next_tooltip_id(&self.ctx, self.id);
|
||||
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
||||
|
||||
let tooltip_has_interactive_widget = self.ctx.viewport(|vp| {
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.get_layer(tooltip_layer_id)
|
||||
.any(|w| w.enabled && w.sense.interactive())
|
||||
});
|
||||
|
||||
if tooltip_has_interactive_widget {
|
||||
// We keep the tooltip open if hovered,
|
||||
// or if the pointer is on its way to it,
|
||||
// so that the user can interact with the tooltip
|
||||
// (i.e. click links that are in it).
|
||||
if let Some(area) = AreaState::load(&self.ctx, tooltip_id) {
|
||||
let rect = area.rect();
|
||||
|
||||
if let Some(pos) = pointer_pos {
|
||||
if rect.contains(pos) {
|
||||
return true; // hovering interactive tooltip
|
||||
}
|
||||
if pointer_dir != Vec2::ZERO
|
||||
&& rect.intersects_ray(pos, pointer_dir.normalized())
|
||||
{
|
||||
return true; // on the way to interactive tooltip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let clicked_more_recently_than_moved =
|
||||
time_since_last_click < time_since_last_pointer_movement + 0.1;
|
||||
if clicked_more_recently_than_moved {
|
||||
// It is common to click a widget and then rest the mouse there.
|
||||
// It would be annoying to then see a tooltip for it immediately.
|
||||
// Similarly, clicking should hide the existing tooltip.
|
||||
// Only hovering should lead to a tooltip, not clicking.
|
||||
// The offset is only to allow small movement just right after the click.
|
||||
return false;
|
||||
}
|
||||
|
||||
if is_our_tooltip_open {
|
||||
// Check if we should automatically stay open:
|
||||
|
||||
if pointer_pos.is_some_and(|pointer_pos| self.rect.contains(pointer_pos)) {
|
||||
// Handle the case of a big tooltip that covers the widget:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let is_other_tooltip_open = self.ctx.prev_pass_state(|fs| {
|
||||
if let Some(already_open_tooltip) = fs
|
||||
.layers
|
||||
.get(&self.layer_id)
|
||||
.and_then(|layer| layer.widget_with_tooltip)
|
||||
{
|
||||
already_open_tooltip != self.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if is_other_tooltip_open {
|
||||
// We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast early-outs:
|
||||
if self.enabled {
|
||||
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
|
||||
return false;
|
||||
}
|
||||
} else if !self.ctx.rect_contains_pointer(self.layer_id, self.rect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// There is a tooltip_delay before showing the first tooltip,
|
||||
// but once one tooltips is show, moving the mouse cursor to
|
||||
// another widget should show the tooltip for that widget right away.
|
||||
|
||||
// Let the user quickly move over some dead space to hover the next thing
|
||||
let tooltip_was_recently_shown =
|
||||
crate::popup::seconds_since_last_tooltip(&self.ctx) < tooltip_grace_time;
|
||||
|
||||
if !tooltip_was_recently_shown && !is_our_tooltip_open {
|
||||
if style.interaction.show_tooltips_only_when_still {
|
||||
// We only show the tooltip when the mouse pointer is still.
|
||||
if !self
|
||||
.ctx
|
||||
.input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
|
||||
{
|
||||
// wait for mouse to stop
|
||||
self.ctx.request_repaint();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let time_since_last_interaction = time_since_last_scroll
|
||||
.min(time_since_last_pointer_movement)
|
||||
.min(time_since_last_click);
|
||||
let time_til_tooltip = tooltip_delay - time_since_last_interaction;
|
||||
|
||||
if 0.0 < time_til_tooltip {
|
||||
// Wait until the mouse has been still for a while
|
||||
self.ctx.request_repaint_after_secs(time_til_tooltip);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't want tooltips of things while we are dragging them,
|
||||
// but we do want tooltips while holding down on an item on a touch screen.
|
||||
if self
|
||||
.ctx
|
||||
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// All checks passed: show the tooltip!
|
||||
|
||||
true
|
||||
Tooltip::was_tooltip_open_last_frame(&self.ctx, self.id)
|
||||
}
|
||||
|
||||
/// Like `on_hover_text`, but show the text next to cursor.
|
||||
@@ -817,7 +654,7 @@ impl Response {
|
||||
#[inline]
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.ctx.highlight_widget(self.id);
|
||||
self.highlighted = true;
|
||||
self.flags.set(Flags::HIGHLIGHTED, true);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -888,7 +725,7 @@ impl Response {
|
||||
rect: self.rect,
|
||||
interact_rect: self.interact_rect,
|
||||
sense: self.sense | sense,
|
||||
enabled: self.enabled,
|
||||
enabled: self.enabled(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
@@ -951,7 +788,7 @@ impl Response {
|
||||
Some(OutputEvent::TripleClicked(make_info()))
|
||||
} else if self.gained_focus() {
|
||||
Some(OutputEvent::FocusGained(make_info()))
|
||||
} else if self.changed {
|
||||
} else if self.changed() {
|
||||
Some(OutputEvent::ValueChanged(make_info()))
|
||||
} else {
|
||||
None
|
||||
@@ -983,7 +820,7 @@ impl Response {
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::Node) {
|
||||
if !self.enabled {
|
||||
if !self.enabled() {
|
||||
builder.set_disabled();
|
||||
}
|
||||
builder.set_bounds(accesskit::Rect {
|
||||
@@ -992,10 +829,10 @@ impl Response {
|
||||
x1: self.rect.max.x.into(),
|
||||
y1: self.rect.max.y.into(),
|
||||
});
|
||||
if self.sense.focusable {
|
||||
if self.sense.is_focusable() {
|
||||
builder.add_action(accesskit::Action::Focus);
|
||||
}
|
||||
if self.sense.click {
|
||||
if self.sense.senses_click() {
|
||||
builder.add_action(accesskit::Action::Click);
|
||||
}
|
||||
}
|
||||
@@ -1056,6 +893,9 @@ impl Response {
|
||||
// Indeterminate state
|
||||
builder.set_toggled(Toggled::Mixed);
|
||||
}
|
||||
if let Some(hint_text) = info.hint_text {
|
||||
builder.set_placeholder(hint_text);
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate a label with a control for accessibility.
|
||||
@@ -1094,22 +934,22 @@ impl Response {
|
||||
/// let response = ui.add(Label::new("Right-click me!").sense(Sense::click()));
|
||||
/// response.context_menu(|ui| {
|
||||
/// if ui.button("Close the menu").clicked() {
|
||||
/// ui.close_menu();
|
||||
/// ui.close();
|
||||
/// }
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See also: [`Ui::menu_button`] and [`Ui::close_menu`].
|
||||
/// See also: [`Ui::menu_button`] and [`Ui::close`].
|
||||
pub fn context_menu(&self, add_contents: impl FnOnce(&mut Ui)) -> Option<InnerResponse<()>> {
|
||||
menu::context_menu(self, add_contents)
|
||||
Popup::context_menu(self).show(add_contents)
|
||||
}
|
||||
|
||||
/// Returns whether a context menu is currently open for this widget.
|
||||
///
|
||||
/// See [`Self::context_menu`].
|
||||
pub fn context_menu_opened(&self) -> bool {
|
||||
menu::context_menu_opened(self)
|
||||
Popup::context_menu(self).is_open()
|
||||
}
|
||||
|
||||
/// Draw a debug rectangle over the response displaying the response's id and whether it is
|
||||
@@ -1125,9 +965,9 @@ impl Response {
|
||||
pub fn paint_debug_info(&self) {
|
||||
self.ctx.debug_painter().debug_rect(
|
||||
self.rect,
|
||||
if self.hovered {
|
||||
if self.hovered() {
|
||||
crate::Color32::DARK_GREEN
|
||||
} else if self.enabled {
|
||||
} else if self.enabled() {
|
||||
crate::Color32::BLUE
|
||||
} else {
|
||||
crate::Color32::RED
|
||||
@@ -1145,7 +985,10 @@ impl Response {
|
||||
///
|
||||
/// You may not call [`Self::interact`] on the resulting `Response`.
|
||||
pub fn union(&self, other: Self) -> Self {
|
||||
assert!(self.ctx == other.ctx);
|
||||
assert!(
|
||||
self.ctx == other.ctx,
|
||||
"Responses must be from the same `Context`"
|
||||
);
|
||||
debug_assert!(
|
||||
self.layer_id == other.layer_id,
|
||||
"It makes no sense to combine Responses from two different layers"
|
||||
@@ -1157,20 +1000,8 @@ impl Response {
|
||||
rect: self.rect.union(other.rect),
|
||||
interact_rect: self.interact_rect.union(other.interact_rect),
|
||||
sense: self.sense.union(other.sense),
|
||||
enabled: self.enabled || other.enabled,
|
||||
contains_pointer: self.contains_pointer || other.contains_pointer,
|
||||
hovered: self.hovered || other.hovered,
|
||||
highlighted: self.highlighted || other.highlighted,
|
||||
clicked: self.clicked || other.clicked,
|
||||
fake_primary_click: self.fake_primary_click || other.fake_primary_click,
|
||||
long_touched: self.long_touched || other.long_touched,
|
||||
drag_started: self.drag_started || other.drag_started,
|
||||
dragged: self.dragged || other.dragged,
|
||||
drag_stopped: self.drag_stopped || other.drag_stopped,
|
||||
is_pointer_button_down_on: self.is_pointer_button_down_on
|
||||
|| other.is_pointer_button_down_on,
|
||||
flags: self.flags | other.flags,
|
||||
interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos),
|
||||
changed: self.changed || other.changed,
|
||||
intrinsic_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
/// What sort of interaction is a widget sensitive to?
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
// #[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct Sense {
|
||||
/// Buttons, sliders, windows, …
|
||||
pub click: bool,
|
||||
pub struct Sense(u8);
|
||||
|
||||
/// Sliders, windows, scroll bars, scroll areas, …
|
||||
pub drag: bool,
|
||||
bitflags::bitflags! {
|
||||
impl Sense: u8 {
|
||||
|
||||
/// This widget wants focus.
|
||||
///
|
||||
/// Anything interactive + labels that can be focused
|
||||
/// for the benefit of screen readers.
|
||||
pub focusable: bool,
|
||||
const HOVER = 0;
|
||||
|
||||
/// Buttons, sliders, windows, …
|
||||
const CLICK = 1<<0;
|
||||
|
||||
/// Sliders, windows, scroll bars, scroll areas, …
|
||||
const DRAG = 1<<1;
|
||||
|
||||
/// This widget wants focus.
|
||||
///
|
||||
/// Anything interactive + labels that can be focused
|
||||
/// for the benefit of screen readers.
|
||||
const FOCUSABLE = 1<<2;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Sense {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let Self {
|
||||
click,
|
||||
drag,
|
||||
focusable,
|
||||
} = self;
|
||||
|
||||
write!(f, "Sense {{")?;
|
||||
if *click {
|
||||
if self.senses_click() {
|
||||
write!(f, " click")?;
|
||||
}
|
||||
if *drag {
|
||||
if self.senses_drag() {
|
||||
write!(f, " drag")?;
|
||||
}
|
||||
if *focusable {
|
||||
if self.is_focusable() {
|
||||
write!(f, " focusable")?;
|
||||
}
|
||||
write!(f, " }}")
|
||||
@@ -42,42 +43,26 @@ impl Sense {
|
||||
#[doc(alias = "none")]
|
||||
#[inline]
|
||||
pub fn hover() -> Self {
|
||||
Self {
|
||||
click: false,
|
||||
drag: false,
|
||||
focusable: false,
|
||||
}
|
||||
Self::empty()
|
||||
}
|
||||
|
||||
/// Senses no clicks or drags, but can be focused with the keyboard.
|
||||
/// Used for labels that can be focused for the benefit of screen readers.
|
||||
#[inline]
|
||||
pub fn focusable_noninteractive() -> Self {
|
||||
Self {
|
||||
click: false,
|
||||
drag: false,
|
||||
focusable: true,
|
||||
}
|
||||
Self::FOCUSABLE
|
||||
}
|
||||
|
||||
/// Sense clicks and hover, but not drags.
|
||||
#[inline]
|
||||
pub fn click() -> Self {
|
||||
Self {
|
||||
click: true,
|
||||
drag: false,
|
||||
focusable: true,
|
||||
}
|
||||
Self::CLICK | Self::FOCUSABLE
|
||||
}
|
||||
|
||||
/// Sense drags and hover, but not clicks.
|
||||
#[inline]
|
||||
pub fn drag() -> Self {
|
||||
Self {
|
||||
click: false,
|
||||
drag: true,
|
||||
focusable: true,
|
||||
}
|
||||
Self::DRAG | Self::FOCUSABLE
|
||||
}
|
||||
|
||||
/// Sense both clicks, drags and hover (e.g. a slider or window).
|
||||
@@ -90,43 +75,27 @@ impl Sense {
|
||||
/// See [`crate::PointerState::is_decidedly_dragging`] for details.
|
||||
#[inline]
|
||||
pub fn click_and_drag() -> Self {
|
||||
Self {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The logical "or" of two [`Sense`]s.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn union(self, other: Self) -> Self {
|
||||
Self {
|
||||
click: self.click | other.click,
|
||||
drag: self.drag | other.drag,
|
||||
focusable: self.focusable | other.focusable,
|
||||
}
|
||||
Self::CLICK | Self::FOCUSABLE | Self::DRAG
|
||||
}
|
||||
|
||||
/// Returns true if we sense either clicks or drags.
|
||||
#[inline]
|
||||
pub fn interactive(&self) -> bool {
|
||||
self.click || self.drag
|
||||
self.intersects(Self::CLICK | Self::DRAG)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for Sense {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn bitor(self, rhs: Self) -> Self {
|
||||
self.union(rhs)
|
||||
pub fn senses_click(&self) -> bool {
|
||||
self.contains(Self::CLICK)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOrAssign for Sense {
|
||||
#[inline]
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
*self = self.union(rhs);
|
||||
pub fn senses_drag(&self) -> bool {
|
||||
self.contains(Self::DRAG)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_focusable(&self) -> bool {
|
||||
self.contains(Self::FOCUSABLE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
#![allow(clippy::if_same_then_else)]
|
||||
|
||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use emath::Align;
|
||||
use epaint::{text::FontTweak, Rounding, Shadow, Stroke};
|
||||
use epaint::{text::FontTweak, CornerRadius, Shadow, Stroke};
|
||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
ecolor::Color32,
|
||||
@@ -174,6 +173,49 @@ impl From<TextStyle> for FontSelection {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Utility to modify a [`Style`] in some way.
|
||||
/// Constructed via [`StyleModifier::from`] from a `Fn(&mut Style)` or a [`Style`].
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StyleModifier(Option<Arc<dyn Fn(&mut Style) + Send + Sync>>);
|
||||
|
||||
impl std::fmt::Debug for StyleModifier {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("StyleModifier")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for StyleModifier
|
||||
where
|
||||
T: Fn(&mut Style) + Send + Sync + 'static,
|
||||
{
|
||||
fn from(f: T) -> Self {
|
||||
Self(Some(Arc::new(f)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Style> for StyleModifier {
|
||||
fn from(style: Style) -> Self {
|
||||
Self(Some(Arc::new(move |s| *s = style.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleModifier {
|
||||
/// Create a new [`StyleModifier`] from a function.
|
||||
pub fn new(f: impl Fn(&mut Style) + Send + Sync + 'static) -> Self {
|
||||
Self::from(f)
|
||||
}
|
||||
|
||||
/// Apply the modification to the given [`Style`].
|
||||
/// Usually used with [`Ui::style_mut`].
|
||||
pub fn apply(&self, style: &mut Style) {
|
||||
if let Some(f) = &self.0 {
|
||||
f(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Specifies the look and feel of egui.
|
||||
///
|
||||
/// You can change the visuals of a [`Ui`] with [`Ui::style_mut`]
|
||||
@@ -290,6 +332,9 @@ pub struct Style {
|
||||
|
||||
/// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [`Ui::scroll_to_rect`].
|
||||
pub scroll_animation: ScrollAnimation,
|
||||
|
||||
/// Use a more compact style for menus.
|
||||
pub compact_menu_style: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -915,7 +960,7 @@ pub struct Visuals {
|
||||
/// A good color for error text (e.g. red).
|
||||
pub error_fg_color: Color32,
|
||||
|
||||
pub window_rounding: Rounding,
|
||||
pub window_corner_radius: CornerRadius,
|
||||
pub window_shadow: Shadow,
|
||||
pub window_fill: Color32,
|
||||
pub window_stroke: Stroke,
|
||||
@@ -923,7 +968,7 @@ pub struct Visuals {
|
||||
/// Highlight the topmost window.
|
||||
pub window_highlight_topmost: bool,
|
||||
|
||||
pub menu_rounding: Rounding,
|
||||
pub menu_corner_radius: CornerRadius,
|
||||
|
||||
/// Panel background color
|
||||
pub panel_fill: Color32,
|
||||
@@ -944,7 +989,7 @@ pub struct Visuals {
|
||||
/// Show a background behind collapsing headers.
|
||||
pub collapsing_header_frame: bool,
|
||||
|
||||
/// Draw a vertical lien left of indented region, in e.g. [`crate::CollapsingHeader`].
|
||||
/// Draw a vertical line left of indented region, in e.g. [`crate::CollapsingHeader`].
|
||||
pub indent_has_left_vline: bool,
|
||||
|
||||
/// Whether or not Grids and Tables should be striped by default
|
||||
@@ -1107,7 +1152,7 @@ pub struct WidgetVisuals {
|
||||
pub bg_stroke: Stroke,
|
||||
|
||||
/// Button frames etc.
|
||||
pub rounding: Rounding,
|
||||
pub corner_radius: CornerRadius,
|
||||
|
||||
/// Stroke and text color of the interactive part of a component (button text, slider grab, check-mark, …).
|
||||
pub fg_stroke: Stroke,
|
||||
@@ -1121,6 +1166,11 @@ 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
|
||||
@@ -1230,6 +1280,7 @@ impl Default for Style {
|
||||
url_in_tooltip: false,
|
||||
always_scroll_the_only_direction: false,
|
||||
scroll_animation: ScrollAnimation::default(),
|
||||
compact_menu_style: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1291,7 +1342,7 @@ impl Visuals {
|
||||
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
|
||||
window_rounding: Rounding::same(6),
|
||||
window_corner_radius: CornerRadius::same(6),
|
||||
window_shadow: Shadow {
|
||||
offset: [10, 20],
|
||||
blur: 15,
|
||||
@@ -1302,7 +1353,7 @@ impl Visuals {
|
||||
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
window_highlight_topmost: true,
|
||||
|
||||
menu_rounding: Rounding::same(6),
|
||||
menu_corner_radius: CornerRadius::same(6),
|
||||
|
||||
panel_fill: Color32::from_gray(27),
|
||||
|
||||
@@ -1412,7 +1463,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
|
||||
rounding: Rounding::same(2),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
@@ -1420,7 +1471,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(60), // checkbox background
|
||||
bg_stroke: Default::default(),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
|
||||
rounding: Rounding::same(2),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
@@ -1428,7 +1479,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(70),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
||||
rounding: Rounding::same(3),
|
||||
corner_radius: CornerRadius::same(3),
|
||||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
@@ -1436,7 +1487,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(55),
|
||||
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
||||
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
||||
rounding: Rounding::same(2),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
@@ -1444,7 +1495,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
||||
rounding: Rounding::same(2),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
expansion: 0.0,
|
||||
},
|
||||
}
|
||||
@@ -1457,7 +1508,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(248),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
||||
rounding: Rounding::same(2),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
@@ -1465,7 +1516,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(230), // checkbox background
|
||||
bg_stroke: Default::default(),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
||||
rounding: Rounding::same(2),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
@@ -1473,7 +1524,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
||||
rounding: Rounding::same(3),
|
||||
corner_radius: CornerRadius::same(3),
|
||||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
@@ -1481,7 +1532,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(165),
|
||||
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
||||
rounding: Rounding::same(2),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
@@ -1489,7 +1540,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
rounding: Rounding::same(2),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
expansion: 0.0,
|
||||
},
|
||||
}
|
||||
@@ -1531,6 +1582,7 @@ impl Style {
|
||||
url_in_tooltip,
|
||||
always_scroll_the_only_direction,
|
||||
scroll_animation,
|
||||
compact_menu_style,
|
||||
} = self;
|
||||
|
||||
crate::Grid::new("_options").show(ui, |ui| {
|
||||
@@ -1636,6 +1688,8 @@ impl Style {
|
||||
#[cfg(debug_assertions)]
|
||||
ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
|
||||
|
||||
ui.checkbox(compact_menu_style, "Compact menu style");
|
||||
|
||||
ui.checkbox(explanation_tooltips, "Explanation tooltips")
|
||||
.on_hover_text(
|
||||
"Show explanatory text when hovering DragValue:s and other egui widgets",
|
||||
@@ -1924,7 +1978,7 @@ impl WidgetVisuals {
|
||||
weak_bg_fill,
|
||||
bg_fill: mandatory_bg_fill,
|
||||
bg_stroke,
|
||||
rounding,
|
||||
corner_radius,
|
||||
fg_stroke,
|
||||
expansion,
|
||||
} = self;
|
||||
@@ -1948,8 +2002,8 @@ impl WidgetVisuals {
|
||||
ui.add(bg_stroke);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Rounding");
|
||||
ui.add(rounding);
|
||||
ui.label("Corner radius");
|
||||
ui.add(corner_radius);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Foreground stroke (text)");
|
||||
@@ -1978,13 +2032,13 @@ impl Visuals {
|
||||
warn_fg_color,
|
||||
error_fg_color,
|
||||
|
||||
window_rounding,
|
||||
window_corner_radius,
|
||||
window_shadow,
|
||||
window_fill,
|
||||
window_stroke,
|
||||
window_highlight_topmost,
|
||||
|
||||
menu_rounding,
|
||||
menu_corner_radius,
|
||||
|
||||
panel_fill,
|
||||
|
||||
@@ -2066,8 +2120,8 @@ impl Visuals {
|
||||
ui.add(window_stroke);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Rounding");
|
||||
ui.add(window_rounding);
|
||||
ui.label("Corner radius");
|
||||
ui.add(window_corner_radius);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Shadow");
|
||||
@@ -2084,8 +2138,8 @@ impl Visuals {
|
||||
.spacing([12.0, 8.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Rounding");
|
||||
ui.add(menu_rounding);
|
||||
ui.label("Corner radius");
|
||||
ui.add(menu_corner_radius);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Shadow");
|
||||
@@ -2388,7 +2442,7 @@ impl Widget for &mut Margin {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut Rounding {
|
||||
impl Widget for &mut CornerRadius {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let mut same = self.is_same();
|
||||
|
||||
@@ -2398,37 +2452,39 @@ impl Widget for &mut Rounding {
|
||||
|
||||
let mut cr = self.nw;
|
||||
ui.add(DragValue::new(&mut cr).range(0.0..=f32::INFINITY));
|
||||
*self = Rounding::same(cr);
|
||||
*self = CornerRadius::same(cr);
|
||||
})
|
||||
.response
|
||||
} else {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut same, "same");
|
||||
|
||||
crate::Grid::new("rounding").num_columns(2).show(ui, |ui| {
|
||||
ui.label("NW");
|
||||
ui.add(DragValue::new(&mut self.nw).range(0.0..=f32::INFINITY));
|
||||
ui.end_row();
|
||||
crate::Grid::new("Corner radius")
|
||||
.num_columns(2)
|
||||
.show(ui, |ui| {
|
||||
ui.label("NW");
|
||||
ui.add(DragValue::new(&mut self.nw).range(0.0..=f32::INFINITY));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("NE");
|
||||
ui.add(DragValue::new(&mut self.ne).range(0.0..=f32::INFINITY));
|
||||
ui.end_row();
|
||||
ui.label("NE");
|
||||
ui.add(DragValue::new(&mut self.ne).range(0.0..=f32::INFINITY));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("SW");
|
||||
ui.add(DragValue::new(&mut self.sw).range(0.0..=f32::INFINITY));
|
||||
ui.end_row();
|
||||
ui.label("SW");
|
||||
ui.add(DragValue::new(&mut self.sw).range(0.0..=f32::INFINITY));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("SE");
|
||||
ui.add(DragValue::new(&mut self.se).range(0.0..=f32::INFINITY));
|
||||
ui.end_row();
|
||||
});
|
||||
ui.label("SE");
|
||||
ui.add(DragValue::new(&mut self.se).range(0.0..=f32::INFINITY));
|
||||
ui.end_row();
|
||||
});
|
||||
})
|
||||
.response
|
||||
};
|
||||
|
||||
// Apply the checkbox:
|
||||
if same {
|
||||
*self = Rounding::from(self.average());
|
||||
*self = CornerRadius::from(self.average());
|
||||
} else {
|
||||
// Make sure we aren't same:
|
||||
if self.is_same() {
|
||||
@@ -2513,7 +2569,7 @@ impl Widget for &mut crate::Frame {
|
||||
let crate::Frame {
|
||||
inner_margin,
|
||||
outer_margin,
|
||||
rounding,
|
||||
corner_radius,
|
||||
shadow,
|
||||
fill,
|
||||
stroke,
|
||||
@@ -2533,8 +2589,8 @@ impl Widget for &mut crate::Frame {
|
||||
ui.push_id("outer", |ui| ui.add(outer_margin));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Rounding");
|
||||
ui.add(rounding);
|
||||
ui.label("Corner radius");
|
||||
ui.add(corner_radius);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Shadow");
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
use crate::{Context, Galley, Id, Pos2};
|
||||
use emath::TSTransform;
|
||||
|
||||
use super::{text_cursor_state::is_word_char, CursorRange};
|
||||
use crate::{Context, Galley, Id};
|
||||
|
||||
use super::{text_cursor_state::is_word_char, CCursorRange};
|
||||
|
||||
/// Update accesskit with the current text state.
|
||||
pub fn update_accesskit_for_text_widget(
|
||||
ctx: &Context,
|
||||
widget_id: Id,
|
||||
cursor_range: Option<CursorRange>,
|
||||
cursor_range: Option<CCursorRange>,
|
||||
role: accesskit::Role,
|
||||
galley_pos: Pos2,
|
||||
global_from_galley: TSTransform,
|
||||
galley: &Galley,
|
||||
) {
|
||||
let parent_id = ctx.accesskit_node_builder(widget_id, |builder| {
|
||||
let parent_id = widget_id;
|
||||
|
||||
if let Some(cursor_range) = &cursor_range {
|
||||
let anchor = &cursor_range.secondary.rcursor;
|
||||
let focus = &cursor_range.primary.rcursor;
|
||||
let anchor = galley.layout_from_cursor(cursor_range.secondary);
|
||||
let focus = galley.layout_from_cursor(cursor_range.primary);
|
||||
builder.set_text_selection(accesskit::TextSelection {
|
||||
anchor: accesskit::TextPosition {
|
||||
node: parent_id.with(anchor.row).accesskit_id(),
|
||||
@@ -43,7 +45,7 @@ pub fn update_accesskit_for_text_widget(
|
||||
let row_id = parent_id.with(row_index);
|
||||
ctx.accesskit_node_builder(row_id, |builder| {
|
||||
builder.set_role(accesskit::Role::TextRun);
|
||||
let rect = row.rect().translate(galley_pos.to_vec2());
|
||||
let rect = global_from_galley * row.rect();
|
||||
builder.set_bounds(accesskit::Rect {
|
||||
x0: rect.min.x.into(),
|
||||
y0: rect.min.y.into(),
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
use epaint::{
|
||||
text::cursor::{CCursor, Cursor, PCursor},
|
||||
Galley,
|
||||
};
|
||||
use epaint::{text::cursor::CCursor, Galley};
|
||||
|
||||
use crate::{os::OperatingSystem, Event, Id, Key, Modifiers};
|
||||
|
||||
use super::text_cursor_state::{ccursor_next_word, ccursor_previous_word, slice_char_range};
|
||||
|
||||
/// A selected text range (could be a range of length zero).
|
||||
///
|
||||
/// The selection is based on character count (NOT byte count!).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct CursorRange {
|
||||
pub struct CCursorRange {
|
||||
/// When selecting with a mouse, this is where the mouse was released.
|
||||
/// When moving with e.g. shift+arrows, this is what moves.
|
||||
/// Note that the two ends can come in any order, and also be equal (no selection).
|
||||
pub primary: Cursor,
|
||||
pub primary: CCursor,
|
||||
|
||||
/// When selecting with a mouse, this is where the mouse was first pressed.
|
||||
/// This part of the cursor does not move when shift is down.
|
||||
pub secondary: Cursor,
|
||||
pub secondary: CCursor,
|
||||
|
||||
/// Saved horizontal position of the cursor.
|
||||
pub h_pos: Option<f32>,
|
||||
}
|
||||
|
||||
impl CursorRange {
|
||||
impl CCursorRange {
|
||||
/// The empty range.
|
||||
#[inline]
|
||||
pub fn one(cursor: Cursor) -> Self {
|
||||
pub fn one(ccursor: CCursor) -> Self {
|
||||
Self {
|
||||
primary: cursor,
|
||||
secondary: cursor,
|
||||
primary: ccursor,
|
||||
secondary: ccursor,
|
||||
h_pos: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn two(min: Cursor, max: Cursor) -> Self {
|
||||
pub fn two(min: impl Into<CCursor>, max: impl Into<CCursor>) -> Self {
|
||||
Self {
|
||||
primary: max,
|
||||
secondary: min,
|
||||
primary: max.into(),
|
||||
secondary: min.into(),
|
||||
h_pos: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,39 +48,31 @@ impl CursorRange {
|
||||
Self::two(galley.begin(), galley.end())
|
||||
}
|
||||
|
||||
pub fn as_ccursor_range(&self) -> CCursorRange {
|
||||
CCursorRange {
|
||||
primary: self.primary.ccursor,
|
||||
secondary: self.secondary.ccursor,
|
||||
}
|
||||
}
|
||||
|
||||
/// The range of selected character indices.
|
||||
pub fn as_sorted_char_range(&self) -> std::ops::Range<usize> {
|
||||
let [start, end] = self.sorted_cursors();
|
||||
std::ops::Range {
|
||||
start: start.ccursor.index,
|
||||
end: end.ccursor.index,
|
||||
start: start.index,
|
||||
end: end.index,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the selected range contains no characters.
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.primary.ccursor == self.secondary.ccursor
|
||||
self.primary == self.secondary
|
||||
}
|
||||
|
||||
/// Is `self` a super-set of the other range?
|
||||
pub fn contains(&self, other: &Self) -> bool {
|
||||
pub fn contains(&self, other: Self) -> bool {
|
||||
let [self_min, self_max] = self.sorted_cursors();
|
||||
let [other_min, other_max] = other.sorted_cursors();
|
||||
self_min.ccursor.index <= other_min.ccursor.index
|
||||
&& other_max.ccursor.index <= self_max.ccursor.index
|
||||
self_min.index <= other_min.index && other_max.index <= self_max.index
|
||||
}
|
||||
|
||||
/// If there is a selection, None is returned.
|
||||
/// If the two ends are the same, that is returned.
|
||||
pub fn single(&self) -> Option<Cursor> {
|
||||
pub fn single(&self) -> Option<CCursor> {
|
||||
if self.is_empty() {
|
||||
Some(self.primary)
|
||||
} else {
|
||||
@@ -84,25 +80,16 @@ impl CursorRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_sorted(&self) -> bool {
|
||||
let p = self.primary.ccursor;
|
||||
let s = self.secondary.ccursor;
|
||||
let p = self.primary;
|
||||
let s = self.secondary;
|
||||
(p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
|
||||
}
|
||||
|
||||
pub fn sorted(self) -> Self {
|
||||
if self.is_sorted() {
|
||||
self
|
||||
} else {
|
||||
Self {
|
||||
primary: self.secondary,
|
||||
secondary: self.primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the two ends ordered.
|
||||
pub fn sorted_cursors(&self) -> [Cursor; 2] {
|
||||
/// returns the two ends ordered
|
||||
#[inline]
|
||||
pub fn sorted_cursors(&self) -> [CCursor; 2] {
|
||||
if self.is_sorted() {
|
||||
[self.primary, self.secondary]
|
||||
} else {
|
||||
@@ -110,9 +97,15 @@ impl CursorRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[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.ccursor.index..max.ccursor.index)
|
||||
slice_char_range(text, min.index..max.index)
|
||||
}
|
||||
|
||||
/// Check for key presses that are moving the cursor.
|
||||
@@ -146,7 +139,14 @@ impl CursorRange {
|
||||
| Key::ArrowDown
|
||||
| Key::Home
|
||||
| Key::End => {
|
||||
move_single_cursor(os, &mut self.primary, galley, key, modifiers);
|
||||
move_single_cursor(
|
||||
os,
|
||||
&mut self.primary,
|
||||
&mut self.h_pos,
|
||||
galley,
|
||||
key,
|
||||
modifiers,
|
||||
);
|
||||
if !modifiers.shift {
|
||||
self.secondary = self.primary;
|
||||
}
|
||||
@@ -156,7 +156,14 @@ impl CursorRange {
|
||||
Key::P | Key::N | Key::B | Key::F | Key::A | Key::E
|
||||
if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift =>
|
||||
{
|
||||
move_single_cursor(os, &mut self.primary, galley, key, modifiers);
|
||||
move_single_cursor(
|
||||
os,
|
||||
&mut self.primary,
|
||||
&mut self.h_pos,
|
||||
galley,
|
||||
key,
|
||||
modifiers,
|
||||
);
|
||||
self.secondary = self.primary;
|
||||
true
|
||||
}
|
||||
@@ -196,8 +203,9 @@ impl CursorRange {
|
||||
ccursor_from_accesskit_text_position(_widget_id, galley, &selection.anchor);
|
||||
if let (Some(primary), Some(secondary)) = (primary, secondary) {
|
||||
*self = Self {
|
||||
primary: galley.from_ccursor(primary),
|
||||
secondary: galley.from_ccursor(secondary),
|
||||
primary,
|
||||
secondary,
|
||||
h_pos: None,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
@@ -210,71 +218,6 @@ impl CursorRange {
|
||||
}
|
||||
}
|
||||
|
||||
/// A selected text range (could be a range of length zero).
|
||||
///
|
||||
/// The selection is based on character count (NOT byte count!).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct CCursorRange {
|
||||
/// When selecting with a mouse, this is where the mouse was released.
|
||||
/// When moving with e.g. shift+arrows, this is what moves.
|
||||
/// Note that the two ends can come in any order, and also be equal (no selection).
|
||||
pub primary: CCursor,
|
||||
|
||||
/// When selecting with a mouse, this is where the mouse was first pressed.
|
||||
/// This part of the cursor does not move when shift is down.
|
||||
pub secondary: CCursor,
|
||||
}
|
||||
|
||||
impl CCursorRange {
|
||||
/// The empty range.
|
||||
#[inline]
|
||||
pub fn one(ccursor: CCursor) -> Self {
|
||||
Self {
|
||||
primary: ccursor,
|
||||
secondary: ccursor,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn two(min: impl Into<CCursor>, max: impl Into<CCursor>) -> Self {
|
||||
Self {
|
||||
primary: max.into(),
|
||||
secondary: min.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_sorted(&self) -> bool {
|
||||
let p = self.primary;
|
||||
let s = self.secondary;
|
||||
(p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
|
||||
}
|
||||
|
||||
/// returns the two ends ordered
|
||||
#[inline]
|
||||
pub fn sorted(&self) -> [CCursor; 2] {
|
||||
if self.is_sorted() {
|
||||
[self.primary, self.secondary]
|
||||
} else {
|
||||
[self.secondary, self.primary]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct PCursorRange {
|
||||
/// When selecting with a mouse, this is where the mouse was released.
|
||||
/// When moving with e.g. shift+arrows, this is what moves.
|
||||
/// Note that the two ends can come in any order, and also be equal (no selection).
|
||||
pub primary: PCursor,
|
||||
|
||||
/// When selecting with a mouse, this is where the mouse was first pressed.
|
||||
/// This part of the cursor does not move when shift is down.
|
||||
pub secondary: PCursor,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
@@ -304,78 +247,83 @@ fn ccursor_from_accesskit_text_position(
|
||||
/// Move a text cursor based on keyboard
|
||||
fn move_single_cursor(
|
||||
os: OperatingSystem,
|
||||
cursor: &mut Cursor,
|
||||
cursor: &mut CCursor,
|
||||
h_pos: &mut Option<f32>,
|
||||
galley: &Galley,
|
||||
key: Key,
|
||||
modifiers: &Modifiers,
|
||||
) {
|
||||
if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift {
|
||||
match key {
|
||||
Key::A => *cursor = galley.cursor_begin_of_row(cursor),
|
||||
Key::E => *cursor = galley.cursor_end_of_row(cursor),
|
||||
Key::P => *cursor = galley.cursor_up_one_row(cursor),
|
||||
Key::N => *cursor = galley.cursor_down_one_row(cursor),
|
||||
Key::B => *cursor = galley.cursor_left_one_character(cursor),
|
||||
Key::F => *cursor = galley.cursor_right_one_character(cursor),
|
||||
_ => (),
|
||||
}
|
||||
return;
|
||||
}
|
||||
match key {
|
||||
Key::ArrowLeft => {
|
||||
if modifiers.alt || modifiers.ctrl {
|
||||
// alt on mac, ctrl on windows
|
||||
*cursor = galley.from_ccursor(ccursor_previous_word(galley, cursor.ccursor));
|
||||
} else if modifiers.mac_cmd {
|
||||
*cursor = galley.cursor_begin_of_row(cursor);
|
||||
} else {
|
||||
*cursor = galley.cursor_left_one_character(cursor);
|
||||
let (new_cursor, new_h_pos) =
|
||||
if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift {
|
||||
match key {
|
||||
Key::A => (galley.cursor_begin_of_row(cursor), None),
|
||||
Key::E => (galley.cursor_end_of_row(cursor), None),
|
||||
Key::P => galley.cursor_up_one_row(cursor, *h_pos),
|
||||
Key::N => galley.cursor_down_one_row(cursor, *h_pos),
|
||||
Key::B => (galley.cursor_left_one_character(cursor), None),
|
||||
Key::F => (galley.cursor_right_one_character(cursor), None),
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
Key::ArrowRight => {
|
||||
if modifiers.alt || modifiers.ctrl {
|
||||
// alt on mac, ctrl on windows
|
||||
*cursor = galley.from_ccursor(ccursor_next_word(galley, cursor.ccursor));
|
||||
} else if modifiers.mac_cmd {
|
||||
*cursor = galley.cursor_end_of_row(cursor);
|
||||
} else {
|
||||
*cursor = galley.cursor_right_one_character(cursor);
|
||||
}
|
||||
}
|
||||
Key::ArrowUp => {
|
||||
if modifiers.command {
|
||||
// mac and windows behavior
|
||||
*cursor = galley.begin();
|
||||
} else {
|
||||
*cursor = galley.cursor_up_one_row(cursor);
|
||||
}
|
||||
}
|
||||
Key::ArrowDown => {
|
||||
if modifiers.command {
|
||||
// mac and windows behavior
|
||||
*cursor = galley.end();
|
||||
} else {
|
||||
*cursor = galley.cursor_down_one_row(cursor);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match key {
|
||||
Key::ArrowLeft => {
|
||||
if modifiers.alt || modifiers.ctrl {
|
||||
// alt on mac, ctrl on windows
|
||||
(ccursor_previous_word(galley, *cursor), None)
|
||||
} else if modifiers.mac_cmd {
|
||||
(galley.cursor_begin_of_row(cursor), None)
|
||||
} else {
|
||||
(galley.cursor_left_one_character(cursor), None)
|
||||
}
|
||||
}
|
||||
Key::ArrowRight => {
|
||||
if modifiers.alt || modifiers.ctrl {
|
||||
// alt on mac, ctrl on windows
|
||||
(ccursor_next_word(galley, *cursor), None)
|
||||
} else if modifiers.mac_cmd {
|
||||
(galley.cursor_end_of_row(cursor), None)
|
||||
} else {
|
||||
(galley.cursor_right_one_character(cursor), None)
|
||||
}
|
||||
}
|
||||
Key::ArrowUp => {
|
||||
if modifiers.command {
|
||||
// mac and windows behavior
|
||||
(galley.begin(), None)
|
||||
} else {
|
||||
galley.cursor_up_one_row(cursor, *h_pos)
|
||||
}
|
||||
}
|
||||
Key::ArrowDown => {
|
||||
if modifiers.command {
|
||||
// mac and windows behavior
|
||||
(galley.end(), None)
|
||||
} else {
|
||||
galley.cursor_down_one_row(cursor, *h_pos)
|
||||
}
|
||||
}
|
||||
|
||||
Key::Home => {
|
||||
if modifiers.ctrl {
|
||||
// windows behavior
|
||||
*cursor = galley.begin();
|
||||
} else {
|
||||
*cursor = galley.cursor_begin_of_row(cursor);
|
||||
}
|
||||
}
|
||||
Key::End => {
|
||||
if modifiers.ctrl {
|
||||
// windows behavior
|
||||
*cursor = galley.end();
|
||||
} else {
|
||||
*cursor = galley.cursor_end_of_row(cursor);
|
||||
}
|
||||
}
|
||||
Key::Home => {
|
||||
if modifiers.ctrl {
|
||||
// windows behavior
|
||||
(galley.begin(), None)
|
||||
} else {
|
||||
(galley.cursor_begin_of_row(cursor), None)
|
||||
}
|
||||
}
|
||||
Key::End => {
|
||||
if modifiers.ctrl {
|
||||
// windows behavior
|
||||
(galley.end(), None)
|
||||
} else {
|
||||
(galley.cursor_end_of_row(cursor), None)
|
||||
}
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
};
|
||||
|
||||
*cursor = new_cursor;
|
||||
*h_pos = new_h_pos;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use emath::TSTransform;
|
||||
|
||||
use crate::{
|
||||
layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event,
|
||||
Galley, Id, LayerId, Pos2, Rect, Response, Ui,
|
||||
@@ -8,7 +10,7 @@ use crate::{
|
||||
use super::{
|
||||
text_cursor_state::cursor_rect,
|
||||
visuals::{paint_text_selection, RowVertexIndices},
|
||||
CursorRange, TextCursorState,
|
||||
TextCursorState,
|
||||
};
|
||||
|
||||
/// Turn on to help debug this
|
||||
@@ -25,9 +27,14 @@ struct WidgetTextCursor {
|
||||
}
|
||||
|
||||
impl WidgetTextCursor {
|
||||
fn new(widget_id: Id, cursor: impl Into<CCursor>, galley_pos: Pos2, galley: &Galley) -> Self {
|
||||
fn new(
|
||||
widget_id: Id,
|
||||
cursor: impl Into<CCursor>,
|
||||
global_from_galley: TSTransform,
|
||||
galley: &Galley,
|
||||
) -> Self {
|
||||
let ccursor = cursor.into();
|
||||
let pos = pos_in_galley(galley_pos, galley, ccursor);
|
||||
let pos = global_from_galley * pos_in_galley(galley, ccursor);
|
||||
Self {
|
||||
widget_id,
|
||||
ccursor,
|
||||
@@ -36,8 +43,8 @@ impl WidgetTextCursor {
|
||||
}
|
||||
}
|
||||
|
||||
fn pos_in_galley(galley_pos: Pos2, galley: &Galley, ccursor: CCursor) -> Pos2 {
|
||||
galley_pos + galley.pos_from_ccursor(ccursor).center().to_vec2()
|
||||
fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
|
||||
galley.pos_from_cursor(ccursor).center()
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WidgetTextCursor {
|
||||
@@ -231,8 +238,7 @@ impl LabelSelectionState {
|
||||
self.selection = None;
|
||||
}
|
||||
|
||||
fn copy_text(&mut self, galley_pos: Pos2, galley: &Galley, cursor_range: &CursorRange) {
|
||||
let new_galley_rect = Rect::from_min_size(galley_pos, galley.size());
|
||||
fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CCursorRange) {
|
||||
let new_text = selected_text(galley, cursor_range);
|
||||
if new_text.is_empty() {
|
||||
return;
|
||||
@@ -266,7 +272,7 @@ impl LabelSelectionState {
|
||||
let new_text_starts_with_space_or_punctuation = new_text
|
||||
.chars()
|
||||
.next()
|
||||
.map_or(false, |c| c.is_whitespace() || c.is_ascii_punctuation());
|
||||
.is_some_and(|c| c.is_whitespace() || c.is_ascii_punctuation());
|
||||
|
||||
if existing_ends_with_space == Some(false) && !new_text_starts_with_space_or_punctuation
|
||||
{
|
||||
@@ -311,7 +317,7 @@ impl LabelSelectionState {
|
||||
&mut self,
|
||||
ui: &Ui,
|
||||
response: &Response,
|
||||
galley_pos: Pos2,
|
||||
global_from_galley: TSTransform,
|
||||
galley: &Galley,
|
||||
) -> TextCursorState {
|
||||
let Some(selection) = &mut self.selection else {
|
||||
@@ -324,6 +330,8 @@ impl LabelSelectionState {
|
||||
return TextCursorState::default();
|
||||
}
|
||||
|
||||
let galley_from_global = global_from_galley.inverse();
|
||||
|
||||
let multi_widget_text_select = ui.style().interaction.multi_widget_text_select;
|
||||
|
||||
let may_select_widget =
|
||||
@@ -331,7 +339,8 @@ impl LabelSelectionState {
|
||||
|
||||
if self.is_dragging && may_select_widget {
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
let galley_rect = Rect::from_min_size(galley_pos, galley.size());
|
||||
let galley_rect =
|
||||
global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
|
||||
let galley_rect = galley_rect.intersect(ui.clip_rect());
|
||||
|
||||
let is_in_same_column = galley_rect
|
||||
@@ -345,7 +354,7 @@ impl LabelSelectionState {
|
||||
|
||||
let new_primary = if response.contains_pointer() {
|
||||
// Dragging into this widget - easy case:
|
||||
Some(galley.cursor_from_pos(pointer_pos - galley_pos))
|
||||
Some(galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2()))
|
||||
} else if is_in_same_column
|
||||
&& !self.has_reached_primary
|
||||
&& selection.primary.pos.y <= selection.secondary.pos.y
|
||||
@@ -379,7 +388,7 @@ impl LabelSelectionState {
|
||||
|
||||
if let Some(new_primary) = new_primary {
|
||||
selection.primary =
|
||||
WidgetTextCursor::new(response.id, new_primary, galley_pos, galley);
|
||||
WidgetTextCursor::new(response.id, new_primary, global_from_galley, galley);
|
||||
|
||||
// We don't want the latency of `drag_started`.
|
||||
let drag_started = ui.input(|i| i.pointer.any_pressed());
|
||||
@@ -405,11 +414,12 @@ impl LabelSelectionState {
|
||||
let has_secondary = response.id == selection.secondary.widget_id;
|
||||
|
||||
if has_primary {
|
||||
selection.primary.pos = pos_in_galley(galley_pos, galley, selection.primary.ccursor);
|
||||
selection.primary.pos =
|
||||
global_from_galley * pos_in_galley(galley, selection.primary.ccursor);
|
||||
}
|
||||
if has_secondary {
|
||||
selection.secondary.pos =
|
||||
pos_in_galley(galley_pos, galley, selection.secondary.ccursor);
|
||||
global_from_galley * pos_in_galley(galley, selection.secondary.ccursor);
|
||||
}
|
||||
|
||||
self.has_reached_primary |= has_primary;
|
||||
@@ -426,7 +436,11 @@ impl LabelSelectionState {
|
||||
match (primary, secondary) {
|
||||
(Some(primary), Some(secondary)) => {
|
||||
// This is the only selected label.
|
||||
TextCursorState::from(CCursorRange { primary, secondary })
|
||||
TextCursorState::from(CCursorRange {
|
||||
primary,
|
||||
secondary,
|
||||
h_pos: None,
|
||||
})
|
||||
}
|
||||
|
||||
(Some(primary), None) => {
|
||||
@@ -435,12 +449,16 @@ impl LabelSelectionState {
|
||||
// Secondary was before primary.
|
||||
// Select everything up to the cursor.
|
||||
// We assume normal left-to-right and top-down layout order here.
|
||||
galley.begin().ccursor
|
||||
galley.begin()
|
||||
} else {
|
||||
// Select everything from the cursor onward:
|
||||
galley.end().ccursor
|
||||
galley.end()
|
||||
};
|
||||
TextCursorState::from(CCursorRange { primary, secondary })
|
||||
TextCursorState::from(CCursorRange {
|
||||
primary,
|
||||
secondary,
|
||||
h_pos: None,
|
||||
})
|
||||
}
|
||||
|
||||
(None, Some(secondary)) => {
|
||||
@@ -449,12 +467,16 @@ impl LabelSelectionState {
|
||||
// Primary was before secondary.
|
||||
// Select everything up to the cursor.
|
||||
// We assume normal left-to-right and top-down layout order here.
|
||||
galley.begin().ccursor
|
||||
galley.begin()
|
||||
} else {
|
||||
// Select everything from the cursor onward:
|
||||
galley.end().ccursor
|
||||
galley.end()
|
||||
};
|
||||
TextCursorState::from(CCursorRange { primary, secondary })
|
||||
TextCursorState::from(CCursorRange {
|
||||
primary,
|
||||
secondary,
|
||||
h_pos: None,
|
||||
})
|
||||
}
|
||||
|
||||
(None, None) => {
|
||||
@@ -482,12 +504,22 @@ impl LabelSelectionState {
|
||||
&mut self,
|
||||
ui: &Ui,
|
||||
response: &Response,
|
||||
galley_pos: Pos2,
|
||||
galley_pos_in_layer: Pos2,
|
||||
galley: &mut Arc<Galley>,
|
||||
) -> Vec<RowVertexIndices> {
|
||||
let widget_id = response.id;
|
||||
|
||||
if response.hovered {
|
||||
let global_from_layer = ui
|
||||
.ctx()
|
||||
.layer_transform_to_global(ui.layer_id())
|
||||
.unwrap_or_default();
|
||||
let layer_from_galley = TSTransform::from_translation(galley_pos_in_layer.to_vec2());
|
||||
let galley_from_layer = layer_from_galley.inverse();
|
||||
let layer_from_global = global_from_layer.inverse();
|
||||
let galley_from_global = galley_from_layer * layer_from_global;
|
||||
let global_from_galley = global_from_layer * layer_from_galley;
|
||||
|
||||
if response.hovered() {
|
||||
ui.ctx().set_cursor_icon(CursorIcon::Text);
|
||||
}
|
||||
|
||||
@@ -496,13 +528,14 @@ impl LabelSelectionState {
|
||||
|
||||
let old_selection = self.selection;
|
||||
|
||||
let mut cursor_state = self.cursor_for(ui, response, galley_pos, galley);
|
||||
let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley);
|
||||
|
||||
let old_range = cursor_state.range(galley);
|
||||
let old_range = cursor_state.char_range();
|
||||
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
if response.contains_pointer() {
|
||||
let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - galley_pos);
|
||||
let cursor_at_pointer =
|
||||
galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2());
|
||||
|
||||
// This is where we handle start-of-drag and double-click-to-select.
|
||||
// Actual drag-to-select happens elsewhere.
|
||||
@@ -511,8 +544,8 @@ impl LabelSelectionState {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut cursor_range) = cursor_state.range(galley) {
|
||||
let galley_rect = Rect::from_min_size(galley_pos, galley.size());
|
||||
if let Some(mut cursor_range) = cursor_state.char_range() {
|
||||
let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
|
||||
self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
|
||||
|
||||
if let Some(selection) = &self.selection {
|
||||
@@ -522,14 +555,14 @@ impl LabelSelectionState {
|
||||
}
|
||||
|
||||
if got_copy_event(ui.ctx()) {
|
||||
self.copy_text(galley_pos, galley, &cursor_range);
|
||||
self.copy_text(galley_rect, galley, &cursor_range);
|
||||
}
|
||||
|
||||
cursor_state.set_range(Some(cursor_range));
|
||||
cursor_state.set_char_range(Some(cursor_range));
|
||||
}
|
||||
|
||||
// Look for changes due to keyboard and/or mouse interaction:
|
||||
let new_range = cursor_state.range(galley);
|
||||
let new_range = cursor_state.char_range();
|
||||
let selection_changed = old_range != new_range;
|
||||
|
||||
if let (true, Some(range)) = (selection_changed, new_range) {
|
||||
@@ -544,23 +577,32 @@ impl LabelSelectionState {
|
||||
|
||||
if primary_changed || !ui.style().interaction.multi_widget_text_select {
|
||||
selection.primary =
|
||||
WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley);
|
||||
WidgetTextCursor::new(widget_id, range.primary, global_from_galley, galley);
|
||||
self.has_reached_primary = true;
|
||||
}
|
||||
if secondary_changed || !ui.style().interaction.multi_widget_text_select {
|
||||
selection.secondary =
|
||||
WidgetTextCursor::new(widget_id, range.secondary, galley_pos, galley);
|
||||
selection.secondary = WidgetTextCursor::new(
|
||||
widget_id,
|
||||
range.secondary,
|
||||
global_from_galley,
|
||||
galley,
|
||||
);
|
||||
self.has_reached_secondary = true;
|
||||
}
|
||||
} else {
|
||||
// Start of a new selection
|
||||
self.selection = Some(CurrentSelection {
|
||||
layer_id: response.layer_id,
|
||||
primary: WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley),
|
||||
primary: WidgetTextCursor::new(
|
||||
widget_id,
|
||||
range.primary,
|
||||
global_from_galley,
|
||||
galley,
|
||||
),
|
||||
secondary: WidgetTextCursor::new(
|
||||
widget_id,
|
||||
range.secondary,
|
||||
galley_pos,
|
||||
global_from_galley,
|
||||
galley,
|
||||
),
|
||||
});
|
||||
@@ -583,14 +625,14 @@ impl LabelSelectionState {
|
||||
// Scroll to keep primary cursor in view:
|
||||
let row_height = estimate_row_height(galley);
|
||||
let primary_cursor_rect =
|
||||
cursor_rect(galley_pos, galley, &range.primary, row_height);
|
||||
global_from_galley * cursor_rect(galley, &range.primary, row_height);
|
||||
ui.scroll_to_rect(primary_cursor_rect, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_range = cursor_state.range(galley);
|
||||
let cursor_range = cursor_state.char_range();
|
||||
|
||||
let mut new_vertex_indices = vec![];
|
||||
|
||||
@@ -609,7 +651,7 @@ impl LabelSelectionState {
|
||||
response.id,
|
||||
cursor_range,
|
||||
accesskit::Role::Label,
|
||||
galley_pos,
|
||||
global_from_galley,
|
||||
galley,
|
||||
);
|
||||
|
||||
@@ -630,7 +672,7 @@ fn process_selection_key_events(
|
||||
ctx: &Context,
|
||||
galley: &Galley,
|
||||
widget_id: Id,
|
||||
cursor_range: &mut CursorRange,
|
||||
cursor_range: &mut CCursorRange,
|
||||
) -> bool {
|
||||
let os = ctx.os();
|
||||
|
||||
@@ -647,10 +689,10 @@ fn process_selection_key_events(
|
||||
changed
|
||||
}
|
||||
|
||||
fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String {
|
||||
fn selected_text(galley: &Galley, cursor_range: &CCursorRange) -> String {
|
||||
// This logic means we can select everything in an elided label (including the `…`)
|
||||
// and still copy the entire un-elided text!
|
||||
let everything_is_selected = cursor_range.contains(&CursorRange::select_all(galley));
|
||||
let everything_is_selected = cursor_range.contains(CCursorRange::select_all(galley));
|
||||
|
||||
let copy_everything = cursor_range.is_empty() || everything_is_selected;
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@ mod label_text_selection;
|
||||
pub mod text_cursor_state;
|
||||
pub mod visuals;
|
||||
|
||||
pub use cursor_range::{CCursorRange, CursorRange, PCursorRange};
|
||||
pub use cursor_range::CCursorRange;
|
||||
pub use label_text_selection::LabelSelectionState;
|
||||
pub use text_cursor_state::TextCursorState;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
//! Text cursor changes/interaction, without modifying the text.
|
||||
|
||||
use epaint::text::{
|
||||
cursor::{CCursor, Cursor},
|
||||
Galley,
|
||||
};
|
||||
use epaint::text::{cursor::CCursor, Galley};
|
||||
|
||||
use crate::{epaint, NumExt, Pos2, Rect, Response, Ui};
|
||||
use crate::{epaint, NumExt, Rect, Response, Ui};
|
||||
|
||||
use super::{CCursorRange, CursorRange};
|
||||
use super::CCursorRange;
|
||||
|
||||
/// The state of a text cursor selection.
|
||||
///
|
||||
@@ -16,29 +13,12 @@ use super::{CCursorRange, CursorRange};
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct TextCursorState {
|
||||
cursor_range: Option<CursorRange>,
|
||||
|
||||
/// This is what is easiest to work with when editing text,
|
||||
/// so users are more likely to read/write this.
|
||||
ccursor_range: Option<CCursorRange>,
|
||||
}
|
||||
|
||||
impl From<CursorRange> for TextCursorState {
|
||||
fn from(cursor_range: CursorRange) -> Self {
|
||||
Self {
|
||||
cursor_range: Some(cursor_range),
|
||||
ccursor_range: Some(CCursorRange {
|
||||
primary: cursor_range.primary.ccursor,
|
||||
secondary: cursor_range.secondary.ccursor,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CCursorRange> for TextCursorState {
|
||||
fn from(ccursor_range: CCursorRange) -> Self {
|
||||
Self {
|
||||
cursor_range: None,
|
||||
ccursor_range: Some(ccursor_range),
|
||||
}
|
||||
}
|
||||
@@ -46,50 +26,18 @@ impl From<CCursorRange> for TextCursorState {
|
||||
|
||||
impl TextCursorState {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.cursor_range.is_none() && self.ccursor_range.is_none()
|
||||
self.ccursor_range.is_none()
|
||||
}
|
||||
|
||||
/// The currently selected range of characters.
|
||||
pub fn char_range(&self) -> Option<CCursorRange> {
|
||||
self.ccursor_range.or_else(|| {
|
||||
self.cursor_range
|
||||
.map(|cursor_range| cursor_range.as_ccursor_range())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn range(&self, galley: &Galley) -> Option<CursorRange> {
|
||||
self.cursor_range
|
||||
.map(|cursor_range| {
|
||||
// We only use the PCursor (paragraph number, and character offset within that paragraph).
|
||||
// This is so that if we resize the [`TextEdit`] region, and text wrapping changes,
|
||||
// we keep the same byte character offset from the beginning of the text,
|
||||
// even though the number of rows changes
|
||||
// (each paragraph can be several rows, due to word wrapping).
|
||||
// The column (character offset) should be able to extend beyond the last word so that we can
|
||||
// go down and still end up on the same column when we return.
|
||||
CursorRange {
|
||||
primary: galley.from_pcursor(cursor_range.primary.pcursor),
|
||||
secondary: galley.from_pcursor(cursor_range.secondary.pcursor),
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
self.ccursor_range.map(|ccursor_range| CursorRange {
|
||||
primary: galley.from_ccursor(ccursor_range.primary),
|
||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
||||
})
|
||||
})
|
||||
self.ccursor_range
|
||||
}
|
||||
|
||||
/// Sets the currently selected range of characters.
|
||||
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
|
||||
self.cursor_range = None;
|
||||
self.ccursor_range = ccursor_range;
|
||||
}
|
||||
|
||||
pub fn set_range(&mut self, cursor_range: Option<CursorRange>) {
|
||||
self.cursor_range = cursor_range;
|
||||
self.ccursor_range = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl TextCursorState {
|
||||
@@ -100,7 +48,7 @@ impl TextCursorState {
|
||||
&mut self,
|
||||
ui: &Ui,
|
||||
response: &Response,
|
||||
cursor_at_pointer: Cursor,
|
||||
cursor_at_pointer: CCursor,
|
||||
galley: &Galley,
|
||||
is_being_dragged: bool,
|
||||
) -> bool {
|
||||
@@ -108,39 +56,33 @@ impl TextCursorState {
|
||||
|
||||
if response.double_clicked() {
|
||||
// Select word:
|
||||
let ccursor_range = select_word_at(text, cursor_at_pointer.ccursor);
|
||||
self.set_range(Some(CursorRange {
|
||||
primary: galley.from_ccursor(ccursor_range.primary),
|
||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
||||
}));
|
||||
let ccursor_range = select_word_at(text, cursor_at_pointer);
|
||||
self.set_char_range(Some(ccursor_range));
|
||||
true
|
||||
} else if response.triple_clicked() {
|
||||
// Select line:
|
||||
let ccursor_range = select_line_at(text, cursor_at_pointer.ccursor);
|
||||
self.set_range(Some(CursorRange {
|
||||
primary: galley.from_ccursor(ccursor_range.primary),
|
||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
||||
}));
|
||||
let ccursor_range = select_line_at(text, cursor_at_pointer);
|
||||
self.set_char_range(Some(ccursor_range));
|
||||
true
|
||||
} else if response.sense.drag {
|
||||
} else if response.sense.senses_drag() {
|
||||
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
|
||||
// The start of a drag (or a click).
|
||||
if ui.input(|i| i.modifiers.shift) {
|
||||
if let Some(mut cursor_range) = self.range(galley) {
|
||||
if let Some(mut cursor_range) = self.char_range() {
|
||||
cursor_range.primary = cursor_at_pointer;
|
||||
self.set_range(Some(cursor_range));
|
||||
self.set_char_range(Some(cursor_range));
|
||||
} else {
|
||||
self.set_range(Some(CursorRange::one(cursor_at_pointer)));
|
||||
self.set_char_range(Some(CCursorRange::one(cursor_at_pointer)));
|
||||
}
|
||||
} else {
|
||||
self.set_range(Some(CursorRange::one(cursor_at_pointer)));
|
||||
self.set_char_range(Some(CCursorRange::one(cursor_at_pointer)));
|
||||
}
|
||||
true
|
||||
} else if is_being_dragged {
|
||||
// Drag to select text:
|
||||
if let Some(mut cursor_range) = self.range(galley) {
|
||||
if let Some(mut cursor_range) = self.char_range() {
|
||||
cursor_range.primary = cursor_at_pointer;
|
||||
self.set_range(Some(cursor_range));
|
||||
self.set_char_range(Some(cursor_range));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
@@ -329,20 +271,25 @@ pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
|
||||
}
|
||||
|
||||
pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
|
||||
assert!(char_range.start <= char_range.end);
|
||||
assert!(
|
||||
char_range.start <= char_range.end,
|
||||
"Invalid range, start must be less than end, but start = {}, end = {}",
|
||||
char_range.start,
|
||||
char_range.end
|
||||
);
|
||||
let start_byte = byte_index_from_char_index(s, char_range.start);
|
||||
let end_byte = byte_index_from_char_index(s, char_range.end);
|
||||
&s[start_byte..end_byte]
|
||||
}
|
||||
|
||||
/// The thin rectangle of one end of the selection, e.g. the primary cursor.
|
||||
pub fn cursor_rect(galley_pos: Pos2, galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect {
|
||||
let mut cursor_pos = galley
|
||||
.pos_from_cursor(cursor)
|
||||
.translate(galley_pos.to_vec2());
|
||||
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);
|
||||
/// The thin rectangle of one end of the selection, e.g. the primary cursor, in local galley coordinates.
|
||||
pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
|
||||
let mut cursor_pos = galley.pos_from_cursor(*cursor);
|
||||
|
||||
// Handle completely empty galleys
|
||||
cursor_pos = cursor_pos.expand(1.5);
|
||||
// slightly above/below row
|
||||
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);
|
||||
|
||||
cursor_pos = cursor_pos.expand(1.5); // slightly above/below row
|
||||
|
||||
cursor_pos
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::{pos2, vec2, Galley, Painter, Rect, Ui, Visuals};
|
||||
|
||||
use super::CursorRange;
|
||||
use super::CCursorRange;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RowVertexIndices {
|
||||
@@ -14,7 +14,7 @@ pub struct RowVertexIndices {
|
||||
pub fn paint_text_selection(
|
||||
galley: &mut Arc<Galley>,
|
||||
visuals: &Visuals,
|
||||
cursor_range: &CursorRange,
|
||||
cursor_range: &CCursorRange,
|
||||
mut new_vertex_indices: Option<&mut Vec<RowVertexIndices>>,
|
||||
) {
|
||||
if cursor_range.is_empty() {
|
||||
@@ -27,8 +27,8 @@ pub fn paint_text_selection(
|
||||
|
||||
let color = visuals.selection.bg_fill;
|
||||
let [min, max] = cursor_range.sorted_cursors();
|
||||
let min = min.rcursor;
|
||||
let max = max.rcursor;
|
||||
let min = galley.layout_from_cursor(min);
|
||||
let max = galley.layout_from_cursor(max);
|
||||
|
||||
for ri in min.row..=max.row {
|
||||
let row = Arc::make_mut(&mut galley.rows[ri].row);
|
||||
@@ -60,7 +60,11 @@ pub fn paint_text_selection(
|
||||
// Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices):
|
||||
let num_indices_before = mesh.indices.len();
|
||||
mesh.add_colored_rect(rect, color);
|
||||
assert_eq!(num_indices_before + 6, mesh.indices.len());
|
||||
assert_eq!(
|
||||
num_indices_before + 6,
|
||||
mesh.indices.len(),
|
||||
"We expect exactly 6 new indices"
|
||||
);
|
||||
|
||||
// Copy out the new triangles:
|
||||
let selection_triangles = [
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#![warn(missing_docs)] // Let's keep `Ui` well-documented.
|
||||
#![allow(clippy::use_self)]
|
||||
|
||||
use std::{any::Any, hash::Hash, sync::Arc};
|
||||
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::mutex::RwLock;
|
||||
use std::{any::Any, hash::Hash, sync::Arc};
|
||||
|
||||
use crate::close_tag::ClosableTag;
|
||||
use crate::containers::menu;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::Stroke;
|
||||
use crate::{
|
||||
containers::{CollapsingHeader, CollapsingResponse, Frame},
|
||||
ecolor::Hsva,
|
||||
@@ -13,8 +16,6 @@ use crate::{
|
||||
epaint::text::Fonts,
|
||||
grid,
|
||||
layout::{Direction, Layout},
|
||||
menu,
|
||||
menu::MenuState,
|
||||
pass_state,
|
||||
placer::Placer,
|
||||
pos2, style,
|
||||
@@ -26,11 +27,9 @@ use crate::{
|
||||
},
|
||||
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, LayerId,
|
||||
Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense,
|
||||
Style, TextStyle, TextWrapMode, UiBuilder, UiStack, UiStackInfo, Vec2, WidgetRect, WidgetText,
|
||||
Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, WidgetRect,
|
||||
WidgetText,
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::Stroke;
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// This is what you use to place widgets.
|
||||
@@ -99,7 +98,8 @@ pub struct Ui {
|
||||
sizing_pass: bool,
|
||||
|
||||
/// Indicates whether this Ui belongs to a Menu.
|
||||
menu_state: Option<Arc<RwLock<MenuState>>>,
|
||||
#[allow(deprecated)]
|
||||
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
|
||||
|
||||
/// The [`UiStack`] for this [`Ui`].
|
||||
stack: Arc<UiStack>,
|
||||
@@ -286,7 +286,7 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(!max_rect.any_nan());
|
||||
debug_assert!(!max_rect.any_nan(), "max_rect is NaN: {max_rect:?}");
|
||||
let stable_id = self.id.with(id_salt);
|
||||
let unique_id = stable_id.with(self.next_auto_id_salt);
|
||||
let next_auto_id_salt = unique_id.value().wrapping_add(1);
|
||||
@@ -914,14 +914,20 @@ impl Ui {
|
||||
/// Set the minimum width of the ui.
|
||||
/// This can't shrink the ui, only make it larger.
|
||||
pub fn set_min_width(&mut self, width: f32) {
|
||||
debug_assert!(0.0 <= width);
|
||||
debug_assert!(
|
||||
0.0 <= width,
|
||||
"Negative width makes no sense, but got: {width}"
|
||||
);
|
||||
self.placer.set_min_width(width);
|
||||
}
|
||||
|
||||
/// Set the minimum height of the ui.
|
||||
/// This can't shrink the ui, only make it larger.
|
||||
pub fn set_min_height(&mut self, height: f32) {
|
||||
debug_assert!(0.0 <= height);
|
||||
debug_assert!(
|
||||
0.0 <= height,
|
||||
"Negative height makes no sense, but got: {height}"
|
||||
);
|
||||
self.placer.set_min_height(height);
|
||||
}
|
||||
|
||||
@@ -1095,7 +1101,8 @@ impl Ui {
|
||||
// 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.
|
||||
self.ctx()
|
||||
let mut response = self
|
||||
.ctx()
|
||||
.viewport(|viewport| {
|
||||
viewport
|
||||
.prev_pass
|
||||
@@ -1107,7 +1114,11 @@ impl Ui {
|
||||
.map(|widget_rect| self.ctx().get_response(widget_rect))
|
||||
.expect(
|
||||
"Since we always call Context::create_widget in Ui::new, this should never be None",
|
||||
)
|
||||
);
|
||||
if self.should_close() {
|
||||
response.set_close();
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
/// Update the [`WidgetRect`] created in [`Ui::new`] or [`Ui::new_child`] with the current
|
||||
@@ -1121,7 +1132,7 @@ impl Ui {
|
||||
fs.used_ids.remove(&self.unique_id);
|
||||
});
|
||||
// This will update the WidgetRect that was first created in `Ui::new`.
|
||||
self.ctx().create_widget(
|
||||
let mut response = self.ctx().create_widget(
|
||||
WidgetRect {
|
||||
id: self.unique_id,
|
||||
layer_id: self.layer_id(),
|
||||
@@ -1131,7 +1142,11 @@ impl Ui {
|
||||
enabled: self.enabled,
|
||||
},
|
||||
false,
|
||||
)
|
||||
);
|
||||
if self.should_close() {
|
||||
response.set_close();
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
/// Interact with the background of this [`Ui`],
|
||||
@@ -1165,6 +1180,104 @@ impl Ui {
|
||||
pub fn ui_contains_pointer(&self) -> bool {
|
||||
self.rect_contains_pointer(self.min_rect())
|
||||
}
|
||||
|
||||
/// Find and close the first closable parent.
|
||||
///
|
||||
/// Use [`UiBuilder::closable`] to make a [`Ui`] closable.
|
||||
/// You can then use [`Ui::should_close`] to check if it should be closed.
|
||||
///
|
||||
/// This is implemented for all egui containers, e.g. [`crate::Popup`], [`crate::Modal`],
|
||||
/// [`crate::Area`], [`crate::Window`], [`crate::CollapsingHeader`], etc.
|
||||
///
|
||||
/// What exactly happens when you close a container depends on the container implementation.
|
||||
/// [`crate::Area`] e.g. will return true from it's [`Response::should_close`] method.
|
||||
///
|
||||
/// If you want to close a specific kind of container, use [`Ui::close_kind`] instead.
|
||||
///
|
||||
/// Also note that this won't bubble up across [`crate::Area`]s. If needed, you can check
|
||||
/// `response.should_close()` and close the parent manually. ([`menu`] does this for example).
|
||||
///
|
||||
/// See also:
|
||||
/// - [`Ui::close_kind`]
|
||||
/// - [`Ui::should_close`]
|
||||
/// - [`Ui::will_parent_close`]
|
||||
pub fn close(&self) {
|
||||
let tag = self.stack.iter().find_map(|stack| {
|
||||
stack
|
||||
.info
|
||||
.tags
|
||||
.get_downcast::<ClosableTag>(ClosableTag::NAME)
|
||||
});
|
||||
if let Some(tag) = tag {
|
||||
tag.set_close();
|
||||
} else {
|
||||
#[cfg(feature = "log")]
|
||||
log::warn!("Called ui.close() on a Ui that has no closable parent.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Find and close the first closable parent of a specific [`UiKind`].
|
||||
///
|
||||
/// This is useful if you want to e.g. close a [`crate::Window`]. Since it contains a
|
||||
/// `Collapsible`, [`Ui::close`] would close the `Collapsible` instead.
|
||||
/// You can close the [`crate::Window`] by calling `ui.close_kind(UiKind::Window)`.
|
||||
///
|
||||
/// See also:
|
||||
/// - [`Ui::close`]
|
||||
/// - [`Ui::should_close`]
|
||||
/// - [`Ui::will_parent_close`]
|
||||
pub fn close_kind(&self, ui_kind: UiKind) {
|
||||
let tag = self
|
||||
.stack
|
||||
.iter()
|
||||
.filter(|stack| stack.info.kind == Some(ui_kind))
|
||||
.find_map(|stack| {
|
||||
stack
|
||||
.info
|
||||
.tags
|
||||
.get_downcast::<ClosableTag>(ClosableTag::NAME)
|
||||
});
|
||||
if let Some(tag) = tag {
|
||||
tag.set_close();
|
||||
} else {
|
||||
#[cfg(feature = "log")]
|
||||
log::warn!("Called ui.close_kind({ui_kind:?}) on ui with no such closable parent.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Was [`Ui::close`] called on this [`Ui`] or any of its children?
|
||||
/// Only works if the [`Ui`] was created with [`UiBuilder::closable`].
|
||||
///
|
||||
/// You can also check via this [`Ui`]'s [`Response::should_close`].
|
||||
///
|
||||
/// See also:
|
||||
/// - [`Ui::will_parent_close`]
|
||||
/// - [`Ui::close`]
|
||||
/// - [`Ui::close_kind`]
|
||||
/// - [`Response::should_close`]
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.stack
|
||||
.info
|
||||
.tags
|
||||
.get_downcast(ClosableTag::NAME)
|
||||
.is_some_and(|tag: &ClosableTag| tag.should_close())
|
||||
}
|
||||
|
||||
/// Will this [`Ui`] or any of its parents close this frame?
|
||||
///
|
||||
/// See also
|
||||
/// - [`Ui::should_close`]
|
||||
/// - [`Ui::close`]
|
||||
/// - [`Ui::close_kind`]
|
||||
pub fn will_parent_close(&self) -> bool {
|
||||
self.stack.iter().any(|stack| {
|
||||
stack
|
||||
.info
|
||||
.tags
|
||||
.get_downcast::<ClosableTag>(ClosableTag::NAME)
|
||||
.is_some_and(|tag| tag.should_close())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// # Allocating space: where do I put my widgets?
|
||||
@@ -1186,7 +1299,7 @@ impl Ui {
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// let response = ui.allocate_response(egui::vec2(100.0, 200.0), egui::Sense::click());
|
||||
/// if response.clicked() { /* … */ }
|
||||
/// ui.painter().rect_stroke(response.rect, 0.0, (1.0, egui::Color32::WHITE));
|
||||
/// ui.painter().rect_stroke(response.rect, 0.0, (1.0, egui::Color32::WHITE), egui::StrokeKind::Inside);
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response {
|
||||
@@ -1253,8 +1366,12 @@ impl Ui {
|
||||
let debug_expand_height = self.style().debug.show_expand_height;
|
||||
|
||||
if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
|
||||
self.painter
|
||||
.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE));
|
||||
self.painter.rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
(1.0, Color32::LIGHT_BLUE),
|
||||
crate::StrokeKind::Inside,
|
||||
);
|
||||
|
||||
let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
|
||||
let paint_line_seg = |a, b| self.painter().line_segment([a, b], stroke);
|
||||
@@ -1288,7 +1405,7 @@ impl Ui {
|
||||
fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect {
|
||||
let item_spacing = self.spacing().item_spacing;
|
||||
let frame_rect = self.placer.next_space(desired_size, item_spacing);
|
||||
debug_assert!(!frame_rect.any_nan());
|
||||
debug_assert!(!frame_rect.any_nan(), "frame_rect is nan in allocate_space");
|
||||
let widget_rect = self.placer.justify_and_align(frame_rect, desired_size);
|
||||
|
||||
self.placer
|
||||
@@ -1311,7 +1428,7 @@ impl Ui {
|
||||
|
||||
/// Allocate a rect without interacting with it.
|
||||
pub fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id {
|
||||
debug_assert!(!rect.any_nan());
|
||||
debug_assert!(!rect.any_nan(), "rect is nan in advance_cursor_after_rect");
|
||||
let rect = rect.round_ui();
|
||||
|
||||
let item_spacing = self.spacing().item_spacing;
|
||||
@@ -1383,11 +1500,14 @@ impl Ui {
|
||||
layout: Layout,
|
||||
add_contents: Box<dyn FnOnce(&mut Self) -> R + 'c>,
|
||||
) -> InnerResponse<R> {
|
||||
debug_assert!(desired_size.x >= 0.0 && desired_size.y >= 0.0);
|
||||
debug_assert!(
|
||||
desired_size.x >= 0.0 && desired_size.y >= 0.0,
|
||||
"Negative desired size: {desired_size:?}"
|
||||
);
|
||||
let item_spacing = self.spacing().item_spacing;
|
||||
let frame_rect = self.placer.next_space(desired_size, item_spacing);
|
||||
let child_rect = self.placer.justify_and_align(frame_rect, desired_size);
|
||||
self.allocate_new_ui(
|
||||
self.scope_dyn(
|
||||
UiBuilder::new().max_rect(child_rect).layout(layout),
|
||||
add_contents,
|
||||
)
|
||||
@@ -1404,7 +1524,7 @@ impl Ui {
|
||||
max_rect: Rect,
|
||||
add_contents: impl FnOnce(&mut Self) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.allocate_new_ui(UiBuilder::new().max_rect(max_rect), add_contents)
|
||||
self.scope_builder(UiBuilder::new().max_rect(max_rect), add_contents)
|
||||
}
|
||||
|
||||
/// Allocated space (`UiBuilder::max_rect`) and then add content to it.
|
||||
@@ -1412,27 +1532,13 @@ impl Ui {
|
||||
/// 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.allocate_new_ui_dyn(ui_builder, Box::new(add_contents))
|
||||
}
|
||||
|
||||
fn allocate_new_ui_dyn<'c, R>(
|
||||
&mut self,
|
||||
ui_builder: UiBuilder,
|
||||
add_contents: Box<dyn FnOnce(&mut Self) -> R + 'c>,
|
||||
) -> InnerResponse<R> {
|
||||
let mut child_ui = self.new_child(ui_builder);
|
||||
let inner = add_contents(&mut child_ui);
|
||||
let rect = child_ui.min_rect();
|
||||
let item_spacing = self.spacing().item_spacing;
|
||||
self.placer.advance_after_rects(rect, rect, item_spacing);
|
||||
register_rect(self, rect);
|
||||
let response = self.interact(rect, child_ui.unique_id, Sense::hover());
|
||||
InnerResponse::new(inner, response)
|
||||
self.scope_dyn(ui_builder, Box::new(add_contents))
|
||||
}
|
||||
|
||||
/// Convenience function to get a region to paint on.
|
||||
@@ -1638,7 +1744,7 @@ impl Ui {
|
||||
///
|
||||
/// See also [`Self::add`] and [`Self::add_sized`].
|
||||
pub fn put(&mut self, max_rect: Rect, widget: impl Widget) -> Response {
|
||||
self.allocate_new_ui(
|
||||
self.scope_builder(
|
||||
UiBuilder::new()
|
||||
.max_rect(max_rect)
|
||||
.layout(Layout::centered_and_justified(Direction::TopDown)),
|
||||
@@ -1771,12 +1877,14 @@ impl Ui {
|
||||
/// Add extra space before the next widget.
|
||||
///
|
||||
/// The direction is dependent on the layout.
|
||||
/// This will be in addition to the [`crate::style::Spacing::item_spacing`].
|
||||
///
|
||||
/// This will be in addition to the [`crate::style::Spacing::item_spacing`]
|
||||
/// that is always added, but `item_spacing` won't be added _again_ by `add_space`.
|
||||
///
|
||||
/// [`Self::min_rect`] will expand to contain the space.
|
||||
#[inline]
|
||||
pub fn add_space(&mut self, amount: f32) {
|
||||
self.placer.advance_cursor(amount);
|
||||
self.placer.advance_cursor(amount.round_ui());
|
||||
}
|
||||
|
||||
/// Show some text.
|
||||
@@ -2077,7 +2185,7 @@ impl Ui {
|
||||
// only touch `*radians` if we actually changed the degree value
|
||||
if degrees != radians.to_degrees() {
|
||||
*radians = degrees.to_radians();
|
||||
response.changed = true;
|
||||
response.mark_changed();
|
||||
}
|
||||
|
||||
response
|
||||
@@ -2100,7 +2208,7 @@ impl Ui {
|
||||
// only touch `*radians` if we actually changed the value
|
||||
if taus != *radians / TAU {
|
||||
*radians = taus * TAU;
|
||||
response.changed = true;
|
||||
response.mark_changed();
|
||||
}
|
||||
|
||||
response
|
||||
@@ -2121,7 +2229,7 @@ impl Ui {
|
||||
/// ui.add(
|
||||
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
|
||||
/// .max_width(200.0)
|
||||
/// .rounding(10.0),
|
||||
/// .corner_radius(10),
|
||||
/// );
|
||||
/// # });
|
||||
/// ```
|
||||
@@ -2141,18 +2249,21 @@ impl Ui {
|
||||
/// # Colors
|
||||
impl Ui {
|
||||
/// Shows a button with the given color.
|
||||
///
|
||||
/// If the user clicks the button, a full color picker is shown.
|
||||
pub fn color_edit_button_srgba(&mut self, srgba: &mut Color32) -> Response {
|
||||
color_picker::color_edit_button_srgba(self, srgba, color_picker::Alpha::BlendOrAdditive)
|
||||
}
|
||||
|
||||
/// Shows a button with the given color.
|
||||
///
|
||||
/// If the user clicks the button, a full color picker is shown.
|
||||
pub fn color_edit_button_hsva(&mut self, hsva: &mut Hsva) -> Response {
|
||||
color_picker::color_edit_button_hsva(self, hsva, color_picker::Alpha::BlendOrAdditive)
|
||||
}
|
||||
|
||||
/// Shows a button with the given color.
|
||||
///
|
||||
/// If the user clicks the button, a full color picker is shown.
|
||||
/// The given color is in `sRGB` space.
|
||||
pub fn color_edit_button_srgb(&mut self, srgb: &mut [u8; 3]) -> Response {
|
||||
@@ -2160,6 +2271,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
/// Shows a button with the given color.
|
||||
///
|
||||
/// If the user clicks the button, a full color picker is shown.
|
||||
/// The given color is in linear RGB space.
|
||||
pub fn color_edit_button_rgb(&mut self, rgb: &mut [f32; 3]) -> Response {
|
||||
@@ -2167,6 +2279,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
/// Shows a button with the given color.
|
||||
///
|
||||
/// If the user clicks the button, a full color picker is shown.
|
||||
/// The given color is in `sRGBA` space with premultiplied alpha
|
||||
pub fn color_edit_button_srgba_premultiplied(&mut self, srgba: &mut [u8; 4]) -> Response {
|
||||
@@ -2177,6 +2290,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
/// Shows a button with the given color.
|
||||
///
|
||||
/// If the user clicks the button, a full color picker is shown.
|
||||
/// The given color is in `sRGBA` space without premultiplied alpha.
|
||||
/// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use.
|
||||
@@ -2189,6 +2303,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
/// Shows a button with the given color.
|
||||
///
|
||||
/// If the user clicks the button, a full color picker is shown.
|
||||
/// The given color is in linear RGBA space with premultiplied alpha
|
||||
pub fn color_edit_button_rgba_premultiplied(&mut self, rgba_premul: &mut [f32; 4]) -> Response {
|
||||
@@ -2208,6 +2323,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
/// Shows a button with the given color.
|
||||
///
|
||||
/// If the user clicks the button, a full color picker is shown.
|
||||
/// The given color is in linear RGBA space without premultiplied alpha.
|
||||
/// If unsure, what "premultiplied alpha" is, then this is probably the function you want to use.
|
||||
@@ -2527,7 +2643,7 @@ impl Ui {
|
||||
/// See also [`Self::with_layout`] for more options.
|
||||
#[inline]
|
||||
pub fn vertical<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
self.allocate_new_ui(
|
||||
self.scope_builder(
|
||||
UiBuilder::new().layout(Layout::top_down(Align::Min)),
|
||||
add_contents,
|
||||
)
|
||||
@@ -2549,7 +2665,7 @@ impl Ui {
|
||||
&mut self,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.allocate_new_ui(
|
||||
self.scope_builder(
|
||||
UiBuilder::new().layout(Layout::top_down(Align::Center)),
|
||||
add_contents,
|
||||
)
|
||||
@@ -2570,7 +2686,7 @@ impl Ui {
|
||||
&mut self,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.allocate_new_ui(
|
||||
self.scope_builder(
|
||||
UiBuilder::new().layout(Layout::top_down(Align::Center).with_cross_justify(true)),
|
||||
add_contents,
|
||||
)
|
||||
@@ -2596,7 +2712,7 @@ impl Ui {
|
||||
layout: Layout,
|
||||
add_contents: impl FnOnce(&mut Self) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.allocate_new_ui(UiBuilder::new().layout(layout), add_contents)
|
||||
self.scope_builder(UiBuilder::new().layout(layout), add_contents)
|
||||
}
|
||||
|
||||
/// This will make the next added widget centered and justified in the available space.
|
||||
@@ -2606,7 +2722,7 @@ impl Ui {
|
||||
&mut self,
|
||||
add_contents: impl FnOnce(&mut Self) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.allocate_new_ui(
|
||||
self.scope_builder(
|
||||
UiBuilder::new().layout(Layout::centered_and_justified(Direction::TopDown)),
|
||||
add_contents,
|
||||
)
|
||||
@@ -2894,14 +3010,16 @@ impl Ui {
|
||||
/// Close the menu we are in (including submenus), if any.
|
||||
///
|
||||
/// See also: [`Self::menu_button`] and [`Response::context_menu`].
|
||||
pub fn close_menu(&mut self) {
|
||||
if let Some(menu_state) = &mut self.menu_state {
|
||||
menu_state.write().close();
|
||||
}
|
||||
self.menu_state = None;
|
||||
#[deprecated = "Use `ui.close()` or `ui.close_kind(UiKind::Menu)` instead"]
|
||||
pub fn close_menu(&self) {
|
||||
self.close_kind(UiKind::Menu);
|
||||
}
|
||||
|
||||
pub(crate) fn set_menu_state(&mut self, menu_state: Option<Arc<RwLock<MenuState>>>) {
|
||||
#[allow(deprecated)]
|
||||
pub(crate) fn set_menu_state(
|
||||
&mut self,
|
||||
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
|
||||
) {
|
||||
self.menu_state = menu_state;
|
||||
}
|
||||
|
||||
@@ -2915,24 +3033,25 @@ impl Ui {
|
||||
/// ui.menu_button("My menu", |ui| {
|
||||
/// ui.menu_button("My sub-menu", |ui| {
|
||||
/// if ui.button("Close the menu").clicked() {
|
||||
/// ui.close_menu();
|
||||
/// ui.close();
|
||||
/// }
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
||||
/// See also: [`Self::close`] and [`Response::context_menu`].
|
||||
pub fn menu_button<R>(
|
||||
&mut self,
|
||||
title: impl Into<WidgetText>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, title, add_contents)
|
||||
let (response, inner) = if menu::is_in_menu(self) {
|
||||
menu::SubMenuButton::new(title).ui(self, add_contents)
|
||||
} else {
|
||||
menu::menu_button(self, title, add_contents)
|
||||
}
|
||||
menu::MenuButton::new(title).ui(self, add_contents)
|
||||
};
|
||||
InnerResponse::new(inner.map(|i| i.inner), response)
|
||||
}
|
||||
|
||||
/// Create a menu button with an image that when clicked will show the given menu.
|
||||
@@ -2946,7 +3065,7 @@ impl Ui {
|
||||
/// ui.menu_image_button(title, img, |ui| {
|
||||
/// ui.menu_button("My sub-menu", |ui| {
|
||||
/// if ui.button("Close the menu").clicked() {
|
||||
/// ui.close_menu();
|
||||
/// ui.close();
|
||||
/// }
|
||||
/// });
|
||||
/// });
|
||||
@@ -2954,18 +3073,22 @@ impl Ui {
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
||||
/// See also: [`Self::close`] and [`Response::context_menu`].
|
||||
#[inline]
|
||||
pub fn menu_image_button<'a, R>(
|
||||
&mut self,
|
||||
image: impl Into<Image<'a>>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
||||
let (response, inner) = if menu::is_in_menu(self) {
|
||||
menu::SubMenuButton::from_button(
|
||||
Button::image(image).right_text(menu::SubMenuButton::RIGHT_ARROW),
|
||||
)
|
||||
.ui(self, add_contents)
|
||||
} else {
|
||||
menu::menu_custom_button(self, Button::image(image), add_contents)
|
||||
}
|
||||
menu::MenuButton::from_button(Button::image(image)).ui(self, add_contents)
|
||||
};
|
||||
InnerResponse::new(inner.map(|i| i.inner), response)
|
||||
}
|
||||
|
||||
/// Create a menu button with an image and a text that when clicked will show the given menu.
|
||||
@@ -2980,14 +3103,14 @@ impl Ui {
|
||||
/// ui.menu_image_text_button(img, title, |ui| {
|
||||
/// ui.menu_button("My sub-menu", |ui| {
|
||||
/// if ui.button("Close the menu").clicked() {
|
||||
/// ui.close_menu();
|
||||
/// ui.close();
|
||||
/// }
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
||||
/// See also: [`Self::close`] and [`Response::context_menu`].
|
||||
#[inline]
|
||||
pub fn menu_image_text_button<'a, R>(
|
||||
&mut self,
|
||||
@@ -2995,11 +3118,16 @@ impl Ui {
|
||||
title: impl Into<WidgetText>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, title, add_contents)
|
||||
let (response, inner) = if menu::is_in_menu(self) {
|
||||
menu::SubMenuButton::from_button(
|
||||
Button::image_and_text(image, title).right_text(menu::SubMenuButton::RIGHT_ARROW),
|
||||
)
|
||||
.ui(self, add_contents)
|
||||
} else {
|
||||
menu::menu_custom_button(self, Button::image_and_text(image, title), add_contents)
|
||||
}
|
||||
menu::MenuButton::from_button(Button::image_and_text(image, title))
|
||||
.ui(self, add_contents)
|
||||
};
|
||||
InnerResponse::new(inner.map(|i| i.inner), response)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{hash::Hash, sync::Arc};
|
||||
|
||||
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo};
|
||||
|
||||
use crate::close_tag::ClosableTag;
|
||||
#[allow(unused_imports)] // Used for doclinks
|
||||
use crate::Ui;
|
||||
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo};
|
||||
|
||||
/// Build a [`Ui`] as the child of another [`Ui`].
|
||||
///
|
||||
@@ -125,6 +125,7 @@ impl UiBuilder {
|
||||
}
|
||||
|
||||
/// Set if you want sense clicks and/or drags. Default is [`Sense::hover`].
|
||||
///
|
||||
/// The sense will be registered below the Senses of any widgets contained in this [`Ui`], so
|
||||
/// if the user clicks a button contained within this [`Ui`], that button will receive the click
|
||||
/// instead.
|
||||
@@ -135,4 +136,19 @@ impl UiBuilder {
|
||||
self.sense = Some(sense);
|
||||
self
|
||||
}
|
||||
|
||||
/// Make this [`Ui`] closable.
|
||||
///
|
||||
/// Calling [`Ui::close`] in a child [`Ui`] will mark this [`Ui`] for closing.
|
||||
/// After [`Ui::close`] was called, [`Ui::should_close`] and [`crate::Response::should_close`] will
|
||||
/// return `true` (for this frame).
|
||||
///
|
||||
/// This works by adding a [`ClosableTag`] to the [`UiStackInfo`].
|
||||
#[inline]
|
||||
pub fn closable(mut self) -> Self {
|
||||
self.ui_stack_info
|
||||
.tags
|
||||
.insert(ClosableTag::NAME, Some(Arc::new(ClosableTag::default())));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ pub enum UiKind {
|
||||
|
||||
/// An [`crate::Area`] that is not of any other kind.
|
||||
GenericArea,
|
||||
|
||||
/// A collapsible container, e.g. a [`crate::CollapsingHeader`].
|
||||
Collapsible,
|
||||
}
|
||||
|
||||
impl UiKind {
|
||||
@@ -81,6 +84,7 @@ impl UiKind {
|
||||
| Self::Frame
|
||||
| Self::ScrollArea
|
||||
| Self::Resize
|
||||
| Self::Collapsible
|
||||
| Self::TableCell => false,
|
||||
|
||||
Self::Window
|
||||
@@ -229,13 +233,13 @@ impl UiStack {
|
||||
/// Is this [`crate::Ui`] a panel?
|
||||
#[inline]
|
||||
pub fn is_panel_ui(&self) -> bool {
|
||||
self.kind().map_or(false, |kind| kind.is_panel())
|
||||
self.kind().is_some_and(|kind| kind.is_panel())
|
||||
}
|
||||
|
||||
/// Is this [`crate::Ui`] an [`crate::Area`]?
|
||||
#[inline]
|
||||
pub fn is_area_ui(&self) -> bool {
|
||||
self.kind().map_or(false, |kind| kind.is_area())
|
||||
self.kind().is_some_and(|kind| kind.is_area())
|
||||
}
|
||||
|
||||
/// Is this a root [`crate::Ui`], i.e. created with [`crate::Ui::new()`]?
|
||||
@@ -285,4 +289,4 @@ impl<'a> Iterator for UiStackIterator<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FusedIterator for UiStackIterator<'a> {}
|
||||
impl FusedIterator for UiStackIterator<'_> {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user