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

Merge branch 'master' into cache_galley_lines

This commit is contained in:
Hubert Głuchowski
2024-12-19 23:21:09 +01:00
committed by GitHub
138 changed files with 2386 additions and 1394 deletions

5
.gitattributes vendored
View File

@@ -1,5 +1,8 @@
* text=auto eol=lf
Cargo.lock linguist-generated=false
*.png filter=lfs diff=lfs merge=lfs -text
# The icon.png is needed when including eframe via git, so it may not be in lfs
# Exclude some small files from LFS:
crates/eframe/data/* !filter !diff !merge text=auto eol=lf
crates/egui_demo_lib/data/* !filter !diff !merge text=auto eol=lf
crates/egui/assets/* !filter !diff !merge text=auto eol=lf

View File

@@ -39,7 +39,7 @@ jobs:
with:
profile: minimal
target: wasm32-unknown-unknown
toolchain: 1.79.0
toolchain: 1.80.0
override: true
- uses: Swatinem/rust-cache@v2

View File

@@ -13,11 +13,18 @@ jobs:
- name: Check that png files are on git LFS
run: |
binary_extensions="png"
exclude="crates/eframe/data"
exclude_paths=(
"crates/eframe/data"
"crates/egui_demo_lib/data/"
"crates/egui/assets/"
)
# Find binary files that are not tracked by Git LFS
for ext in $binary_extensions; do
if comm -23 <(git ls-files | grep -v "^$exclude" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then
# Create grep pattern to exclude multiple paths
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"
exit 1
fi

View File

@@ -42,9 +42,11 @@ jobs:
- name: Generate meta.json
env:
PR_NUMBER: ${{ github.event.number }}
PR_BRANCH: ${{ github.head_ref }}
URL_SLUG: ${{ github.event.number }}-${{ github.head_ref }}
run: |
echo "{\"pr_number\": \"$PR_NUMBER\", \"pr_branch\": \"$PR_BRANCH\"}" > meta.json
# Sanitize the URL_SLUG to only contain alphanumeric characters and dashes
URL_SLUG=$(echo $URL_SLUG | tr -cd '[:alnum:]-')
echo "{\"pr_number\": \"$PR_NUMBER\", \"url_slug\": \"$URL_SLUG\"}" > meta.json
- uses: actions/upload-artifact@v4
with:

View File

@@ -15,9 +15,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- run: mkdir -p empty_dir
- name: Url slug variable
- name: Generate URL_SLUG
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
URL_SLUG: ${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }}
run: |
echo "URL_SLUG=${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV
# Sanitize the URL_SLUG to only contain alphanumeric characters and dashes
URL_SLUG=$(echo $URL_SLUG | tr -cd '[:alnum:]-')
echo "URL_SLUG=$URL_SLUG" >> $GITHUB_ENV
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
with:

View File

@@ -40,11 +40,7 @@ jobs:
- name: Parse meta.json
run: |
echo "PR_NUMBER=$(jq -r .pr_number meta.json)" >> $GITHUB_ENV
echo "PR_BRANCH=$(jq -r .pr_branch meta.json)" >> $GITHUB_ENV
- name: Url slug variable
run: |
echo "URL_SLUG=${{ env.PR_NUMBER }}-${{ env.PR_BRANCH }}" >> $GITHUB_ENV
echo "URL_SLUG=$(jq -r .url_slug meta.json)" >> $GITHUB_ENV
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4

View File

@@ -18,7 +18,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.79.0
toolchain: 1.80.0
- name: Install packages (Linux)
if: runner.os == 'Linux'
@@ -83,7 +83,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.79.0
toolchain: 1.80.0
targets: wasm32-unknown-unknown
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
with:
rust-version: "1.79.0"
rust-version: "1.80.0"
log-level: error
command: check
arguments: --target ${{ matrix.target }}
@@ -170,7 +170,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.79.0
toolchain: 1.80.0
targets: aarch64-linux-android
- name: Set up cargo cache
@@ -189,7 +189,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.79.0
toolchain: 1.80.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.79.0
toolchain: 1.80.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
@@ -232,7 +232,7 @@ jobs:
lfs: true
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.79.0
toolchain: 1.80.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2

View File

@@ -1,12 +1,107 @@
# egui changelog
All notable changes to the `egui` crate will be documented in this file.
NOTE: this is just the changelog for the core `egui` crate. [`eframe`](crates/eframe/CHANGELOG.md), [`ecolor`](crates/ecolor/CHANGELOG.md), [`epaint`](crates/epaint/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs!
This is just the changelog for the core `egui` crate. Every crate in this repository has their own changelog:
* [`epaint` changelog](crates/epaint/CHANGELOG.md)
* [`egui-winit` changelog](crates/egui-winit/CHANGELOG.md)
* [`egui-wgpu` changelog](crates/egui-wgpu/CHANGELOG.md)
* [`egui_kittest` changelog](crates/egui_kittest/CHANGELOG.md)
* [`egui_glow` changelog](crates/egui_glow/CHANGELOG.md)
* [`ecolor` changelog](crates/ecolor/CHANGELOG.md)
* [`eframe` changelog](crates/eframe/CHANGELOG.md)
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.30.0 - 2024-12-16 - Modals and better layer support
### ✨ Highlights
* Add `Modal`, a popup that blocks input to the rest of the application ([#5358](https://github.com/emilk/egui/pull/5358) by [@lucasmerlin](https://github.com/lucasmerlin))
* Improved support for transform layers ([#5465](https://github.com/emilk/egui/pull/5465), [#5468](https://github.com/emilk/egui/pull/5468), [#5429](https://github.com/emilk/egui/pull/5429))
#### `egui_kittest`
This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/master/crates/egui_kittest).
`egui_kittest` is a testing framework for egui, allowing you to test both automation (simulated clicks and other events),
and also do screenshot testing (useful for regression tests).
`egui_kittest` is built using [`kittest`](https://github.com/rerun-io/kittest), which is a general GUI testing framework that aims to work with any Rust GUI (not just egui!).
`kittest` uses the accessibility library [`AccessKit`](https://github.com/AccessKit/accesskit/) for automatation and to query the widget tree.
`kittest` and `egui_kittest` are written by [@lucasmerlin](https://github.com/lucasmerlin).
Here's a quick example of how to use `egui_kittest` to test a checkbox:
```rust
use egui::accesskit::Toggled;
use egui_kittest::{Harness, kittest::Queryable};
fn main() {
let mut checked = false;
let app = |ui: &mut egui::Ui| {
ui.checkbox(&mut checked, "Check me!");
};
let mut harness = egui_kittest::Harness::new_ui(app);
let checkbox = harness.get_by_label("Check me!");
assert_eq!(checkbox.toggled(), Some(Toggled::False));
checkbox.click();
harness.run();
let checkbox = harness.get_by_label("Check me!");
assert_eq!(checkbox.toggled(), Some(Toggled::True));
// You can even render the ui and do image snapshot tests
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
harness.wgpu_snapshot("readme_example");
}
```
### ⭐ Added
* Add `Modal` and `Memory::set_modal_layer` [#5358](https://github.com/emilk/egui/pull/5358) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add `UiBuilder::layer_id` and remove `layer_id` from `Ui::new` [#5195](https://github.com/emilk/egui/pull/5195) by [@emilk](https://github.com/emilk)
* Allow easier setting of background color for `TextEdit` [#5203](https://github.com/emilk/egui/pull/5203) by [@bircni](https://github.com/bircni)
* Set `Response::intrinsic_size` for `TextEdit` [#5266](https://github.com/emilk/egui/pull/5266) by [@lucasmerlin](https://github.com/lucasmerlin)
* Expose center position in `MultiTouchInfo` [#5247](https://github.com/emilk/egui/pull/5247) by [@lucasmerlin](https://github.com/lucasmerlin)
* `Context::add_font` [#5228](https://github.com/emilk/egui/pull/5228) by [@frederik-uni](https://github.com/frederik-uni)
* Impl from `Box<str>` for `WidgetText`, `RichText` [#5309](https://github.com/emilk/egui/pull/5309) by [@dimtpap](https://github.com/dimtpap)
* Add `Window::scroll_bar_visibility` [#5231](https://github.com/emilk/egui/pull/5231) by [@Zeenobit](https://github.com/Zeenobit)
* Add `ComboBox::close_behavior` [#5305](https://github.com/emilk/egui/pull/5305) by [@avalsch](https://github.com/avalsch)
* Add `painter.line()` [#5291](https://github.com/emilk/egui/pull/5291) by [@bircni](https://github.com/bircni)
* Allow attaching custom user data to a screenshot command [#5416](https://github.com/emilk/egui/pull/5416) by [@emilk](https://github.com/emilk)
* Add `Button::image_tint_follows_text_color` [#5430](https://github.com/emilk/egui/pull/5430) by [@emilk](https://github.com/emilk)
* Consume escape keystroke when bailing out from a drag operation [#5433](https://github.com/emilk/egui/pull/5433) by [@abey79](https://github.com/abey79)
* Add `Context::layer_transform_to_global` & `layer_transform_from_global` [#5465](https://github.com/emilk/egui/pull/5465) by [@emilk](https://github.com/emilk)
### 🔧 Changed
* Update MSRV to Rust 1.80 [#5421](https://github.com/emilk/egui/pull/5421), [#5457](https://github.com/emilk/egui/pull/5457) by [@emilk](https://github.com/emilk)
* Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic)
* Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ)
* Move `egui::util::cache` to `egui::cache`; add `FramePublisher` [#5426](https://github.com/emilk/egui/pull/5426) by [@emilk](https://github.com/emilk)
* Remove `Order::PanelResizeLine` [#5455](https://github.com/emilk/egui/pull/5455) by [@emilk](https://github.com/emilk)
* Drag-and-drop: keep cursor set by user, if any [#5467](https://github.com/emilk/egui/pull/5467) by [@abey79](https://github.com/abey79)
* Use `profiling` crate to support more profiler backends [#5150](https://github.com/emilk/egui/pull/5150) by [@teddemunnik](https://github.com/teddemunnik)
* Improve hit-test of thin widgets, and widgets across layers [#5468](https://github.com/emilk/egui/pull/5468) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Update `ScrollArea` drag velocity when drag stopped [#5175](https://github.com/emilk/egui/pull/5175) by [@valadaptive](https://github.com/valadaptive)
* Fix bug causing wrong-fire of `ViewportCommand::Visible` [#5244](https://github.com/emilk/egui/pull/5244) by [@rustbasic](https://github.com/rustbasic)
* Fix: `Ui::new_child` does not consider the `sizing_pass` field of `UiBuilder` [#5262](https://github.com/emilk/egui/pull/5262) by [@zhatuokun](https://github.com/zhatuokun)
* Fix Ctrl+Shift+Z redo shortcut [#5258](https://github.com/emilk/egui/pull/5258) by [@YgorSouza](https://github.com/YgorSouza)
* Fix: `Window::default_pos` does not work [#5315](https://github.com/emilk/egui/pull/5315) by [@rustbasic](https://github.com/rustbasic)
* Fix: `Sides` did not apply the layout position correctly [#5303](https://github.com/emilk/egui/pull/5303) by [@zhatuokun](https://github.com/zhatuokun)
* Respect `Style::override_font_id` in `RichText` [#5310](https://github.com/emilk/egui/pull/5310) by [@MStarha](https://github.com/MStarha)
* Fix disabled widgets "eating" focus [#5370](https://github.com/emilk/egui/pull/5370) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix cursor clipping in `TextEdit` inside a `ScrollArea` [#3660](https://github.com/emilk/egui/pull/3660) by [@juancampa](https://github.com/juancampa)
* Make text cursor always appear on click [#5420](https://github.com/emilk/egui/pull/5420) by [@juancampa](https://github.com/juancampa)
* Fix `on_hover_text_at_pointer` for transformed layers [#5429](https://github.com/emilk/egui/pull/5429) by [@emilk](https://github.com/emilk)
* Fix: don't interact with `Area` outside its `constrain_rect` [#5459](https://github.com/emilk/egui/pull/5459) by [@MScottMcBee](https://github.com/MScottMcBee)
* Fix broken images on egui.rs (move from git lfs to normal git) [#5480](https://github.com/emilk/egui/pull/5480) by [@emilk](https://github.com/emilk)
* Fix: `ui.new_child` should now respect `disabled` [#5483](https://github.com/emilk/egui/pull/5483) by [@emilk](https://github.com/emilk)
* Fix zero-width strokes still affecting the feathering color of boxes [#5485](https://github.com/emilk/egui/pull/5485) by [@emilk](https://github.com/emilk)
## 0.29.1 - 2024-10-01 - Bug fixes
* Remove debug-assert triggered by `with_layer_id/dnd_drag_source` [#5191](https://github.com/emilk/egui/pull/5191) by [@emilk](https://github.com/emilk)
* Fix id clash in `Ui::response` [#5192](https://github.com/emilk/egui/pull/5192) by [@emilk](https://github.com/emilk)

File diff suppressed because it is too large Load Diff

View File

@@ -23,8 +23,8 @@ members = [
[workspace.package]
edition = "2021"
license = "MIT OR Apache-2.0"
rust-version = "1.79"
version = "0.29.1"
rust-version = "1.80"
version = "0.30.0"
[profile.release]
@@ -55,18 +55,18 @@ opt-level = 2
[workspace.dependencies]
emath = { version = "0.29.1", path = "crates/emath", default-features = false }
ecolor = { version = "0.29.1", path = "crates/ecolor", default-features = false }
epaint = { version = "0.29.1", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.29.1", path = "crates/epaint_default_fonts" }
egui = { version = "0.29.1", path = "crates/egui", default-features = false }
egui-winit = { version = "0.29.1", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.29.1", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.29.1", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.29.1", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.29.1", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.29.1", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.29.1", path = "crates/eframe", default-features = false }
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 }
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
@@ -82,11 +82,12 @@ 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 = { git = "https://github.com/rerun-io/kittest", version = "0.1", branch = "main" }
kittest = { version = "0.1" }
log = { version = "0.4", features = ["std"] }
nohash-hasher = "0.2"
parking_lot = "0.12"
pollster = "0.4"
profiling = { version = "1.0.16", default-features = false }
puffin = "0.19"
puffin_http = "0.16"
raw-window-handle = "0.6.0"
@@ -106,13 +107,13 @@ winit = { version = "0.30.5", default-features = false }
unsafe_code = "deny"
elided_lifetimes_in_paths = "warn"
future_incompatible = "warn"
nonstandard_style = "warn"
rust_2018_idioms = "warn"
future_incompatible = { level = "warn", priority = -1 }
nonstandard_style = { level = "warn", priority = -1 }
rust_2018_idioms = { level = "warn", priority = -1 }
rust_2021_prelude_collisions = "warn"
semicolon_in_expressions_from_macros = "warn"
trivial_numeric_casts = "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
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"
unused_lifetimes = "warn"
@@ -233,6 +234,7 @@ ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn"
same_functions_in_if_condition = "warn"
semicolon_if_nothing_returned = "warn"
single_char_pattern = "warn"
single_match_else = "warn"
str_split_at_newline = "warn"
str_to_string = "warn"

View File

@@ -53,8 +53,7 @@ We don't update the MSRV in a patch release, unless we really, really need to.
* [ ] 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)
* [ ] update changelogs using `scripts/generate_changelog.py --write`
- For major releases, always diff to the latest MAJOR release, e.g. `--commit-range 0.29.0..HEAD`
* [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write`
* [ ] bump version numbers in workspace `Cargo.toml`
## Actual release
@@ -79,8 +78,9 @@ I usually do this all on the `master` branch, but doing it in a release branch i
(cd crates/epaint && cargo publish --quiet) && echo "✅ epaint"
(cd crates/egui && cargo publish --quiet) && echo "✅ egui"
(cd crates/egui-winit && cargo publish --quiet) && echo "✅ egui-winit"
(cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras"
(cd crates/egui-wgpu && cargo publish --quiet) && echo "✅ egui-wgpu"
(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"

View File

@@ -3,7 +3,7 @@
# -----------------------------------------------------------------------------
# Section identical to scripts/clippy_wasm/clippy.toml:
msrv = "1.79"
msrv = "1.80"
allow-unwrap-in-tests = true

View File

@@ -6,6 +6,11 @@ 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.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)
## 0.29.1 - 2024-10-01
Nothing new

View File

@@ -7,6 +7,25 @@ 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.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.
### ⭐ Added
* Support `ViewportCommand::Screenshot` on web [#5438](https://github.com/emilk/egui/pull/5438) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🔧 Changed
* Android support [#5318](https://github.com/emilk/egui/pull/5318) by [@parasyte](https://github.com/parasyte)
* Update MSRV to 1.80 [#5457](https://github.com/emilk/egui/pull/5457) by [@emilk](https://github.com/emilk)
* Use `profiling` crate to support more profiler backends [#5150](https://github.com/emilk/egui/pull/5150) by [@teddemunnik](https://github.com/teddemunnik)
* Update glow to 0.16 [#5395](https://github.com/emilk/egui/pull/5395) by [@sagudev](https://github.com/sagudev)
* Forward `x11` and `wayland` features to `glutin` [#5391](https://github.com/emilk/egui/pull/5391) by [@e00E](https://github.com/e00E)
### 🐛 Fixed
* 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)
* Prevent panic when copying text outside of a secure context [#5326](https://github.com/emilk/egui/pull/5326) by [@YgorSouza](https://github.com/YgorSouza)
* Fix accidental change of `FallbackEgl` to `PreferEgl` [#5408](https://github.com/emilk/egui/pull/5408) by [@e00E](https://github.com/e00E)
## 0.29.1 - 2024-10-01 - Fix backspace/arrow keys on X11
* Linux: Disable IME to fix backspace/arrow keys [#5188](https://github.com/emilk/egui/pull/5188) by [@emilk](https://github.com/emilk)

View File

@@ -35,7 +35,7 @@ default = [
"accesskit",
"default_fonts",
"glow",
"wayland",
"wayland", # Required for Linux support (including CI!)
"web_screen_reader",
"winit/default",
"x11",
@@ -71,21 +71,16 @@ persistence = [
"serde",
]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
puffin = [
"dep:puffin",
"egui/puffin",
"egui_glow?/puffin",
"egui-wgpu?/puffin",
"egui-winit/puffin",
]
## Enables wayland support and fixes clipboard issue.
wayland = ["egui-winit/wayland", "egui-wgpu?/wayland", "egui_glow?/wayland", "glutin?/wayland", "glutin-winit?/wayland"]
##
## If you are compiling for Linux (or want to test on a CI system using Linux), you should enable this feature.
wayland = [
"egui-winit/wayland",
"egui-wgpu?/wayland",
"egui_glow?/wayland",
"glutin?/wayland",
"glutin-winit?/wayland",
]
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
##
@@ -111,7 +106,15 @@ web_screen_reader = [
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
## Enables compiling for x11.
x11 = ["egui-winit/x11", "egui-wgpu?/x11", "egui_glow?/x11", "glutin?/x11", "glutin?/glx", "glutin-winit?/x11", "glutin-winit?/glx"]
x11 = [
"egui-winit/x11",
"egui-wgpu?/x11",
"egui_glow?/x11",
"glutin?/x11",
"glutin?/glx",
"glutin-winit?/x11",
"glutin-winit?/glx",
]
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
## This is used to generate images for examples.
@@ -127,6 +130,7 @@ ahash.workspace = true
document-features.workspace = true
log.workspace = true
parking_lot.workspace = true
profiling.workspace = true
raw-window-handle.workspace = true
static_assertions = "1.1.0"
web-time.workspace = true
@@ -154,10 +158,15 @@ egui-wgpu = { workspace = true, optional = true, features = [
] } # if wgpu is used, use it with winit
pollster = { workspace = true, optional = true } # needed for wgpu
glutin = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] }
glutin-winit = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] }
glutin = { workspace = true, optional = true, default-features = false, features = [
"egl",
"wgl",
] }
glutin-winit = { workspace = true, optional = true, default-features = false, features = [
"egl",
"wgl",
] }
home = { workspace = true, optional = true }
puffin = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true, features = [
# Let's enable some backends so that users can use `eframe` out-of-the-box
# without having to explicitly opt-in to backends

View File

@@ -364,6 +364,16 @@ pub struct NativeOptions {
///
/// Defaults to true.
pub dithering: bool,
/// Android application for `winit`'s event loop.
///
/// This value is required on Android to correctly create the event loop. See
/// [`EventLoopBuilder::build`] and [`with_android_app`] for details.
///
/// [`EventLoopBuilder::build`]: winit::event_loop::EventLoopBuilder::build
/// [`with_android_app`]: winit::platform::android::EventLoopBuilderExtAndroid::with_android_app
#[cfg(target_os = "android")]
pub android_app: Option<winit::platform::android::activity::AndroidApp>,
}
#[cfg(not(target_arch = "wasm32"))]
@@ -383,6 +393,9 @@ impl Clone for NativeOptions {
persistence_path: self.persistence_path.clone(),
#[cfg(target_os = "android")]
android_app: self.android_app.clone(),
..*self
}
}
@@ -424,6 +437,9 @@ impl Default for NativeOptions {
persistence_path: None,
dithering: true,
#[cfg(target_os = "android")]
android_app: None,
}
}
}
@@ -772,8 +788,7 @@ pub struct IntegrationInfo {
///
/// This includes [`App::update`] as well as rendering (except for vsync waiting).
///
/// For a more detailed view of cpu usage, use the [`puffin`](https://crates.io/crates/puffin)
/// profiler together with the `puffin` feature of `eframe`.
/// For a more detailed view of cpu usage, connect your preferred profiler by enabling it's feature in [`profiling`](https://crates.io/crates/profiling).
///
/// `None` if this is the first frame.
pub cpu_usage: Option<f32>,
@@ -815,7 +830,7 @@ impl Storage for DummyStorage {
/// 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> {
crate::profile_function!(key);
profiling::function_scope!(key);
storage
.get_string(key)
.and_then(|value| match ron::from_str(&value) {
@@ -831,7 +846,7 @@ pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &st
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
#[cfg(feature = "ron")]
pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) {
crate::profile_function!(key);
profiling::function_scope!(key);
match ron::ser::to_string(value) {
Ok(string) => storage.set_string(key, string),
Err(err) => log::error!("eframe failed to encode data using ron: {}", err),

View File

@@ -22,7 +22,7 @@ pub trait IconDataExt {
/// # Errors
/// If this is not a valid png.
pub fn from_png_bytes(png_bytes: &[u8]) -> Result<IconData, image::ImageError> {
crate::profile_function!();
profiling::function_scope!();
let image = image::load_from_memory(png_bytes)?;
Ok(from_image(image))
}
@@ -38,7 +38,7 @@ fn from_image(image: image::DynamicImage) -> IconData {
impl IconDataExt for IconData {
fn to_image(&self) -> Result<image::RgbaImage, String> {
crate::profile_function!();
profiling::function_scope!();
let Self {
rgba,
width,
@@ -48,7 +48,7 @@ impl IconDataExt for IconData {
}
fn to_png_bytes(&self) -> Result<Vec<u8>, String> {
crate::profile_function!();
profiling::function_scope!();
let image = self.to_image()?;
let mut png_bytes: Vec<u8> = Vec::new();
image

View File

@@ -129,6 +129,17 @@
//! ## Feature flags
#![doc = document_features::document_features!()]
//!
//! ## Instrumentation
//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation.
//! You can enable features on the profiling crates in your application to add instrumentation for all
//! crates that support it, including egui. See the profiling crate docs for more information.
//! ```toml
//! [dependencies]
//! profiling = "1.0"
//! [features]
//! profile-with-puffin = ["profiling/profile-with-puffin"]
//! ```
//!
#![warn(missing_docs)] // let's keep eframe well-documented
#![allow(clippy::needless_doctest_main)]
@@ -445,33 +456,3 @@ impl std::fmt::Display for Error {
/// Short for `Result<T, eframe::Error>`.
pub type Result<T = (), E = Error> = std::result::Result<T, E>;
// ---------------------------------------------------------------------------
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[allow(unused_imports)]
pub(crate) use profiling_scopes::{profile_function, profile_scope};

View File

@@ -59,7 +59,7 @@ enum AppIconStatus {
/// Since window creation can be lazy, call this every frame until it's either successfully or gave up.
/// (See [`AppIconStatus`])
fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconStatus {
crate::profile_function!();
profiling::function_scope!();
#[cfg(target_os = "windows")]
{
@@ -201,7 +201,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
#[allow(unsafe_code)]
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
crate::profile_function!();
profiling::function_scope!();
use objc2::ClassType;
use objc2_app_kit::{NSApplication, NSImage};
@@ -237,7 +237,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
log::trace!("NSImage::initWithData…");
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
crate::profile_scope!("setApplicationIconImage_");
profiling::scope!("setApplicationIconImage_");
log::trace!("setApplicationIconImage…");
app.setApplicationIconImage(app_icon.as_deref());
}
@@ -246,7 +246,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
if let Some(main_menu) = app.mainMenu() {
if let Some(item) = main_menu.itemAtIndex(0) {
if let Some(app_menu) = item.submenu() {
crate::profile_scope!("setTitle_");
profiling::scope!("setTitle_");
app_menu.setTitle(&NSString::from_str(title));
}
}

View File

@@ -19,7 +19,7 @@ pub fn viewport_builder(
native_options: &mut epi::NativeOptions,
window_settings: Option<WindowSettings>,
) -> ViewportBuilder {
crate::profile_function!();
profiling::function_scope!();
let mut viewport_builder = native_options.viewport.clone();
@@ -67,7 +67,7 @@ pub fn viewport_builder(
#[cfg(not(target_os = "ios"))]
if native_options.centered {
crate::profile_scope!("center");
profiling::scope!("center");
if let Some(monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
@@ -94,8 +94,7 @@ pub fn apply_window_settings(
window: &winit::window::Window,
window_settings: Option<WindowSettings>,
) {
crate::profile_function!();
profiling::function_scope!();
if let Some(window_settings) = window_settings {
window_settings.initialize_window(window);
}
@@ -103,12 +102,11 @@ pub fn apply_window_settings(
#[cfg(not(target_os = "ios"))]
fn largest_monitor_point_size(egui_zoom_factor: f32, event_loop: &ActiveEventLoop) -> egui::Vec2 {
crate::profile_function!();
profiling::function_scope!();
let mut max_size = egui::Vec2::ZERO;
let available_monitors = {
crate::profile_scope!("available_monitors");
profiling::scope!("available_monitors");
event_loop.available_monitors()
};
@@ -238,7 +236,7 @@ impl EpiIntegration {
egui_winit: &mut egui_winit::State,
event: &winit::event::WindowEvent,
) -> EventResponse {
crate::profile_function!(egui_winit::short_window_event_description(event));
profiling::function_scope!(egui_winit::short_window_event_description(event));
use winit::event::{ElementState, MouseButton, WindowEvent};
@@ -276,10 +274,10 @@ impl EpiIntegration {
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport
crate::profile_scope!("viewport_callback");
profiling::scope!("viewport_callback");
viewport_ui_cb(egui_ctx);
} else {
crate::profile_scope!("App::update");
profiling::scope!("App::update");
app.update(egui_ctx, &mut self.frame);
}
});
@@ -306,7 +304,7 @@ impl EpiIntegration {
}
pub fn post_rendering(&mut self, window: &winit::window::Window) {
crate::profile_function!();
profiling::function_scope!();
if std::mem::take(&mut self.is_first_frame) {
// We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
window.set_visible(true);
@@ -332,11 +330,11 @@ impl EpiIntegration {
pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
#[cfg(feature = "persistence")]
if let Some(storage) = self.frame.storage_mut() {
crate::profile_function!();
profiling::function_scope!();
if let Some(window) = _window {
if self.persist_window {
crate::profile_scope!("native_window");
profiling::scope!("native_window");
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
@@ -345,23 +343,23 @@ impl EpiIntegration {
}
}
if _app.persist_egui_memory() {
crate::profile_scope!("egui_memory");
profiling::scope!("egui_memory");
self.egui_ctx
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
}
{
crate::profile_scope!("App::save");
profiling::scope!("App::save");
_app.save(storage);
}
crate::profile_scope!("Storage::flush");
profiling::scope!("Storage::flush");
storage.flush();
}
}
}
fn load_default_egui_icon() -> egui::IconData {
crate::profile_function!();
profiling::function_scope!();
crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap()
}
@@ -372,7 +370,7 @@ const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
const STORAGE_WINDOW_KEY: &str = "window";
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
crate::profile_function!();
profiling::function_scope!();
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
@@ -382,7 +380,7 @@ pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<Windo
}
pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
crate::profile_function!();
profiling::function_scope!();
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)

View File

@@ -100,7 +100,7 @@ pub struct FileStorage {
impl Drop for FileStorage {
fn drop(&mut self) {
if let Some(join_handle) = self.last_save_join_handle.take() {
crate::profile_scope!("wait_for_save");
profiling::scope!("wait_for_save");
join_handle.join().ok();
}
}
@@ -109,7 +109,7 @@ impl Drop for FileStorage {
impl FileStorage {
/// Store the state in this .ron file.
pub(crate) fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
crate::profile_function!();
profiling::function_scope!();
let ron_filepath: PathBuf = ron_filepath.into();
log::debug!("Loading app state from {:?}…", ron_filepath);
Self {
@@ -122,7 +122,7 @@ impl FileStorage {
/// Find a good place to put the files that the OS likes.
pub fn from_app_id(app_id: &str) -> Option<Self> {
crate::profile_function!(app_id);
profiling::function_scope!();
if let Some(data_dir) = storage_dir(app_id) {
if let Err(err) = std::fs::create_dir_all(&data_dir) {
log::warn!(
@@ -155,7 +155,7 @@ impl crate::Storage for FileStorage {
fn flush(&mut self) {
if self.dirty {
crate::profile_function!();
profiling::scope!("FileStorage::flush");
self.dirty = false;
let file_path = self.ron_filepath.clone();
@@ -184,7 +184,7 @@ impl crate::Storage for FileStorage {
}
fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
crate::profile_function!();
profiling::function_scope!();
if let Some(parent_dir) = file_path.parent() {
if !parent_dir.exists() {
@@ -199,7 +199,7 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
let mut writer = std::io::BufWriter::new(file);
let config = Default::default();
crate::profile_scope!("ron::serialize");
profiling::scope!("ron::serialize");
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
{
@@ -220,7 +220,7 @@ fn read_ron<T>(ron_path: impl AsRef<Path>) -> Option<T>
where
T: serde::de::DeserializeOwned,
{
crate::profile_function!();
profiling::function_scope!();
match std::fs::File::open(ron_path) {
Ok(file) => {
let reader = std::io::BufReader::new(file);

View File

@@ -129,7 +129,7 @@ impl<'app> GlowWinitApp<'app> {
native_options: NativeOptions,
app_creator: AppCreator<'app>,
) -> Self {
crate::profile_function!();
profiling::function_scope!();
Self {
repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())),
app_name: app_name.to_owned(),
@@ -146,8 +146,7 @@ impl<'app> GlowWinitApp<'app> {
storage: Option<&dyn Storage>,
native_options: &mut NativeOptions,
) -> Result<(GlutinWindowContext, egui_glow::Painter)> {
crate::profile_function!();
profiling::function_scope!();
let window_settings = epi_integration::load_window_settings(storage);
let winit_window_builder = epi_integration::viewport_builder(
@@ -172,7 +171,7 @@ impl<'app> GlowWinitApp<'app> {
}
let gl = unsafe {
crate::profile_scope!("glow::Context::from_loader_function");
profiling::scope!("glow::Context::from_loader_function");
Arc::new(glow::Context::from_loader_function(|s| {
let s = std::ffi::CString::new(s)
.expect("failed to construct C string from string for gl proc address");
@@ -195,7 +194,7 @@ impl<'app> GlowWinitApp<'app> {
&mut self,
event_loop: &ActiveEventLoop,
) -> Result<&mut GlowWinitRunning<'app>> {
crate::profile_function!();
profiling::function_scope!();
let storage = if let Some(file) = &self.native_options.persistence_path {
epi_integration::create_storage_with_file(file)
@@ -308,7 +307,7 @@ impl<'app> GlowWinitApp<'app> {
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
};
crate::profile_scope!("app_creator");
profiling::scope!("app_creator");
app_creator(&cc).map_err(crate::Error::AppCreation)?
};
@@ -369,7 +368,7 @@ impl<'app> WinitApp for GlowWinitApp<'app> {
fn save_and_destroy(&mut self) {
if let Some(mut running) = self.running.take() {
crate::profile_function!();
profiling::function_scope!();
running.integration.save(
running.app.as_mut(),
@@ -486,7 +485,7 @@ impl<'app> GlowWinitRunning<'app> {
event_loop: &ActiveEventLoop,
window_id: WindowId,
) -> Result<EventResult> {
crate::profile_function!();
profiling::function_scope!();
let Some(viewport_id) = self
.glutin
@@ -498,8 +497,7 @@ impl<'app> GlowWinitRunning<'app> {
return Ok(EventResult::Wait);
};
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
profiling::finish_frame!();
let mut frame_timer = crate::stopwatch::Stopwatch::new();
frame_timer.start();
@@ -698,7 +696,7 @@ impl<'app> GlowWinitRunning<'app> {
{
// vsync - don't count as frame-time:
frame_timer.pause();
crate::profile_scope!("swap_buffers");
profiling::scope!("swap_buffers");
let context = current_gl_context
.as_ref()
.ok_or(egui_glow::PainterError::from(
@@ -726,7 +724,7 @@ impl<'app> GlowWinitRunning<'app> {
if window.is_minimized() == Some(true) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
crate::profile_scope!("minimized_sleep");
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
@@ -857,7 +855,7 @@ fn change_gl_context(
not_current_gl_context: &mut Option<glutin::context::NotCurrentContext>,
gl_surface: &glutin::surface::Surface<glutin::surface::WindowSurface>,
) {
crate::profile_function!();
profiling::function_scope!();
if !cfg!(target_os = "windows") {
// According to https://github.com/emilk/egui/issues/4289
@@ -866,7 +864,7 @@ fn change_gl_context(
// See https://github.com/emilk/egui/issues/4173
if let Some(current_gl_context) = current_gl_context {
crate::profile_scope!("is_current");
profiling::scope!("is_current");
if gl_surface.is_current(current_gl_context) {
return; // Early-out to save a lot of time.
}
@@ -876,7 +874,7 @@ fn change_gl_context(
let not_current = if let Some(not_current_context) = not_current_gl_context.take() {
not_current_context
} else {
crate::profile_scope!("make_not_current");
profiling::scope!("make_not_current");
current_gl_context
.take()
.unwrap()
@@ -884,7 +882,7 @@ fn change_gl_context(
.unwrap()
};
crate::profile_scope!("make_current");
profiling::scope!("make_current");
*current_gl_context = Some(not_current.make_current(gl_surface).unwrap());
}
@@ -896,7 +894,7 @@ impl GlutinWindowContext {
native_options: &NativeOptions,
event_loop: &ActiveEventLoop,
) -> Result<Self> {
crate::profile_function!();
profiling::function_scope!();
// There is a lot of complexity with opengl creation,
// so prefer extensive logging to get all the help we can to debug issues.
@@ -952,7 +950,7 @@ impl GlutinWindowContext {
)));
let (window, gl_config) = {
crate::profile_scope!("DisplayBuilder::build");
profiling::scope!("DisplayBuilder::build");
display_builder
.build(
@@ -995,7 +993,7 @@ impl GlutinWindowContext {
.build(glutin_raw_window_handle);
let gl_context_result = unsafe {
crate::profile_scope!("create_context");
profiling::scope!("create_context");
gl_config
.display()
.create_context(&gl_config, &context_attributes)
@@ -1070,7 +1068,7 @@ impl GlutinWindowContext {
///
/// Errors will be logged.
fn initialize_all_windows(&mut self, event_loop: &ActiveEventLoop) {
crate::profile_function!();
profiling::function_scope!();
let viewports: Vec<ViewportId> = self.viewports.keys().copied().collect();
@@ -1088,7 +1086,7 @@ impl GlutinWindowContext {
viewport_id: ViewportId,
event_loop: &ActiveEventLoop,
) -> Result {
crate::profile_function!();
profiling::function_scope!();
let viewport = self
.viewports
@@ -1268,7 +1266,7 @@ impl GlutinWindowContext {
egui_ctx: &egui::Context,
viewport_output: &ViewportIdMap<ViewportOutput>,
) {
crate::profile_function!();
profiling::function_scope!();
for (
viewport_id,
@@ -1329,7 +1327,7 @@ fn initialize_or_update_viewport(
mut builder: ViewportBuilder,
viewport_ui_cb: Option<Arc<dyn Fn(&egui::Context) + Send + Sync>>,
) -> &mut Viewport {
crate::profile_function!();
profiling::function_scope!();
if builder.icon.is_none() {
// Inherit icon from parent
@@ -1393,7 +1391,7 @@ fn render_immediate_viewport(
beginning: Instant,
immediate_viewport: ImmediateViewport<'_>,
) {
crate::profile_function!();
profiling::function_scope!();
let ImmediateViewport {
ids,
@@ -1516,7 +1514,7 @@ fn render_immediate_viewport(
);
{
crate::profile_scope!("swap_buffers");
profiling::scope!("swap_buffers");
if let Err(err) = gl_surface.swap_buffers(current_gl_context) {
log::error!("swap_buffers failed: {err}");
}

View File

@@ -17,14 +17,25 @@ use crate::{
// ----------------------------------------------------------------------------
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoop<UserEvent>> {
crate::profile_function!();
#[cfg(target_os = "android")]
use winit::platform::android::EventLoopBuilderExtAndroid as _;
profiling::function_scope!();
let mut builder = winit::event_loop::EventLoop::with_user_event();
#[cfg(target_os = "android")]
let mut builder =
builder.with_android_app(native_options.android_app.take().ok_or_else(|| {
crate::Error::AppCreation(Box::from(
"`NativeOptions` is missing required `android_app`",
))
})?);
if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) {
hook(&mut builder);
}
crate::profile_scope!("EventLoopBuilder::build");
profiling::scope!("EventLoopBuilder::build");
Ok(builder.build()?)
}
@@ -175,7 +186,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
crate::profile_function!("Event::Suspended");
profiling::scope!("Event::Suspended");
event_loop_context::with_event_loop_context(event_loop, move || {
let event_result = self.winit_app.suspended(event_loop);
@@ -184,7 +195,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
}
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
crate::profile_function!("Event::Resumed");
profiling::scope!("Event::Resumed");
// Nb: Make sure this guard is dropped after this function returns.
event_loop_context::with_event_loop_context(event_loop, move || {
@@ -208,7 +219,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
crate::profile_function!(egui_winit::short_device_event_description(&event));
profiling::function_scope!(egui_winit::short_device_event_description(&event));
// Nb: Make sure this guard is dropped after this function returns.
event_loop_context::with_event_loop_context(event_loop, move || {
@@ -218,7 +229,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
crate::profile_function!(match &event {
profiling::function_scope!(match &event {
UserEvent::RequestRepaint { .. } => "UserEvent::RequestRepaint",
#[cfg(feature = "accesskit")]
UserEvent::AccessKitActionRequest(_) => "UserEvent::AccessKitActionRequest",
@@ -274,7 +285,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
window_id: WindowId,
event: winit::event::WindowEvent,
) {
crate::profile_function!(egui_winit::short_window_event_description(&event));
profiling::function_scope!(egui_winit::short_window_event_description(&event));
// Nb: Make sure this guard is dropped after this function returns.
event_loop_context::with_event_loop_context(event_loop, move || {

View File

@@ -102,7 +102,7 @@ impl<'app> WgpuWinitApp<'app> {
native_options: NativeOptions,
app_creator: AppCreator<'app>,
) -> Self {
crate::profile_function!();
profiling::function_scope!();
#[cfg(feature = "__screenshot")]
assert!(
@@ -181,10 +181,10 @@ impl<'app> WgpuWinitApp<'app> {
window: Window,
builder: ViewportBuilder,
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
crate::profile_function!();
profiling::function_scope!();
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
self.native_options.wgpu_options.clone(),
self.native_options.multisampling.max(1) as _,
egui_wgpu::depth_format_from_bits(
@@ -198,7 +198,7 @@ impl<'app> WgpuWinitApp<'app> {
let window = Arc::new(window);
{
crate::profile_scope!("set_window");
profiling::scope!("set_window");
pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone())))?;
}
@@ -267,7 +267,7 @@ impl<'app> WgpuWinitApp<'app> {
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
};
let app = {
crate::profile_scope!("user_app_creator");
profiling::scope!("user_app_creator");
app_creator(&cc).map_err(crate::Error::AppCreation)?
};
@@ -489,7 +489,7 @@ impl<'app> WinitApp for WgpuWinitApp<'app> {
impl<'app> WgpuWinitRunning<'app> {
fn save_and_destroy(&mut self) {
crate::profile_function!();
profiling::function_scope!();
let mut shared = self.shared.borrow_mut();
if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) {
@@ -507,7 +507,7 @@ impl<'app> WgpuWinitRunning<'app> {
/// This is called both for the root viewport, and all deferred viewports
fn run_ui_and_paint(&mut self, window_id: WindowId) -> Result<EventResult> {
crate::profile_function!();
profiling::function_scope!();
let Some(viewport_id) = self
.shared
@@ -519,8 +519,7 @@ impl<'app> WgpuWinitRunning<'app> {
return Ok(EventResult::Wait);
};
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
profiling::finish_frame!();
let Self {
app,
@@ -532,7 +531,7 @@ impl<'app> WgpuWinitRunning<'app> {
frame_timer.start();
let (viewport_ui_cb, raw_input) = {
crate::profile_scope!("Prepare");
profiling::scope!("Prepare");
let mut shared_lock = shared.borrow_mut();
let SharedState {
@@ -576,7 +575,7 @@ impl<'app> WgpuWinitRunning<'app> {
egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false);
{
crate::profile_scope!("set_window");
profiling::scope!("set_window");
pollster::block_on(painter.set_window(viewport_id, Some(window.clone())))?;
}
@@ -593,6 +592,8 @@ impl<'app> WgpuWinitRunning<'app> {
.map(|(id, viewport)| (*id, viewport.info.clone()))
.collect();
painter.handle_screenshots(&mut raw_input.events);
(viewport_ui_cb, raw_input)
};
@@ -652,37 +653,14 @@ impl<'app> WgpuWinitRunning<'app> {
true
}
});
let screenshot_requested = !screenshot_commands.is_empty();
let (vsync_secs, screenshot) = painter.paint_and_update_textures(
let vsync_secs = painter.paint_and_update_textures(
viewport_id,
pixels_per_point,
app.clear_color(&egui_ctx.style().visuals),
&clipped_primitives,
&textures_delta,
screenshot_requested,
screenshot_commands,
);
match (screenshot_requested, screenshot) {
(false, None) => {}
(true, Some(screenshot)) => {
let screenshot = Arc::new(screenshot);
for user_data in screenshot_commands {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.clone(),
});
}
}
(true, None) => {
log::error!("Bug in egui_wgpu: screenshot requested, but no screenshot was taken");
}
(false, Some(_)) => {
log::warn!("Bug in egui_wgpu: Got screenshot without requesting it");
}
}
for action in viewport.actions_requested.drain() {
match action {
@@ -739,7 +717,7 @@ impl<'app> WgpuWinitRunning<'app> {
if window.is_minimized() == Some(true) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
crate::profile_scope!("minimized_sleep");
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
@@ -866,7 +844,7 @@ impl Viewport {
return; // we already have one
}
crate::profile_function!();
profiling::function_scope!();
let viewport_id = self.ids.this;
@@ -907,7 +885,7 @@ fn create_window(
storage: Option<&dyn Storage>,
native_options: &mut NativeOptions,
) -> Result<(Window, ViewportBuilder), winit::error::OsError> {
crate::profile_function!();
profiling::function_scope!();
let window_settings = epi_integration::load_window_settings(storage);
let viewport_builder = epi_integration::viewport_builder(
@@ -928,7 +906,7 @@ fn render_immediate_viewport(
shared: &RefCell<SharedState>,
immediate_viewport: ImmediateViewport<'_>,
) {
crate::profile_function!();
profiling::function_scope!();
let ImmediateViewport {
ids,
@@ -1008,7 +986,7 @@ fn render_immediate_viewport(
};
{
crate::profile_scope!("set_window");
profiling::scope!("set_window");
if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window.clone()))) {
log::error!(
"when rendering viewport_id={:?}, set_window Error {err}",
@@ -1024,7 +1002,7 @@ fn render_immediate_viewport(
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&textures_delta,
false,
vec![],
);
egui_winit.handle_platform_output(window, platform_output);
@@ -1116,7 +1094,7 @@ fn initialize_or_update_viewport<'a>(
viewport_ui_cb: Option<Arc<dyn Fn(&egui::Context) + Send + Sync>>,
painter: &mut egui_wgpu::winit::Painter,
) -> &'a mut Viewport {
crate::profile_function!();
profiling::function_scope!();
if builder.icon.is_none() {
// Inherit icon from parent

View File

@@ -11,7 +11,7 @@ use egui_winit::accesskit_winit;
/// Create an egui context, restoring it from storage if possible.
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
crate::profile_function!();
profiling::function_scope!();
pub const IS_DESKTOP: bool = cfg!(any(
target_os = "freebsd",

View File

@@ -1,4 +1,4 @@
use egui::TexturesDelta;
use egui::{TexturesDelta, UserData, ViewportCommand};
use crate::{epi, App};
@@ -16,6 +16,10 @@ pub struct AppRunner {
last_save_time: f64,
pub(crate) text_agent: TextAgent,
// If not empty, the painter should capture n frames from now.
// zero means capture the exact next frame.
screenshot_commands_with_frame_delay: Vec<(UserData, usize)>,
// Output for the last run:
textures_delta: TexturesDelta,
clipped_primitives: Option<Vec<egui::ClippedPrimitive>>,
@@ -36,7 +40,8 @@ impl AppRunner {
app_creator: epi::AppCreator<'static>,
text_agent: TextAgent,
) -> Result<Self, String> {
let painter = super::ActiveWebPainter::new(canvas, &web_options).await?;
let egui_ctx = egui::Context::default();
let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?;
let info = epi::IntegrationInfo {
web_info: epi::WebInfo {
@@ -47,7 +52,6 @@ impl AppRunner {
};
let storage = LocalStorage::default();
let egui_ctx = egui::Context::default();
egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent(
&super::user_agent().unwrap_or_default(),
));
@@ -110,6 +114,7 @@ impl AppRunner {
needs_repaint,
last_save_time: now_sec(),
text_agent,
screenshot_commands_with_frame_delay: vec![],
textures_delta: Default::default(),
clipped_primitives: None,
};
@@ -205,6 +210,8 @@ impl AppRunner {
pub fn logic(&mut self) {
// We sometimes miss blur/focus events due to the text agent, so let's just poll each frame:
self.update_focus();
// We might have received a screenshot
self.painter.handle_screenshots(&mut self.input.raw.events);
let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx());
let mut raw_input = self.input.new_frame(canvas_size);
@@ -225,12 +232,20 @@ impl AppRunner {
if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported on the web");
}
for viewport_output in viewport_output.values() {
for command in &viewport_output.commands {
// TODO(emilk): handle some of the commands
log::warn!(
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
);
for (_viewport_id, viewport_output) in viewport_output {
for command in viewport_output.commands {
match command {
ViewportCommand::Screenshot(user_data) => {
self.screenshot_commands_with_frame_delay
.push((user_data, 1));
}
_ => {
// TODO(emilk): handle some of the commands
log::warn!(
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
);
}
}
}
}
@@ -245,11 +260,27 @@ impl AppRunner {
let clipped_primitives = std::mem::take(&mut self.clipped_primitives);
if let Some(clipped_primitives) = clipped_primitives {
let mut screenshot_commands = vec![];
self.screenshot_commands_with_frame_delay
.retain_mut(|(user_data, frame_delay)| {
if *frame_delay == 0 {
screenshot_commands.push(user_data.clone());
false
} else {
*frame_delay -= 1;
true
}
});
if !self.screenshot_commands_with_frame_delay.is_empty() {
self.egui_ctx().request_repaint();
}
if let Err(err) = self.painter.paint_and_update_textures(
self.app.clear_color(&self.egui_ctx.style().visuals),
&clipped_primitives,
self.egui_ctx.pixels_per_point(),
&textures_delta,
screenshot_commands,
) {
log::error!("Failed to paint: {}", super::string_from_js_value(&err));
}
@@ -260,7 +291,7 @@ impl AppRunner {
self.frame.info.cpu_usage = Some(cpu_usage_seconds);
}
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
fn handle_platform_output(&self, platform_output: egui::PlatformOutput) {
#[cfg(feature = "web_screen_reader")]
if self.egui_ctx.options(|o| o.screen_reader) {
super::screen_reader::speak(&platform_output.events_description());

View File

@@ -1,3 +1,4 @@
use egui::{Event, UserData};
use wasm_bindgen::JsValue;
/// Renderer for a browser canvas.
@@ -16,14 +17,19 @@ pub(crate) trait WebPainter {
fn max_texture_side(&self) -> usize;
/// Update all internal textures and paint gui.
/// When `capture` isn't empty, the rendered screen should be captured.
/// Once the screenshot is ready, the screenshot should be returned via [`Self::handle_screenshots`].
fn paint_and_update_textures(
&mut self,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
capture: Vec<UserData>,
) -> Result<(), JsValue>;
fn handle_screenshots(&mut self, events: &mut Vec<Event>);
/// Destroy all resources.
fn destroy(&mut self);
}

View File

@@ -1,9 +1,10 @@
use egui::{Event, UserData, ViewportId};
use egui_glow::glow;
use std::sync::Arc;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use egui_glow::glow;
use crate::{WebGlContextOption, WebOptions};
use super::web_painter::WebPainter;
@@ -11,6 +12,7 @@ use super::web_painter::WebPainter;
pub(crate) struct WebPainterGlow {
canvas: HtmlCanvasElement,
painter: egui_glow::Painter,
screenshots: Vec<(egui::ColorImage, Vec<UserData>)>,
}
impl WebPainterGlow {
@@ -18,7 +20,11 @@ impl WebPainterGlow {
self.painter.gl()
}
pub async fn new(canvas: HtmlCanvasElement, options: &WebOptions) -> Result<Self, String> {
pub async fn new(
_ctx: egui::Context,
canvas: HtmlCanvasElement,
options: &WebOptions,
) -> Result<Self, String> {
let (gl, shader_prefix) =
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
#[allow(clippy::arc_with_non_send_sync)]
@@ -27,7 +33,11 @@ impl WebPainterGlow {
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)
.map_err(|err| format!("Error starting glow painter: {err}"))?;
Ok(Self { canvas, painter })
Ok(Self {
canvas,
painter,
screenshots: Vec::new(),
})
}
}
@@ -46,6 +56,7 @@ impl WebPainter for WebPainterGlow {
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
capture: Vec<UserData>,
) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
@@ -57,6 +68,11 @@ impl WebPainter for WebPainterGlow {
self.painter
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
if !capture.is_empty() {
let image = self.painter.read_screen_rgba(canvas_dimension);
self.screenshots.push((image, capture));
}
for &id in &textures_delta.free {
self.painter.free_texture(id);
}
@@ -67,6 +83,19 @@ impl WebPainter for WebPainterGlow {
fn destroy(&mut self) {
self.painter.destroy();
}
fn handle_screenshots(&mut self, events: &mut Vec<Event>) {
for (image, data) in self.screenshots.drain(..) {
let image = Arc::new(image);
for data in data {
events.push(Event::Screenshot {
viewport_id: ViewportId::default(),
image: image.clone(),
user_data: data,
});
}
}
}
}
/// Returns glow context and shader prefix.

View File

@@ -1,14 +1,13 @@
use std::sync::Arc;
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 wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup};
use crate::WebOptions;
use super::web_painter::WebPainter;
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
surface: wgpu::Surface<'static>,
@@ -17,6 +16,10 @@ pub(crate) struct WebPainterWgpu {
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
depth_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
screen_capture_state: Option<CaptureState>,
capture_tx: CaptureSender,
capture_rx: CaptureReceiver,
ctx: egui::Context,
}
impl WebPainterWgpu {
@@ -54,6 +57,7 @@ impl WebPainterWgpu {
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub async fn new(
ctx: egui::Context,
canvas: web_sys::HtmlCanvasElement,
options: &WebOptions,
) -> Result<Self, String> {
@@ -119,17 +123,21 @@ impl WebPainterWgpu {
.await
.map_err(|err| err.to_string())?;
let default_configuration = surface
.get_default_config(&render_state.adapter, 0, 0) // Width/height is set later.
.ok_or("The surface isn't supported by this adapter")?;
let surface_configuration = wgpu::SurfaceConfiguration {
format: render_state.target_format,
present_mode: options.wgpu_options.present_mode,
view_formats: vec![render_state.target_format],
..surface
.get_default_config(&render_state.adapter, 0, 0) // Width/height is set later.
.ok_or("The surface isn't supported by this adapter")?
..default_configuration
};
log::debug!("wgpu painter initialized.");
let (capture_tx, capture_rx) = capture_channel();
Ok(Self {
canvas,
render_state: Some(render_state),
@@ -138,6 +146,10 @@ impl WebPainterWgpu {
depth_format,
depth_texture_view: None,
on_surface_error: options.wgpu_options.on_surface_error.clone(),
screen_capture_state: None,
capture_tx,
capture_rx,
ctx,
})
}
}
@@ -159,7 +171,10 @@ impl WebPainter for WebPainterWgpu {
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
capture_data: Vec<UserData>,
) -> Result<(), JsValue> {
let capture = !capture_data.is_empty();
let size_in_pixels = [self.canvas.width(), self.canvas.height()];
let Some(render_state) = &self.render_state else {
@@ -203,7 +218,7 @@ impl WebPainter for WebPainterWgpu {
// Resize surface if needed
let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0;
let frame = if is_zero_sized_surface {
let frame_and_capture_buffer = if is_zero_sized_surface {
None
} else {
if size_in_pixels[0] != self.surface_configuration.width
@@ -220,7 +235,7 @@ impl WebPainter for WebPainterWgpu {
);
}
let frame = match self.surface.get_current_texture() {
let output_frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(err) => match (*self.on_surface_error)(err) {
SurfaceErrorAction::RecreateSurface => {
@@ -236,12 +251,23 @@ impl WebPainter for WebPainterWgpu {
{
let renderer = render_state.renderer.read();
let frame_view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let target_texture = if capture {
let capture_state = self.screen_capture_state.get_or_insert_with(|| {
CaptureState::new(&render_state.device, &output_frame.texture)
});
capture_state.update(&render_state.device, &output_frame.texture);
&capture_state.texture
} else {
&output_frame.texture
};
let target_view =
target_texture.create_view(&wgpu::TextureViewDescriptor::default());
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame_view,
view: &target_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
@@ -280,7 +306,19 @@ impl WebPainter for WebPainterWgpu {
);
}
Some(frame)
let mut capture_buffer = None;
if capture {
if let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
};
Some((output_frame, capture_buffer))
};
{
@@ -295,13 +333,38 @@ impl WebPainter for WebPainterWgpu {
.queue
.submit(user_cmd_bufs.into_iter().chain([encoder.finish()]));
if let Some(frame) = frame {
if let Some((frame, capture_buffer)) = frame_and_capture_buffer {
if let Some(capture_buffer) = capture_buffer {
if let Some(capture_state) = &self.screen_capture_state {
capture_state.read_screen_rgba(
self.ctx.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
ViewportId::ROOT,
);
}
}
frame.present();
}
Ok(())
}
fn handle_screenshots(&mut self, events: &mut Vec<Event>) {
for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() {
let screenshot = Arc::new(screenshot);
for data in user_data {
events.push(Event::Screenshot {
viewport_id,
user_data: data,
image: screenshot.clone(),
});
}
}
}
fn destroy(&mut self) {
self.render_state = None;
}

View File

@@ -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.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)
* Add option to initialize with existing wgpu instance/adapter/device/queue [#5319](https://github.com/emilk/egui/pull/5319) by [@ArthurBrussee](https://github.com/ArthurBrussee)
* Updare to `wgpu` 23.0.0 and `wasm-bindgen` to 0.2.95 [#5330](https://github.com/emilk/egui/pull/5330) by [@torokati44](https://github.com/torokati44)
* Support wgpu-tracing with same mechanism as wgpu examples [#5450](https://github.com/emilk/egui/pull/5450) by [@EriKWDev](https://github.com/EriKWDev)
## 0.29.1 - 2024-10-01
Nothing new

View File

@@ -33,9 +33,6 @@ rustdoc-args = ["--generate-link-to-definition"]
[features]
default = ["fragile-send-sync-non-atomic-wasm"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin"]
## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11`
winit = ["dep:winit", "winit/rwh_06"]
@@ -60,6 +57,7 @@ ahash.workspace = true
bytemuck.workspace = true
document-features.workspace = true
log.workspace = true
profiling.workspace = true
thiserror.workspace = true
type-map.workspace = true
web-time.workspace = true
@@ -68,7 +66,3 @@ wgpu = { workspace = true, features = ["wgsl"] }
# Optional dependencies:
winit = { workspace = true, optional = true, default-features = false }
# Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
puffin = { workspace = true, optional = true }

View File

@@ -0,0 +1,257 @@
use egui::{UserData, ViewportId};
use epaint::ColorImage;
use std::sync::{mpsc, Arc};
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
/// A texture and a buffer for reading the rendered frame back to the cpu.
/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
/// flag for the surface texture on all platforms. This means that anytime we want to
/// capture the frame, we first render it to this texture, and then we can copy it to
/// both the surface texture (via a render pass) and the buffer (via a texture to buffer copy),
/// from where we can pull it back
/// to the cpu.
pub struct CaptureState {
padding: BufferPadding,
pub texture: wgpu::Texture,
pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup,
}
pub type CaptureReceiver = mpsc::Receiver<(ViewportId, Vec<UserData>, ColorImage)>;
pub type CaptureSender = mpsc::Sender<(ViewportId, Vec<UserData>, ColorImage)>;
pub use mpsc::channel as capture_channel;
impl CaptureState {
pub fn new(device: &wgpu::Device, surface_texture: &wgpu::Texture) -> Self {
let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl"));
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("texture_copy"),
layout: None,
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(surface_texture.format().into())],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: MultisampleState::default(),
multiview: None,
cache: None,
});
let bind_group_layout = pipeline.get_bind_group_layout(0);
let (texture, padding, bind_group) =
Self::create_texture(device, surface_texture, &bind_group_layout);
Self {
padding,
texture,
pipeline,
bind_group,
}
}
fn create_texture(
device: &wgpu::Device,
surface_texture: &wgpu::Texture,
layout: &BindGroupLayout,
) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_screen_capture_texture"),
size: surface_texture.size(),
mip_level_count: surface_texture.mip_level_count(),
sample_count: surface_texture.sample_count(),
dimension: surface_texture.dimension(),
format: surface_texture.format(),
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let padding = BufferPadding::new(surface_texture.width());
let view = texture.create_view(&Default::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
}],
label: None,
});
(texture, padding, bind_group)
}
/// Updates the [`CaptureState`] if the size of the surface texture has changed
pub fn update(&mut self, device: &wgpu::Device, texture: &wgpu::Texture) {
if self.texture.size() != texture.size() {
let (new_texture, padding, bind_group) =
Self::create_texture(device, texture, &self.pipeline.get_bind_group_layout(0));
self.texture = new_texture;
self.padding = padding;
self.bind_group = bind_group;
}
}
/// Handles copying from the [`CaptureState`] texture to the surface texture and the buffer.
/// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu.
pub fn copy_textures(
&mut self,
device: &wgpu::Device,
output_frame: &wgpu::SurfaceTexture,
encoder: &mut wgpu::CommandEncoder,
) -> wgpu::Buffer {
debug_assert_eq!(
self.texture.size(),
output_frame.texture.size(),
"Texture sizes must match, `CaptureState::update` was probably not called"
);
// It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
// for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
// it might make sense to revisit this and implement a more efficient solution.
#[allow(clippy::arc_with_non_send_sync)]
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_screen_capture_buffer"),
size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let padding = self.padding;
let tex = &mut self.texture;
let tex_extent = tex.size();
encoder.copy_texture_to_buffer(
tex.as_image_copy(),
wgpu::ImageCopyBuffer {
buffer: &buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(padding.padded_bytes_per_row),
rows_per_image: None,
},
},
tex_extent,
);
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("texture_copy"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &output_frame.texture.create_view(&Default::default()),
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.draw(0..3, 0..1);
buffer
}
/// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu
/// This function is non-blocking and will send the data to the given sender when it's ready.
/// Pass in the buffer returned from [`CaptureState::copy_textures`].
/// Make sure to call this after the encoder has been submitted.
pub fn read_screen_rgba(
&self,
ctx: egui::Context,
buffer: wgpu::Buffer,
data: Vec<UserData>,
tx: CaptureSender,
viewport_id: ViewportId,
) {
#[allow(clippy::arc_with_non_send_sync)]
let buffer = Arc::new(buffer);
let buffer_clone = buffer.clone();
let buffer_slice = buffer_clone.slice(..);
let format = self.texture.format();
let tex_extent = self.texture.size();
let padding = self.padding;
let to_rgba = match format {
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
_ => {
log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", format);
return;
}
};
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
if let Err(err) = result {
log::error!("Failed to map buffer for reading: {:?}", err);
return;
}
let buffer_slice = buffer.slice(..);
let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize);
for padded_row in buffer_slice
.get_mapped_range()
.chunks(padding.padded_bytes_per_row as usize)
{
let row = &padded_row[..padding.unpadded_bytes_per_row as usize];
for color in row.chunks(4) {
pixels.push(epaint::Color32::from_rgba_premultiplied(
color[to_rgba[0]],
color[to_rgba[1]],
color[to_rgba[2]],
color[to_rgba[3]],
));
}
}
buffer.unmap();
tx.send((
viewport_id,
data,
ColorImage {
size: [tex_extent.width as usize, tex_extent.height as usize],
pixels,
},
))
.ok();
ctx.request_repaint();
});
}
}
#[derive(Copy, Clone)]
struct BufferPadding {
unpadded_bytes_per_row: u32,
padded_bytes_per_row: u32,
}
impl BufferPadding {
fn new(width: u32) -> Self {
let bytes_per_pixel = std::mem::size_of::<u32>() as u32;
let unpadded_bytes_per_row = width * bytes_per_pixel;
let padded_bytes_per_row =
wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
Self {
unpadded_bytes_per_row,
padded_bytes_per_row,
}
}
}

View File

@@ -26,6 +26,9 @@ mod renderer;
pub use renderer::*;
use wgpu::{Adapter, Device, Instance, Queue};
/// Helpers for capturing screenshots of the UI.
pub mod capture;
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")]
pub mod winit;
@@ -93,7 +96,7 @@ impl RenderState {
msaa_samples: u32,
dithering: bool,
) -> Result<Self, WgpuError> {
crate::profile_scope!("RenderState::create"); // async yield give bad names using `profile_function`
profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
// This is always an empty list on web.
#[cfg(not(target_arch = "wasm32"))]
@@ -106,7 +109,7 @@ impl RenderState {
device_descriptor,
} => {
let adapter = {
crate::profile_scope!("request_adapter");
profiling::scope!("request_adapter");
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
@@ -159,10 +162,14 @@ impl RenderState {
);
}
let trace_path = std::env::var("WGPU_TRACE");
let (device, queue) = {
crate::profile_scope!("request_device");
profiling::scope!("request_device");
adapter
.request_device(&(*device_descriptor)(&adapter), None)
.request_device(
&(*device_descriptor)(&adapter),
trace_path.ok().as_ref().map(std::path::Path::new),
)
.await?
};
@@ -180,7 +187,7 @@ impl RenderState {
};
let capabilities = {
crate::profile_scope!("get_capabilities");
profiling::scope!("get_capabilities");
surface.get_capabilities(&adapter).formats
};
let target_format = crate::preferred_framebuffer_format(&capabilities)?;
@@ -467,33 +474,3 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
summary
}
// ---------------------------------------------------------------------------
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[allow(unused_imports)]
pub(crate) use profiling_scopes::{profile_function, profile_scope};

View File

@@ -214,14 +214,14 @@ impl Renderer {
msaa_samples: u32,
dithering: bool,
) -> Self {
crate::profile_function!();
profiling::function_scope!();
let shader = wgpu::ShaderModuleDescriptor {
label: Some("egui"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))),
};
let module = {
crate::profile_scope!("create_shader_module");
profiling::scope!("create_shader_module");
device.create_shader_module(shader)
};
@@ -236,7 +236,7 @@ impl Renderer {
});
let uniform_bind_group_layout = {
crate::profile_scope!("create_bind_group_layout");
profiling::scope!("create_bind_group_layout");
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("egui_uniform_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
@@ -253,7 +253,7 @@ impl Renderer {
};
let uniform_bind_group = {
crate::profile_scope!("create_bind_group");
profiling::scope!("create_bind_group");
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("egui_uniform_bind_group"),
layout: &uniform_bind_group_layout,
@@ -269,7 +269,7 @@ impl Renderer {
};
let texture_bind_group_layout = {
crate::profile_scope!("create_bind_group_layout");
profiling::scope!("create_bind_group_layout");
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("egui_texture_bind_group_layout"),
entries: &[
@@ -308,7 +308,7 @@ impl Renderer {
});
let pipeline = {
crate::profile_scope!("create_render_pipeline");
profiling::scope!("create_render_pipeline");
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("egui_pipeline"),
layout: Some(&pipeline_layout),
@@ -420,7 +420,7 @@ impl Renderer {
paint_jobs: &[epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
) {
crate::profile_function!();
profiling::function_scope!();
let pixels_per_point = screen_descriptor.pixels_per_point;
let size_in_pixels = screen_descriptor.size_in_pixels;
@@ -506,7 +506,7 @@ impl Renderer {
let viewport_px = info.viewport_in_pixels();
if viewport_px.width_px > 0 && viewport_px.height_px > 0 {
crate::profile_scope!("callback");
profiling::scope!("callback");
needs_reset = true;
@@ -544,7 +544,7 @@ impl Renderer {
id: epaint::TextureId,
image_delta: &epaint::ImageDelta,
) {
crate::profile_function!();
profiling::function_scope!();
let width = image_delta.image.width() as u32;
let height = image_delta.image.height() as u32;
@@ -570,14 +570,14 @@ impl Renderer {
image.pixels.len(),
"Mismatch between texture size and texel count"
);
crate::profile_scope!("font -> sRGBA");
profiling::scope!("font -> sRGBA");
Cow::Owned(image.srgba_pixels(None).collect::<Vec<epaint::Color32>>())
}
};
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
let queue_write_data_to_texture = |texture, origin| {
crate::profile_scope!("write_texture");
profiling::scope!("write_texture");
queue.write_texture(
wgpu::ImageCopyTexture {
texture,
@@ -631,7 +631,7 @@ impl Renderer {
} else {
// allocate a new texture
let texture = {
crate::profile_scope!("create_texture");
profiling::scope!("create_texture");
device.create_texture(&wgpu::TextureDescriptor {
label,
size,
@@ -756,7 +756,7 @@ impl Renderer {
texture: &wgpu::TextureView,
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
) -> epaint::TextureId {
crate::profile_function!();
profiling::function_scope!();
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
@@ -804,7 +804,7 @@ impl Renderer {
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
id: epaint::TextureId,
) {
crate::profile_function!();
profiling::function_scope!();
let Texture {
bind_group: user_texture_binding,
@@ -849,7 +849,7 @@ impl Renderer {
paint_jobs: &[epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
) -> Vec<wgpu::CommandBuffer> {
crate::profile_function!();
profiling::function_scope!();
let screen_size_in_points = screen_descriptor.screen_size_in_points();
@@ -859,7 +859,7 @@ impl Renderer {
_padding: Default::default(),
};
if uniform_buffer_content != self.previous_uniform_buffer_content {
crate::profile_scope!("update uniforms");
profiling::scope!("update uniforms");
queue.write_buffer(
&self.uniform_buffer,
0,
@@ -871,7 +871,7 @@ impl Renderer {
// Determine how many vertices & indices need to be rendered, and gather prepare callbacks
let mut callbacks = Vec::new();
let (vertex_count, index_count) = {
crate::profile_scope!("count_vertices_indices");
profiling::scope!("count_vertices_indices");
paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| {
match &clipped_primitive.primitive {
Primitive::Mesh(mesh) => {
@@ -890,7 +890,7 @@ impl Renderer {
};
if index_count > 0 {
crate::profile_scope!("indices", index_count.to_string());
profiling::scope!("indices", index_count.to_string().as_str());
self.index_buffer.slices.clear();
@@ -928,7 +928,7 @@ impl Renderer {
}
}
if vertex_count > 0 {
crate::profile_scope!("vertices", vertex_count.to_string());
profiling::scope!("vertices", vertex_count.to_string().as_str());
self.vertex_buffer.slices.clear();
@@ -969,7 +969,7 @@ impl Renderer {
let mut user_cmd_bufs = Vec::new();
{
crate::profile_scope!("prepare callbacks");
profiling::scope!("prepare callbacks");
for callback in &callbacks {
user_cmd_bufs.extend(callback.prepare(
device,
@@ -981,7 +981,7 @@ impl Renderer {
}
}
{
crate::profile_scope!("finish prepare callbacks");
profiling::scope!("finish prepare callbacks");
for callback in &callbacks {
user_cmd_bufs.extend(callback.finish_prepare(
device,
@@ -1026,7 +1026,7 @@ fn create_sampler(
}
fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
crate::profile_function!();
profiling::function_scope!();
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_vertex_buffer"),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
@@ -1036,7 +1036,7 @@ fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
}
fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
crate::profile_function!();
profiling::function_scope!();
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_index_buffer"),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,

View File

@@ -0,0 +1,43 @@
struct VertexOutput {
@builtin(position) position: vec4<f32>,
};
var<private> positions: array<vec2f, 3> = array<vec2f, 3>(
vec2f(-1.0, -3.0),
vec2f(-1.0, 1.0),
vec2f(3.0, 1.0)
);
// meant to be called with 3 vertex indices: 0, 1, 2
// draws one large triangle over the clip space like this:
// (the asterisks represent the clip space bounds)
//-1,1 1,1
// ---------------------------------
// | * .
// | * .
// | * .
// | * .
// | * .
// | * .
// |***************
// | . 1,-1
// | .
// | .
// | .
// | .
// |.
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var result: VertexOutput;
result.position = vec4f(positions[vertex_index], 0.0, 1.0);
return result;
}
@group(0)
@binding(0)
var r_color: texture_2d<f32>;
@fragment
fn fs_main(vertex: VertexOutput) -> @location(0) vec4<f32> {
return textureLoad(r_color, vec2i(vertex.position.xy), 0);
}

View File

@@ -1,77 +1,16 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::undocumented_unsafe_blocks)]
use std::{num::NonZeroU32, sync::Arc};
use egui::{ViewportId, ViewportIdMap, ViewportIdSet};
use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration};
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
use std::{num::NonZeroU32, sync::Arc};
struct SurfaceState {
surface: wgpu::Surface<'static>,
alpha_mode: wgpu::CompositeAlphaMode,
width: u32,
height: u32,
supports_screenshot: bool,
}
/// A texture and a buffer for reading the rendered frame back to the cpu.
/// The texture is required since [`wgpu::TextureUsages::COPY_DST`] is not an allowed
/// flag for the surface texture on all platforms. This means that anytime we want to
/// capture the frame, we first render it to this texture, and then we can copy it to
/// both the surface texture and the buffer, from where we can pull it back to the cpu.
struct CaptureState {
texture: wgpu::Texture,
buffer: wgpu::Buffer,
padding: BufferPadding,
}
impl CaptureState {
fn new(device: &Arc<wgpu::Device>, surface_texture: &wgpu::Texture) -> Self {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_screen_capture_texture"),
size: surface_texture.size(),
mip_level_count: surface_texture.mip_level_count(),
sample_count: surface_texture.sample_count(),
dimension: surface_texture.dimension(),
format: surface_texture.format(),
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let padding = BufferPadding::new(surface_texture.width());
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_screen_capture_buffer"),
size: (padding.padded_bytes_per_row * texture.height()) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
Self {
texture,
buffer,
padding,
}
}
}
struct BufferPadding {
unpadded_bytes_per_row: u32,
padded_bytes_per_row: u32,
}
impl BufferPadding {
fn new(width: u32) -> Self {
let bytes_per_pixel = std::mem::size_of::<u32>() as u32;
let unpadded_bytes_per_row = width * bytes_per_pixel;
let padded_bytes_per_row =
wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
Self {
unpadded_bytes_per_row,
padded_bytes_per_row,
}
}
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
@@ -80,6 +19,7 @@ impl BufferPadding {
///
/// NOTE: all egui viewports share the same painter.
pub struct Painter {
context: Context,
configuration: WgpuConfiguration,
msaa_samples: u32,
support_transparent_backbuffer: bool,
@@ -94,6 +34,8 @@ pub struct Painter {
depth_texture_view: ViewportIdMap<wgpu::TextureView>,
msaa_texture_view: ViewportIdMap<wgpu::TextureView>,
surfaces: ViewportIdMap<SurfaceState>,
capture_tx: CaptureSender,
capture_rx: CaptureReceiver,
}
impl Painter {
@@ -110,6 +52,7 @@ impl Painter {
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
/// associated.
pub fn new(
context: Context,
configuration: WgpuConfiguration,
msaa_samples: u32,
depth_format: Option<wgpu::TextureFormat>,
@@ -126,7 +69,10 @@ impl Painter {
crate::WgpuSetup::Existing { instance, .. } => instance.clone(),
};
let (capture_tx, capture_rx) = capture_channel();
Self {
context,
configuration,
msaa_samples,
support_transparent_backbuffer,
@@ -140,6 +86,9 @@ impl Painter {
depth_texture_view: Default::default(),
surfaces: Default::default(),
msaa_texture_view: Default::default(),
capture_tx,
capture_rx,
}
}
@@ -155,19 +104,13 @@ impl Painter {
render_state: &RenderState,
config: &WgpuConfiguration,
) {
crate::profile_function!();
let usage = if surface_state.supports_screenshot {
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST
} else {
wgpu::TextureUsages::RENDER_ATTACHMENT
};
profiling::function_scope!();
let width = surface_state.width;
let height = surface_state.height;
let mut surf_config = wgpu::SurfaceConfiguration {
usage,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: render_state.target_format,
present_mode: config.present_mode,
alpha_mode: surface_state.alpha_mode,
@@ -213,7 +156,7 @@ impl Painter {
viewport_id: ViewportId,
window: Option<Arc<winit::window::Window>>,
) -> Result<(), crate::WgpuError> {
crate::profile_scope!("Painter::set_window"); // profile_function gives bad names for async functions
profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions
if let Some(window) = window {
let size = window.inner_size();
@@ -239,7 +182,7 @@ impl Painter {
viewport_id: ViewportId,
window: Option<&winit::window::Window>,
) -> Result<(), crate::WgpuError> {
crate::profile_scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions
profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions
if let Some(window) = window {
let size = window.inner_size();
@@ -292,8 +235,6 @@ impl Painter {
} else {
wgpu::CompositeAlphaMode::Auto
};
let supports_screenshot =
!matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl);
self.surfaces.insert(
viewport_id,
SurfaceState {
@@ -301,7 +242,6 @@ impl Painter {
width: size.width,
height: size.height,
alpha_mode,
supports_screenshot,
},
);
let Some(width) = NonZeroU32::new(size.width) else {
@@ -333,7 +273,7 @@ impl Painter {
width_in_pixels: NonZeroU32,
height_in_pixels: NonZeroU32,
) {
crate::profile_function!();
profiling::function_scope!();
let width = width_in_pixels.get();
let height = height_in_pixels.get();
@@ -404,7 +344,7 @@ impl Painter {
width_in_pixels: NonZeroU32,
height_in_pixels: NonZeroU32,
) {
crate::profile_function!();
profiling::function_scope!();
if self.surfaces.contains_key(&viewport_id) {
self.resize_and_generate_depth_texture_view_and_msaa_view(
@@ -417,109 +357,12 @@ impl Painter {
}
}
// CaptureState only needs to be updated when the size of the two textures don't match and we want to
// capture a frame
fn update_capture_state(
screen_capture_state: &mut Option<CaptureState>,
surface_texture: &wgpu::SurfaceTexture,
render_state: &RenderState,
) {
let surface_texture = &surface_texture.texture;
match screen_capture_state {
Some(capture_state) => {
if capture_state.texture.size() != surface_texture.size() {
*capture_state = CaptureState::new(&render_state.device, surface_texture);
}
}
None => {
*screen_capture_state =
Some(CaptureState::new(&render_state.device, surface_texture));
}
}
}
// Handles copying from the CaptureState texture to the surface texture and the cpu
fn read_screen_rgba(
screen_capture_state: &CaptureState,
render_state: &RenderState,
output_frame: &wgpu::SurfaceTexture,
) -> Option<epaint::ColorImage> {
let CaptureState {
texture: tex,
buffer,
padding,
} = screen_capture_state;
let device = &render_state.device;
let queue = &render_state.queue;
let tex_extent = tex.size();
let mut encoder = device.create_command_encoder(&Default::default());
encoder.copy_texture_to_buffer(
tex.as_image_copy(),
wgpu::ImageCopyBuffer {
buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(padding.padded_bytes_per_row),
rows_per_image: None,
},
},
tex_extent,
);
encoder.copy_texture_to_texture(
tex.as_image_copy(),
output_frame.texture.as_image_copy(),
tex.size(),
);
let id = queue.submit(Some(encoder.finish()));
let buffer_slice = buffer.slice(..);
let (sender, receiver) = std::sync::mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
drop(sender.send(v));
});
device.poll(wgpu::Maintain::WaitForSubmissionIndex(id));
receiver.recv().ok()?.ok()?;
let to_rgba = match tex.format() {
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
_ => {
log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", tex.format());
return None;
}
};
let mut pixels = Vec::with_capacity((tex.width() * tex.height()) as usize);
for padded_row in buffer_slice
.get_mapped_range()
.chunks(padding.padded_bytes_per_row as usize)
{
let row = &padded_row[..padding.unpadded_bytes_per_row as usize];
for color in row.chunks(4) {
pixels.push(epaint::Color32::from_rgba_premultiplied(
color[to_rgba[0]],
color[to_rgba[1]],
color[to_rgba[2]],
color[to_rgba[3]],
));
}
}
buffer.unmap();
Some(epaint::ColorImage {
size: [tex.width() as usize, tex.height() as usize],
pixels,
})
}
/// Returns two things:
///
/// The approximate number of seconds spent on vsync-waiting (if any),
/// and the captures captured screenshot if it was requested.
///
/// If `capture_data` isn't empty, a screenshot will be captured.
pub fn paint_and_update_textures(
&mut self,
viewport_id: ViewportId,
@@ -527,17 +370,18 @@ impl Painter {
clear_color: [f32; 4],
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
capture: bool,
) -> (f32, Option<epaint::ColorImage>) {
crate::profile_function!();
capture_data: Vec<UserData>,
) -> f32 {
profiling::function_scope!();
let capture = !capture_data.is_empty();
let mut vsync_sec = 0.0;
let Some(render_state) = self.render_state.as_mut() else {
return (vsync_sec, None);
return vsync_sec;
};
let Some(surface_state) = self.surfaces.get(&viewport_id) else {
return (vsync_sec, None);
return vsync_sec;
};
let mut encoder =
@@ -573,17 +417,8 @@ impl Painter {
)
};
let capture = match (capture, surface_state.supports_screenshot) {
(false, _) => false,
(true, true) => true,
(true, false) => {
log::error!("The active render surface doesn't support taking screenshots.");
false
}
};
let output_frame = {
crate::profile_scope!("get_current_texture");
profiling::scope!("get_current_texture");
// This is what vsync-waiting happens on my Mac.
let start = web_time::Instant::now();
let output_frame = surface_state.surface.get_current_texture();
@@ -596,40 +431,35 @@ impl Painter {
Err(err) => match (*self.configuration.on_surface_error)(err) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
return (vsync_sec, None);
return vsync_sec;
}
SurfaceErrorAction::SkipFrame => {
return (vsync_sec, None);
return vsync_sec;
}
},
};
let mut capture_buffer = None;
{
let renderer = render_state.renderer.read();
let frame_view = if capture {
Self::update_capture_state(
&mut self.screen_capture_state,
&output_frame,
render_state,
);
self.screen_capture_state
.as_ref()
.map_or_else(
|| &output_frame.texture,
|capture_state| &capture_state.texture,
)
.create_view(&wgpu::TextureViewDescriptor::default())
let target_texture = if capture {
let capture_state = self.screen_capture_state.get_or_insert_with(|| {
CaptureState::new(&render_state.device, &output_frame.texture)
});
capture_state.update(&render_state.device, &output_frame.texture);
&capture_state.texture
} else {
output_frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default())
&output_frame.texture
};
let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
let (view, resolve_target) = (self.msaa_samples > 1)
.then_some(self.msaa_texture_view.get(&viewport_id))
.flatten()
.map_or((&frame_view, None), |texture_view| {
(texture_view, Some(&frame_view))
.map_or((&target_view, None), |texture_view| {
(texture_view, Some(&target_view))
});
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -671,16 +501,26 @@ impl Painter {
clipped_primitives,
&screen_descriptor,
);
if capture {
if let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
}
}
let encoded = {
crate::profile_scope!("CommandEncoder::finish");
profiling::scope!("CommandEncoder::finish");
encoder.finish()
};
// Submit the commands: both the main buffer and user-defined ones.
{
crate::profile_scope!("Queue::submit");
profiling::scope!("Queue::submit");
// wgpu doesn't document where vsync can happen. Maybe here?
let start = web_time::Instant::now();
render_state
@@ -699,25 +539,41 @@ impl Painter {
}
}
let screenshot = if capture {
self.screen_capture_state
.as_ref()
.and_then(|screen_capture_state| {
Self::read_screen_rgba(screen_capture_state, render_state, &output_frame)
})
} else {
None
};
if let Some(capture_buffer) = capture_buffer {
if let Some(screen_capture_state) = &mut self.screen_capture_state {
screen_capture_state.read_screen_rgba(
self.context.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
viewport_id,
);
}
}
{
crate::profile_scope!("present");
profiling::scope!("present");
// wgpu doesn't document where vsync can happen. Maybe here?
let start = web_time::Instant::now();
output_frame.present();
vsync_sec += start.elapsed().as_secs_f32();
}
(vsync_sec, screenshot)
vsync_sec
}
/// Call this at the beginning of each frame to receive the requested screenshots.
pub fn handle_screenshots(&self, events: &mut Vec<Event>) {
for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() {
let screenshot = Arc::new(screenshot);
for data in user_data {
events.push(Event::Screenshot {
viewport_id,
user_data: data,
image: screenshot.clone(),
});
}
}
}
pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
@@ -728,7 +584,7 @@ impl Painter {
.retain(|id, _| active_viewports.contains(id));
}
#[allow(clippy::unused_self)]
#[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}

View File

@@ -5,6 +5,11 @@ 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.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)
## 0.29.1 - 2024-10-01 - Fix backspace/arrow keys on X11
* Linux: Disable IME to fix backspace/arrow keys [#5188](https://github.com/emilk/egui/pull/5188) by [@emilk](https://github.com/emilk)

View File

@@ -45,9 +45,6 @@ clipboard = ["arboard", "smithay-clipboard"]
## Enable opening links in a browser when an egui hyperlink is clicked.
links = ["webbrowser"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin", "egui/puffin"]
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
serde = ["egui/serde", "dep:serde"]
@@ -62,6 +59,7 @@ egui = { workspace = true, default-features = false, features = ["log"] }
ahash.workspace = true
log.workspace = true
profiling.workspace = true
raw-window-handle.workspace = true
web-time.workspace = true
winit = { workspace = true, default-features = false }
@@ -74,7 +72,6 @@ accesskit_winit = { version = "0.23", optional = true }
## Enable this when generating docs.
document-features = { workspace = true, optional = true }
puffin = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
webbrowser = { version = "1.0.0", optional = true }

View File

@@ -112,7 +112,7 @@ impl Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
fn init_arboard() -> Option<arboard::Clipboard> {
crate::profile_function!();
profiling::function_scope!();
log::trace!("Initializing arboard clipboard…");
match arboard::Clipboard::new() {
@@ -139,7 +139,7 @@ fn init_smithay_clipboard(
) -> Option<smithay_clipboard::Clipboard> {
#![allow(clippy::undocumented_unsafe_blocks)]
crate::profile_function!();
profiling::function_scope!();
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
log::trace!("Initializing smithay clipboard…");

View File

@@ -25,9 +25,6 @@ pub use window_settings::WindowSettings;
use ahash::HashSet;
use raw_window_handle::HasDisplayHandle;
#[allow(unused_imports)]
pub(crate) use profiling_scopes::{profile_function, profile_scope};
use winit::{
dpi::{PhysicalPosition, PhysicalSize},
event::ElementState,
@@ -121,7 +118,7 @@ impl State {
theme: Option<winit::window::Theme>,
max_texture_side: Option<usize>,
) -> Self {
crate::profile_function!();
profiling::function_scope!();
let egui_input = egui::RawInput {
focused: false, // winit will tell us when we have focus
@@ -172,7 +169,7 @@ impl State {
window: &Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
) {
crate::profile_function!();
profiling::function_scope!();
self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy(
window,
@@ -233,7 +230,7 @@ impl State {
/// Use [`update_viewport_info`] to update the info for each
/// viewport.
pub fn take_egui_input(&mut self, window: &Window) -> egui::RawInput {
crate::profile_function!();
profiling::function_scope!();
self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
@@ -268,7 +265,7 @@ impl State {
window: &Window,
event: &winit::event::WindowEvent,
) -> EventResponse {
crate::profile_function!(short_window_event_description(event));
profiling::function_scope!(short_window_event_description(event));
#[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_mut() {
@@ -823,7 +820,7 @@ impl State {
window: &Window,
platform_output: egui::PlatformOutput,
) {
crate::profile_function!();
profiling::function_scope!();
let egui::PlatformOutput {
cursor_icon,
@@ -851,7 +848,7 @@ impl State {
let allow_ime = ime.is_some();
if self.allow_ime != allow_ime {
self.allow_ime = allow_ime;
crate::profile_scope!("set_ime_allowed");
profiling::scope!("set_ime_allowed");
window.set_ime_allowed(allow_ime);
}
@@ -862,7 +859,7 @@ impl State {
|| self.egui_ctx.input(|i| !i.events.is_empty())
{
self.ime_rect_px = Some(ime_rect_px);
crate::profile_scope!("set_ime_cursor_area");
profiling::scope!("set_ime_cursor_area");
window.set_ime_cursor_area(
winit::dpi::PhysicalPosition {
x: ime_rect_px.min.x,
@@ -881,7 +878,7 @@ impl State {
#[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_mut() {
if let Some(update) = accesskit_update {
crate::profile_scope!("accesskit");
profiling::scope!("accesskit");
accesskit.update_if_active(|| update);
}
}
@@ -953,8 +950,7 @@ pub fn update_viewport_info(
window: &Window,
is_init: bool,
) {
crate::profile_function!();
profiling::function_scope!();
let pixels_per_point = pixels_per_point(egui_ctx, window);
let has_a_position = match window.is_minimized() {
@@ -975,7 +971,7 @@ pub fn update_viewport_info(
};
let monitor_size = {
crate::profile_scope!("monitor_size");
profiling::scope!("monitor_size");
if let Some(monitor) = window.current_monitor() {
let size = monitor.size().to_logical::<f32>(pixels_per_point.into());
Some(egui::vec2(size.width, size.height))
@@ -1326,7 +1322,7 @@ fn process_viewport_command(
info: &mut ViewportInfo,
actions_requested: &mut HashSet<ActionRequested>,
) {
crate::profile_function!();
profiling::function_scope!();
use winit::window::ResizeDirection;
@@ -1542,7 +1538,7 @@ pub fn create_window(
event_loop: &ActiveEventLoop,
viewport_builder: &ViewportBuilder,
) -> Result<Window, winit::error::OsError> {
crate::profile_function!();
profiling::function_scope!();
let window_attributes =
create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone());
@@ -1556,7 +1552,7 @@ pub fn create_winit_window_attributes(
event_loop: &ActiveEventLoop,
viewport_builder: ViewportBuilder,
) -> winit::window::WindowAttributes {
crate::profile_function!();
profiling::function_scope!();
// We set sizes and positions in egui:s own ui points, which depends on the egui
// zoom_factor and the native pixels per point, so we need to know that here.
@@ -1752,7 +1748,7 @@ fn to_winit_icon(icon: &egui::IconData) -> Option<winit::window::Icon> {
if icon.is_empty() {
None
} else {
crate::profile_function!();
profiling::function_scope!();
match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) {
Ok(winit_icon) => Some(winit_icon),
Err(err) => {
@@ -1867,30 +1863,3 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
WindowEvent::PanGesture { .. } => "WindowEvent::PanGesture",
}
}
// ---------------------------------------------------------------------------
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}

View File

@@ -56,7 +56,7 @@ impl WindowSettings {
event_loop: &winit::event_loop::ActiveEventLoop,
mut viewport_builder: ViewportBuilder,
) -> ViewportBuilder {
crate::profile_function!();
profiling::function_scope!();
// `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere
// See [`winit::window::WindowBuilder::with_position`] for details.
@@ -143,8 +143,7 @@ fn find_active_monitor(
window_size_pts: egui::Vec2,
position_px: &egui::Pos2,
) -> Option<winit::monitor::MonitorHandle> {
crate::profile_function!();
profiling::function_scope!();
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
@@ -178,7 +177,7 @@ fn clamp_pos_to_monitors(
window_size_pts: egui::Vec2,
position_px: &mut egui::Pos2,
) {
crate::profile_function!();
profiling::function_scope!();
let Some(active_monitor) =
find_active_monitor(egui_zoom_factor, event_loop, window_size_pts, position_px)

View File

@@ -62,10 +62,6 @@ mint = ["epaint/mint"]
## Enable persistence of memory (window positions etc).
persistence = ["serde", "epaint/serde", "ron"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
puffin = ["dep:puffin", "epaint/puffin"]
## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon).
##
@@ -85,6 +81,7 @@ epaint = { workspace = true, default-features = false }
ahash.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true
#! ### Optional dependencies
accesskit = { version = "0.17.0", optional = true }
@@ -95,10 +92,5 @@ backtrace = { workspace = true, optional = true }
document-features = { workspace = true, optional = true }
log = { workspace = true, optional = true }
puffin = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
[dev-dependencies]
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -467,7 +467,7 @@ impl Area {
id: interact_id,
layer_id,
rect: state.rect(),
interact_rect: state.rect(),
interact_rect: state.rect().intersect(constrain_rect),
sense,
enabled,
},

View File

@@ -93,8 +93,8 @@ pub fn show_tooltip_at_pointer<R>(
pointer_rect.min.x = pointer_pos.x;
// Transform global coords to layer coords:
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
pointer_rect = transform.inverse() * pointer_rect;
if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) {
pointer_rect = from_global * pointer_rect;
}
show_tooltip_at_dyn(
@@ -162,8 +162,8 @@ fn show_tooltip_at_dyn<'c, R>(
) -> R {
// Transform layer coords to global coords:
let mut widget_rect = *widget_rect;
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
widget_rect = transform * widget_rect;
if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) {
widget_rect = to_global * widget_rect;
}
remember_that_tooltip_was_shown(ctx);
@@ -404,11 +404,12 @@ pub fn popup_above_or_below_widget<R>(
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
};
if let Some(transform) = parent_ui
if let Some(to_global) = parent_ui
.ctx()
.memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied())
.layer_transform_to_global(parent_ui.layer_id())
{
pos = transform * pos;
pos = to_global * pos;
}
let frame = Frame::popup(parent_ui.style());

View File

@@ -205,7 +205,7 @@ struct Prepared {
}
impl Resize {
fn begin(&mut self, ui: &mut Ui) -> Prepared {
fn begin(&self, ui: &mut Ui) -> Prepared {
let position = ui.available_rect_before_wrap().min;
let id = self.id.unwrap_or_else(|| {
let id_salt = self.id_salt.unwrap_or_else(|| Id::new("resize"));
@@ -295,7 +295,7 @@ impl Resize {
}
}
pub fn show<R>(mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
let mut prepared = self.begin(ui);
let ret = add_contents(&mut prepared.content_ui);
self.end(ui, prepared);

View File

@@ -109,13 +109,13 @@ struct Plugins {
impl Plugins {
fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) {
crate::profile_scope!("plugins", _cb_name);
profiling::scope!("plugins", _cb_name);
for NamedContextCallback {
debug_name: _name,
callback,
} in callbacks
{
crate::profile_scope!("plugin", _name);
profiling::scope!("plugin", _name);
(callback)(ctx);
}
}
@@ -498,19 +498,8 @@ impl ContextImpl {
viewport.this_pass.begin_pass(screen_rect);
{
let area_order = self.memory.areas().order_map();
let mut layers: Vec<LayerId> = viewport.prev_pass.widgets.layer_ids().collect();
layers.sort_by(|a, b| {
if a.order == b.order {
// Maybe both are windows, so respect area order:
area_order.get(a).cmp(&area_order.get(b))
} else {
// comparing e.g. background to tooltips
a.order.cmp(&b.order)
}
});
layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b));
viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() {
let interact_radius = self.memory.options.style().interaction.interact_radius;
@@ -518,7 +507,7 @@ impl ContextImpl {
crate::hit_test::hit_test(
&viewport.prev_pass.widgets,
&layers,
&self.memory.layer_transforms,
&self.memory.to_global,
pos,
interact_radius,
)
@@ -549,7 +538,7 @@ impl ContextImpl {
#[cfg(feature = "accesskit")]
if self.is_accesskit_enabled {
crate::profile_scope!("accesskit");
profiling::scope!("accesskit");
use crate::pass_state::AccessKitPassState;
let id = crate::accesskit_root_id();
let mut root_node = accesskit::Node::new(accesskit::Role::Window);
@@ -568,8 +557,7 @@ impl ContextImpl {
/// Load fonts unless already loaded.
fn update_fonts_mut(&mut self) {
crate::profile_function!();
profiling::function_scope!();
let input = &self.viewport().input;
let pixels_per_point = input.pixels_per_point();
let max_texture_side = input.max_texture_side;
@@ -616,7 +604,7 @@ impl ContextImpl {
log::trace!("Creating new Fonts for pixels_per_point={pixels_per_point}");
is_new = true;
crate::profile_scope!("Fonts::new");
profiling::scope!("Fonts::new");
Fonts::new(
pixels_per_point,
max_texture_side,
@@ -625,12 +613,12 @@ impl ContextImpl {
});
{
crate::profile_scope!("Fonts::begin_pass");
profiling::scope!("Fonts::begin_pass");
fonts.begin_pass(pixels_per_point, max_texture_side);
}
if is_new && self.memory.options.preload_font_glyphs {
crate::profile_scope!("preload_font_glyphs");
profiling::scope!("preload_font_glyphs");
// Preload the most common characters for the most common fonts.
// This is not very important to do, but may save a few GPU operations.
for font_id in self.memory.options.style().text_styles.values() {
@@ -812,8 +800,7 @@ impl Context {
/// ```
#[must_use]
pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput {
crate::profile_function!();
profiling::function_scope!();
let viewport_id = new_input.viewport_id;
let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get());
@@ -821,9 +808,13 @@ impl Context {
debug_assert_eq!(output.platform_output.num_completed_passes, 0);
loop {
crate::profile_scope!(
profiling::scope!(
"pass",
output.platform_output.num_completed_passes.to_string()
output
.platform_output
.num_completed_passes
.to_string()
.as_str()
);
// We must move the `num_passes` (back) to the viewport output so that [`Self::will_discard`]
@@ -886,7 +877,7 @@ impl Context {
/// // handle full_output
/// ```
pub fn begin_pass(&self, new_input: RawInput) {
crate::profile_function!();
profiling::function_scope!();
self.write(|ctx| ctx.begin_pass(new_input));
@@ -1329,11 +1320,11 @@ impl Context {
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(transform), Some(pos)) = (
memory.layer_transforms.get(&res.layer_id),
if let (Some(to_global), Some(pos)) = (
memory.to_global.get(&res.layer_id),
&mut res.interact_pointer_pos,
) {
*pos = transform.inverse() * *pos;
*pos = to_global.inverse() * *pos;
}
}
@@ -1760,7 +1751,7 @@ impl Context {
/// The new fonts will become active at the start of the next pass.
/// This will overwrite the existing fonts.
pub fn set_fonts(&self, font_definitions: FontDefinitions) {
crate::profile_function!();
profiling::function_scope!();
let pixels_per_point = self.pixels_per_point();
@@ -1788,7 +1779,7 @@ impl Context {
/// The new font will become active at the start of the next pass.
/// This will keep the existing fonts.
pub fn add_font(&self, new_font: FontInsert) {
crate::profile_function!();
profiling::function_scope!();
let pixels_per_point = self.pixels_per_point();
@@ -2152,7 +2143,7 @@ impl Context {
/// Call at the end of each frame if you called [`Context::begin_pass`].
#[must_use]
pub fn end_pass(&self) -> FullOutput {
crate::profile_function!();
profiling::function_scope!();
if self.options(|o| o.zoom_with_keyboard) {
crate::gui_zoom::zoom_with_keyboard(self);
@@ -2246,7 +2237,8 @@ impl Context {
for id in contains_pointer {
let mut widget_text = format!("{id:?}");
if let Some(rect) = widget_rects.get(id) {
widget_text += &format!(" {:?} {:?}", rect.rect, rect.sense);
widget_text +=
&format!(" {:?} {:?} {:?}", rect.layer_id, rect.rect, rect.sense);
}
if let Some(info) = widget_rects.info(id) {
widget_text += &format!(" {info:?}");
@@ -2272,11 +2264,17 @@ impl Context {
if self.style().debug.show_widget_hits {
let hits = self.write(|ctx| ctx.viewport().hits.clone());
let WidgetHits {
close,
contains_pointer,
click,
drag,
} = hits;
if false {
for widget in &close {
paint_widget(widget, "close", Color32::from_gray(70));
}
}
if true {
for widget in &contains_pointer {
paint_widget(widget, "contains_pointer", Color32::BLUE);
@@ -2342,7 +2340,7 @@ impl ContextImpl {
// https://github.com/emilk/egui/issues/3664
// at the cost of a lot of performance.
// (This will override any smaller delta that was uploaded above.)
crate::profile_scope!("full_font_atlas_update");
profiling::scope!("full_font_atlas_update");
let full_delta = ImageDelta::full(fonts.image(), TextureAtlas::texture_options());
tex_mngr.set(TextureId::default(), full_delta);
}
@@ -2356,7 +2354,7 @@ impl ContextImpl {
#[cfg(feature = "accesskit")]
{
crate::profile_scope!("accesskit");
profiling::scope!("accesskit");
let state = viewport.this_pass.accesskit_state.take();
if let Some(state) = state {
let root_id = crate::accesskit_root_id().accesskit_id();
@@ -2381,12 +2379,12 @@ impl ContextImpl {
let shapes = viewport
.graphics
.drain(self.memory.areas().order(), &self.memory.layer_transforms);
.drain(self.memory.areas().order(), &self.memory.to_global);
let mut repaint_needed = false;
if self.memory.options.repaint_on_widget_change {
crate::profile_function!("compare-widget-rects");
profiling::scope!("compare-widget-rects");
if viewport.prev_pass.widgets != viewport.this_pass.widgets {
repaint_needed = true; // Some widget has moved
}
@@ -2525,7 +2523,7 @@ impl Context {
shapes: Vec<ClippedShape>,
pixels_per_point: f32,
) -> Vec<ClippedPrimitive> {
crate::profile_function!();
profiling::function_scope!();
// A tempting optimization is to reuse the tessellation from last frame if the
// shapes are the same, but just comparing the shapes takes about 50% of the time
@@ -2552,7 +2550,7 @@ impl Context {
let paint_stats = PaintStats::from_shapes(&shapes);
let clipped_primitives = {
crate::profile_scope!("tessellator::tessellate_shapes");
profiling::scope!("tessellator::tessellate_shapes");
tessellator::Tessellator::new(
pixels_per_point,
tessellation_options,
@@ -2697,6 +2695,7 @@ impl Context {
/// Transform the graphics of the given layer.
///
/// This will also affect input.
/// The direction of the given transform is "into the global coordinate system".
///
/// This is a sticky setting, remembered from one frame to the next.
///
@@ -2706,13 +2705,28 @@ impl Context {
pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) {
self.memory_mut(|m| {
if transform == TSTransform::IDENTITY {
m.layer_transforms.remove(&layer_id)
m.to_global.remove(&layer_id)
} else {
m.layer_transforms.insert(layer_id, transform)
m.to_global.insert(layer_id, transform)
}
});
}
/// Return how to transform the graphics of the given layer into the global coordinate system.
///
/// Set this with [`Self::layer_transform_to_global`].
pub fn layer_transform_to_global(&self, layer_id: LayerId) -> Option<TSTransform> {
self.memory(|m| m.to_global.get(&layer_id).copied())
}
/// Return how to transform the graphics of the global coordinate system into the local coordinate system of the given layer.
///
/// This returns the inverse of [`Self::layer_transform_to_global`].
pub fn layer_transform_from_global(&self, layer_id: LayerId) -> Option<TSTransform> {
self.layer_transform_to_global(layer_id)
.map(|t| t.inverse())
}
/// Move all the graphics at the given layer.
///
/// Is used to implement drag-and-drop preview.
@@ -2777,12 +2791,11 @@ impl Context {
///
/// See also [`Response::contains_pointer`].
pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
let rect =
if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).copied()) {
transform * rect
} else {
rect
};
let rect = if let Some(to_global) = self.layer_transform_to_global(layer_id) {
to_global * rect
} else {
rect
};
if !rect.is_positive() {
return false;
}
@@ -3144,28 +3157,26 @@ impl Context {
self.memory_mut(|mem| *mem.areas_mut() = Default::default());
}
});
ui.indent("areas", |ui| {
ui.label("Visible areas, ordered back to front.");
ui.label("Hover to highlight");
ui.indent("layers", |ui| {
ui.label("Layers, ordered back to front.");
let layers_ids: Vec<LayerId> = self.memory(|mem| mem.areas().order().to_vec());
for layer_id in layers_ids {
let area = AreaState::load(self, layer_id.id);
if let Some(area) = area {
if let Some(area) = AreaState::load(self, layer_id.id) {
let is_visible = self.memory(|mem| mem.areas().is_visible(&layer_id));
if !is_visible {
continue;
}
let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect(),);
// TODO(emilk): `Sense::hover_highlight()`
if ui
.add(Label::new(RichText::new(text).monospace()).sense(Sense::click()))
.hovered
&& is_visible
{
let response =
ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click()));
if response.hovered && is_visible {
ui.ctx()
.debug_painter()
.debug_rect(area.rect(), Color32::RED, "");
}
} else {
ui.monospace(layer_id.short_debug_format());
}
}
});
@@ -3353,7 +3364,7 @@ impl Context {
pub fn forget_image(&self, uri: &str) {
use load::BytesLoader as _;
crate::profile_function!();
profiling::function_scope!();
let loaders = self.loaders();
@@ -3375,7 +3386,7 @@ impl Context {
pub fn forget_all_images(&self) {
use load::BytesLoader as _;
crate::profile_function!();
profiling::function_scope!();
let loaders = self.loaders();
@@ -3410,7 +3421,7 @@ impl Context {
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Loading
pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
crate::profile_function!(uri);
profiling::function_scope!(uri);
let loaders = self.loaders();
let bytes_loaders = loaders.bytes.lock();
@@ -3447,7 +3458,7 @@ impl Context {
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Loading
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
crate::profile_function!(uri);
profiling::function_scope!(uri);
let loaders = self.loaders();
let image_loaders = loaders.image.lock();
@@ -3498,7 +3509,7 @@ impl Context {
texture_options: TextureOptions,
size_hint: load::SizeHint,
) -> load::TextureLoadResult {
crate::profile_function!(uri);
profiling::function_scope!(uri);
let loaders = self.loaders();
let texture_loaders = loaders.texture.lock();
@@ -3516,7 +3527,7 @@ impl Context {
/// The loaders of bytes, images, and textures.
pub fn loaders(&self) -> Arc<Loaders> {
crate::profile_function!();
profiling::function_scope!();
self.read(|this| this.loaders.clone())
}
}
@@ -3648,7 +3659,7 @@ impl Context {
viewport_builder: ViewportBuilder,
viewport_ui_cb: impl Fn(&Self, ViewportClass) + Send + Sync + 'static,
) {
crate::profile_function!();
profiling::function_scope!();
if self.embed_viewports() {
viewport_ui_cb(self, ViewportClass::Embedded);
@@ -3700,7 +3711,7 @@ impl Context {
builder: ViewportBuilder,
mut viewport_ui_cb: impl FnMut(&Self, ViewportClass) -> T,
) -> T {
crate::profile_function!();
profiling::function_scope!();
if self.embed_viewports() {
return viewport_ui_cb(self, ViewportClass::Embedded);

View File

@@ -27,31 +27,44 @@ impl DragAndDrop {
ctx.on_end_pass("drag_and_drop_end_pass", Arc::new(Self::end_pass));
}
/// Interrupt drag-and-drop if the user presses the escape key.
///
/// This needs to happen at frame start so we can properly capture the escape key.
fn begin_pass(ctx: &Context) {
let has_any_payload = Self::has_any_payload(ctx);
if has_any_payload {
let abort_dnd = ctx.input_mut(|i| {
i.pointer.any_released()
|| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)
});
let abort_dnd_due_to_escape_key =
ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
if abort_dnd {
if abort_dnd_due_to_escape_key {
Self::clear_payload(ctx);
}
}
}
/// Interrupt drag-and-drop if the user releases the mouse button.
///
/// This is a catch-all safety net in case user code doesn't capture the drag payload itself.
/// This must happen at end-of-frame such that we don't shadow the mouse release event from user
/// code.
fn end_pass(ctx: &Context) {
let mut is_dragging = false;
let has_any_payload = Self::has_any_payload(ctx);
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
is_dragging = state.payload.is_some();
});
if has_any_payload {
let abort_dnd_due_to_mouse_release = ctx.input_mut(|i| i.pointer.any_released());
if is_dragging {
ctx.set_cursor_icon(CursorIcon::Grabbing);
if abort_dnd_due_to_mouse_release {
Self::clear_payload(ctx);
} else {
// We set the cursor icon only if its default, as the user code might have
// explicitly set it already.
ctx.output_mut(|o| {
if o.cursor_icon == CursorIcon::Default {
o.cursor_icon = CursorIcon::Grabbing;
}
});
}
}
}

View File

@@ -227,7 +227,7 @@ impl GridLayout {
self.col += 1;
}
fn paint_row(&mut self, cursor: &Rect, painter: &Painter) {
fn paint_row(&self, cursor: &Rect, painter: &Painter) {
// handle row color painting based on color-picker function
let Some(color_picker) = self.color_picker.as_ref() else {
return;
@@ -450,7 +450,7 @@ impl Grid {
ui.allocate_new_ui(ui_builder, |ui| {
ui.horizontal(|ui| {
let is_color = color_picker.is_some();
let mut grid = GridLayout {
let grid = GridLayout {
num_columns,
color_picker,
min_cell_size: vec2(min_col_width, min_row_height),

View File

@@ -2,7 +2,7 @@ use ahash::HashMap;
use emath::TSTransform;
use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects};
use crate::{ahash, emath, LayerId, Pos2, Rect, WidgetRect, WidgetRects};
/// Result of a hit-test against [`WidgetRects`].
///
@@ -12,11 +12,18 @@ use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects};
/// or if we're currently already dragging something.
#[derive(Clone, Debug, Default)]
pub struct WidgetHits {
/// All widgets close to the pointer, back-to-front.
///
/// This is a superset of all other widgets in this struct.
pub close: Vec<WidgetRect>,
/// All widgets that contains the pointer, back-to-front.
///
/// i.e. both a Window and the button in it can contain the pointer.
/// i.e. both a Window and the Button in it can contain the pointer.
///
/// Some of these may be widgets in a layer below the top-most layer.
///
/// This will be used for hovering.
pub contains_pointer: Vec<WidgetRect>,
/// If the user would start a clicking now, this is what would be clicked.
@@ -35,18 +42,18 @@ pub struct WidgetHits {
pub fn hit_test(
widgets: &WidgetRects,
layer_order: &[LayerId],
layer_transforms: &HashMap<LayerId, TSTransform>,
layer_to_global: &HashMap<LayerId, TSTransform>,
pos: Pos2,
search_radius: f32,
) -> WidgetHits {
crate::profile_function!();
profiling::function_scope!();
let search_radius_sq = search_radius * search_radius;
// Transform the position into the local coordinate space of each layer:
let pos_in_layers: HashMap<LayerId, Pos2> = layer_transforms
let pos_in_layers: HashMap<LayerId, Pos2> = layer_to_global
.iter()
.map(|(layer_id, t)| (*layer_id, t.inverse() * pos))
.map(|(layer_id, to_global)| (*layer_id, to_global.inverse() * pos))
.collect();
let mut closest_dist_sq = f32::INFINITY;
@@ -63,6 +70,7 @@ pub fn hit_test(
}
let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos);
// TODO(emilk): we should probably do the distance testing in global space instead
let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer);
// In tie, pick last = topmost.
@@ -76,51 +84,103 @@ pub fn hit_test(
.copied()
.collect();
// We need to pick one single layer for the interaction.
if let Some(closest_hit) = closest_hit {
// Select the top layer, and ignore widgets in any other layer:
let top_layer = closest_hit.layer_id;
close.retain(|w| w.layer_id == top_layer);
// If the widget is disabled, treat it as if it isn't sensing anything.
// This simplifies the code in `hit_test_on_close` so it doesn't have to check
// the `enabled` flag everywhere:
for w in &mut close {
if !w.enabled {
w.sense.click = false;
w.sense.drag = false;
}
// Transform to global coordinates:
for hit in &mut close {
if let Some(to_global) = layer_to_global.get(&hit.layer_id).copied() {
*hit = hit.transform(to_global);
}
let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos);
let hits = hit_test_on_close(&close, pos_in_layer);
if let Some(drag) = hits.drag {
debug_assert!(drag.sense.drag);
}
if let Some(click) = hits.click {
debug_assert!(click.sense.click);
}
hits
} else {
// No close widgets.
Default::default()
}
}
fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
#![allow(clippy::collapsible_else_if)]
// When using layer transforms it is common to stack layers close to each other.
// For instance, you may have a resize-separator on a panel, with two
// transform-layers on either side.
// The resize-separator is technically in a layer _behind_ the transform-layers,
// but the user doesn't perceive it as such.
// So how do we handle this case?
//
// If we just allow interactions with ALL close widgets,
// then we might accidentally allow clicks through windows and other bad stuff.
//
// Let's try this:
// * Set up a hit-area (based on search_radius)
// * Iterate over all hits top-to-bottom
// * Stop if any hit covers the whole hit-area, otherwise keep going
// * Collect the layers ids in a set
// * Remove all widgets not in the above layer set
//
// This will most often result in only one layer,
// but if the pointer is at the edge of a layer, we might include widgets in
// a layer behind it.
// Only those widgets directly under the `pos`.
let hits: Vec<WidgetRect> = close
let mut included_layers: ahash::HashSet<LayerId> = Default::default();
for hit in close.iter().rev() {
included_layers.insert(hit.layer_id);
let hit_covers_search_area = contains_circle(hit.interact_rect, pos, search_radius);
if hit_covers_search_area {
break; // nothing behind this layer could ever be interacted with
}
}
close.retain(|hit| included_layers.contains(&hit.layer_id));
// If a widget is disabled, treat it as if it isn't sensing anything.
// This simplifies the code in `hit_test_on_close` so it doesn't have to check
// the `enabled` flag everywhere:
for w in &mut close {
if !w.enabled {
w.sense.click = false;
w.sense.drag = false;
}
}
let mut hits = hit_test_on_close(&close, pos);
hits.contains_pointer = close
.iter()
.filter(|widget| widget.interact_rect.contains(pos))
.copied()
.collect();
let hit_click = hits.iter().copied().filter(|w| w.sense.click).last();
let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last();
hits.close = close;
{
// Undo the to_global-transform we applied earlier,
// go back to local layer-coordinates:
let restore_widget_rect = |w: &mut WidgetRect| {
*w = widgets.get(w.id).copied().unwrap_or(*w);
};
for wr in &mut hits.close {
restore_widget_rect(wr);
}
for wr in &mut hits.contains_pointer {
restore_widget_rect(wr);
}
if let Some(wr) = &mut hits.drag {
debug_assert!(wr.sense.drag);
restore_widget_rect(wr);
}
if let Some(wr) = &mut hits.click {
debug_assert!(wr.sense.click);
restore_widget_rect(wr);
}
}
hits
}
/// Returns true if the rectangle contains the whole circle.
fn contains_circle(interact_rect: emath::Rect, pos: Pos2, radius: f32) -> bool {
interact_rect.shrink(radius).contains(pos)
}
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);
match (hit_click, hit_drag) {
(None, None) => {
@@ -136,16 +196,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
if let Some(closest) = closest {
WidgetHits {
contains_pointer: hits,
click: closest.sense.click.then_some(closest),
drag: closest.sense.drag.then_some(closest),
..Default::default()
}
} else {
// Found nothing
WidgetHits {
contains_pointer: hits,
click: None,
drag: None,
..Default::default()
}
}
}
@@ -170,17 +230,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// This is a smaller thing on a big background - help the user hit it,
// and ignore the big drag background.
WidgetHits {
contains_pointer: hits,
click: Some(closest_click),
drag: Some(closest_click),
..Default::default()
}
} else {
// The drag wiudth is separate from the click wiudth,
// so return only the drag widget
// The drag-widget is separate from the click-widget,
// so return only the drag-widget
WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(hit_drag),
..Default::default()
}
}
} else {
@@ -194,17 +254,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// The drag widget is a big background thing (scroll area),
// so returning a separate click widget should not be confusing
WidgetHits {
contains_pointer: hits,
click: Some(closest_click),
drag: Some(hit_drag),
..Default::default()
}
} else {
// The two widgets are just two normal small widgets close to each other.
// Highlighting both would be very confusing.
WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(hit_drag),
..Default::default()
}
}
}
@@ -229,17 +289,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// `hit_drag` is a big background thing and `closest_drag` is something small on top of it.
// Be helpful and return the small things:
return WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(closest_drag),
..Default::default()
};
}
}
WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(hit_drag),
..Default::default()
}
}
}
@@ -253,57 +313,57 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// where when hovering directly over a drag-widget (like a big ScrollArea),
// we look for close click-widgets (e.g. buttons).
// This is because big background drag-widgets (ScrollArea, Window) are common,
// but bit clickable things aren't.
// but big clickable things aren't.
// Even if they were, I think it would be confusing for a user if clicking
// a drag-only widget would click something _behind_ it.
WidgetHits {
contains_pointer: hits,
click: Some(hit_click),
drag: None,
..Default::default()
}
}
(Some(hit_click), Some(hit_drag)) => {
// We have a perfect hit on both click and drag. Which is the topmost?
let click_idx = hits.iter().position(|w| *w == hit_click).unwrap();
let drag_idx = hits.iter().position(|w| *w == hit_drag).unwrap();
let click_idx = close.iter().position(|w| *w == hit_click).unwrap();
let drag_idx = close.iter().position(|w| *w == hit_drag).unwrap();
let click_is_on_top_of_drag = drag_idx < click_idx;
if click_is_on_top_of_drag {
if hit_click.sense.drag {
// The top thing senses both clicks and drags.
WidgetHits {
contains_pointer: hits,
click: Some(hit_click),
drag: Some(hit_click),
..Default::default()
}
} else {
// They are interested in different things,
// and click is on top. Report both hits,
// e.g. the top Button and the ScrollArea behind it.
WidgetHits {
contains_pointer: hits,
click: Some(hit_click),
drag: Some(hit_drag),
..Default::default()
}
}
} else {
if hit_drag.sense.click {
// The top thing senses both clicks and drags.
WidgetHits {
contains_pointer: hits,
click: Some(hit_drag),
drag: Some(hit_drag),
..Default::default()
}
} else {
// The top things senses only drags,
// so we ignore the click-widget, because it would be confusing
// if clicking a drag-widget would actually click something else below it.
WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(hit_drag),
..Default::default()
}
}
}
@@ -312,8 +372,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
}
fn find_closest(widgets: impl Iterator<Item = WidgetRect>, pos: Pos2) -> Option<WidgetRect> {
let mut closest = None;
let mut closest_dist_sq = f32::INFINITY;
find_closest_within(widgets, pos, f32::INFINITY)
}
fn find_closest_within(
widgets: impl Iterator<Item = WidgetRect>,
pos: Pos2,
max_dist: f32,
) -> Option<WidgetRect> {
let mut closest: Option<WidgetRect> = None;
let mut closest_dist_sq = max_dist * max_dist;
for widget in widgets {
if widget.interact_rect.is_negative() {
continue;
@@ -321,6 +389,16 @@ fn find_closest(widgets: impl Iterator<Item = WidgetRect>, pos: Pos2) -> Option<
let dist_sq = widget.interact_rect.distance_sq_to_pos(pos);
if let Some(closest) = closest {
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) {
continue;
}
}
}
// In case of a tie, take the last one = the one on top.
if dist_sq <= closest_dist_sq {
closest_dist_sq = dist_sq;
@@ -331,6 +409,27 @@ fn find_closest(widgets: impl Iterator<Item = WidgetRect>, pos: Pos2) -> Option<
closest
}
/// Should we prioritizie 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 {
if front.contains_rect(back) {
return false; // back widget is fully occluded; no way to hit it
}
// Reduce each rect to its width or height, whichever is smaller:
let back = back.width().min(back.height());
let front = front.width().min(front.height());
// These are hard-coded heuristics that could surely be improved.
let back_is_much_thinner = back <= 0.5 * front;
let back_is_thin = back <= 16.0;
back_is_much_thinner && back_is_thin
}
#[cfg(test)]
mod tests {
use emath::{pos2, vec2, Rect};

View File

@@ -269,7 +269,7 @@ impl InputState {
pixels_per_point: f32,
options: &crate::Options,
) -> Self {
crate::profile_function!();
profiling::function_scope!();
let time = new.time.unwrap_or(self.time + new.predicted_dt as f64);
let unstable_dt = (time - self.time) as f32;

View File

@@ -113,7 +113,7 @@ pub(crate) fn interact(
input: &InputState,
interaction: &mut InteractionState,
) -> InteractionSnapshot {
crate::profile_function!();
profiling::function_scope!();
if let Some(id) = interaction.potential_click_id {
if !widgets.contains(id) {
@@ -249,7 +249,7 @@ pub(crate) fn interact(
.copied()
.collect()
} else {
// We may be hovering a an interactive widget or two.
// We may be hovering an interactive widget or two.
// We must also consider the case where non-interactive widgets
// are _on top_ of an interactive widget.
// For instance: a label in a draggable window.
@@ -264,9 +264,9 @@ pub(crate) fn interact(
// but none below it (an interactive widget stops the hover search).
//
// To know when to stop we need to first know the order of the widgets,
// which luckily we have in the `WidgetRects`.
// which luckily we already have in `hits.close`.
let order = |id| widgets.order(id).map(|(_layer, order)| order); // we ignore the layer, since all widgets at this point is in the same layer
let order = |id| hits.close.iter().position(|w| w.id == id);
let click_order = hits.click.and_then(|w| order(w.id)).unwrap_or(0);
let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0);

View File

@@ -11,9 +11,6 @@ pub enum Order {
/// Painted behind all floating windows
Background,
/// Special layer between panels and windows
PanelResizeLine,
/// Normal moveable windows that you reorder by click
Middle,
@@ -30,10 +27,9 @@ pub enum Order {
}
impl Order {
const COUNT: usize = 6;
const COUNT: usize = 5;
const ALL: [Self; Self::COUNT] = [
Self::Background,
Self::PanelResizeLine,
Self::Middle,
Self::Foreground,
Self::Tooltip,
@@ -44,12 +40,9 @@ impl Order {
#[inline(always)]
pub fn allow_interaction(&self) -> bool {
match self {
Self::Background
| Self::PanelResizeLine
| Self::Middle
| Self::Foreground
| Self::Tooltip
| Self::Debug => true,
Self::Background | Self::Middle | Self::Foreground | Self::Tooltip | Self::Debug => {
true
}
}
}
@@ -57,7 +50,6 @@ impl Order {
pub fn short_debug_format(&self) -> &'static str {
match self {
Self::Background => "backg",
Self::PanelResizeLine => "panel",
Self::Middle => "middl",
Self::Foreground => "foreg",
Self::Tooltip => "toolt",
@@ -68,7 +60,7 @@ impl Order {
/// An identifier for a paint layer.
/// Also acts as an identifier for [`crate::Area`]:s.
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct LayerId {
pub order: Order,
@@ -110,6 +102,13 @@ impl LayerId {
}
}
impl std::fmt::Debug for LayerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { order, id } = self;
write!(f, "LayerId {{ {order:?} {id:?} }}")
}
}
/// A unique identifier of a specific [`Shape`] in a [`PaintList`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -221,9 +220,9 @@ impl GraphicLayers {
pub fn drain(
&mut self,
area_order: &[LayerId],
transforms: &ahash::HashMap<LayerId, TSTransform>,
to_global: &ahash::HashMap<LayerId, TSTransform>,
) -> Vec<ClippedShape> {
crate::profile_function!();
profiling::function_scope!();
let mut all_shapes: Vec<_> = Default::default();
@@ -239,10 +238,10 @@ impl GraphicLayers {
for layer_id in area_order {
if layer_id.order == order {
if let Some(list) = order_map.get_mut(&layer_id.id) {
if let Some(transform) = transforms.get(layer_id) {
if let Some(to_global) = to_global.get(layer_id) {
for clipped_shape in &mut list.0 {
clipped_shape.clip_rect = *transform * clipped_shape.clip_rect;
clipped_shape.shape.transform(*transform);
clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect;
clipped_shape.shape.transform(*to_global);
}
}
all_shapes.append(&mut list.0);
@@ -254,10 +253,10 @@ impl GraphicLayers {
for (id, list) in order_map {
let layer_id = LayerId::new(order, *id);
if let Some(transform) = transforms.get(&layer_id) {
if let Some(to_global) = to_global.get(&layer_id) {
for clipped_shape in &mut list.0 {
clipped_shape.clip_rect = *transform * clipped_shape.clip_rect;
clipped_shape.shape.transform(*transform);
clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect;
clipped_shape.shape.transform(*to_global);
}
}

View File

@@ -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.79.0 or later to use `egui`.
//! You need to have rust 1.80.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).
@@ -388,6 +388,18 @@
//! ## Installing additional fonts
//! The default egui fonts only support latin and cryllic characters, and some emojis.
//! To use egui with e.g. asian characters you need to install your own font (`.ttf` or `.otf`) using [`Context::set_fonts`].
//!
//! ## Instrumentation
//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation.
//! You can enable features on the profiling crates in your application to add instrumentation for all
//! crates that support it, including egui. See the profiling crate docs for more information.
//! ```toml
//! [dependencies]
//! profiling = "1.0"
//! [features]
//! profile-with-puffin = ["profiling/profile-with-puffin"]
//! ```
//!
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
@@ -691,33 +703,3 @@ pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) {
pub fn accesskit_root_id() -> Id {
Id::new("accesskit_root")
}
// ---------------------------------------------------------------------------
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[allow(unused_imports)]
pub(crate) use profiling_scopes::{profile_function, profile_scope};

View File

@@ -95,8 +95,13 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
everything_is_visible: bool,
/// Transforms per layer
pub layer_transforms: HashMap<LayerId, TSTransform>,
/// Transforms per layer.
///
/// Instead of using this directly, use:
/// * [`crate::Context::set_transform_layer`]
/// * [`crate::Context::layer_transform_to_global`]
/// * [`crate::Context::layer_transform_from_global`]
pub to_global: HashMap<LayerId, TSTransform>,
// -------------------------------------------------
// Per-viewport:
@@ -120,7 +125,7 @@ impl Default for Memory {
focus: Default::default(),
viewport_id: Default::default(),
areas: Default::default(),
layer_transforms: Default::default(),
to_global: Default::default(),
popup: Default::default(),
everything_is_visible: Default::default(),
add_fonts: Default::default(),
@@ -774,7 +779,7 @@ impl Focus {
impl Memory {
pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) {
crate::profile_function!();
profiling::function_scope!();
self.viewport_id = new_raw_input.viewport_id;
@@ -819,7 +824,7 @@ impl Memory {
/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> {
self.areas()
.layer_id_at(pos, &self.layer_transforms)
.layer_id_at(pos, &self.to_global)
.and_then(|layer_id| {
if self.is_above_modal_layer(layer_id) {
Some(layer_id)
@@ -829,6 +834,12 @@ impl Memory {
})
}
/// The currently set transform of a layer.
#[deprecated = "Use `Context::layer_transform_to_global` instead"]
pub fn layer_transforms(&self, layer_id: LayerId) -> Option<TSTransform> {
self.to_global.get(&layer_id).copied()
}
/// An iterator over all layers. Back-to-front, top is last.
pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ {
self.areas().order().iter().copied()
@@ -1121,15 +1132,18 @@ type OrderMap = HashMap<LayerId, usize>;
pub struct Areas {
areas: IdMap<area::AreaState>,
visible_areas_last_frame: ahash::HashSet<LayerId>,
visible_areas_current_frame: ahash::HashSet<LayerId>,
// ----------------------------
// Everything below this is general to all layers, not just areas.
// TODO(emilk): move this to a separate struct.
/// Back-to-front, top is last.
order: Vec<LayerId>,
/// Actual order of the layers, pre-calculated each frame.
/// Inverse of [`Self::order`], calculated at the end of the frame.
order_map: OrderMap,
visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: ahash::HashSet<LayerId>,
/// When an area wants to be on top, it is assigned here.
/// This is used to reorder the layers at the end of the frame.
/// If several layers want to be on top, they will keep their relative order.
@@ -1137,9 +1151,9 @@ pub struct Areas {
/// results in them being sent to the top and keeping their previous internal order.
wants_to_be_on_top: ahash::HashSet<LayerId>,
/// List of sublayers for each layer.
/// The sublayers that each layer has.
///
/// When a layer has sublayers, they are moved directly above it in the ordering.
/// The parent sublayer is moved directly above the child sublayers in the ordering.
sublayers: ahash::HashMap<LayerId, HashSet<LayerId>>,
}
@@ -1152,17 +1166,13 @@ impl Areas {
self.areas.get(&id)
}
/// Back-to-front, top is last.
/// All layers back-to-front, top is last.
pub(crate) fn order(&self) -> &[LayerId] {
&self.order
}
/// For each layer, which [`Self::order`] is it in?
pub(crate) fn order_map(&self) -> &OrderMap {
&self.order_map
}
/// Compare the order of two layers, based on the order list from last frame.
///
/// 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)) {
@@ -1172,18 +1182,8 @@ impl Areas {
}
}
/// Calculates the order map.
fn calculate_order_map(&mut self) {
self.order_map = self
.order
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect();
}
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) {
self.visible_current_frame.insert(layer_id);
self.visible_areas_current_frame.insert(layer_id);
self.areas.insert(layer_id.id, state);
if !self.order.iter().any(|x| *x == layer_id) {
self.order.push(layer_id);
@@ -1194,15 +1194,15 @@ impl Areas {
pub fn layer_id_at(
&self,
pos: Pos2,
layer_transforms: &HashMap<LayerId, TSTransform>,
layer_to_global: &HashMap<LayerId, TSTransform>,
) -> Option<LayerId> {
for layer in self.order.iter().rev() {
if self.is_visible(layer) {
if let Some(state) = self.areas.get(&layer.id) {
let mut rect = state.rect();
if state.interactable {
if let Some(transform) = layer_transforms.get(layer) {
rect = *transform * rect;
if let Some(to_global) = layer_to_global.get(layer) {
rect = *to_global * rect;
}
if rect.contains(pos) {
@@ -1216,18 +1216,19 @@ impl Areas {
}
pub fn visible_last_frame(&self, layer_id: &LayerId) -> bool {
self.visible_last_frame.contains(layer_id)
self.visible_areas_last_frame.contains(layer_id)
}
pub fn is_visible(&self, layer_id: &LayerId) -> bool {
self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id)
self.visible_areas_last_frame.contains(layer_id)
|| self.visible_areas_current_frame.contains(layer_id)
}
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
self.visible_last_frame
self.visible_areas_last_frame
.iter()
.copied()
.chain(self.visible_current_frame.iter().copied())
.chain(self.visible_areas_current_frame.iter().copied())
.collect()
}
@@ -1240,7 +1241,7 @@ impl Areas {
}
pub fn move_to_top(&mut self, layer_id: LayerId) {
self.visible_current_frame.insert(layer_id);
self.visible_areas_current_frame.insert(layer_id);
self.wants_to_be_on_top.insert(layer_id);
if !self.order.iter().any(|x| *x == layer_id) {
@@ -1255,8 +1256,21 @@ impl Areas {
///
/// This currently only supports one level of nesting. If `parent` is a sublayer of another
/// layer, the behavior is unspecified.
///
/// The two layers must have the same [`LayerId::order`].
pub fn set_sublayer(&mut self, parent: LayerId, child: LayerId) {
debug_assert_eq!(parent.order, child.order,
"DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui", parent.order, child.order);
self.sublayers.entry(parent).or_default().insert(child);
// Make sure the layers are in the order list:
if !self.order.iter().any(|x| *x == parent) {
self.order.push(parent);
}
if !self.order.iter().any(|x| *x == child) {
self.order.push(child);
}
}
pub fn top_layer_id(&self, order: Order) -> Option<LayerId> {
@@ -1267,26 +1281,42 @@ impl Areas {
.copied()
}
/// If this layer is the sublayer of another layer, return the parent.
pub fn parent_layer(&self, layer_id: LayerId) -> Option<LayerId> {
self.sublayers.iter().find_map(|(parent, children)| {
if children.contains(&layer_id) {
Some(*parent)
} else {
None
}
})
}
/// All the child layers of this layer.
pub fn child_layers(&self, layer_id: LayerId) -> impl Iterator<Item = LayerId> + '_ {
self.sublayers.get(&layer_id).into_iter().flatten().copied()
}
pub(crate) fn is_sublayer(&self, layer: &LayerId) -> bool {
self.sublayers
.iter()
.any(|(_, children)| children.contains(layer))
self.parent_layer(*layer).is_some()
}
pub(crate) fn end_pass(&mut self) {
let Self {
visible_last_frame,
visible_current_frame,
visible_areas_last_frame,
visible_areas_current_frame,
order,
wants_to_be_on_top,
sublayers,
..
} = self;
std::mem::swap(visible_last_frame, visible_current_frame);
visible_current_frame.clear();
std::mem::swap(visible_areas_last_frame, visible_areas_current_frame);
visible_areas_current_frame.clear();
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
wants_to_be_on_top.clear();
// For all layers with sublayers, put the sublayers directly after the parent layer:
let sublayers = std::mem::take(sublayers);
for (parent, children) in sublayers {
@@ -1304,7 +1334,13 @@ impl Areas {
};
order.splice(parent_pos..=parent_pos, moved_layers);
}
self.calculate_order_map();
self.order_map = self
.order
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect();
}
}

View File

@@ -406,11 +406,8 @@ impl MenuRoot {
}
}
if let Some(transform) = button
.ctx
.memory(|m| m.layer_transforms.get(&button.layer_id).copied())
{
pos = transform * pos;
if let Some(to_global) = button.ctx.layer_transform_to_global(button.layer_id) {
pos = to_global * pos;
}
return MenuResponse::Create(pos, id);

View File

@@ -248,7 +248,7 @@ impl Default for PassState {
impl PassState {
pub(crate) fn begin_pass(&mut self, screen_rect: Rect) {
crate::profile_function!();
profiling::function_scope!();
let Self {
used_ids,
widgets,

View File

@@ -392,11 +392,8 @@ impl Response {
pub fn drag_delta(&self) -> Vec2 {
if self.dragged() {
let mut delta = self.ctx.input(|i| i.pointer.delta());
if let Some(scaling) = self
.ctx
.memory(|m| m.layer_transforms.get(&self.layer_id).map(|t| t.scaling))
{
delta /= scaling;
if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) {
delta *= from_global.scaling;
}
delta
} else {
@@ -478,11 +475,8 @@ impl Response {
pub fn hover_pos(&self) -> Option<Pos2> {
if self.hovered() {
let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?;
if let Some(transform) = self
.ctx
.memory(|m| m.layer_transforms.get(&self.layer_id).copied())
{
pos = transform.inverse() * pos;
if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) {
pos = from_global * pos;
}
Some(pos)
} else {

View File

@@ -301,7 +301,7 @@ impl Ui {
min_rect: placer.min_rect(),
max_rect: placer.max_rect(),
};
let child_ui = Ui {
let mut child_ui = Ui {
id: stable_id,
unique_id,
next_auto_id_salt,
@@ -316,6 +316,10 @@ impl Ui {
min_rect_already_remembered: false,
};
if disabled {
child_ui.disable();
}
// Register in the widget stack early, to ensure we are behind all widgets we contain:
let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called
child_ui.ctx().create_widget(

View File

@@ -308,7 +308,7 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
use crate::Id;
// TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
/// Stores values identified by an [`Id`] AND a the [`std::any::TypeId`] of the value.
/// Stores values identified by an [`Id`] AND the [`std::any::TypeId`] of the value.
///
/// In other words, it maps `(Id, TypeId)` to any value you want.
///
@@ -574,7 +574,7 @@ struct PersistedMap(Vec<(u64, SerializedElement)>);
#[cfg(feature = "persistence")]
impl PersistedMap {
fn from_map(map: &IdTypeMap) -> Self {
crate::profile_function!();
profiling::function_scope!();
use std::collections::BTreeMap;
@@ -593,7 +593,7 @@ impl PersistedMap {
let max_bytes_per_type = map.max_bytes_per_type;
{
crate::profile_scope!("gather");
profiling::scope!("gather");
for (hash, element) in &map.map {
if let Some(element) = element.to_serialize() {
let stats = types_map.entry(element.type_id).or_default();
@@ -610,7 +610,7 @@ impl PersistedMap {
let mut persisted = vec![];
{
crate::profile_scope!("gc");
profiling::scope!("gc");
for stats in types_map.values() {
let mut bytes_written = 0;
@@ -634,7 +634,7 @@ impl PersistedMap {
}
fn into_map(self) -> IdTypeMap {
crate::profile_function!();
profiling::function_scope!();
let map = self
.0
.into_iter()
@@ -671,7 +671,7 @@ impl serde::Serialize for IdTypeMap {
where
S: serde::Serializer,
{
crate::profile_scope!("IdTypeMap::serialize");
profiling::scope!("IdTypeMap::serialize");
PersistedMap::from_map(self).serialize(serializer)
}
}
@@ -682,7 +682,7 @@ impl<'de> serde::Deserialize<'de> for IdTypeMap {
where
D: serde::Deserializer<'de>,
{
crate::profile_scope!("IdTypeMap::deserialize");
profiling::scope!("IdTypeMap::deserialize");
<PersistedMap>::deserialize(deserializer).map(PersistedMap::into_map)
}
}

View File

@@ -188,7 +188,7 @@ impl std::fmt::Debug for IconData {
impl From<IconData> for epaint::ColorImage {
fn from(icon: IconData) -> Self {
crate::profile_function!();
profiling::function_scope!();
let IconData {
rgba,
width,
@@ -200,7 +200,7 @@ impl From<IconData> for epaint::ColorImage {
impl From<&IconData> for epaint::ColorImage {
fn from(icon: &IconData) -> Self {
crate::profile_function!();
profiling::function_scope!();
let IconData {
rgba,
width,
@@ -1056,7 +1056,7 @@ pub enum ViewportCommand {
/// Enable mouse pass-through: mouse clicks pass through the window, used for non-interactable overlays.
MousePassthrough(bool),
/// Take a screenshot.
/// Take a screenshot of the next frame after this.
///
/// The results are returned in [`crate::Event::Screenshot`].
Screenshot(crate::UserData),

View File

@@ -20,10 +20,10 @@ pub struct WidgetRect {
/// What layer the widget is on.
pub layer_id: LayerId,
/// The full widget rectangle.
/// The full widget rectangle, in local layer coordinates.
pub rect: Rect,
/// Where the widget is.
/// Where the widget is, in local layer coordinates.
///
/// This is after clipping with the parent ui clip rect.
pub interact_rect: Rect,
@@ -42,6 +42,27 @@ pub struct WidgetRect {
pub enabled: bool,
}
impl WidgetRect {
pub fn transform(self, transform: emath::TSTransform) -> Self {
let Self {
id,
layer_id,
rect,
interact_rect,
sense,
enabled,
} = self;
Self {
id,
layer_id,
rect: transform * rect,
interact_rect: transform * interact_rect,
sense,
enabled,
}
}
}
/// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame.
///
/// All [`crate::Ui`]s have a [`WidgetRect`]. It is created in [`crate::Ui::new`] with [`Rect::NOTHING`]

View File

@@ -165,8 +165,11 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color
/// * `x_value` - X axis, either saturation or value (0.0-1.0).
/// * `y_value` - Y axis, either saturation or value (0.0-1.0).
/// * `color_at` - A function that dictates how the mix of saturation and value will be displayed in the 2d slider.
/// E.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows: top-left: white \[s: 0.0, v: 1.0], top-right: fully saturated color \[s: 1.0, v: 1.0], bottom-right: black \[s: 0.0, v: 1.0].
///
/// e.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows:
/// * top-left: white `[s: 0.0, v: 1.0]`
/// * top-right: fully saturated color `[s: 1.0, v: 1.0]`
/// * bottom-right: black `[s: 0.0, v: 1.0].`
fn color_slider_2d(
ui: &mut Ui,
x_value: &mut f32,

View File

@@ -766,14 +766,15 @@ impl<'t> TextEdit<'t> {
}
// Set IME output (in screen coords) when text is editable and visible
let transform = ui
.memory(|m| m.layer_transforms.get(&ui.layer_id()).copied())
let to_global = ui
.ctx()
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
ui.ctx().output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
rect: transform * rect,
cursor_rect: transform * primary_cursor_rect,
rect: to_global * rect,
cursor_rect: to_global * primary_cursor_rect,
});
});
}

View File

@@ -89,6 +89,7 @@ impl TextEditState {
self.undoer.lock().clone()
}
#[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability
pub fn set_undoer(&mut self, undoer: TextEditUndoer) {
*self.undoer.lock() = undoer;
}

View File

@@ -8,6 +8,9 @@ rust-version.workspace = true
publish = false
default-run = "egui_demo_app"
[package.metadata.cargo-machete]
ignored = ["profiling"]
[lints]
workspace = true
@@ -25,10 +28,15 @@ default = ["glow", "persistence"]
# image_viewer adds about 0.9 MB of WASM
web_app = ["http", "persistence"]
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
image_viewer = ["image", "egui_extras/all_loaders", "rfd"]
persistence = ["eframe/persistence", "egui/persistence", "serde", "egui_extras/serde"]
puffin = ["eframe/puffin", "dep:puffin", "dep:puffin_http"]
http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"]
image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"]
persistence = [
"eframe/persistence",
"egui_extras/serde",
"egui/persistence",
"serde",
]
puffin = ["dep:puffin", "dep:puffin_http", "profiling/profile-with-puffin"]
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
syntect = ["egui_demo_lib/syntect"]
@@ -48,7 +56,12 @@ eframe = { workspace = true, default-features = false, features = [
egui = { workspace = true, features = ["callstack", "default", "log"] }
egui_demo_lib = { workspace = true, features = ["default", "chrono"] }
egui_extras = { workspace = true, features = ["default", "image"] }
image = { workspace = true, default-features = false, features = [
# Ensure we can display the test images
"png",
] }
log.workspace = true
profiling.workspace = true
# Optional dependencies:
@@ -61,7 +74,6 @@ wgpu = { workspace = true, features = ["webgpu", "webgl"], optional = true }
# feature "http":
ehttp = { version = "0.5", optional = true }
image = { workspace = true, optional = true, features = ["jpeg", "png"] }
poll-promise = { version = "0.3", optional = true, default-features = false }
# feature "persistence":
@@ -74,7 +86,7 @@ env_logger = { version = "0.10", default-features = false, features = [
"auto-color",
"humantime",
] }
rfd = { version = "0.13", optional = true }
rfd = { version = "0.15", optional = true }
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@@ -32,7 +32,7 @@ impl FrameHistory {
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
pub fn ui(&self, ui: &mut egui::Ui) {
ui.label(format!(
"Mean CPU usage: {:.2} ms / frame",
1e3 * self.mean_frame_time()

View File

@@ -51,6 +51,7 @@ fn main() -> eframe::Result {
..Default::default()
};
eframe::run_native(
"egui demo app",
options,

View File

@@ -111,11 +111,19 @@ impl Anchor {
Self::Rendering,
]
}
#[cfg(target_arch = "wasm32")]
fn from_str_case_insensitive(anchor: &str) -> Option<Self> {
let anchor = anchor.to_lowercase();
Self::all().into_iter().find(|x| x.to_string() == anchor)
}
}
impl std::fmt::Display for Anchor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
let mut name = format!("{self:?}");
name.make_ascii_lowercase();
f.write_str(&name)
}
}
@@ -263,11 +271,15 @@ impl eframe::App for WrapApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
#[cfg(target_arch = "wasm32")]
if let Some(anchor) = frame.info().web_info.location.hash.strip_prefix('#') {
let anchor = Anchor::all().into_iter().find(|x| x.to_string() == anchor);
if let Some(v) = anchor {
self.state.selected_anchor = v;
}
if let Some(anchor) = frame
.info()
.web_info
.location
.hash
.strip_prefix('#')
.and_then(Anchor::from_str_case_insensitive)
{
self.state.selected_anchor = anchor;
}
#[cfg(not(target_arch = "wasm32"))]

View File

@@ -55,14 +55,11 @@ serde = { workspace = true, optional = true }
[dev-dependencies]
# when running tests we always want to use the `chrono` feature
egui_demo_lib = { workspace = true, features = ["chrono"] }
rand = "0.8"
criterion.workspace = true
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
wgpu = { workspace = true, features = ["metal"] }
egui = { workspace = true, features = ["default_fonts"] }
rand = "0.8"
[[bench]]
name = "benchmark"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -38,6 +38,7 @@ impl Default for Demos {
Box::<super::painting::Painting>::default(),
Box::<super::pan_zoom::PanZoom>::default(),
Box::<super::panels::Panels>::default(),
Box::<super::screenshot::Screenshot>::default(),
Box::<super::scrolling::Scrolling>::default(),
Box::<super::sliders::Sliders>::default(),
Box::<super::strip_demo::StripDemo>::default(),

View File

@@ -24,6 +24,7 @@ pub mod painting;
pub mod pan_zoom;
pub mod panels;
pub mod password;
pub mod screenshot;
pub mod scrolling;
pub mod sliders;
pub mod strip_demo;

View File

@@ -0,0 +1,84 @@
use egui::{Image, UserData, ViewportCommand, Widget};
use std::sync::Arc;
/// Showcase [`ViewportCommand::Screenshot`].
#[derive(PartialEq, Eq, Default)]
pub struct Screenshot {
image: Option<(Arc<egui::ColorImage>, egui::TextureHandle)>,
continuous: bool,
}
impl crate::Demo for Screenshot {
fn name(&self) -> &'static str {
"📷 Screenshot"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.open(open)
.resizable(false)
.default_width(250.0)
.show(ctx, |ui| {
use crate::View as _;
self.ui(ui);
});
}
}
impl crate::View for Screenshot {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.set_width(300.0);
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("This demo showcases how to take screenshots via ");
ui.code("ViewportCommand::Screenshot");
ui.label(".");
});
ui.horizontal_top(|ui| {
let capture = ui.button("📷 Take Screenshot").clicked();
ui.checkbox(&mut self.continuous, "Capture continuously");
if capture || self.continuous {
ui.ctx()
.send_viewport_cmd(ViewportCommand::Screenshot(UserData::default()));
}
});
let image = ui.ctx().input(|i| {
i.events
.iter()
.filter_map(|e| {
if let egui::Event::Screenshot { image, .. } = e {
Some(image.clone())
} else {
None
}
})
.last()
});
if let Some(image) = image {
self.image = Some((
image.clone(),
ui.ctx()
.load_texture("screenshot_demo", image, Default::default()),
));
}
if let Some((_, texture)) = &self.image {
Image::new(texture).shrink_to_fit().ui(ui);
} else {
ui.group(|ui| {
ui.set_width(ui.available_width());
ui.set_height(100.0);
ui.centered_and_justified(|ui| {
ui.label("No screenshot taken yet.");
});
});
}
}
}

View File

@@ -286,6 +286,7 @@ fn doc_link_label_with_crate<'a>(
}
}
#[cfg(feature = "chrono")]
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:579a7a66f86ade628e9f469b0014e9010aa56312ad5bd1e8de2faaae7e0d1af6
size 23770

View File

@@ -5,6 +5,12 @@ 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.30.0 - 2024-12-16
* Use `Table::id_salt` on `ScrollArea` [#5282](https://github.com/emilk/egui/pull/5282) by [@jwhear](https://github.com/jwhear)
* Use proper `image` crate URI and MIME support detection [#5324](https://github.com/emilk/egui/pull/5324) by [@xangelix](https://github.com/xangelix)
* Support loading images with weird urls and improve error message [#5431](https://github.com/emilk/egui/pull/5431) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.29.1 - 2024-10-01 - Fix table interaction
* Bug fix: click anywhere on a `Table` row to select it [#5193](https://github.com/emilk/egui/pull/5193) by [@emilk](https://github.com/emilk)

View File

@@ -53,11 +53,6 @@ http = ["dep:ehttp"]
## ```
image = ["dep:image"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
puffin = ["dep:puffin", "egui/puffin"]
## Derive serde Serialize/Deserialize on stateful structs
serde = ["egui/serde", "dep:serde"]
@@ -74,6 +69,7 @@ egui = { workspace = true, default-features = false }
ahash.workspace = true
enum-map = { version = "2", features = ["serde"] }
log.workspace = true
profiling.workspace = true
#! ### Optional dependencies
@@ -96,7 +92,6 @@ image = { workspace = true, optional = true }
# file feature
mime_guess2 = { version = "2", optional = true, default-features = false }
puffin = { workspace = true, optional = true }
syntect = { version = "5", optional = true, default-features = false, features = [
"default-fancy",

View File

@@ -199,7 +199,7 @@ impl RetainedImage {
/// On invalid image or unsupported image format.
#[cfg(feature = "image")]
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::load::LoadError> {
crate::profile_function!();
profiling::function_scope!();
let image = image::load_from_memory(image_bytes).map_err(|err| match err {
image::ImageError::Unsupported(err) => match err.kind() {
image::error::UnsupportedErrorKind::Format(format) => {
@@ -245,7 +245,8 @@ pub fn load_svg_bytes_with_size(
use resvg::tiny_skia::{IntSize, Pixmap};
use resvg::usvg::{Options, Tree, TreeParsing};
crate::profile_function!();
profiling::function_scope!();
let opt = Options::default();
let mut rtree = Tree::from_data(svg_bytes, &opt).map_err(|err| err.to_string())?;

View File

@@ -37,36 +37,6 @@ pub use loaders::install_image_loaders;
// ---------------------------------------------------------------------------
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[allow(unused_imports)]
pub(crate) use profiling_scopes::profile_function;
// ---------------------------------------------------------------------------
/// Panic in debug builds, log otherwise.
macro_rules! log_or_panic {
($fmt: literal) => {$crate::log_or_panic!($fmt,)};

View File

@@ -78,6 +78,12 @@ impl ImageLoader for ImageCrateLoader {
}
}
if bytes.starts_with(b"version https://git-lfs") {
return Err(LoadError::FormatNotSupported {
detected_format: Some("git-lfs".to_owned()),
});
}
// (3)
log::trace!("started loading {uri:?}");
let result = crate::image::load_image_bytes(&bytes).map(Arc::new);

View File

@@ -403,7 +403,7 @@ struct Highlighter {
#[cfg(feature = "syntect")]
impl Default for Highlighter {
fn default() -> Self {
crate::profile_function!();
profiling::function_scope!();
Self {
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
ts: syntect::highlighting::ThemeSet::load_defaults(),
@@ -437,8 +437,7 @@ impl Highlighter {
#[cfg(feature = "syntect")]
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
crate::profile_function!();
profiling::function_scope!();
use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle;
use syntect::util::LinesWithEndings;
@@ -512,7 +511,7 @@ impl Highlighter {
mut text: &str,
language: &str,
) -> Option<LayoutJob> {
crate::profile_function!();
profiling::function_scope!();
let language = Language::new(language)?;

View File

@@ -1227,7 +1227,7 @@ impl<'a> TableBody<'a> {
// Capture the hover information for the just created row. This is used in the next render
// to ensure that the entire row is highlighted.
fn capture_hover_state(&mut self, response: &Option<Response>, row_index: usize) {
fn capture_hover_state(&self, response: &Option<Response>, row_index: usize) {
let is_row_hovered = response.as_ref().map_or(false, |r| r.hovered());
if is_row_hovered {
self.layout

View File

@@ -6,6 +6,10 @@ Changes since the last release can be found at <https://github.com/emilk/egui/co
## 0.30.0 - 2024-12-16
* Update glow to 0.16 [#5395](https://github.com/emilk/egui/pull/5395) by [@sagudev](https://github.com/sagudev)
## 0.29.1 - 2024-10-01
Nothing new

View File

@@ -39,9 +39,6 @@ clipboard = ["egui-winit?/clipboard"]
## enable opening links in a browser when an egui hyperlink is clicked.
links = ["egui-winit?/links"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin", "egui-winit?/puffin", "egui/puffin"]
## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11`
winit = ["egui-winit", "dep:winit"]
@@ -61,14 +58,13 @@ bytemuck.workspace = true
glow.workspace = true
log.workspace = true
memoffset = "0.9"
profiling.workspace = true
#! ### Optional dependencies
## Enable this when generating docs.
document-features = { workspace = true, optional = true }
# Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
puffin = { workspace = true, optional = true }
winit = { workspace = true, optional = true, default-features = false, features = ["rwh_06"] }
# Web:

View File

@@ -110,33 +110,3 @@ pub fn check_for_gl_error_impl(gl: &glow::Context, file: &str, line: u32, contex
}
}
}
// ---------------------------------------------------------------------------
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[allow(unused_imports)]
pub(crate) use profiling_scopes::{profile_function, profile_scope};

View File

@@ -144,7 +144,7 @@ impl Painter {
shader_version: Option<ShaderVersion>,
dithering: bool,
) -> Result<Self, PainterError> {
crate::profile_function!();
profiling::function_scope!();
crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new");
// some useful debug info. all three of them are present in gl 1.1.
@@ -366,7 +366,7 @@ impl Painter {
clipped_primitives: &[egui::ClippedPrimitive],
textures_delta: &egui::TexturesDelta,
) {
crate::profile_function!();
profiling::function_scope!();
for (id, image_delta) in &textures_delta.set {
self.set_texture(*id, image_delta);
@@ -405,7 +405,7 @@ impl Painter {
pixels_per_point: f32,
clipped_primitives: &[egui::ClippedPrimitive],
) {
crate::profile_function!();
profiling::function_scope!();
self.assert_not_destroyed();
unsafe { self.prepare_painting(screen_size_px, pixels_per_point) };
@@ -423,7 +423,7 @@ impl Painter {
}
Primitive::Callback(callback) => {
if callback.rect.is_positive() {
crate::profile_scope!("callback");
profiling::scope!("callback");
let info = egui::PaintCallbackInfo {
viewport: callback.rect,
@@ -508,7 +508,7 @@ impl Painter {
// ------------------------------------------------------------------------
pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) {
crate::profile_function!();
profiling::function_scope!();
self.assert_not_destroyed();
@@ -540,7 +540,7 @@ impl Painter {
);
let data: Vec<u8> = {
crate::profile_scope!("font -> sRGBA");
profiling::scope!("font -> sRGBA");
image
.srgba_pixels(None)
.flat_map(|a| a.to_array())
@@ -559,7 +559,7 @@ impl Painter {
options: egui::TextureOptions,
data: &[u8],
) {
crate::profile_function!();
profiling::function_scope!();
assert_eq!(data.len(), w * h * 4);
assert!(
w <= self.max_texture_side && h <= self.max_texture_side,
@@ -610,7 +610,7 @@ impl Painter {
let level = 0;
if let Some([x, y]) = pos {
crate::profile_scope!("gl.tex_sub_image_2d");
profiling::scope!("gl.tex_sub_image_2d");
self.gl.tex_sub_image_2d(
glow::TEXTURE_2D,
level,
@@ -625,7 +625,7 @@ impl Painter {
check_for_gl_error!(&self.gl, "tex_sub_image_2d");
} else {
let border = 0;
crate::profile_scope!("gl.tex_image_2d");
profiling::scope!("gl.tex_image_2d");
self.gl.tex_image_2d(
glow::TEXTURE_2D,
level,
@@ -675,7 +675,7 @@ impl Painter {
}
pub fn read_screen_rgba(&self, [w, h]: [u32; 2]) -> egui::ColorImage {
crate::profile_function!();
profiling::function_scope!();
let mut pixels = vec![0_u8; (w * h * 4) as usize];
unsafe {
@@ -700,8 +700,7 @@ impl Painter {
}
pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec<u8> {
crate::profile_function!();
profiling::function_scope!();
let mut pixels = vec![0_u8; (w * h * 3) as usize];
unsafe {
self.gl.read_pixels(
@@ -748,7 +747,7 @@ impl Painter {
}
pub fn clear(gl: &glow::Context, screen_size_in_pixels: [u32; 2], clear_color: [f32; 4]) {
crate::profile_function!();
profiling::function_scope!();
unsafe {
gl.disable(glow::SCISSOR_TEST);

View File

@@ -0,0 +1,12 @@
# Changelog for egui_kittest
All notable changes to the `egui_kittest` crate will be noted in this file.
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.30.0 - 2024-12-16 - Initial relrease
* Support for egui 0.30.0
* Automate clicks and text input
* Automatic screenshot testing with wgpu

View File

@@ -1,7 +1,10 @@
[package]
name = "egui_kittest"
version.workspace = true
authors = ["Lucas Meurer <lucasmeurer96@gmail.com>", "Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
authors = [
"Lucas Meurer <lucasmeurer96@gmail.com>",
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
]
description = "Testing library for egui based on kittest and AccessKit"
edition.workspace = true
rust-version.workspace = true
@@ -39,10 +42,9 @@ dify = { workspace = true, optional = true }
document-features = { workspace = true, optional = true }
[dev-dependencies]
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
wgpu = { workspace = true, features = ["metal"] }
image = { workspace = true, features = ["png"] }
egui = { workspace = true, features = ["default_fonts"] }
image = { workspace = true, features = ["png"] }
wgpu = { workspace = true, features = ["metal"] }
[lints]
workspace = true

View File

@@ -4,21 +4,16 @@ Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kitt
## Example usage
```rust
use egui::accesskit::{Role, Toggled};
use egui::{CentralPanel, Context, TextEdit, Vec2};
use egui_kittest::Harness;
use kittest::Queryable;
use std::cell::RefCell;
use egui::accesskit::Toggled;
use egui_kittest::{Harness, kittest::Queryable};
fn main() {
let mut checked = false;
let app = |ctx: &Context| {
CentralPanel::default().show(ctx, |ui| {
ui.checkbox(&mut checked, "Check me!");
});
let app = |ui: &mut egui::Ui| {
ui.checkbox(&mut checked, "Check me!");
};
let mut harness = Harness::builder().with_size(egui::Vec2::new(200.0, 100.0)).build(app);
let mut harness = Harness::new_ui(app);
let checkbox = harness.get_by_label("Check me!");
assert_eq!(checkbox.toggled(), Some(Toggled::False));
@@ -28,6 +23,9 @@ fn main() {
let checkbox = harness.get_by_label("Check me!");
assert_eq!(checkbox.toggled(), Some(Toggled::True));
// Shrink the window size to the smallest size possible
harness.fit_contents();
// You can even render the ui and do image snapshot tests
#[cfg(all(feature = "wgpu", feature = "snapshot"))]

View File

@@ -60,7 +60,7 @@ impl TestRenderer {
}
/// Render the [`Harness`] and return the resulting image.
pub fn render<State>(&mut self, harness: &Harness<'_, State>) -> RgbaImage {
pub fn render<State>(&self, harness: &Harness<'_, State>) -> RgbaImage {
// We need to create a new renderer each time we render, since the renderer stores
// textures related to the Harnesses' egui Context.
// Calling the renderer from different Harnesses would cause problems if we store the renderer.

View File

@@ -1,8 +1,9 @@
//! Tests the accesskit accessibility output of egui.
#![cfg(feature = "accesskit")]
use accesskit::{NodeId, Role, TreeUpdate};
use egui::{CentralPanel, Context, RawInput, Window};
use egui::{
accesskit::{NodeId, Role, TreeUpdate},
CentralPanel, Context, RawInput, Window,
};
/// Baseline test that asserts there are no spurious nodes in the
/// accesskit output when the ui is empty.

View File

@@ -1,6 +1,5 @@
use egui::Button;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use egui_kittest::{kittest::Queryable, Harness};
#[test]
pub fn focus_should_skip_over_disabled_buttons() {

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:36c1b432140456ea5cbb687076b1c910aea8b31affd33a0ece22218f60af2d6e
size 2296
oid sha256:31bd906040fcc356c19dc36036fbfd2a28dfcef54c7a073f584f4a9abddbdb4c
size 1699

View File

@@ -10,5 +10,6 @@ fn test_shrink() {
harness.fit_contents();
#[cfg(all(feature = "snapshot", feature = "wgpu"))]
harness.wgpu_snapshot("test_shrink");
}

View File

@@ -649,7 +649,11 @@ impl Rect {
///
/// A ray that starts inside the rect will return `true`.
pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool {
debug_assert!(d.is_normalized(), "expected normalized direction");
debug_assert!(
d.is_normalized(),
"expected normalized direction, but `d` has length {}",
d.length()
);
let mut tmin = -f32::INFINITY;
let mut tmax = f32::INFINITY;
@@ -677,7 +681,11 @@ impl Rect {
///
/// `d` is the direction of the ray and assumed to be normalized.
pub fn intersects_ray_from_center(&self, d: Vec2) -> Pos2 {
debug_assert!(d.is_normalized(), "expected normalized direction");
debug_assert!(
d.is_normalized(),
"expected normalized direction, but `d` has length {}",
d.length()
);
let mut tmin = f32::NEG_INFINITY;
let mut tmax = f32::INFINITY;

View File

@@ -5,6 +5,13 @@ 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.30.0 - 2024-12-16
* Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic)
* Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ)
* Reduce aliasing when painting thin box outlines [#5484](https://github.com/emilk/egui/pull/5484) by [@emilk](https://github.com/emilk)
* Fix zero-width strokes still affecting the feathering color of boxes [#5485](https://github.com/emilk/egui/pull/5485) by [@emilk](https://github.com/emilk)
## 0.29.1 - 2024-10-01
Nothing new

View File

@@ -55,11 +55,6 @@ log = ["dep:log"]
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
mint = ["emath/mint"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
puffin = ["dep:puffin"]
## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon).
##
## This can help performance for graphics-intense applications.
@@ -79,6 +74,7 @@ ab_glyph = "0.2.11"
ahash.workspace = true
nohash-hasher.workspace = true
parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
profiling = { workspace = true}
#! ### Optional dependencies
bytemuck = { workspace = true, optional = true, features = ["derive"] }
@@ -87,7 +83,6 @@ bytemuck = { workspace = true, optional = true, features = ["derive"] }
document-features = { workspace = true, optional = true }
log = { workspace = true, optional = true }
puffin = { workspace = true, optional = true }
rayon = { version = "1.7", optional = true }
## Allow serialization using [`serde`](https://docs.rs/serde) .

View File

@@ -207,17 +207,21 @@ impl CubicBezierShape {
/// B.x = (P3.x - 3 * P2.x + 3 * P1.x - P0.x) * t^3 + (3 * P2.x - 6 * P1.x + 3 * P0.x) * t^2 + (3 * P1.x - 3 * P0.x) * t + P0.x
/// B.y = (P3.y - 3 * P2.y + 3 * P1.y - P0.y) * t^3 + (3 * P2.y - 6 * P1.y + 3 * P0.y) * t^2 + (3 * P1.y - 3 * P0.y) * t + P0.y
/// Combine the above three equations and iliminate B.x and B.y, we get:
/// ```text
/// t^3 * ( (P3.x - 3*P2.x + 3*P1.x - P0.x) * (P3.y - P0.y) - (P3.y - 3*P2.y + 3*P1.y - P0.y) * (P3.x - P0.x))
/// + t^2 * ( (3 * P2.x - 6 * P1.x + 3 * P0.x) * (P3.y - P0.y) - (3 * P2.y - 6 * P1.y + 3 * P0.y) * (P3.x - P0.x))
/// + t^1 * ( (3 * P1.x - 3 * P0.x) * (P3.y - P0.y) - (3 * P1.y - 3 * P0.y) * (P3.x - P0.x))
/// + (P0.x * (P3.y - P0.y) - P0.y * (P3.x - P0.x)) + P0.x * (P0.y - P3.y) + P0.y * (P3.x - P0.x)
/// = 0
/// or a * t^3 + b * t^2 + c * t + d = 0
/// ```
/// or `a * t^3 + b * t^2 + c * t + d = 0`
///
/// let x = t - b / (3 * a), then we have:
/// ```text
/// x^3 + p * x + q = 0, where:
/// p = (3.0 * a * c - b^2) / (3.0 * a^2)
/// q = (2.0 * b^3 - 9.0 * a * b * c + 27.0 * a^2 * d) / (27.0 * a^3)
/// ```
///
/// when p > 0, there will be one real root, two complex roots
/// when p = 0, there will be two real roots, when p=q=0, there will be three real roots but all 0.

View File

@@ -154,8 +154,10 @@ impl ColorImage {
let max_x = (region.max.x * pixels_per_point) as usize;
let min_y = (region.min.y * pixels_per_point) as usize;
let max_y = (region.max.y * pixels_per_point) as usize;
assert!(min_x <= max_x);
assert!(min_y <= max_y);
assert!(
min_x <= max_x && min_y <= max_y,
"Screenshot region is invalid: {region:?}"
);
let width = max_x - min_x;
let height = max_y - min_y;
let mut output = Vec::with_capacity(width * height);

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