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:
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/deploy_web_demo.yml
vendored
2
.github/workflows/deploy_web_demo.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
with:
|
||||
profile: minimal
|
||||
target: wasm32-unknown-unknown
|
||||
toolchain: 1.79.0
|
||||
toolchain: 1.80.0
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
11
.github/workflows/png_only_on_lfs.yml
vendored
11
.github/workflows/png_only_on_lfs.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/preview_build.yml
vendored
6
.github/workflows/preview_build.yml
vendored
@@ -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:
|
||||
|
||||
9
.github/workflows/preview_cleanup.yml
vendored
9
.github/workflows/preview_cleanup.yml
vendored
@@ -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:
|
||||
|
||||
6
.github/workflows/preview_deploy.yml
vendored
6
.github/workflows/preview_deploy.yml
vendored
@@ -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
|
||||
|
||||
14
.github/workflows/rust.yml
vendored
14
.github/workflows/rust.yml
vendored
@@ -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
|
||||
|
||||
97
CHANGELOG.md
97
CHANGELOG.md
@@ -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)
|
||||
|
||||
831
Cargo.lock
831
Cargo.lock
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Section identical to scripts/clippy_wasm/clippy.toml:
|
||||
|
||||
msrv = "1.79"
|
||||
msrv = "1.80"
|
||||
|
||||
allow-unwrap-in-tests = true
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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 || {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
257
crates/egui-wgpu/src/capture.rs
Normal file
257
crates/egui-wgpu/src/capture.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
43
crates/egui-wgpu/src/texture_copy.wgsl
Normal file
43
crates/egui-wgpu/src/texture_copy.wgsl
Normal 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);
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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…");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -51,6 +51,7 @@ fn main() -> eframe::Result {
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"egui demo app",
|
||||
options,
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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 |
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
84
crates/egui_demo_lib/src/demo/screenshot.rs
Normal file
84
crates/egui_demo_lib/src/demo/screenshot.rs
Normal 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.");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,6 +286,7 @@ fn doc_link_label_with_crate<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:579a7a66f86ade628e9f469b0014e9010aa56312ad5bd1e8de2faaae7e0d1af6
|
||||
size 23770
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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,)};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
12
crates/egui_kittest/CHANGELOG.md
Normal file
12
crates/egui_kittest/CHANGELOG.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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() {
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:36c1b432140456ea5cbb687076b1c910aea8b31affd33a0ece22218f60af2d6e
|
||||
size 2296
|
||||
oid sha256:31bd906040fcc356c19dc36036fbfd2a28dfcef54c7a073f584f4a9abddbdb4c
|
||||
size 1699
|
||||
|
||||
@@ -10,5 +10,6 @@ fn test_shrink() {
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
#[cfg(all(feature = "snapshot", feature = "wgpu"))]
|
||||
harness.wgpu_snapshot("test_shrink");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) .
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user