mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Merge branch 'main' into common-panels
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,6 +1,7 @@
|
||||
* text=auto eol=lf
|
||||
Cargo.lock linguist-generated=false
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Exclude some small files from LFS:
|
||||
crates/eframe/data/* !filter !diff !merge text=auto eol=lf
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -10,13 +10,13 @@ assignees: ''
|
||||
<!--
|
||||
First look if there is already a similar bug report. If there is, upvote the issue with 👍
|
||||
|
||||
Please also check if the bug is still present in latest master! Do so by adding the following lines to your Cargo.toml:
|
||||
Please also check if the bug is still present in latest main! Do so by adding the following lines to your Cargo.toml:
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
egui = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||
egui = { git = "https://github.com/emilk/egui", branch = "main" }
|
||||
# if you're using eframe:
|
||||
eframe = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||
eframe = { git = "https://github.com/emilk/egui", branch = "main" }
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) before opening a Pull Request!
|
||||
Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request!
|
||||
|
||||
* Keep your PR:s small and focused.
|
||||
* The PR title is what ends up in the changelog, so make it descriptive!
|
||||
|
||||
13
.github/workflows/cargo_machete.yml
vendored
13
.github/workflows/cargo_machete.yml
vendored
@@ -5,8 +5,15 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
cargo-machete:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.88
|
||||
- name: Machete install
|
||||
## The official cargo-machete action
|
||||
uses: bnjbvr/cargo-machete@v0.9.1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Machete
|
||||
run: cargo install cargo-machete --locked && cargo machete
|
||||
uses: actions/checkout@v4
|
||||
- name: Machete Check
|
||||
run: cargo machete
|
||||
|
||||
18
.github/workflows/deploy_web_demo.yml
vendored
18
.github/workflows/deploy_web_demo.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: Deploy web demo
|
||||
|
||||
on:
|
||||
# We only run this on merges to master
|
||||
# We only run this on merges to main
|
||||
push:
|
||||
branches: ["master"]
|
||||
branches: ["main"]
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
|
||||
@@ -11,7 +11,6 @@ on:
|
||||
# release:
|
||||
# types: ["published"]
|
||||
|
||||
|
||||
permissions:
|
||||
contents: write # for committing to gh-pages branch
|
||||
|
||||
@@ -29,8 +28,8 @@ jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
name: Deploy web demo
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -39,16 +38,19 @@ jobs:
|
||||
with:
|
||||
profile: minimal
|
||||
target: wasm32-unknown-unknown
|
||||
toolchain: 1.81.0
|
||||
toolchain: 1.88.0
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: "web-demo-"
|
||||
|
||||
- name: "Install wasmopt / binaryen"
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install binaryen
|
||||
- name: Install wasm-opt
|
||||
uses: sigoden/install-binary@v1
|
||||
with:
|
||||
repo: WebAssembly/binaryen
|
||||
tag: version_123
|
||||
name: wasm-opt
|
||||
|
||||
- run: |
|
||||
scripts/build_demo_web.sh --release
|
||||
|
||||
35
.github/workflows/enforce_branch_name.yml
vendored
Normal file
35
.github/workflows/enforce_branch_name.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: PR Branch Name Check
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-source-branch:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check PR source branch
|
||||
run: |
|
||||
# Check if PR is from a fork
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
# Check if PR is from the master/main branch of a fork
|
||||
if [[ "${{ github.event.pull_request.head.ref }}" == "master" || "${{ github.event.pull_request.head.ref }}" == "main" ]]; then
|
||||
echo "ERROR: Pull requests from the master/main branch of forks are not allowed, because it prevents maintainers from contributing to your PR"
|
||||
echo "Please create a feature branch in your fork and submit the PR from that branch instead."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Leave comment if PR is from master/main branch of fork d
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '⚠️ **ERROR:** Pull requests from the `master`/`main` branch of forks are not allowed, because it prevents maintainers from contributing to your PR. Please create a feature branch in your fork and submit the PR from that branch instead.'
|
||||
})
|
||||
3
.github/workflows/labels.yml
vendored
3
.github/workflows/labels.yml
vendored
@@ -16,6 +16,7 @@ on:
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check for a "do-not-merge" label
|
||||
uses: mheap/github-action-required-labels@v3
|
||||
@@ -29,4 +30,4 @@ jobs:
|
||||
with:
|
||||
mode: minimum
|
||||
count: 1
|
||||
labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_kittest, egui-wgpu, egui-winit, egui, epaint, epaint_default_fonts, exclude from changelog, typo"
|
||||
labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_kittest, egui-wgpu, egui-winit, egui, emath, epaint, epaint_default_fonts, exclude from changelog, typo"
|
||||
|
||||
3
.github/workflows/png_only_on_lfs.yml
vendored
3
.github/workflows/png_only_on_lfs.yml
vendored
@@ -5,6 +5,7 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
check-binary-files:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -25,7 +26,7 @@ jobs:
|
||||
exclude_pattern=$(printf "|^%s" "${exclude_paths[@]}" | sed 's/^|//')
|
||||
|
||||
if comm -23 <(git ls-files | grep -Ev "$exclude_pattern" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then
|
||||
echo "Error: Found binary file with extension .$ext not tracked by git LFS. See CONTRIBUTING.md"
|
||||
echo "Error: Found binary file with extension .$ext not tracked by git LFS. See https://github.com/emilk/egui/blob/main/CONTRIBUTING.md#working-with-git-lfs"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
17
.github/workflows/preview_build.yml
vendored
17
.github/workflows/preview_build.yml
vendored
@@ -15,16 +15,23 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: rustup toolchain install stable --profile minimal --target wasm32-unknown-unknown
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.88.0
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: "pr-preview-"
|
||||
|
||||
- name: "Install wasmopt / binaryen"
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install binaryen
|
||||
- name: Install wasm-opt
|
||||
uses: sigoden/install-binary@v1
|
||||
with:
|
||||
repo: WebAssembly/binaryen
|
||||
tag: version_123
|
||||
name: wasm-opt
|
||||
|
||||
- run: |
|
||||
scripts/build_demo_web.sh --release
|
||||
|
||||
1
.github/workflows/preview_cleanup.yml
vendored
1
.github/workflows/preview_cleanup.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
23
.github/workflows/preview_comment.yml
vendored
Normal file
23
.github/workflows/preview_comment.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: preview_comment.yml
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Comment PR
|
||||
uses: thollander/actions-comment-pull-request@v2
|
||||
env:
|
||||
URL_SLUG: ${{ github.event.number }}-${{ github.head_ref }}
|
||||
with:
|
||||
message: |
|
||||
Preview is being built...
|
||||
|
||||
Preview will be available at https://egui-pr-preview.github.io/pr/${{ env.URL_SLUG }}
|
||||
|
||||
View snapshot changes at [kitdiff](https://rerun-io.github.io/kitdiff/?url=${{ github.event.pull_request.html_url }})
|
||||
comment_tag: 'egui-preview'
|
||||
|
||||
3
.github/workflows/preview_deploy.yml
vendored
3
.github/workflows/preview_deploy.yml
vendored
@@ -20,6 +20,7 @@ concurrency:
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -60,5 +61,7 @@ jobs:
|
||||
message: |
|
||||
Preview available at https://egui-pr-preview.github.io/pr/${{ env.URL_SLUG }}
|
||||
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.
|
||||
|
||||
View snapshot changes at [kitdiff](https://rerun-io.github.io/kitdiff/?url=https://github.com/emilk/egui/pull/${{ env.PR_NUMBER }})
|
||||
pr_number: ${{ env.PR_NUMBER }}
|
||||
comment_tag: 'egui-preview'
|
||||
|
||||
100
.github/workflows/rust.yml
vendored
100
.github/workflows/rust.yml
vendored
@@ -5,20 +5,21 @@ name: Rust
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
NIGHTLY_VERSION: nightly-2024-09-11
|
||||
NIGHTLY_VERSION: nightly-2025-09-16
|
||||
|
||||
jobs:
|
||||
fmt-crank-check-test:
|
||||
name: Format + check
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.81.0
|
||||
toolchain: 1.88.0
|
||||
|
||||
- name: Install packages (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
@@ -37,39 +38,28 @@ jobs:
|
||||
- name: Lint vertical spacing
|
||||
run: ./scripts/lint.py
|
||||
|
||||
- name: check --all-features
|
||||
run: cargo check --locked --all-features --all-targets
|
||||
- run: cargo clippy --locked --all-features --all-targets
|
||||
|
||||
- name: check egui_extras --all-features
|
||||
run: cargo check --locked --all-features -p egui_extras
|
||||
- run: cargo clippy --locked --all-features -p egui_extras
|
||||
|
||||
- name: check default features
|
||||
run: cargo check --locked --all-targets
|
||||
- run: cargo clippy --locked --all-targets
|
||||
|
||||
- name: check --no-default-features
|
||||
run: cargo check --locked --no-default-features --lib --all-targets
|
||||
- run: cargo clippy --locked --no-default-features --lib --all-targets
|
||||
|
||||
- name: check eframe --no-default-features
|
||||
run: cargo check --locked --no-default-features --features x11 --lib -p eframe
|
||||
- run: cargo clippy --locked --no-default-features --lib -p eframe --features x11
|
||||
|
||||
- name: check egui_extras --no-default-features
|
||||
run: cargo check --locked --no-default-features --lib -p egui_extras
|
||||
- run: cargo clippy --locked --no-default-features --lib -p eframe --features x11,wgpu_no_default_features
|
||||
|
||||
- name: check epaint --no-default-features
|
||||
run: cargo check --locked --no-default-features --lib -p epaint
|
||||
- run: cargo clippy --locked --no-default-features --lib -p egui_extras
|
||||
|
||||
- run: cargo clippy --locked --no-default-features --lib -p epaint
|
||||
|
||||
# Regression test for https://github.com/emilk/egui/issues/4771
|
||||
- name: cargo check -p test_egui_extras_compilation
|
||||
run: cargo check -p test_egui_extras_compilation
|
||||
- run: cargo clippy -p test_egui_extras_compilation
|
||||
|
||||
- name: cargo doc --lib
|
||||
run: cargo doc --lib --no-deps --all-features
|
||||
- run: cargo doc --lib --no-deps --all-features
|
||||
|
||||
- name: cargo doc --document-private-items
|
||||
run: cargo doc --document-private-items --no-deps --all-features
|
||||
|
||||
- name: clippy
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
- run: cargo doc --document-private-items --no-deps --all-features
|
||||
|
||||
- name: clippy release
|
||||
run: cargo clippy --all-targets --all-features --release -- -D warnings
|
||||
@@ -79,11 +69,12 @@ jobs:
|
||||
check_wasm:
|
||||
name: Check wasm32 + wasm-bindgen
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.81.0
|
||||
toolchain: 1.88.0
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
|
||||
@@ -91,19 +82,19 @@ jobs:
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Check wasm32 egui_demo_app
|
||||
run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown
|
||||
- name: clippy wasm32 egui_demo_app
|
||||
run: cargo clippy -p egui_demo_app --lib --target wasm32-unknown-unknown
|
||||
|
||||
- name: Check wasm32 egui_demo_app --all-features
|
||||
run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
|
||||
- name: clippy wasm32 egui_demo_app --all-features
|
||||
run: cargo clippy -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
|
||||
|
||||
- name: Check wasm32 eframe
|
||||
run: cargo check -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
|
||||
- name: clippy wasm32 eframe
|
||||
run: cargo clippy -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
|
||||
|
||||
- name: wasm-bindgen
|
||||
uses: jetli/wasm-bindgen-action@v0.1.0
|
||||
with:
|
||||
version: "0.2.97"
|
||||
version: "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
|
||||
|
||||
- run: ./scripts/wasm_bindgen_check.sh --skip-setup
|
||||
|
||||
@@ -114,20 +105,21 @@ jobs:
|
||||
check_wasm_atomics:
|
||||
name: Check wasm32+atomics
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{env.NIGHTLY_VERSION}}
|
||||
targets: wasm32-unknown-unknown
|
||||
components: rust-src
|
||||
|
||||
- name: Check wasm32+atomics eframe with wgpu
|
||||
run: RUSTFLAGS='-C target-feature=+atomics' cargo +${{env.NIGHTLY_VERSION}} check -p eframe --lib --no-default-features --features wgpu --target wasm32-unknown-unknown -Z build-std=std,panic_abort
|
||||
run: RUSTFLAGS='-C target-feature=+atomics' cargo +${{env.NIGHTLY_VERSION}} check -p eframe --lib --no-default-features --features wgpu,wgpu/webgpu --target wasm32-unknown-unknown -Z build-std=std,panic_abort
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -151,11 +143,12 @@ jobs:
|
||||
|
||||
name: cargo-deny ${{ matrix.target }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
rust-version: "1.81.0"
|
||||
rust-version: "1.88.0"
|
||||
log-level: error
|
||||
command: check
|
||||
arguments: --target ${{ matrix.target }}
|
||||
@@ -165,18 +158,21 @@ jobs:
|
||||
android:
|
||||
name: android
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.81.0
|
||||
toolchain: 1.88.0
|
||||
targets: aarch64-linux-android
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- run: cargo check --features wgpu,android-native-activity --target aarch64-linux-android
|
||||
# Default features disabled to turn off accesskit, which does not work
|
||||
# with NativeActivity.
|
||||
- run: cargo check --features wgpu,android-native-activity --target aarch64-linux-android --no-default-features
|
||||
working-directory: crates/eframe
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -184,12 +180,13 @@ jobs:
|
||||
ios:
|
||||
name: ios
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.81.0
|
||||
toolchain: 1.88.0
|
||||
targets: aarch64-apple-ios
|
||||
|
||||
- name: Set up cargo cache
|
||||
@@ -204,20 +201,19 @@ jobs:
|
||||
windows:
|
||||
name: Check Windows
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.81.0
|
||||
toolchain: 1.88.0
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Check all
|
||||
run: cargo check --all-targets --all-features
|
||||
- run: cargo clippy --all-targets --all-features
|
||||
|
||||
- name: Check hello_world
|
||||
run: cargo check -p hello_world
|
||||
- run: cargo clippy -p hello_world
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -225,14 +221,14 @@ jobs:
|
||||
name: Run tests
|
||||
# We run the tests on macOS because it will run with an actual GPU
|
||||
runs-on: macos-latest
|
||||
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.81.0
|
||||
toolchain: 1.88.0
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
26
.github/workflows/spelling_and_links.yml
vendored
26
.github/workflows/spelling_and_links.yml
vendored
@@ -8,23 +8,29 @@ jobs:
|
||||
# install and run locally: cargo install typos-cli && typos
|
||||
name: typos
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check spelling of entire workspace
|
||||
uses: crate-ci/typos@master
|
||||
uses: crate-ci/typos@v1.38.0
|
||||
|
||||
linkinator:
|
||||
name: linkinator
|
||||
lychee:
|
||||
name: lychee
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: jprochazk/linkinator-action@main
|
||||
with:
|
||||
linksToSkip: "https://crates.io/crates/.*, http://localhost:.*" # Avoid crates.io rate-limiting
|
||||
retry: true
|
||||
retryErrors: true
|
||||
retryErrorsCount: 5
|
||||
retryErrorsJitter: 2000
|
||||
- name: Don't check CHANGELOG.md files
|
||||
# This is really stupid but lychee doesn't have a way of excluding files via GLOB:
|
||||
# https://github.com/lycheeverse/lychee/issues/1608
|
||||
|
||||
# We need to exclude CHANGELOG.md since we don't want to have a CI failure everytime some contributor decides
|
||||
# to change their username.
|
||||
run: rm -r */**/CHANGELOG.md CHANGELOG.md
|
||||
- name: Link Checker
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
with:
|
||||
args: "'**/*.md' '**/*.toml' --exclude localhost --exclude reddit.com" # I guess reddit doesn't like github action IPs
|
||||
|
||||
|
||||
42
.github/workflows/update_kittest_snapshots.yml
vendored
Normal file
42
.github/workflows/update_kittest_snapshots.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_id:
|
||||
description: 'The run ID that produced the artifact'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
name: Update kittest snapshots
|
||||
jobs:
|
||||
update-snapshots:
|
||||
name: Update snapshots from artifact
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
# We can't use the workflow token since that would prevent our commit to cause further workflows.
|
||||
# See https://github.com/stefanzweifel/git-auto-commit-action#commits-made-by-this-action-do-not-trigger-new-workflow-runs
|
||||
# This token should be a personal access token with at least Read and write permission to `Contents`.
|
||||
# The commit action below will use the token this the code was checked out with.
|
||||
token: '${{ secrets.SNAPSHOT_COMMIT_GITHUB_TOKEN }}'
|
||||
|
||||
- name: Accept snapshots
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_ID: ${{ github.event.inputs.run_id }}
|
||||
run: ./scripts/update_snapshots_from_ci.sh
|
||||
|
||||
- name: Git status
|
||||
run: git status
|
||||
|
||||
- uses: stefanzweifel/git-auto-commit-action@v6
|
||||
with:
|
||||
commit_message: 'Update snapshot images'
|
||||
|
||||
135
.typos.toml
135
.typos.toml
@@ -3,9 +3,12 @@
|
||||
# run: typos
|
||||
|
||||
[default.extend-words]
|
||||
ime = "ime" # Input Method Editor
|
||||
ime = "ime" # Input Method Editor
|
||||
nknown = "nknown" # part of @55nknown username
|
||||
ro = "ro" # read-only, also part of the username @Phen-Ro
|
||||
isse = "isse" # part of @IsseW username
|
||||
tye = "tye" # part of @tye-exe username
|
||||
ro = "ro" # read-only, also part of the username @Phen-Ro
|
||||
typ = "typ" # Often used because `type` is a keyword in Rust
|
||||
|
||||
# I mistype these so often
|
||||
tesalator = "tessellator"
|
||||
@@ -17,5 +20,133 @@ teselation = "tessellation"
|
||||
tessalation = "tessellation"
|
||||
tesselation = "tessellation"
|
||||
|
||||
|
||||
# Use the more common spelling
|
||||
adaptor = "adapter"
|
||||
adaptors = "adapters"
|
||||
|
||||
# For consistency we prefer American English:
|
||||
aeroplane = "airplane"
|
||||
analogue = "analog"
|
||||
analyse = "analyze"
|
||||
appetiser = "appetizer"
|
||||
arbour = "arbor"
|
||||
ardour = "arbor"
|
||||
armour = "armor"
|
||||
artefact = "artifact"
|
||||
authorise = "authorize"
|
||||
behaviour = "behavior"
|
||||
behavioural = "behavioral"
|
||||
British = "American"
|
||||
calibre = "caliber"
|
||||
# cancelled = "canceled" # winit uses this :(
|
||||
candour = "candor"
|
||||
capitalise = "capitalize"
|
||||
catalogue = "catalog"
|
||||
centre = "center"
|
||||
characterise = "characterize"
|
||||
chequerboard = "checkerboard"
|
||||
chequered = "checkered"
|
||||
civilise = "civilize"
|
||||
clamour = "clamor"
|
||||
colonise = "colonize"
|
||||
colour = "color"
|
||||
coloured = "colored"
|
||||
cosy = "cozy"
|
||||
criticise = "criticize"
|
||||
defence = "defense"
|
||||
demeanour = "demeanor"
|
||||
dialogue = "dialog"
|
||||
distil = "distill"
|
||||
doughnut = "donut"
|
||||
dramatise = "dramatize"
|
||||
draught = "draft"
|
||||
emphasise = "emphasize"
|
||||
endeavour = "endeavor"
|
||||
enrol = "enroll"
|
||||
epilogue = "epilog"
|
||||
equalise = "equalize"
|
||||
favour = "favor"
|
||||
favourite = "favorite"
|
||||
fibre = "fiber"
|
||||
flavour = "flavor"
|
||||
fulfil = "fufill"
|
||||
gaol = "jail"
|
||||
grey = "gray"
|
||||
greys = "grays"
|
||||
greyscale = "grayscale"
|
||||
harbour = "habor"
|
||||
honour = "honor"
|
||||
humour = "humor"
|
||||
instalment = "installment"
|
||||
instil = "instill"
|
||||
jewellery = "jewelry"
|
||||
kerb = "curb"
|
||||
labour = "labor"
|
||||
litre = "liter"
|
||||
lustre = "luster"
|
||||
meagre = "meager"
|
||||
metre = "meter"
|
||||
mobilise = "mobilize"
|
||||
monologue = "monolog"
|
||||
naturalise = "naturalize"
|
||||
neighbour = "neighbor"
|
||||
neighbourhood = "neighborhood"
|
||||
normalise = "normalize"
|
||||
normalised = "normalized"
|
||||
odour = "odor"
|
||||
offence = "offense"
|
||||
organise = "organize"
|
||||
parlour = "parlor"
|
||||
plough = "plow"
|
||||
popularise = "popularize"
|
||||
pretence = "pretense"
|
||||
programme = "program"
|
||||
prologue = "prolog"
|
||||
rancour = "rancor"
|
||||
realise = "realize"
|
||||
recognise = "recognize"
|
||||
recognised = "recognized"
|
||||
rigour = "rigor"
|
||||
rumour = "rumor"
|
||||
sabre = "saber"
|
||||
satirise = "satirize"
|
||||
saviour = "savior"
|
||||
savour = "savor"
|
||||
sceptical = "skeptical"
|
||||
sceptre = "scepter"
|
||||
sepulchre = "sepulcher"
|
||||
serialisation = "serialization"
|
||||
serialise = "serialize"
|
||||
serialised = "serialized"
|
||||
skilful = "skillful"
|
||||
sombre = "somber"
|
||||
specialisation = "specialization"
|
||||
specialise = "specialize"
|
||||
specialised = "specialized"
|
||||
splendour = "splendor"
|
||||
standardise = "standardize"
|
||||
sulphur = "sulfur"
|
||||
symbolise = "symbolize"
|
||||
theatre = "theater"
|
||||
tonne = "ton"
|
||||
travelogue = "travelog"
|
||||
tumour = "tumor"
|
||||
valour = "valor"
|
||||
vaporise = "vaporize"
|
||||
vigour = "vigor"
|
||||
|
||||
# null-terminated is the name of the wikipedia article!
|
||||
# https://en.wikipedia.org/wiki/Null-terminated_string
|
||||
nullterminated = "null-terminated"
|
||||
zeroterminated = "null-terminated"
|
||||
zero-terminated = "null-terminated"
|
||||
|
||||
|
||||
[files]
|
||||
extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated
|
||||
|
||||
[default]
|
||||
extend-ignore-re = [
|
||||
"#\\[doc\\(alias = .*", # We suggest "grey" in some doc
|
||||
]
|
||||
|
||||
28
.vscode/settings.json
vendored
28
.vscode/settings.json
vendored
@@ -12,28 +12,14 @@
|
||||
"target_wasm/**": true,
|
||||
"target/**": true,
|
||||
},
|
||||
// Tell Rust Analyzer to use its own target directory, so we don't need to wait for it to finish wen we want to `cargo run`
|
||||
"rust-analyzer.check.overrideCommand": [
|
||||
"cargo",
|
||||
"clippy",
|
||||
"--target-dir=target_ra",
|
||||
"--workspace",
|
||||
"--message-format=json",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
],
|
||||
"rust-analyzer.cargo.buildScripts.overrideCommand": [
|
||||
"cargo",
|
||||
"clippy",
|
||||
"--quiet",
|
||||
"--target-dir=target_ra",
|
||||
"--workspace",
|
||||
"--message-format=json",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
],
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
// Whether `--workspace` should be passed to `cargo clippy`. If false, `-p <package>` will be passed instead.
|
||||
"rust-analyzer.check.workspace": false,
|
||||
"rust-analyzer.cargo.allTargets": true,
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
// Use a separate target directory for Rust Analyzer so it doesn't prevent cargo/clippy from doing things.
|
||||
"rust-analyzer.cargo.targetDir": "target_ra",
|
||||
"rust-analyzer.showUnlinkedFileNotification": false,
|
||||
|
||||
// Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`.
|
||||
// Don't forget to put it in a comment again before committing.
|
||||
// "rust-analyzer.cargo.target": "wasm32-unknown-unknown",
|
||||
|
||||
@@ -51,7 +51,7 @@ Thin wrapper around `egui_demo_lib` so we can compile it to a web site or a nati
|
||||
Depends on `egui_demo_lib` + `eframe`.
|
||||
|
||||
### `egui_kittest`
|
||||
A test harness for egui based on [kittest](https://github.com/rerun/kittest) and [AccessKit](https://github.com/AccessKit/accesskit/).
|
||||
A test harness for egui based on [kittest](https://github.com/rerun-io/kittest) and [AccessKit](https://github.com/AccessKit/accesskit/).
|
||||
|
||||
### Other integrations
|
||||
|
||||
|
||||
394
CHANGELOG.md
394
CHANGELOG.md
@@ -14,6 +14,394 @@ 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.33.2 - 2025-11-13
|
||||
### ⭐ Added
|
||||
* Add `Plugin::on_widget_under_pointer` to support widget inspector [#7652](https://github.com/emilk/egui/pull/7652) by [@juancampa](https://github.com/juancampa)
|
||||
* Add `Response::total_drag_delta` and `PointerState::total_drag_delta` [#7708](https://github.com/emilk/egui/pull/7708) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🔧 Changed
|
||||
* Improve accessibility and testability of `ComboBox` [#7658](https://github.com/emilk/egui/pull/7658) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Fix `profiling::scope` compile error when profiling using `tracing` backend [#7646](https://github.com/emilk/egui/pull/7646) by [@PPakalns](https://github.com/PPakalns)
|
||||
* Fix edge cases in "smart aiming" in sliders [#7680](https://github.com/emilk/egui/pull/7680) by [@emilk](https://github.com/emilk)
|
||||
* Hide scroll bars when dragging other things [#7689](https://github.com/emilk/egui/pull/7689) by [@emilk](https://github.com/emilk)
|
||||
* Prevent widgets sometimes appearing to move relative to each other [#7710](https://github.com/emilk/egui/pull/7710) by [@emilk](https://github.com/emilk)
|
||||
* Fix `ui.response().interact(Sense::click())` being flakey [#7713](https://github.com/emilk/egui/pull/7713) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09 - `egui::Plugin`, better kerning, kitdiff viewer
|
||||
Highlights from this release:
|
||||
- `egui::Plugin` a improved way to create and access egui plugins
|
||||
- [kitdiff](https://github.com/rerun-io/kitdiff), a viewer for egui_kittest image snapshots (and a general image diff tool)
|
||||
- better kerning
|
||||
|
||||
|
||||
### Improved kerning
|
||||
As a step towards using [parley](https://github.com/linebender/parley) for font rendering, @valadaptive has refactored the font loading and rendering code. A result of this (next to the font rendering code being much nicer now) is improved kerning.
|
||||
Notice how the c moved away from the k:
|
||||
|
||||

|
||||
|
||||
|
||||
### `egui::Plugin` trait
|
||||
We've added a new trait-based plugin api, meant to replace `Context::on_begin_pass` and `Context::on_end_pass`.
|
||||
This makes it a lot easier to handle state in your plugins. Instead of having to write to egui memory it can live right on your plugin struct.
|
||||
The trait based api also makes easier to add new hooks that plugins can use. In addition to `on_begin_pass` and `on_end_pass`, the `Plugin` trait now has a `input_hook` and `output_hook` which you can use to inspect / modify the `RawInput` / `FullOutput`.
|
||||
|
||||
### kitdiff, a image diff viewer
|
||||
At rerun we have a ton of snapshots. Some PRs will change most of them (e.g. [the](https://github.com/rerun-io/rerun/pull/11253/files) [one](https://rerun-io.github.io/kitdiff/?url=https://github.com/rerun-io/rerun/pull/11253/files) that updated egui and introduced the kerning improvements, ~500 snapshots changed!).
|
||||
If you really want to look at every changed snapshot it better be as efficient as possible, and the experience on github, fiddeling with the sliders, is kind of frustrating.
|
||||
In order to fix this, we've made [kitdiff](https://rerun-io.github.io/kitdiff/).
|
||||
You can use it locally via
|
||||
- `kitdiff files .` will search for .new.png and .diff.png files
|
||||
- `kitdiff git` will compare the current files to the default branch (main/master)
|
||||
Or in the browser via
|
||||
- going to https://rerun-io.github.io/kitdiff/ and pasting a PR or github artifact url
|
||||
- linking to kitdiff via e.g. a github workflow `https://rerun-io.github.io/kitdiff/?url=<link_to_pr_or_artifact>`
|
||||
|
||||
To install kitdiff run `cargo install --git https://github.com/rerun-io/kitdiff`
|
||||
|
||||
Here is a video showing the kerning changes in kitdiff ([try it yourself](https://rerun-io.github.io/kitdiff/?url=https://github.com/rerun-io/rerun/pull/11253/files)):
|
||||
|
||||
https://github.com/user-attachments/assets/74640af1-09ba-435a-9d0c-2cbeee140c8f
|
||||
|
||||
### Migration guide
|
||||
- `egui::Mutex` now has a timeout as a simple deadlock detection
|
||||
- If you use a `egui::Mutex` in some place where it's held for longer than a single frame, you should switch to the std mutex or parking_lot instead (egui mutexes are wrappers around parking lot)
|
||||
- `screen_rect` is deprecated
|
||||
- In order to support safe areas, egui now has `viewport_rect` and `content_rect`.
|
||||
- Update all usages of `screen_rect` to `content_rect`, unless you are sure that you want to draw outside the `safe area` (which would mean your Ui may be covered by notches, system ui, etc.)
|
||||
|
||||
|
||||
### ⭐ Added
|
||||
* New Plugin trait [#7385](https://github.com/emilk/egui/pull/7385) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Add `Ui::take_available_space()` helper function, which sets the Ui's minimum size to the available space [#7573](https://github.com/emilk/egui/pull/7573) by [@IsseW](https://github.com/IsseW)
|
||||
* Add support for the safe area on iOS [#7578](https://github.com/emilk/egui/pull/7578) by [@irh](https://github.com/irh)
|
||||
* Add `UiBuilder::global_scope` and `UiBuilder::id` [#7372](https://github.com/emilk/egui/pull/7372) by [@Icekey](https://github.com/Icekey)
|
||||
* Add `emath::fast_midpoint` [#7435](https://github.com/emilk/egui/pull/7435) by [@emilk](https://github.com/emilk)
|
||||
* Make the `hex_color` macro `const` [#7444](https://github.com/emilk/egui/pull/7444) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Add `SurrenderFocusOn` option [#7471](https://github.com/emilk/egui/pull/7471) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Add `Memory::move_focus` [#7476](https://github.com/emilk/egui/pull/7476) by [@darkwater](https://github.com/darkwater)
|
||||
* Support on hover tooltip that is noninteractable even with interactable content [#5543](https://github.com/emilk/egui/pull/5543) by [@PPakalns](https://github.com/PPakalns)
|
||||
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)
|
||||
|
||||
### 🔧 Changed
|
||||
* Document platform compatibility on `viewport::WindowLevel` and dependents [#7432](https://github.com/emilk/egui/pull/7432) by [@lkdm](https://github.com/lkdm)
|
||||
* Deprecated `ImageButton` and removed `WidgetType::ImageButton` [#7483](https://github.com/emilk/egui/pull/7483) by [@Stelios-Kourlis](https://github.com/Stelios-Kourlis)
|
||||
* More even text kerning [#7431](https://github.com/emilk/egui/pull/7431) by [@valadaptive](https://github.com/valadaptive)
|
||||
* Increase default text size from 12.5 to 13.0 [#7521](https://github.com/emilk/egui/pull/7521) by [@emilk](https://github.com/emilk)
|
||||
* Update accesskit to 0.21.0 [#7550](https://github.com/emilk/egui/pull/7550) by [@fundon](https://github.com/fundon)
|
||||
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
|
||||
* Group AccessKit nodes by `Ui` [#7386](https://github.com/emilk/egui/pull/7386) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
### 🔥 Removed
|
||||
* Remove the `deadlock_detection` feature [#7497](https://github.com/emilk/egui/pull/7497) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Remove deprecated fields from `PlatformOutput` [#7523](https://github.com/emilk/egui/pull/7523) by [@emilk](https://github.com/emilk)
|
||||
* Remove `log` feature [#7583](https://github.com/emilk/egui/pull/7583) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Enable `clippy::iter_over_hash_type` lint [#7421](https://github.com/emilk/egui/pull/7421) by [@emilk](https://github.com/emilk)
|
||||
* Fixes sense issues in TextEdit when vertical alignment is used [#7436](https://github.com/emilk/egui/pull/7436) by [@RndUsr123](https://github.com/RndUsr123)
|
||||
* Fix stuck menu when submenu vanishes [#7589](https://github.com/emilk/egui/pull/7589) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Change Spinner widget to account for width as well as height [#7560](https://github.com/emilk/egui/pull/7560) by [@bryceberger](https://github.com/bryceberger)
|
||||
|
||||
|
||||
## 0.32.3 - 2025-09-12
|
||||
* Preserve text format in truncated label tooltip [#7514](https://github.com/emilk/egui/pull/7514) [#7535](https://github.com/emilk/egui/pull/7535) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix `TextEdit`'s in RTL layouts [#5547](https://github.com/emilk/egui/pull/5547) by [@zakarumych](https://github.com/zakarumych)
|
||||
|
||||
|
||||
## 0.32.2 - 2025-09-04
|
||||
* Fix: `SubMenu` should not display when ui is disabled [#7428](https://github.com/emilk/egui/pull/7428) by [@ozwaldorf](https://github.com/ozwaldorf)
|
||||
* Remove line breaks when pasting into single line TextEdit [#7441](https://github.com/emilk/egui/pull/7441) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Panic mutexes that can't lock for 30 seconds, in debug builds [#7468](https://github.com/emilk/egui/pull/7468) by [@emilk](https://github.com/emilk)
|
||||
* Add `Ui::place`, to place widgets without changing the cursor [#7359](https://github.com/emilk/egui/pull/7359) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix: prevent calendar popup from closing on dropdown change [#7409](https://github.com/emilk/egui/pull/7409) by [@AStrizh](https://github.com/AStrizh)
|
||||
|
||||
|
||||
## 0.32.1 - 2025-08-15 - Misc bug fixes
|
||||
### ⭐ Added
|
||||
* Add `ComboBox::popup_style` [#7360](https://github.com/emilk/egui/pull/7360) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Fix glyph rendering: clamp coverage to [0, 1] [#7415](https://github.com/emilk/egui/pull/7415) by [@emilk](https://github.com/emilk)
|
||||
* Fix manual `Popup` not closing [#7383](https://github.com/emilk/egui/pull/7383) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix `WidgetText::Text` ignoring fallback font and overrides [#7361](https://github.com/emilk/egui/pull/7361) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix `override_text_color` priority [#7439](https://github.com/emilk/egui/pull/7439) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Fix debug-panic in ScrollArea if contents fit without scrolling [#7440](https://github.com/emilk/egui/pull/7440) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
|
||||
|
||||
## 0.32.0 - 2025-07-10 - Atoms, popups, and better SVG support
|
||||
This is a big egui release, with several exciting new features!
|
||||
|
||||
* _Atoms_ are new layout primitives in egui, for text and images
|
||||
* Popups, tooltips and menus have undergone a complete rewrite
|
||||
* Much improved SVG support
|
||||
* Crisper graphics (especially text!)
|
||||
|
||||
Let's dive in!
|
||||
|
||||
### ⚛️ Atoms
|
||||
|
||||
`egui::Atom` is the new, indivisible building blocks of egui (hence their name).
|
||||
An `Atom` is an `enum` that can be either `WidgetText`, `Image`, or `Custom`.
|
||||
|
||||
The new `AtomLayout` can be used within widgets to do basic layout.
|
||||
The initial implementation is as minimal as possible, doing just enough to implement what `Button` could do before.
|
||||
There is a new `IntoAtoms` trait that works with tuples of `Atom`s. Each atom can be customized with the `AtomExt` trait
|
||||
which works on everything that implements `Into<Atom>`, so e.g. `RichText` or `Image`.
|
||||
So to create a `Button` with text and image you can now do:
|
||||
```rs
|
||||
let image = include_image!("my_icon.png").atom_size(Vec2::splat(12.0));
|
||||
ui.button((image, "Click me!"));
|
||||
```
|
||||
|
||||
Anywhere you see `impl IntoAtoms` you can add any number of images and text, in any order.
|
||||
|
||||
As of 0.32, we have ported the `Button`, `Checkbox`, `RadioButton` to use atoms
|
||||
(meaning they support adding Atoms and are built on top of `AtomLayout`).
|
||||
The `Button` implementation is not only more powerful now, but also much simpler, removing ~130 lines of layout math.
|
||||
|
||||
In combination with `ui.read_response`, custom widgets are really simple now, here is a minimal button implementation:
|
||||
|
||||
```rs
|
||||
pub struct ALButton<'a> {
|
||||
al: AtomLayout<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ALButton<'a> {
|
||||
pub fn new(content: impl IntoAtoms<'a>) -> Self {
|
||||
Self {
|
||||
al: AtomLayout::new(content.into_atoms()).sense(Sense::click()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for ALButton<'a> {
|
||||
fn ui(mut self, ui: &mut Ui) -> Response {
|
||||
let Self { al } = self;
|
||||
let response = ui.ctx().read_response(ui.next_auto_id());
|
||||
|
||||
let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| {
|
||||
ui.style().interact(&response)
|
||||
});
|
||||
|
||||
let al = al.frame(
|
||||
Frame::new()
|
||||
.inner_margin(ui.style().spacing.button_padding)
|
||||
.fill(visuals.bg_fill)
|
||||
.stroke(visuals.bg_stroke)
|
||||
.corner_radius(visuals.corner_radius),
|
||||
);
|
||||
|
||||
al.show(ui).response
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can even use `Atom::custom` to add custom content to Widgets. Here is a button in a button:
|
||||
|
||||
https://github.com/user-attachments/assets/8c649784-dcc5-4979-85f8-e735b9cdd090
|
||||
|
||||
```rs
|
||||
let custom_button_id = Id::new("custom_button");
|
||||
let response = Button::new((
|
||||
Atom::custom(custom_button_id, Vec2::splat(18.0)),
|
||||
"Look at my mini button!",
|
||||
))
|
||||
.atom_ui(ui);
|
||||
if let Some(rect) = response.rect(custom_button_id) {
|
||||
ui.put(rect, Button::new("🔎").frame_when_inactive(false));
|
||||
}
|
||||
```
|
||||
Currently, you need to use `atom_ui` to get a `AtomResponse` which will have the `Rect` to use, but in the future
|
||||
this could be streamlined, e.g. by adding a `AtomKind::Callback` or by passing the Rects back with `egui::Response`.
|
||||
|
||||
Basing our widgets on `AtomLayout` also allowed us to improve `Response::intrinsic_size`, which will now report the
|
||||
correct size even if widgets are truncated. `intrinsic_size` is the size that a non-wrapped, non-truncated,
|
||||
non-justified version of the widget would have, and can be useful in advanced layout
|
||||
calculations like [egui_flex](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex).
|
||||
|
||||
##### Details
|
||||
* Add `AtomLayout`, abstracting layouting within widgets [#5830](https://github.com/emilk/egui/pull/5830) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Add `Galley::intrinsic_size` and use it in `AtomLayout` [#7146](https://github.com/emilk/egui/pull/7146) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
|
||||
### ❕ Improved popups, tooltips, and menus
|
||||
|
||||
Introduces a new `egui::Popup` api. Checkout the new demo on https://egui.rs:
|
||||
|
||||
https://github.com/user-attachments/assets/74e45243-7d05-4fc3-b446-2387e1412c05
|
||||
|
||||
We introduced a new `RectAlign` helper to align a rect relative to an other rect. The `Popup` will by default try to find the best `RectAlign` based on the source widgets position (previously submenus would annoyingly overlap if at the edge of the window):
|
||||
|
||||
https://github.com/user-attachments/assets/0c5adb6b-8310-4e0a-b936-646bb4ec02f7
|
||||
|
||||
`Tooltip` and `menu` have been rewritten based on the new `Popup` api. They are now compatible with each other, meaning you can just show a `ui.menu_button()` in any `Popup` to get a sub menu. There are now customizable `MenuButton` and `SubMenuButton` structs, to help with customizing your menu buttons. This means menus now also support `PopupCloseBehavior` so you can remove your `close_menu` calls from your click handlers!
|
||||
|
||||
The old tooltip and popup apis have been ported to the new api so there should be very little breaking changes. The old menu is still around but deprecated. `ui.menu_button` etc now open the new menu, if you can't update to the new one immediately you can use the old buttons from the deprecated `egui::menu` menu.
|
||||
|
||||
We also introduced `ui.close()` which closes the nearest container. So you can now conveniently close `Window`s, `Collapsible`s, `Modal`s and `Popup`s from within. To use this for your own containers, call `UiBuilder::closable` and then check for closing within that ui via `ui.should_close()`.
|
||||
|
||||
##### Details
|
||||
* Add `Popup` and `Tooltip`, unifying the previous behaviours [#5713](https://github.com/emilk/egui/pull/5713) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Add `Ui::close` and `Response::should_close` [#5729](https://github.com/emilk/egui/pull/5729) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* ⚠️ Improved menu based on `egui::Popup` [#5716](https://github.com/emilk/egui/pull/5716) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Add a toggle for the compact menu style [#5777](https://github.com/emilk/egui/pull/5777) by [@s-nie](https://github.com/s-nie)
|
||||
* Use the new `Popup` API for the color picker button [#7137](https://github.com/emilk/egui/pull/7137) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* ⚠️ Close popup if `Memory::keep_popup_open` isn't called [#5814](https://github.com/emilk/egui/pull/5814) by [@juancampa](https://github.com/juancampa)
|
||||
* Fix tooltips sometimes changing position each frame [#7304](https://github.com/emilk/egui/pull/7304) by [@emilk](https://github.com/emilk)
|
||||
* Change popup memory to be per-viewport [#6753](https://github.com/emilk/egui/pull/6753) by [@mkalte666](https://github.com/mkalte666)
|
||||
* Deprecate `Memory::popup` API in favor of new `Popup` API [#7317](https://github.com/emilk/egui/pull/7317) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
### ▲ Improved SVG support
|
||||
You can render SVG in egui with
|
||||
|
||||
```rs
|
||||
ui.add(egui::Image::new(egui::include_image!("icon.svg"));
|
||||
```
|
||||
|
||||
(Requires the use of `egui_extras`, with the `svg` feature enabled and a call to [`install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html)).
|
||||
|
||||
Previously this would sometimes result in a blurry SVG, epecially if the `Image` was set to be dynamically scale based on the size of the `Ui` that contained it. Now SVG:s are always pixel-perfect, for truly scalable graphics.
|
||||
|
||||

|
||||
|
||||
##### Details
|
||||
* Support text in SVGs [#5979](https://github.com/emilk/egui/pull/5979) by [@cernec1999](https://github.com/cernec1999)
|
||||
* Fix sometimes blurry SVGs [#7071](https://github.com/emilk/egui/pull/7071) by [@emilk](https://github.com/emilk)
|
||||
* Fix incorrect color fringe colors on SVG:s [#7069](https://github.com/emilk/egui/pull/7069) by [@emilk](https://github.com/emilk)
|
||||
* Make `Image::paint_at` pixel-perfect crisp for SVG images [#7078](https://github.com/emilk/egui/pull/7078) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
### ✨ Crisper graphics
|
||||
Non-SVG icons are also rendered better, and text sharpness has been improved, especially in light mode.
|
||||
|
||||

|
||||
|
||||
##### Details
|
||||
* Improve text sharpness [#5838](https://github.com/emilk/egui/pull/5838) by [@emilk](https://github.com/emilk)
|
||||
* Improve text rendering in light mode [#7290](https://github.com/emilk/egui/pull/7290) by [@emilk](https://github.com/emilk)
|
||||
* Improve texture filtering by doing it in gamma space [#7311](https://github.com/emilk/egui/pull/7311) by [@emilk](https://github.com/emilk)
|
||||
* Make text underline and strikethrough pixel perfect crisp [#5857](https://github.com/emilk/egui/pull/5857) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### Migration guide
|
||||
We have some silently breaking changes (code compiles fine but behavior changed) that require special care:
|
||||
|
||||
#### Menus close on click by default
|
||||
- previously menus would only close on click outside
|
||||
- either
|
||||
- remove the `ui.close_menu()` calls from button click handlers since they are obsolete
|
||||
- if the menu should stay open on clicks, change the `PopupCloseBehavior`:
|
||||
```rs
|
||||
// Change this
|
||||
ui.menu_button("Text", |ui| { /* Menu Content */ });
|
||||
// To this:
|
||||
MenuButton::new("Text").config(
|
||||
MenuConfig::default().close_behavior(PopupCloseBehavior::CloseOnClickOutside),
|
||||
).ui(ui, |ui| { /* Menu Content */ });
|
||||
```
|
||||
You can also change the behavior only for a single SubMenu by using `SubMenuButton`, but by default it should be passed to any submenus when using `MenuButton`.
|
||||
|
||||
#### `Memory::is_popup_open` api now requires calls to `Memory::keep_popup_open`
|
||||
- The popup will immediately close if `keep_popup_open` is not called.
|
||||
- It's recommended to use the new `Popup` api which handles this for you.
|
||||
- If you can't switch to the new api for some reason, update the code to call `keep_popup_open`:
|
||||
```rs
|
||||
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
|
||||
ui.memory_mut(|mem| mem.keep_popup_open(popup_id)); // <- add this line
|
||||
let area_response = Area::new(popup_id).show(...)
|
||||
}
|
||||
```
|
||||
|
||||
### ⭐ Other improvements
|
||||
* Add `Label::show_tooltip_when_elided` [#5710](https://github.com/emilk/egui/pull/5710) by [@bryceberger](https://github.com/bryceberger)
|
||||
* Deprecate `Ui::allocate_new_ui` in favor of `Ui::scope_builder` [#5764](https://github.com/emilk/egui/pull/5764) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Add `expand_bg` to customize size of text background [#5365](https://github.com/emilk/egui/pull/5365) by [@MeGaGiGaGon](https://github.com/MeGaGiGaGon)
|
||||
* Add assert messages and print bad argument values in asserts [#5216](https://github.com/emilk/egui/pull/5216) by [@bircni](https://github.com/bircni)
|
||||
* Use `TextBuffer` for `layouter` in `TextEdit` instead of `&str` [#5712](https://github.com/emilk/egui/pull/5712) by [@kernelkind](https://github.com/kernelkind)
|
||||
* Add a `Slider::update_while_editing(bool)` API [#5978](https://github.com/emilk/egui/pull/5978) by [@mbernat](https://github.com/mbernat)
|
||||
* Add `Scene::drag_pan_buttons` option. Allows specifying which pointer buttons pan the scene by dragging [#5892](https://github.com/emilk/egui/pull/5892) by [@mitchmindtree](https://github.com/mitchmindtree)
|
||||
* Add `Scene::sense` to customize how `Scene` responds to user input [#5893](https://github.com/emilk/egui/pull/5893) by [@mitchmindtree](https://github.com/mitchmindtree)
|
||||
* Rework `TextEdit` arrow navigation to handle Unicode graphemes [#5812](https://github.com/emilk/egui/pull/5812) by [@MStarha](https://github.com/MStarha)
|
||||
* `ScrollArea` improvements for user configurability [#5443](https://github.com/emilk/egui/pull/5443) by [@MStarha](https://github.com/MStarha)
|
||||
* Add `Response::clicked_with_open_in_background` [#7093](https://github.com/emilk/egui/pull/7093) by [@emilk](https://github.com/emilk)
|
||||
* Add `Modifiers::matches_any` [#7123](https://github.com/emilk/egui/pull/7123) by [@emilk](https://github.com/emilk)
|
||||
* Add `Context::format_modifiers` [#7125](https://github.com/emilk/egui/pull/7125) by [@emilk](https://github.com/emilk)
|
||||
* Add `OperatingSystem::is_mac` [#7122](https://github.com/emilk/egui/pull/7122) by [@emilk](https://github.com/emilk)
|
||||
* Support vertical-only scrolling by holding down Alt [#7124](https://github.com/emilk/egui/pull/7124) by [@emilk](https://github.com/emilk)
|
||||
* Support for back-button on Android [#7073](https://github.com/emilk/egui/pull/7073) by [@ardocrat](https://github.com/ardocrat)
|
||||
* Select all text in DragValue when gaining focus via keyboard [#7107](https://github.com/emilk/egui/pull/7107) by [@Azkellas](https://github.com/Azkellas)
|
||||
* Add `Context::current_pass_index` [#7276](https://github.com/emilk/egui/pull/7276) by [@emilk](https://github.com/emilk)
|
||||
* Add `Context::cumulative_frame_nr` [#7278](https://github.com/emilk/egui/pull/7278) by [@emilk](https://github.com/emilk)
|
||||
* Add `Visuals::text_edit_bg_color` [#7283](https://github.com/emilk/egui/pull/7283) by [@emilk](https://github.com/emilk)
|
||||
* Add `Visuals::weak_text_alpha` and `weak_text_color` [#7285](https://github.com/emilk/egui/pull/7285) by [@emilk](https://github.com/emilk)
|
||||
* Add support for scrolling via accesskit / kittest [#7286](https://github.com/emilk/egui/pull/7286) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Update area struct to allow force resizing [#7114](https://github.com/emilk/egui/pull/7114) by [@blackberryfloat](https://github.com/blackberryfloat)
|
||||
* Add `egui::Sides` `shrink_left` / `shrink_right` [#7295](https://github.com/emilk/egui/pull/7295) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Set intrinsic size for Label [#7328](https://github.com/emilk/egui/pull/7328) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
### 🔧 Changed
|
||||
* Raise MSRV to 1.85 [#6848](https://github.com/emilk/egui/pull/6848) by [@torokati44](https://github.com/torokati44), [#7279](https://github.com/emilk/egui/pull/7279) by [@emilk](https://github.com/emilk)
|
||||
* Set `hint_text` in `WidgetInfo` [#5724](https://github.com/emilk/egui/pull/5724) by [@bircni](https://github.com/bircni)
|
||||
* Implement `Default` for `ThemePreference` [#5702](https://github.com/emilk/egui/pull/5702) by [@MichaelGrupp](https://github.com/MichaelGrupp)
|
||||
* Align `available_rect` docs with the new reality after #4590 [#5701](https://github.com/emilk/egui/pull/5701) by [@podusowski](https://github.com/podusowski)
|
||||
* Clarify platform-specific details for `Viewport` positioning [#5715](https://github.com/emilk/egui/pull/5715) by [@aspiringLich](https://github.com/aspiringLich)
|
||||
* Simplify the text cursor API [#5785](https://github.com/emilk/egui/pull/5785) by [@valadaptive](https://github.com/valadaptive)
|
||||
* Bump accesskit to 0.19 [#7040](https://github.com/emilk/egui/pull/7040) by [@valadaptive](https://github.com/valadaptive)
|
||||
* Better define the meaning of `SizeHint` [#7079](https://github.com/emilk/egui/pull/7079) by [@emilk](https://github.com/emilk)
|
||||
* Move all input-related options into `InputOptions` [#7121](https://github.com/emilk/egui/pull/7121) by [@emilk](https://github.com/emilk)
|
||||
* `Button` inherits the `alt_text` of the `Image` in it, if any [#7136](https://github.com/emilk/egui/pull/7136) by [@emilk](https://github.com/emilk)
|
||||
* Change API of `Tooltip` slightly [#7151](https://github.com/emilk/egui/pull/7151) by [@emilk](https://github.com/emilk)
|
||||
* Use Rust edition 2024 [#7280](https://github.com/emilk/egui/pull/7280) by [@emilk](https://github.com/emilk)
|
||||
* Change `ui.disable()` to modify opacity [#7282](https://github.com/emilk/egui/pull/7282) by [@emilk](https://github.com/emilk)
|
||||
* Make the font atlas use a color image [#7298](https://github.com/emilk/egui/pull/7298) by [@valadaptive](https://github.com/valadaptive)
|
||||
* Implement `BitOr` and `BitOrAssign` for `Rect` [#7319](https://github.com/emilk/egui/pull/7319) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
### 🔥 Removed
|
||||
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
|
||||
* Remove `SelectableLabel` [#7277](https://github.com/emilk/egui/pull/7277) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
### 🐛 Fixed
|
||||
* `Scene`: make `scene_rect` full size on reset [#5801](https://github.com/emilk/egui/pull/5801) by [@graydenshand](https://github.com/graydenshand)
|
||||
* `Scene`: `TextEdit` selection when placed in a `Scene` [#5791](https://github.com/emilk/egui/pull/5791) by [@karhu](https://github.com/karhu)
|
||||
* `Scene`: Set transform layer before calling user content [#5884](https://github.com/emilk/egui/pull/5884) by [@mitchmindtree](https://github.com/mitchmindtree)
|
||||
* Fix: transform `TextShape` underline width [#5865](https://github.com/emilk/egui/pull/5865) by [@emilk](https://github.com/emilk)
|
||||
* Fix missing repaint after `consume_key` [#7134](https://github.com/emilk/egui/pull/7134) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Update `emoji-icon-font` with fix for fullwidth latin characters [#7067](https://github.com/emilk/egui/pull/7067) by [@emilk](https://github.com/emilk)
|
||||
* Mark all keys as released if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
|
||||
* Fix scroll handle extending outside of `ScrollArea` [#5286](https://github.com/emilk/egui/pull/5286) by [@gilbertoalexsantos](https://github.com/gilbertoalexsantos)
|
||||
* Fix `Response::clicked_elsewhere` not returning `true` sometimes [#5798](https://github.com/emilk/egui/pull/5798) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix kinetic scrolling on touch devices [#5778](https://github.com/emilk/egui/pull/5778) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix `DragValue` expansion when editing [#5809](https://github.com/emilk/egui/pull/5809) by [@MStarha](https://github.com/MStarha)
|
||||
* Fix disabled `DragValue` eating focus, causing focus to reset [#5826](https://github.com/emilk/egui/pull/5826) by [@KonaeAkira](https://github.com/KonaeAkira)
|
||||
* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk)
|
||||
* Improve drag-to-select text (add margins) [#5797](https://github.com/emilk/egui/pull/5797) by [@hankjordan](https://github.com/hankjordan)
|
||||
* Fix bug in pointer movement detection [#5329](https://github.com/emilk/egui/pull/5329) by [@rustbasic](https://github.com/rustbasic)
|
||||
* Protect against NaN in hit-test code [#6851](https://github.com/emilk/egui/pull/6851) by [@Skgland](https://github.com/Skgland)
|
||||
* Fix image button panicking with tiny `available_space` [#6900](https://github.com/emilk/egui/pull/6900) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix links and text selection in horizontal_wrapped layout [#6905](https://github.com/emilk/egui/pull/6905) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Fix `leading_space` sometimes being ignored during paragraph splitting [#7031](https://github.com/emilk/egui/pull/7031) by [@afishhh](https://github.com/afishhh)
|
||||
* Fix typo in deprecation message for `ComboBox::from_id_source` [#7055](https://github.com/emilk/egui/pull/7055) by [@aelmizeb](https://github.com/aelmizeb)
|
||||
* Bug fix: make sure `end_pass` is called for all loaders [#7072](https://github.com/emilk/egui/pull/7072) by [@emilk](https://github.com/emilk)
|
||||
* Report image alt text as text if widget contains no other text [#7142](https://github.com/emilk/egui/pull/7142) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Slider: move by at least the next increment when using fixed_decimals [#7066](https://github.com/emilk/egui/pull/7066) by [@0x53A](https://github.com/0x53A)
|
||||
* Fix crash when using infinite widgets [#7296](https://github.com/emilk/egui/pull/7296) by [@emilk](https://github.com/emilk)
|
||||
* Fix `debug_assert` triggered by `menu`/`intersect_ray` [#7299](https://github.com/emilk/egui/pull/7299) by [@emilk](https://github.com/emilk)
|
||||
* Change `Rect::area` to return zero for negative rectangles [#7305](https://github.com/emilk/egui/pull/7305) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🚀 Performance
|
||||
* Optimize editing long text by caching each paragraph [#5411](https://github.com/emilk/egui/pull/5411) by [@afishhh](https://github.com/afishhh)
|
||||
* Make `WidgetText` smaller and faster [#6903](https://github.com/emilk/egui/pull/6903) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
|
||||
## 0.31.1 - 2025-03-05
|
||||
* Fix sizing bug in `TextEdit::singleline` [#5640](https://github.com/emilk/egui/pull/5640) by [@IaVashik](https://github.com/IaVashik)
|
||||
* Fix panic when rendering thin textured rectangles [#5692](https://github.com/emilk/egui/pull/5692) by [@PPakalns](https://github.com/PPakalns)
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04 - Scene container, improved rendering quality
|
||||
|
||||
### Highlights ✨
|
||||
@@ -103,7 +491,7 @@ In order to pave the path for more complex and customizable styling solutions, w
|
||||
* 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).
|
||||
This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/main/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!).
|
||||
@@ -321,7 +709,7 @@ There also has been several small improvements to the look of egui:
|
||||
* The `extra_asserts` and `extra_debug_asserts` feature flags have been removed ([#4478](https://github.com/emilk/egui/pull/4478))
|
||||
* Remove `Event::Scroll` and handle it in egui. Use `Event::MouseWheel` instead ([#4524](https://github.com/emilk/egui/pull/4524))
|
||||
* `Event::Zoom` is no longer emitted on ctrl+scroll. Use `InputState::smooth_scroll_delta` instead ([#4524](https://github.com/emilk/egui/pull/4524))
|
||||
* `ui.set_enabled` and `set_visbile` have been deprecated ([#4614](https://github.com/emilk/egui/pull/4614))
|
||||
* `ui.set_enabled` and `set_visible` have been deprecated ([#4614](https://github.com/emilk/egui/pull/4614))
|
||||
* `DragValue::clamp_range` renamed to `range` (([#4728](https://github.com/emilk/egui/pull/4728))
|
||||
|
||||
### ⭐ Added
|
||||
@@ -937,7 +1325,7 @@ egui_extras::install_image_loaders(egui_ctx);
|
||||
## 0.18.0 - 2022-04-30
|
||||
|
||||
### ⭐ Added
|
||||
* Added `Shape::Callback` for backend-specific painting, [with an example](https://github.com/emilk/egui/tree/master/examples/custom_3d_glow) ([#1351](https://github.com/emilk/egui/pull/1351)).
|
||||
* Added `Shape::Callback` for backend-specific painting, [with an example](https://github.com/emilk/egui/tree/main/examples/custom_3d_glow) ([#1351](https://github.com/emilk/egui/pull/1351)).
|
||||
* Added `Frame::canvas` ([#1362](https://github.com/emilk/egui/pull/1362)).
|
||||
* `Context::request_repaint` will now wake up UI thread, if integrations has called `Context::set_request_repaint_callback` ([#1366](https://github.com/emilk/egui/pull/1366)).
|
||||
* Added `Plot::allow_scroll`, `Plot::allow_zoom` no longer affects scrolling ([#1382](https://github.com/emilk/egui/pull/1382)).
|
||||
|
||||
@@ -34,14 +34,11 @@ Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pi
|
||||
You can test your code locally by running `./scripts/check.sh`.
|
||||
There are snapshots test that might need to be updated.
|
||||
Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them.
|
||||
If CI keeps complaining about snapshots (which could happen if you don't use macOS, snapshots in CI are currently
|
||||
rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from
|
||||
the last CI run of your PR (which will download the `test_results` artifact).
|
||||
For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md).
|
||||
|
||||
We use [git-lfs](https://git-lfs.com/) to store big files in the repository.
|
||||
Make sure you have it installed (running `git lfs ls-files` from the repository root should list some files).
|
||||
Don't forget to run `git lfs install` after installing the git-lfs binary.
|
||||
You need to add any .png images to `git lfs`.
|
||||
If the CI complains about this, make sure you run `git add --renormalize .`.
|
||||
|
||||
Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info.
|
||||
If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs.
|
||||
|
||||
When you have something that works, open a draft PR. You may get some helpful feedback early!
|
||||
@@ -51,6 +48,34 @@ Don't worry about having many small commits in the PR - they will be squashed to
|
||||
|
||||
Please keep pull requests small and focused. The smaller it is, the more likely it is to get merged.
|
||||
|
||||
## Working with git lfs
|
||||
|
||||
We use [git-lfs](https://git-lfs.com/) to store big files in the repository.
|
||||
Make sure you have it installed (running `git lfs ls-files` from the repository root should list some files).
|
||||
Don't forget to run `git lfs install` in this repo after installing the git-lfs binary.
|
||||
You need to add any .png images to `git lfs` (see the .gitattributes file for rules and exclusions).
|
||||
If the CI complains about lfs, try running `git add --renormalize .`.
|
||||
|
||||
Common git-lfs commands:
|
||||
```bash
|
||||
# Install git-lfs in the repo (installs git hooks)
|
||||
git lfs install
|
||||
|
||||
# Move a file to git lfs
|
||||
git lfs track "path/to/file/or/pattern" # OR manually edit .gitattributes
|
||||
git add --renormalize . # Moves already added files to lfs (according to .gitattributes)
|
||||
|
||||
# Move a file from lfs to regular git
|
||||
git lfs untrack "path/to/file/or/pattern" # OR manually edit .gitattributes
|
||||
git add --renormalize . # Moves already added files to regular git (according to .gitattributes)
|
||||
|
||||
# Push to a contributor remote (see https://github.com/cli/cli/discussions/8794#discussioncomment-8695076)
|
||||
git push --no-verify
|
||||
|
||||
# Push git lfs files to contributor remote:
|
||||
git push origin $(git branch --show-current) && git push --no-verify && git push origin --delete $(git branch --show-current)
|
||||
```
|
||||
|
||||
## PR review
|
||||
|
||||
Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs!
|
||||
@@ -103,6 +128,7 @@ While using an immediate mode gui is simple, implementing one is a lot more tric
|
||||
* Flip `if !condition {} else {}`
|
||||
* Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`)
|
||||
* Put each type in their own file, unless they are trivial (e.g. a `struct` with no `impl`)
|
||||
* Put most generic arguments first (e.g. `Context`), and most specific last
|
||||
* Break the above rules when it makes sense
|
||||
|
||||
|
||||
|
||||
2575
Cargo.lock
2575
Cargo.lock
File diff suppressed because it is too large
Load Diff
225
Cargo.toml
225
Cargo.toml
@@ -1,30 +1,30 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/ecolor",
|
||||
"crates/egui_demo_app",
|
||||
"crates/egui_demo_lib",
|
||||
"crates/egui_extras",
|
||||
"crates/egui_glow",
|
||||
"crates/egui_kittest",
|
||||
"crates/egui-wgpu",
|
||||
"crates/egui-winit",
|
||||
"crates/egui",
|
||||
"crates/emath",
|
||||
"crates/epaint",
|
||||
"crates/epaint_default_fonts",
|
||||
"crates/ecolor",
|
||||
"crates/egui_demo_app",
|
||||
"crates/egui_demo_lib",
|
||||
"crates/egui_extras",
|
||||
"crates/egui_glow",
|
||||
"crates/egui_kittest",
|
||||
"crates/egui-wgpu",
|
||||
"crates/egui-winit",
|
||||
"crates/egui",
|
||||
"crates/emath",
|
||||
"crates/epaint",
|
||||
"crates/epaint_default_fonts",
|
||||
|
||||
"examples/*",
|
||||
"tests/*",
|
||||
"examples/*",
|
||||
"tests/*",
|
||||
|
||||
"xtask",
|
||||
"xtask",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.81"
|
||||
version = "0.31.0"
|
||||
rust-version = "1.88"
|
||||
version = "0.33.2"
|
||||
|
||||
|
||||
[profile.release]
|
||||
@@ -55,54 +55,95 @@ opt-level = 2
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
emath = { version = "0.31.0", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.31.0", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.31.0", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.31.0", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.31.0", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.31.0", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.31.0", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.31.0", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.31.0", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.31.0", path = "crates/egui_glow", default-features = false }
|
||||
egui_kittest = { version = "0.31.0", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.31.0", path = "crates/eframe", default-features = false }
|
||||
emath = { version = "0.33.2", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.33.2", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.33.2", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.33.2", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.33.2", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.33.2", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.33.2", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.33.2", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.33.2", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.33.2", path = "crates/egui_glow", default-features = false }
|
||||
egui_kittest = { version = "0.33.2", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.33.2", 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
|
||||
"std",
|
||||
accesskit = "0.21.1"
|
||||
accesskit_consumer = "0.30.1"
|
||||
accesskit_winit = "0.29.1"
|
||||
ab_glyph = "0.2.32"
|
||||
ahash = { version = "0.8.12", default-features = false, features = [
|
||||
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
||||
"std",
|
||||
] }
|
||||
backtrace = "0.3"
|
||||
bitflags = "2.6"
|
||||
bytemuck = "1.7.2"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
dify = { version = "0.7", default-features = false }
|
||||
document-features = "0.2.10"
|
||||
glow = "0.16"
|
||||
glutin = { version = "0.32.0", default-features = false }
|
||||
android_logger = "0.15.1"
|
||||
arboard = { version = "3.6.1", default-features = false }
|
||||
backtrace = "0.3.76"
|
||||
bitflags = "2.9.4"
|
||||
bytemuck = "1.24.0"
|
||||
chrono = { version = "0.4.42", default-features = false }
|
||||
cint = "0.3.1"
|
||||
color-hex = "0.2.0"
|
||||
criterion = { version = "0.7.0", default-features = false }
|
||||
dify = { version = "0.7.4", default-features = false }
|
||||
directories = "6.0.0"
|
||||
document-features = "0.2.11"
|
||||
ehttp = { version = "0.5.0", default-features = false }
|
||||
enum-map = "2.7.3"
|
||||
env_logger = { version = "0.11.8", default-features = false }
|
||||
glow = "0.16.0"
|
||||
glutin = { version = "0.32.3", default-features = false }
|
||||
glutin-winit = { version = "0.5.0", default-features = false }
|
||||
home = "0.5.9"
|
||||
image = { version = "0.25", default-features = false }
|
||||
kittest = { version = "0.1" }
|
||||
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"
|
||||
ron = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1.0.37"
|
||||
type-map = "0.5.0"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = "0.3.70"
|
||||
image = { version = "0.25.6", default-features = false }
|
||||
js-sys = "0.3.77"
|
||||
kittest = { version = "0.3.0" }
|
||||
log = { version = "0.4.28", features = ["std"] }
|
||||
memoffset = "0.9.1"
|
||||
mimalloc = "0.1.48"
|
||||
mime_guess2 = { version = "2.3.1", default-features = false }
|
||||
mint = "0.5.9"
|
||||
nohash-hasher = "0.2.0"
|
||||
objc2 = "0.5.2"
|
||||
objc2-app-kit = { version = "0.2.2", default-features = false }
|
||||
objc2-foundation = { version = "0.2.2", default-features = false }
|
||||
objc2-ui-kit = { version = "0.2.2", default-features = false }
|
||||
open = "5.3.2"
|
||||
parking_lot = "0.12.5"
|
||||
percent-encoding = "2.3.2"
|
||||
poll-promise = { version = "0.3.0", default-features = false }
|
||||
pollster = "0.4.0"
|
||||
profiling = { version = "1.0.17", default-features = false }
|
||||
puffin = "0.19.1"
|
||||
puffin_http = "0.16.1"
|
||||
rand = "0.9.2"
|
||||
raw-window-handle = "0.6.2"
|
||||
rayon = "1.11.0"
|
||||
resvg = { version = "0.45.1", default-features = false }
|
||||
rfd = "0.15.4"
|
||||
ron = "0.11.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
similar-asserts = "1.7.0"
|
||||
smallvec = "1.15.1"
|
||||
smithay-clipboard = "0.7.2"
|
||||
static_assertions = "1.1.0"
|
||||
syntect = { version = "5.3.0", default-features = false }
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2.0.17"
|
||||
tokio = "1.47.1"
|
||||
type-map = "0.5.1"
|
||||
unicode_names2 = { version = "2.0.0", default-features = false }
|
||||
unicode-segmentation = "1.12.0"
|
||||
wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
|
||||
wasm-bindgen-futures = "0.4.0"
|
||||
wayland-cursor = { version = "0.31.11", default-features = false }
|
||||
web-sys = "0.3.77"
|
||||
web-time = "1.1.0" # Timekeeping for native and web
|
||||
wgpu = { version = "24.0.0", default-features = false }
|
||||
windows-sys = "0.59"
|
||||
winit = { version = "0.30.7", default-features = false }
|
||||
webbrowser = "1.0.5"
|
||||
wgpu = { version = "27.0.1", default-features = false, features = ["std"] }
|
||||
windows-sys = "0.61.2"
|
||||
winit = { version = "0.30.12", default-features = false }
|
||||
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
@@ -115,7 +156,7 @@ rust_2021_prelude_collisions = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unexpected_cfgs = "warn"
|
||||
unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
|
||||
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"
|
||||
@@ -130,25 +171,33 @@ broken_intra_doc_links = "warn"
|
||||
|
||||
# See also clippy.toml
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
|
||||
allow_attributes = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
await_holding_lock = "warn"
|
||||
bool_to_int_with_if = "warn"
|
||||
branches_sharing_code = "warn"
|
||||
char_lit_as_u8 = "warn"
|
||||
checked_conversions = "warn"
|
||||
clear_with_drain = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
default_union_representation = "warn"
|
||||
derive_partial_eq_without_eq = "warn"
|
||||
disallowed_macros = "warn" # See clippy.toml
|
||||
disallowed_methods = "warn" # See clippy.toml
|
||||
disallowed_names = "warn" # See clippy.toml
|
||||
disallowed_script_idents = "warn" # See clippy.toml
|
||||
disallowed_types = "warn" # See clippy.toml
|
||||
disallowed_macros = "warn" # See clippy.toml
|
||||
disallowed_methods = "warn" # See clippy.toml
|
||||
disallowed_names = "warn" # See clippy.toml
|
||||
disallowed_script_idents = "warn" # See clippy.toml
|
||||
disallowed_types = "warn" # See clippy.toml
|
||||
doc_comment_double_space_linebreaks = "warn"
|
||||
doc_link_with_quotes = "warn"
|
||||
doc_markdown = "warn"
|
||||
elidable_lifetime_names = "warn"
|
||||
empty_enum = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
empty_line_after_outer_attr = "warn"
|
||||
enum_glob_use = "warn"
|
||||
equatable_if_let = "warn"
|
||||
exit = "warn"
|
||||
@@ -165,9 +214,11 @@ fn_to_numeric_cast_any = "warn"
|
||||
from_iter_instead_of_collect = "warn"
|
||||
get_unwrap = "warn"
|
||||
if_let_mutex = "warn"
|
||||
ignore_without_reason = "warn"
|
||||
implicit_clone = "warn"
|
||||
implied_bounds_in_impls = "warn"
|
||||
imprecise_flops = "warn"
|
||||
inconsistent_struct_constructor = "warn"
|
||||
index_refutable_slice = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
infinite_loop = "warn"
|
||||
@@ -178,33 +229,37 @@ iter_filter_is_some = "warn"
|
||||
iter_not_returning_iterator = "warn"
|
||||
iter_on_empty_collections = "warn"
|
||||
iter_on_single_items = "warn"
|
||||
iter_over_hash_type = "warn"
|
||||
iter_without_into_iter = "warn"
|
||||
large_digit_groups = "warn"
|
||||
large_futures = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
large_stack_frames = "warn"
|
||||
large_types_passed_by_value = "warn"
|
||||
let_unit_value = "warn"
|
||||
linkedlist = "warn"
|
||||
literal_string_with_formatting_args = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
macro_use_imports = "warn"
|
||||
manual_assert = "warn"
|
||||
manual_clamp = "warn"
|
||||
manual_instant_elapsed = "warn"
|
||||
manual_is_power_of_two = "warn"
|
||||
manual_is_variant_and = "warn"
|
||||
manual_let_else = "warn"
|
||||
manual_midpoint = "warn" # NOTE `midpoint` is often a lot slower for floats, so we have our own `emath::fast_midpoint` function.
|
||||
manual_ok_or = "warn"
|
||||
manual_string_new = "warn"
|
||||
map_err_ignore = "warn"
|
||||
map_flatten = "warn"
|
||||
map_unwrap_or = "warn"
|
||||
match_bool = "warn"
|
||||
match_on_vec_items = "warn"
|
||||
match_same_arms = "warn"
|
||||
match_wild_err_arm = "warn"
|
||||
match_wildcard_for_single_variants = "warn"
|
||||
mem_forget = "warn"
|
||||
mismatching_type_param_order = "warn"
|
||||
missing_assert_message = "warn"
|
||||
missing_enforced_import_renames = "warn"
|
||||
missing_errors_doc = "warn"
|
||||
missing_safety_doc = "warn"
|
||||
@@ -217,11 +272,16 @@ needless_for_each = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_pass_by_value = "warn"
|
||||
negative_feature_names = "warn"
|
||||
non_std_lazy_statics = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
option_as_ref_cloned = "warn"
|
||||
option_option = "warn"
|
||||
path_buf_push_overwrite = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
precedence_bits = "warn"
|
||||
print_stderr = "warn"
|
||||
print_stdout = "warn"
|
||||
ptr_as_ptr = "warn"
|
||||
ptr_cast_constness = "warn"
|
||||
pub_underscore_fields = "warn"
|
||||
@@ -233,10 +293,13 @@ ref_as_ptr = "warn"
|
||||
ref_option_ref = "warn"
|
||||
ref_patterns = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
return_and_then = "warn"
|
||||
same_functions_in_if_condition = "warn"
|
||||
semicolon_if_nothing_returned = "warn"
|
||||
set_contains_or_insert = "warn"
|
||||
single_char_pattern = "warn"
|
||||
single_match_else = "warn"
|
||||
single_option_map = "warn"
|
||||
str_split_at_newline = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
@@ -247,8 +310,10 @@ string_to_string = "warn"
|
||||
suspicious_command_arg_space = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
todo = "warn"
|
||||
too_long_first_doc_paragraph = "warn"
|
||||
trailing_empty_array = "warn"
|
||||
trait_duplication_in_bounds = "warn"
|
||||
transmute_ptr_to_ptr = "warn"
|
||||
tuple_array_conversions = "warn"
|
||||
unchecked_duration_subtraction = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
@@ -256,14 +321,22 @@ unimplemented = "warn"
|
||||
uninhabited_references = "warn"
|
||||
uninlined_format_args = "warn"
|
||||
unnecessary_box_returns = "warn"
|
||||
unnecessary_debug_formatting = "warn"
|
||||
unnecessary_literal_bound = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
unnecessary_safety_doc = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unnecessary_semicolon = "warn"
|
||||
unnecessary_struct_initialization = "warn"
|
||||
unnecessary_wraps = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
unused_async = "warn"
|
||||
unused_peekable = "warn"
|
||||
unused_rounding = "warn"
|
||||
unused_self = "warn"
|
||||
unused_trait_names = "warn"
|
||||
use_self = "warn"
|
||||
useless_let_if_seq = "warn"
|
||||
useless_transmute = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
@@ -271,17 +344,17 @@ zero_sized_map_values = "warn"
|
||||
|
||||
|
||||
# TODO(emilk): maybe enable more of these lints?
|
||||
iter_over_hash_type = "allow"
|
||||
missing_assert_message = "allow"
|
||||
comparison_chain = "allow"
|
||||
should_panic_without_expect = "allow"
|
||||
too_many_lines = "allow"
|
||||
unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one
|
||||
unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one
|
||||
|
||||
# These are meh:
|
||||
assigning_clones = "allow" # No please
|
||||
assigning_clones = "allow" # No please
|
||||
let_underscore_must_use = "allow"
|
||||
let_underscore_untyped = "allow"
|
||||
manual_range_contains = "allow" # this one is just worse imho
|
||||
self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
|
||||
manual_range_contains = "allow" # this one is just worse imho
|
||||
map_unwrap_or = "allow" # so is this one
|
||||
self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
|
||||
significant_drop_tightening = "allow" # Too many false positives
|
||||
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports
|
||||
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports
|
||||
|
||||
65
README.md
65
README.md
@@ -4,14 +4,14 @@
|
||||
[](https://crates.io/crates/egui)
|
||||
[](https://docs.rs/egui)
|
||||
[](https://github.com/rust-secure-code/safety-dance/)
|
||||
[](https://github.com/emilk/egui/actions?workflow=CI)
|
||||
[](https://github.com/emilk/egui/blob/master/LICENSE-MIT)
|
||||
[](https://github.com/emilk/egui/blob/master/LICENSE-APACHE)
|
||||
[](https://github.com/emilk/egui/actions/workflows/rust.yml)
|
||||
[](https://github.com/emilk/egui/blob/main/LICENSE-MIT)
|
||||
[](https://github.com/emilk/egui/blob/main/LICENSE-APACHE)
|
||||
[](https://discord.gg/JFcEma9bJq)
|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.rerun.io/"><img src="media/rerun_io_logo.png" width="250"></a>
|
||||
<a href="https://www.rerun.io/"><img src="https://github.com/user-attachments/assets/78e79463-4357-461b-bbd1-31aa5ef5e1a2" width="250"></a>
|
||||
|
||||
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
|
||||
an SDK for visualizing streams of multimodal data.
|
||||
@@ -27,7 +27,7 @@ egui aims to be the easiest-to-use Rust GUI library, and the simplest way to mak
|
||||
|
||||
egui can be used anywhere you can draw textured triangles, which means you can easily integrate it into your game engine of choice.
|
||||
|
||||
[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) is the official egui framework, which supports writing apps for Web, Linux, Mac, Windows, and Android.
|
||||
[`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) is the official egui framework, which supports writing apps for Web, Linux, Mac, Windows, and Android.
|
||||
|
||||
|
||||
## Example
|
||||
@@ -46,7 +46,7 @@ ui.label(format!("Hello '{name}', age {age}"));
|
||||
ui.image(egui::include_image!("ferris.png"));
|
||||
```
|
||||
|
||||
<img alt="Dark mode" src="media/demo.gif"> <img alt="Light mode" src="media/demo_light_mode.png" height="278">
|
||||
<img alt="Dark mode" src="https://github.com/user-attachments/assets/3b446d29-99d8-4c82-86bb-4d8ef0516017"> <img alt="Light mode" src="https://github.com/user-attachments/assets/a5e7da93-89a8-4ba0-86b8-0fa2228a4f62" height="278">
|
||||
|
||||
## Sections:
|
||||
|
||||
@@ -68,19 +68,19 @@ ui.image(egui::include_image!("ferris.png"));
|
||||
|
||||
## Quick start
|
||||
|
||||
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/master/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
|
||||
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
|
||||
|
||||
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
|
||||
|
||||
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md).
|
||||
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Demo
|
||||
|
||||
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with Wasm and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
|
||||
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with Wasm and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
|
||||
|
||||
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
|
||||
|
||||
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
|
||||
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
|
||||
|
||||
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
|
||||
|
||||
@@ -99,7 +99,7 @@ On Fedora Rawhide you need to run:
|
||||
* Easy to integrate into any environment
|
||||
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
|
||||
* Pure immediate mode: no callbacks
|
||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
|
||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/toggle_switch.rs)
|
||||
* Modular: You should be able to use small parts of egui and combine them in new ways
|
||||
* Safe: there is no `unsafe` code in egui
|
||||
* Minimal dependencies
|
||||
@@ -136,28 +136,20 @@ Still, egui can be used to create professional looking applications, like [the R
|
||||
Check out the [3rd party egui crates wiki](https://github.com/emilk/egui/wiki/3rd-party-egui-crates) for even more
|
||||
widgets and features, maintained by the community.
|
||||
|
||||
<img src="media/widget_gallery_0.23.gif" width="50%">
|
||||
<img src="https://github.com/user-attachments/assets/13e73b76-e456-42bd-8ec9-220802834268" width="50%">
|
||||
|
||||
Light Theme:
|
||||
|
||||
<img src="media/widget_gallery_0.23_light.png" width="50%">
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/2e38972c-a444-4894-b32f-47a2719cf369" width="50%">
|
||||
|
||||
## Dependencies
|
||||
`egui` has a minimal set of default dependencies:
|
||||
|
||||
* [`ab_glyph`](https://crates.io/crates/ab_glyph)
|
||||
* [`ahash`](https://crates.io/crates/ahash)
|
||||
* [`bitflags`](https://crates.io/crates/bitflags)
|
||||
* [`nohash-hasher`](https://crates.io/crates/nohash-hasher)
|
||||
* [`parking_lot`](https://crates.io/crates/parking_lot)
|
||||
|
||||
`egui` has a minimal set of default dependencies.
|
||||
Heavier dependencies are kept out of `egui`, even as opt-in.
|
||||
All code in `egui` is Wasm-friendly (even outside a browser).
|
||||
|
||||
To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/master/crates/egui_extras) crate.
|
||||
To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/main/crates/egui_extras) crate.
|
||||
|
||||
[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc,
|
||||
[`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc,
|
||||
|
||||
## Who is egui for?
|
||||
|
||||
@@ -178,16 +170,16 @@ An integration needs to do the following each frame:
|
||||
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
|
||||
* Call into the application GUI code
|
||||
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
|
||||
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs))
|
||||
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/main/crates/egui_glow/src/painter.rs))
|
||||
|
||||
### Official integrations
|
||||
|
||||
These are the official egui integrations:
|
||||
|
||||
* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`
|
||||
* [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps
|
||||
* [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API)
|
||||
* [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit)
|
||||
* [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`
|
||||
* [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps
|
||||
* [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API)
|
||||
* [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit)
|
||||
|
||||
### 3rd party integrations
|
||||
|
||||
@@ -268,7 +260,7 @@ This is not yet as powerful as say CSS, [but this is going to improve](https://g
|
||||
|
||||
Here is an example (from https://github.com/a-liashenko/TinyPomodoro):
|
||||
|
||||
<img src="media/pompodoro-skin.png" width="50%">
|
||||
<img src="https://github.com/user-attachments/assets/e6107237-2547-41d6-996b-9a20ae0345ab" width="50%">
|
||||
|
||||
### How do I use egui with `async`?
|
||||
If you call `.await` in your GUI code, the UI will freeze, which is very bad UX. Instead, keep the GUI thread non-blocking and communicate with any concurrent tasks (`async` tasks or other threads) with something like:
|
||||
@@ -287,7 +279,7 @@ egui includes optional support for [AccessKit](https://accesskit.dev/), which cu
|
||||
|
||||
The original discussion of accessibility in egui is at <https://github.com/emilk/egui/issues/167>. Now that AccessKit support is merged, providing a strong foundation for future accessibility work, please open new issues on specific accessibility problems.
|
||||
|
||||
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
|
||||
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/main/crates/eframe)?
|
||||
|
||||
`egui` is a 2D user interface library for laying out and interacting with buttons, sliders, etc.
|
||||
`egui` has no idea if it is running on the web or natively, and does not know how to collect input or show things on screen.
|
||||
@@ -304,15 +296,16 @@ If you want to embed 3D into an egui view there are two options:
|
||||
|
||||
#### `Shape::Callback`
|
||||
Example:
|
||||
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_glow/src/main.rs>
|
||||
* <https://github.com/emilk/egui/blob/main/examples/custom_3d_glow/src/main.rs>
|
||||
|
||||
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
|
||||
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
|
||||
|
||||
#### Render-to-texture
|
||||
You can also render your 3D scene to a texture and display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use.
|
||||
|
||||
Examples:
|
||||
* Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs
|
||||
* Using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) + [`VTK (C++)`](https://vtk.org/): https://github.com/Gerharddc/vtk-egui-demo
|
||||
|
||||
|
||||
## Other
|
||||
@@ -354,11 +347,11 @@ Notable contributions by:
|
||||
* [@AsmPrgmC3](https://github.com/AsmPrgmC3): [Proper sRGBA blending for web](https://github.com/emilk/egui/pull/650)
|
||||
* [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685)
|
||||
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543)
|
||||
* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868)
|
||||
* [@KentaTheBugMaker](https://github.com/KentaTheBugMaker): [Port glow painter to web](https://github.com/emilk/egui/pull/868)
|
||||
* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050)
|
||||
* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625)
|
||||
* [@mwcampbell](https://github.com/mwcampbell): [AccessKit](https://github.com/AccessKit/accesskit) [integration](https://github.com/emilk/egui/pull/2294)
|
||||
* [@hasenbanck](https://github.com/hasenbanck), [@s-nie](https://github.com/s-nie), [@Wumpf](https://github.com/Wumpf): [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)
|
||||
* [@hasenbanck](https://github.com/hasenbanck), [@s-nie](https://github.com/s-nie), [@Wumpf](https://github.com/Wumpf): [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)
|
||||
* [@jprochazk](https://github.com/jprochazk): [egui image API](https://github.com/emilk/egui/issues/3291)
|
||||
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
|
||||
|
||||
@@ -376,7 +369,7 @@ Default fonts:
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.rerun.io/"><img src="media/rerun_io_logo.png" width="440"></a>
|
||||
<a href="https://www.rerun.io/"><img src="https://github.com/user-attachments/assets/78e79463-4357-461b-bbd1-31aa5ef5e1a2" width="440"></a>
|
||||
|
||||
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
|
||||
an SDK for visualizing streams of multimodal data.
|
||||
|
||||
61
RELEASES.md
61
RELEASES.md
@@ -9,7 +9,7 @@ All crates under the [`crates/`](crates/) folder are published in lock-step, wit
|
||||
|
||||
The only exception to this are patch releases, where we sometimes only patch a single crate.
|
||||
|
||||
The egui version in egui `master` is always the version of the last published crates. This is so that users can easily patch their egui crates to egui `master` if they want to.
|
||||
The egui version in egui `main` is always the version of the last published crates. This is so that users can easily patch their egui crates to egui `main` if they want to.
|
||||
|
||||
## Governance
|
||||
Releases are generally done by [emilk](https://github.com/emilk/), but the [rerun-io](https://github.com/rerun-io/) organization (where emilk is CTO) also has publish rights to all the crates.
|
||||
@@ -22,8 +22,11 @@ We don't update the MSRV in a patch release, unless we really, really need to.
|
||||
|
||||
|
||||
# Release process
|
||||
## Patch release
|
||||
* [ ] Make a branch off of the latest release
|
||||
* [ ] copy this checklist to a new egui issue, called "Release 0.xx.y"
|
||||
* [ ] close all issues in the milestone for this release
|
||||
|
||||
## Special steps for patch release
|
||||
* [ ] make a branch off of the _latest_ release
|
||||
* [ ] cherry-pick what you want to release
|
||||
* [ ] run `cargo semver-checks`
|
||||
|
||||
@@ -45,41 +48,32 @@ We don't update the MSRV in a patch release, unless we really, really need to.
|
||||
* [ ] check that CI is green
|
||||
|
||||
## Preparation
|
||||
* [ ] make sure there are no important unmerged PRs
|
||||
* [ ] Ensure we don't have any patch/git dependencies (e.g. kittest)
|
||||
* [ ] Create a branch called `release-0.xx.0` and open a PR for it
|
||||
* [ ] run `scripts/generate_example_screenshots.sh` if needed
|
||||
* [ ] write a short release note that fits in a bluesky post
|
||||
* [ ] record gif for `CHANGELOG.md` release note (and later bluesky post)
|
||||
* [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write`
|
||||
* [ ] bump version numbers in workspace `Cargo.toml`
|
||||
* [ ] update changelogs
|
||||
* [ ] run `scripts/generate_changelog.py --version 0.x.0 --write`
|
||||
* [ ] read changelogs and clean them up if needed
|
||||
* [ ] write a good intro with highlight for the main changelog
|
||||
* [ ] run `typos`
|
||||
|
||||
## Actual release
|
||||
I usually do this all on the `master` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `master` later.
|
||||
|
||||
* [ ] Run `typos`
|
||||
* [ ] `git commit -m 'Release 0.x.0 - <release title>'`
|
||||
* [ ] `cargo publish` (see below)
|
||||
* [ ] bump version numbers in workspace `Cargo.toml`
|
||||
* [ ] check that CI for the PR is green
|
||||
* [ ] publish the crates by running `scripts/publish_crates.sh`
|
||||
* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - <release title>'`
|
||||
* [ ] `git pull --tags ; git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force ; git push --tags`
|
||||
* [ ] merge release PR or push to `master`
|
||||
* [ ] check that CI is green
|
||||
* [ ] merge release PR as `Release 0.x.0 - <release title>`
|
||||
* [ ] check that CI for `main` is green
|
||||
* [ ] do a GitHub release: https://github.com/emilk/egui/releases/new
|
||||
* Follow the format of the last release
|
||||
* [ ] wait for documentation to build: https://docs.rs/releases/queue
|
||||
* follow the format of the last release
|
||||
* [ ] wait for the documentation build to finish: https://docs.rs/releases/queue
|
||||
* [ ] https://docs.rs/egui/ works
|
||||
* [ ] https://docs.rs/eframe/ works
|
||||
|
||||
### `cargo publish`:
|
||||
```
|
||||
(cd crates/emath && cargo publish --quiet) && echo "✅ emath"
|
||||
(cd crates/ecolor && cargo publish --quiet) && echo "✅ ecolor"
|
||||
(cd crates/epaint_default_fonts && cargo publish --quiet) && echo "✅ epaint_default_fonts"
|
||||
(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-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"
|
||||
```
|
||||
|
||||
## Announcements
|
||||
* [ ] [Bluesky](https://bsky.app/profile/ernerfeldt.bsky.social)
|
||||
@@ -88,10 +82,17 @@ I usually do this all on the `master` branch, but doing it in a release branch i
|
||||
* [ ] [r/programming](https://www.reddit.com/r/programming/comments/1bocsf6/announcing_egui_027_an_easytouse_crossplatform/)
|
||||
* [ ] [This Week in Rust](https://github.com/rust-lang/this-week-in-rust/pull/5167)
|
||||
|
||||
|
||||
## After release
|
||||
* [ ] publish new `eframe_template`
|
||||
* [ ] update `eframe_template`
|
||||
* [ ] publish new `egui_plot`
|
||||
* [ ] publish new `egui_table`
|
||||
* [ ] publish new `egui_tiles`
|
||||
* [ ] make a PR to `egui_commonmark`
|
||||
* [ ] make a PR to `rerun`
|
||||
|
||||
|
||||
## Finally
|
||||
* [ ] close the milestone
|
||||
* [ ] close this issue
|
||||
* [ ] improve `RELEASES.md` with what you learned this time around
|
||||
|
||||
64
clippy.toml
64
clippy.toml
@@ -3,7 +3,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Section identical to scripts/clippy_wasm/clippy.toml:
|
||||
|
||||
msrv = "1.81"
|
||||
msrv = "1.88"
|
||||
|
||||
allow-unwrap-in-tests = true
|
||||
|
||||
@@ -23,29 +23,25 @@ type-complexity-threshold = 350
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros
|
||||
disallowed-macros = [
|
||||
'dbg',
|
||||
'std::unimplemented',
|
||||
'std::dbg',
|
||||
'std::unimplemented',
|
||||
|
||||
# TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses
|
||||
# 'std::eprint',
|
||||
# 'std::eprintln',
|
||||
# 'std::print',
|
||||
# 'std::println',
|
||||
# TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses
|
||||
# 'std::eprint',
|
||||
# 'std::eprintln',
|
||||
# 'std::print',
|
||||
# 'std::println',
|
||||
]
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods
|
||||
disallowed-methods = [
|
||||
"std::env::temp_dir", # Use the tempdir crate instead
|
||||
# NOTE: There are many things that aren't allowed on wasm,
|
||||
# but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406)
|
||||
# so we do that in `clipppy_wasm.toml` instead.
|
||||
|
||||
# There are many things that aren't allowed on wasm,
|
||||
# but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406)
|
||||
# so we do that in `clipppy_wasm.toml` instead.
|
||||
|
||||
"std::thread::spawn", # Use `std::thread::Builder` and name the thread
|
||||
|
||||
"sha1::Digest::new", # SHA1 is cryptographically broken
|
||||
|
||||
"std::panic::catch_unwind", # We compile with `panic = "abort"`
|
||||
{ path = "std::env::temp_dir", readon = "Use the tempfile crate instead" },
|
||||
{ path = "std::panic::catch_unwind", reason = "We compile with `panic = abort" },
|
||||
{ path = "std::thread::spawn", readon = "Use `std::thread::Builder` and name the thread" },
|
||||
]
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names
|
||||
@@ -53,28 +49,24 @@ disallowed-names = []
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
|
||||
disallowed-types = [
|
||||
# Use the faster & simpler non-poisonable primitives in `parking_lot` instead
|
||||
"std::sync::Mutex",
|
||||
"std::sync::RwLock",
|
||||
"std::sync::Condvar",
|
||||
# "std::sync::Once", # enabled for now as the `log_once` macro uses it internally
|
||||
|
||||
"ring::digest::SHA1_FOR_LEGACY_USE_ONLY", # SHA1 is cryptographically broken
|
||||
|
||||
"winit::dpi::LogicalSize", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account
|
||||
"winit::dpi::LogicalPosition", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account
|
||||
{ path = "std::sync::Condvar", reason = "Use parking_lot instead" },
|
||||
{ path = "std::sync::Mutex", reason = "Use epaint::mutex instead" },
|
||||
{ path = "std::sync::RwLock", reason = "Use epaint::mutex instead" },
|
||||
{ path = "winit::dpi::LogicalPosition", reason = "We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account" },
|
||||
{ path = "winit::dpi::LogicalSize", reason = "We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account" },
|
||||
# "std::sync::Once", # enabled for now as the `log_once` macro uses it internally
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
|
||||
doc-valid-idents = [
|
||||
# You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
|
||||
"AccessKit",
|
||||
"WebGL",
|
||||
"WebGL1",
|
||||
"WebGL2",
|
||||
"WebGPU",
|
||||
"VirtualBox",
|
||||
"..",
|
||||
# You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
|
||||
"AccessKit",
|
||||
"WebGL",
|
||||
"WebGL1",
|
||||
"WebGL2",
|
||||
"WebGPU",
|
||||
"VirtualBox",
|
||||
"..",
|
||||
]
|
||||
|
||||
@@ -6,6 +6,38 @@ 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.33.2 - 2025-11-13
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09
|
||||
* Align `Color32` to 4 bytes [#7318](https://github.com/emilk/egui/pull/7318) by [@anti-social](https://github.com/anti-social)
|
||||
* Make the `hex_color` macro `const` [#7444](https://github.com/emilk/egui/pull/7444) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
|
||||
|
||||
|
||||
## 0.32.3 - 2025-09-12
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.2 - 2025-09-04
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.1 - 2025-08-15
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.0 - 2025-07-10
|
||||
* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk)
|
||||
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
|
||||
* Make `Hsva` derive serde [#7132](https://github.com/emilk/egui/pull/7132) by [@bircni](https://github.com/bircni)
|
||||
|
||||
|
||||
## 0.31.1 - 2025-03-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04
|
||||
* Add `Color32::CYAN` and `Color32::MAGENTA` [#5663](https://github.com/emilk/egui/pull/5663) by [@juancampa](https://github.com/juancampa)
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
[package]
|
||||
name = "ecolor"
|
||||
version.workspace = true
|
||||
authors = [
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||
"Andreas Reich <reichandreas@gmx.de>",
|
||||
]
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>", "Andreas Reich <reichandreas@gmx.de>"]
|
||||
description = "Color structs and color conversion utilities"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
@@ -39,10 +36,10 @@ emath.workspace = true
|
||||
bytemuck = { workspace = true, optional = true, features = ["derive"] }
|
||||
|
||||
## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries.
|
||||
cint = { version = "0.3.1", optional = true }
|
||||
cint = { workspace = true, optional = true }
|
||||
|
||||
## Enable the [`hex_color`] macro.
|
||||
color-hex = { version = "0.2.0", optional = true }
|
||||
color-hex = { workspace = true, optional = true }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { workspace = true, optional = true }
|
||||
|
||||
@@ -8,4 +8,6 @@
|
||||
|
||||
A simple color storage and conversion library.
|
||||
|
||||
Made for [`egui`](https://github.com/emilk/egui/).
|
||||
This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/).
|
||||
|
||||
If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{linear_f32_from_linear_u8, linear_u8_from_linear_f32, Color32, Hsva, HsvaGamma, Rgba};
|
||||
use super::{Color32, Hsva, HsvaGamma, Rgba, linear_f32_from_linear_u8, linear_u8_from_linear_f32};
|
||||
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
|
||||
|
||||
// ---- Color32 ----
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
|
||||
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
|
||||
|
||||
/// This format is used for space-efficient color representation (32 bits).
|
||||
///
|
||||
/// Instead of manipulating this directly it is often better
|
||||
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
|
||||
///
|
||||
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
|
||||
/// Alpha channel is in linear space.
|
||||
/// Internally this uses 0-255 gamma space `sRGBA` color with _premultiplied alpha_.
|
||||
///
|
||||
/// The special value of alpha=0 means the color is to be treated as an additive color.
|
||||
/// It's the non-linear ("gamma") values that are multiplied with the alpha.
|
||||
///
|
||||
/// Premultiplied alpha means that the color values have been pre-multiplied with the alpha (opacity).
|
||||
/// This is in contrast with "normal" RGBA, where the alpha is _separate_ (or "unmultiplied").
|
||||
/// Using premultiplied alpha has some advantages:
|
||||
/// * It allows encoding additive colors
|
||||
/// * It is the better way to blend colors, e.g. when filtering texture colors
|
||||
/// * Because the above, it is the better way to encode colors in a GPU texture
|
||||
///
|
||||
/// The color space is assumed to be [sRGB](https://en.wikipedia.org/wiki/SRGB).
|
||||
///
|
||||
/// All operations on `Color32` are done in "gamma space" (see <https://en.wikipedia.org/wiki/SRGB>).
|
||||
/// This is not physically correct, but it is fast and sometimes more perceptually even than linear space.
|
||||
/// If you instead want to perform these operations in linear-space color, use [`Rgba`].
|
||||
///
|
||||
/// An `alpha=0` means the color is to be treated as an additive color.
|
||||
#[repr(C)]
|
||||
#[repr(align(4))]
|
||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct Color32(pub(crate) [u8; 4]);
|
||||
|
||||
impl std::fmt::Debug for Color32 {
|
||||
/// Prints the contents with premultiplied alpha!
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let [r, g, b, a] = self.0;
|
||||
write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
|
||||
@@ -87,44 +103,49 @@ impl Color32 {
|
||||
/// i.e. often taken to mean "no color".
|
||||
pub const PLACEHOLDER: Self = Self::from_rgba_premultiplied(64, 254, 0, 128);
|
||||
|
||||
#[deprecated = "Renamed to PLACEHOLDER"]
|
||||
pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER;
|
||||
|
||||
/// From RGB with alpha of 255 (opaque).
|
||||
#[inline]
|
||||
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 255])
|
||||
}
|
||||
|
||||
/// From RGB into an additive color (will make everything it blend with brighter).
|
||||
#[inline]
|
||||
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 0])
|
||||
}
|
||||
|
||||
/// From `sRGBA` with premultiplied alpha.
|
||||
///
|
||||
/// You likely want to use [`Self::from_rgba_unmultiplied`] instead.
|
||||
#[inline]
|
||||
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
/// From `sRGBA` WITHOUT premultiplied alpha.
|
||||
/// From `sRGBA` with separate alpha.
|
||||
///
|
||||
/// This is a "normal" RGBA value that you would find in a color picker or a table somewhere.
|
||||
///
|
||||
/// You can use [`Self::to_srgba_unmultiplied`] to get back these values,
|
||||
/// but for transparent colors what you get back might be slightly different (rounding errors).
|
||||
#[inline]
|
||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
use std::sync::OnceLock;
|
||||
match a {
|
||||
// common-case optimization
|
||||
// common-case optimization:
|
||||
0 => Self::TRANSPARENT,
|
||||
// common-case optimization
|
||||
|
||||
// common-case optimization:
|
||||
255 => Self::from_rgb(r, g, b),
|
||||
|
||||
a => {
|
||||
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
|
||||
let lut = LOOKUP_TABLE.get_or_init(|| {
|
||||
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8};
|
||||
(0..=u16::MAX)
|
||||
.map(|i| {
|
||||
let [value, alpha] = i.to_ne_bytes();
|
||||
let value_lin = linear_f32_from_gamma_u8(value);
|
||||
let alpha_lin = linear_f32_from_linear_u8(alpha);
|
||||
gamma_u8_from_linear_f32(value_lin * alpha_lin)
|
||||
fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
@@ -136,22 +157,47 @@ impl Color32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
|
||||
///
|
||||
/// It is slightly slower when operating on non-const data.
|
||||
#[inline]
|
||||
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
match a {
|
||||
// common-case optimization:
|
||||
0 => Self::TRANSPARENT,
|
||||
|
||||
// common-case optimization:
|
||||
255 => Self::from_rgb(r, g, b),
|
||||
|
||||
a => {
|
||||
let r = fast_round(r as f32 * linear_f32_from_linear_u8(a));
|
||||
let g = fast_round(g as f32 * linear_f32_from_linear_u8(a));
|
||||
let b = fast_round(b as f32 * linear_f32_from_linear_u8(a));
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque gray.
|
||||
#[doc(alias = "from_grey")]
|
||||
#[inline]
|
||||
pub const fn from_gray(l: u8) -> Self {
|
||||
Self([l, l, l, 255])
|
||||
}
|
||||
|
||||
/// Black with the given opacity.
|
||||
#[inline]
|
||||
pub const fn from_black_alpha(a: u8) -> Self {
|
||||
Self([0, 0, 0, a])
|
||||
}
|
||||
|
||||
/// White with the given opacity.
|
||||
#[inline]
|
||||
pub fn from_white_alpha(a: u8) -> Self {
|
||||
Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into()
|
||||
Self([a, a, a, a])
|
||||
}
|
||||
|
||||
/// Additive white.
|
||||
#[inline]
|
||||
pub const fn from_additive_luminance(l: u8) -> Self {
|
||||
Self([l, l, l, 0])
|
||||
@@ -162,21 +208,25 @@ impl Color32 {
|
||||
self.a() == 255
|
||||
}
|
||||
|
||||
/// Red component multiplied by alpha.
|
||||
#[inline]
|
||||
pub const fn r(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
/// Green component multiplied by alpha.
|
||||
#[inline]
|
||||
pub const fn g(&self) -> u8 {
|
||||
self.0[1]
|
||||
}
|
||||
|
||||
/// Blue component multiplied by alpha.
|
||||
#[inline]
|
||||
pub const fn b(&self) -> u8 {
|
||||
self.0[2]
|
||||
}
|
||||
|
||||
/// Alpha (opacity).
|
||||
#[inline]
|
||||
pub const fn a(&self) -> u8 {
|
||||
self.0[3]
|
||||
@@ -213,9 +263,26 @@ impl Color32 {
|
||||
(self.r(), self.g(), self.b(), self.a())
|
||||
}
|
||||
|
||||
/// Convert to a normal "unmultiplied" RGBA color (i.e. with separate alpha).
|
||||
///
|
||||
/// This will unmultiply the alpha.
|
||||
///
|
||||
/// This is the inverse of [`Self::from_rgba_unmultiplied`],
|
||||
/// but due to precision problems it may return slightly different values for transparent colors.
|
||||
#[inline]
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
Rgba::from(*self).to_srgba_unmultiplied()
|
||||
let [r, g, b, a] = self.to_array();
|
||||
match a {
|
||||
// Common-case optimization.
|
||||
0 | 255 => self.to_array(),
|
||||
a => {
|
||||
let factor = 255.0 / a as f32;
|
||||
let r = fast_round(factor * r as f32);
|
||||
let g = fast_round(factor * g as f32);
|
||||
let b = fast_round(factor * b as f32);
|
||||
[r, g, b, a]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiply with 0.5 to make color half as opaque, perceptually.
|
||||
@@ -225,7 +292,10 @@ impl Color32 {
|
||||
/// This is perceptually even, and faster that [`Self::linear_multiply`].
|
||||
#[inline]
|
||||
pub fn gamma_multiply(self, factor: f32) -> Self {
|
||||
debug_assert!(0.0 <= factor && factor.is_finite());
|
||||
debug_assert!(
|
||||
0.0 <= factor && factor.is_finite(),
|
||||
"factor should be finite, but was {factor}"
|
||||
);
|
||||
let Self([r, g, b, a]) = self;
|
||||
Self([
|
||||
(r as f32 * factor + 0.5) as u8,
|
||||
@@ -258,7 +328,10 @@ impl Color32 {
|
||||
/// You likely want to use [`Self::gamma_multiply`] instead.
|
||||
#[inline]
|
||||
pub fn linear_multiply(self, factor: f32) -> Self {
|
||||
debug_assert!(0.0 <= factor && factor.is_finite());
|
||||
debug_assert!(
|
||||
0.0 <= factor && factor.is_finite(),
|
||||
"factor should be finite, but was {factor}"
|
||||
);
|
||||
// As an unfortunate side-effect of using premultiplied alpha
|
||||
// we need a somewhat expensive conversion to linear space and back.
|
||||
Rgba::from(self).multiply(factor).into()
|
||||
@@ -291,10 +364,18 @@ impl Color32 {
|
||||
)
|
||||
}
|
||||
|
||||
/// Blend two colors, so that `self` is behind the argument.
|
||||
/// Blend two colors in gamma space, so that `self` is behind the argument.
|
||||
pub fn blend(self, on_top: Self) -> Self {
|
||||
self.gamma_multiply_u8(255 - on_top.a()) + on_top
|
||||
}
|
||||
|
||||
/// Intensity of the color.
|
||||
///
|
||||
/// Returns a value in the range 0-1.
|
||||
/// The brighter the color, the closer to 1.
|
||||
pub fn intensity(&self) -> f32 {
|
||||
(self.r() as f32 * 0.299 + self.g() as f32 * 0.587 + self.b() as f32 * 0.114) / 255.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for Color32 {
|
||||
@@ -325,3 +406,133 @@ impl std::ops::Add for Color32 {
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn test_rgba() -> impl Iterator<Item = [u8; 4]> {
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 255],
|
||||
[10, 0, 30, 0],
|
||||
[10, 0, 30, 40],
|
||||
[10, 100, 200, 0],
|
||||
[10, 100, 200, 100],
|
||||
[10, 100, 200, 200],
|
||||
[10, 100, 200, 255],
|
||||
[10, 100, 200, 40],
|
||||
[10, 20, 0, 0],
|
||||
[10, 20, 0, 255],
|
||||
[10, 20, 30, 255],
|
||||
[10, 20, 30, 40],
|
||||
[255, 255, 255, 0],
|
||||
[255, 255, 255, 255],
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color32_additive() {
|
||||
let opaque = Color32::from_rgb(40, 50, 60);
|
||||
let additive = Color32::from_rgb(255, 127, 10).additive();
|
||||
assert_eq!(additive.blend(opaque), opaque, "opaque on top of additive");
|
||||
assert_eq!(
|
||||
opaque.blend(additive),
|
||||
Color32::from_rgb(255, 177, 70),
|
||||
"additive on top of opaque"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color32_blend_vs_gamma_blend() {
|
||||
let opaque = Color32::from_rgb(0x60, 0x60, 0x60);
|
||||
let transparent = Color32::from_rgba_unmultiplied(168, 65, 65, 79);
|
||||
assert_eq!(
|
||||
transparent.blend(opaque),
|
||||
opaque,
|
||||
"Opaque on top of transparent"
|
||||
);
|
||||
// Blending in gamma-space is the de-facto standard almost everywhere.
|
||||
// Browsers and most image editors do it, and so it is what users expect.
|
||||
assert_eq!(
|
||||
opaque.blend(transparent),
|
||||
Color32::from_rgb(
|
||||
blend(0x60, 168, 79),
|
||||
blend(0x60, 65, 79),
|
||||
blend(0x60, 65, 79)
|
||||
),
|
||||
"Transparent on top of opaque"
|
||||
);
|
||||
|
||||
fn blend(dest: u8, src: u8, alpha: u8) -> u8 {
|
||||
let src = src as f32 / 255.0;
|
||||
let dest = dest as f32 / 255.0;
|
||||
let alpha = alpha as f32 / 255.0;
|
||||
fast_round((src * alpha + dest * (1.0 - alpha)) * 255.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color32_unmultiplied_round_trip() {
|
||||
for in_rgba in test_rgba() {
|
||||
let [r, g, b, a] = in_rgba;
|
||||
if a == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let c = Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
let out_rgba = c.to_srgba_unmultiplied();
|
||||
|
||||
if a == 255 {
|
||||
assert_eq!(in_rgba, out_rgba);
|
||||
} else {
|
||||
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
||||
// because we multiply and then unmultiply the alpha.
|
||||
for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) {
|
||||
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_black_white_alpha() {
|
||||
for a in 0..=255 {
|
||||
assert_eq!(
|
||||
Color32::from_white_alpha(a),
|
||||
Color32::from_rgba_unmultiplied(255, 255, 255, a)
|
||||
);
|
||||
assert_eq!(
|
||||
Color32::from_white_alpha(a),
|
||||
Color32::WHITE.gamma_multiply_u8(a)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Color32::from_black_alpha(a),
|
||||
Color32::from_rgba_unmultiplied(0, 0, 0, a)
|
||||
);
|
||||
assert_eq!(
|
||||
Color32::from_black_alpha(a),
|
||||
Color32::BLACK.gamma_multiply_u8(a)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_from_rgba() {
|
||||
for [r, g, b, a] in test_rgba() {
|
||||
let original = Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
let constfn = Color32::from_rgba_unmultiplied_const(r, g, b, a);
|
||||
let rgba = Rgba::from(original);
|
||||
let back = Color32::from(rgba);
|
||||
assert_eq!(back, original);
|
||||
assert_eq!(constfn, original);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Color32::from(Rgba::from_rgba_unmultiplied(1.0, 0.0, 0.0, 0.5)),
|
||||
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
/// let _ = ecolor::hex_color!("#20212x");
|
||||
/// ```
|
||||
///
|
||||
/// The macro cannot be used in a `const` context.
|
||||
/// The macro can be used in a `const` context.
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// ```
|
||||
/// const COLOR: ecolor::Color32 = ecolor::hex_color!("#202122");
|
||||
/// assert_eq!(COLOR, ecolor::Color32::from_rgb(0x20, 0x21, 0x22));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! hex_color {
|
||||
@@ -42,7 +43,7 @@ macro_rules! hex_color {
|
||||
let array = $crate::color_hex::color_from_hex!($s);
|
||||
match array.as_slice() {
|
||||
[r, g, b] => $crate::Color32::from_rgb(*r, *g, *b),
|
||||
[r, g, b, a] => $crate::Color32::from_rgba_unmultiplied(*r, *g, *b, *a),
|
||||
[r, g, b, a] => $crate::Color32::from_rgba_unmultiplied_const(*r, *g, *b, *a),
|
||||
_ => panic!("Invalid hex color length: expected 3 (RGB) or 4 (RGBA) bytes"),
|
||||
}
|
||||
}};
|
||||
|
||||
@@ -90,14 +90,15 @@ impl HexColor {
|
||||
let [r, gb] = u16::from_str_radix(s, 16)
|
||||
.map_err(ParseHexColorError::InvalidInt)?
|
||||
.to_be_bytes();
|
||||
let [r, g, b] = [r, gb >> 4, gb & 0x0f].map(|u| u << 4 | u);
|
||||
let [r, g, b] = [r, gb >> 4, gb & 0x0f].map(|u| (u << 4) | u);
|
||||
Ok(Self::Hex3(Color32::from_rgb(r, g, b)))
|
||||
}
|
||||
4 => {
|
||||
let [r_g, b_a] = u16::from_str_radix(s, 16)
|
||||
.map_err(ParseHexColorError::InvalidInt)?
|
||||
.to_be_bytes();
|
||||
let [r, g, b, a] = [r_g >> 4, r_g & 0x0f, b_a >> 4, b_a & 0x0f].map(|u| u << 4 | u);
|
||||
let [r, g, b, a] =
|
||||
[r_g >> 4, r_g & 0x0f, b_a >> 4, b_a & 0x0f].map(|u| (u << 4) | u);
|
||||
Ok(Self::Hex4(Color32::from_rgba_unmultiplied(r, g, b, a)))
|
||||
}
|
||||
6 => {
|
||||
@@ -207,17 +208,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hex_string_round_trip() {
|
||||
use Color32 as C;
|
||||
let cases = [
|
||||
C::from_rgba_unmultiplied(10, 20, 30, 0),
|
||||
C::from_rgba_unmultiplied(10, 20, 30, 40),
|
||||
C::from_rgba_unmultiplied(10, 20, 30, 255),
|
||||
C::from_rgba_unmultiplied(0, 20, 30, 0),
|
||||
C::from_rgba_unmultiplied(10, 0, 30, 40),
|
||||
C::from_rgba_unmultiplied(10, 20, 0, 255),
|
||||
[0, 20, 30, 0],
|
||||
[10, 0, 30, 40],
|
||||
[10, 100, 200, 0],
|
||||
[10, 100, 200, 100],
|
||||
[10, 100, 200, 200],
|
||||
[10, 100, 200, 255],
|
||||
[10, 100, 200, 40],
|
||||
[10, 20, 0, 255],
|
||||
[10, 20, 30, 0],
|
||||
[10, 20, 30, 255],
|
||||
[10, 20, 30, 40],
|
||||
];
|
||||
for color in cases {
|
||||
assert_eq!(C::from_hex(color.to_hex().as_str()), Ok(color));
|
||||
for [r, g, b, a] in cases {
|
||||
let color = Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
assert_eq!(Color32::from_hex(color.to_hex().as_str()), Ok(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::{
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
||||
linear_u8_from_linear_f32, Color32, Rgba,
|
||||
Color32, Rgba, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32,
|
||||
};
|
||||
|
||||
/// Hue, saturation, value, alpha. All in the range [0, 1].
|
||||
/// No premultiplied alpha.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Hsva {
|
||||
/// hue 0-1
|
||||
@@ -29,30 +29,20 @@ impl Hsva {
|
||||
/// From `sRGBA` with premultiplied alpha
|
||||
#[inline]
|
||||
pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self {
|
||||
Self::from_rgba_premultiplied(
|
||||
linear_f32_from_gamma_u8(r),
|
||||
linear_f32_from_gamma_u8(g),
|
||||
linear_f32_from_gamma_u8(b),
|
||||
linear_f32_from_linear_u8(a),
|
||||
)
|
||||
Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
|
||||
}
|
||||
|
||||
/// From `sRGBA` without premultiplied alpha
|
||||
#[inline]
|
||||
pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self {
|
||||
Self::from_rgba_unmultiplied(
|
||||
linear_f32_from_gamma_u8(r),
|
||||
linear_f32_from_gamma_u8(g),
|
||||
linear_f32_from_gamma_u8(b),
|
||||
linear_f32_from_linear_u8(a),
|
||||
)
|
||||
Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
|
||||
}
|
||||
|
||||
/// From linear RGBA with premultiplied alpha
|
||||
#[inline]
|
||||
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
if a == 0.0 {
|
||||
if a <= 0.0 {
|
||||
if r == 0.0 && b == 0.0 && a == 0.0 {
|
||||
Self::default()
|
||||
} else {
|
||||
@@ -152,13 +142,7 @@ impl Hsva {
|
||||
|
||||
#[inline]
|
||||
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_premultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a),
|
||||
]
|
||||
Color32::from(*self).to_array()
|
||||
}
|
||||
|
||||
/// To gamma-space 0-255.
|
||||
@@ -250,7 +234,7 @@ pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // a bit expensive
|
||||
#[ignore = "too expensive"]
|
||||
fn test_hsv_roundtrip() {
|
||||
for r in 0..=255 {
|
||||
for g in 0..=255 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
|
||||
use crate::{Color32, Hsva, Rgba, gamma_from_linear, linear_from_gamma};
|
||||
|
||||
/// Like Hsva but with the `v` value (brightness) being gamma corrected
|
||||
/// so that it is somewhat perceptually even.
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
//! Color conversions and types.
|
||||
//!
|
||||
//! This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/).
|
||||
//!
|
||||
//! If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead.
|
||||
//!
|
||||
//! If you want a compact color representation, use [`Color32`].
|
||||
//! If you want to manipulate RGBA colors use [`Rgba`].
|
||||
//! If you want to manipulate RGBA colors in linear space use [`Rgba`].
|
||||
//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
|
||||
//!
|
||||
//! ## Conventions
|
||||
//! The word "gamma" or "srgb" is used to refer to values in the non-linear space defined by
|
||||
//! [the sRGB transfer function](https://en.wikipedia.org/wiki/SRGB).
|
||||
//! We use `u8` for anything in the "gamma" space.
|
||||
//!
|
||||
//! We use `f32` in 0-1 range for anything in the linear space.
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
@@ -39,23 +50,46 @@ pub use hex_color_runtime::*;
|
||||
|
||||
impl From<Color32> for Rgba {
|
||||
fn from(srgba: Color32) -> Self {
|
||||
Self([
|
||||
linear_f32_from_gamma_u8(srgba.0[0]),
|
||||
linear_f32_from_gamma_u8(srgba.0[1]),
|
||||
linear_f32_from_gamma_u8(srgba.0[2]),
|
||||
linear_f32_from_linear_u8(srgba.0[3]),
|
||||
])
|
||||
let [r, g, b, a] = srgba.to_array();
|
||||
if a == 0 {
|
||||
// Additive, or completely transparent
|
||||
Self([
|
||||
linear_f32_from_gamma_u8(r),
|
||||
linear_f32_from_gamma_u8(g),
|
||||
linear_f32_from_gamma_u8(b),
|
||||
0.0,
|
||||
])
|
||||
} else {
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self([
|
||||
linear_from_gamma(r as f32 / (255.0 * a)) * a,
|
||||
linear_from_gamma(g as f32 / (255.0 * a)) * a,
|
||||
linear_from_gamma(b as f32 / (255.0 * a)) * a,
|
||||
a,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Color32 {
|
||||
fn from(rgba: Rgba) -> Self {
|
||||
Self([
|
||||
gamma_u8_from_linear_f32(rgba.0[0]),
|
||||
gamma_u8_from_linear_f32(rgba.0[1]),
|
||||
gamma_u8_from_linear_f32(rgba.0[2]),
|
||||
linear_u8_from_linear_f32(rgba.0[3]),
|
||||
])
|
||||
let [r, g, b, a] = rgba.to_array();
|
||||
if a == 0.0 {
|
||||
// Additive, or completely transparent
|
||||
Self([
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
0,
|
||||
])
|
||||
} else {
|
||||
Self([
|
||||
fast_round(gamma_u8_from_linear_f32(r / a) as f32 * a),
|
||||
fast_round(gamma_u8_from_linear_f32(g / a) as f32 * a),
|
||||
fast_round(gamma_u8_from_linear_f32(b / a) as f32 * a),
|
||||
linear_u8_from_linear_f32(a),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +105,7 @@ pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
|
||||
/// linear [0, 255] -> linear [0, 1].
|
||||
/// Useful for alpha-channel.
|
||||
#[inline(always)]
|
||||
pub fn linear_f32_from_linear_u8(a: u8) -> f32 {
|
||||
pub const fn linear_f32_from_linear_u8(a: u8) -> f32 {
|
||||
a as f32 / 255.0
|
||||
}
|
||||
|
||||
@@ -96,7 +130,7 @@ pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
|
||||
fast_round(a * 255.0)
|
||||
}
|
||||
|
||||
fn fast_round(r: f32) -> u8 {
|
||||
const fn fast_round(r: f32) -> u8 {
|
||||
(r + 0.5) as _ // rust does a saturating cast since 1.45
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::{
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
||||
linear_u8_from_linear_f32,
|
||||
};
|
||||
use crate::Color32;
|
||||
|
||||
/// 0-1 linear space `RGBA` color with premultiplied alpha.
|
||||
///
|
||||
/// See [`crate::Color32`] for explanation of what "premultiplied alpha" means.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
@@ -34,12 +33,11 @@ pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
|
||||
} else if f.is_nan() {
|
||||
state.write_u8(1);
|
||||
} else {
|
||||
use std::hash::Hash;
|
||||
use std::hash::Hash as _;
|
||||
f.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
impl std::hash::Hash for Rgba {
|
||||
#[inline]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
@@ -70,20 +68,12 @@ impl Rgba {
|
||||
|
||||
#[inline]
|
||||
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
let r = linear_f32_from_gamma_u8(r);
|
||||
let g = linear_f32_from_gamma_u8(g);
|
||||
let b = linear_f32_from_gamma_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
let r = linear_f32_from_gamma_u8(r);
|
||||
let g = linear_f32_from_gamma_u8(g);
|
||||
let b = linear_f32_from_gamma_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self::from_rgba_premultiplied(r * a, g * a, b * a, a)
|
||||
Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -99,15 +89,24 @@ impl Rgba {
|
||||
|
||||
#[inline]
|
||||
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
|
||||
debug_assert!(0.0 <= l && l <= 1.0);
|
||||
debug_assert!(0.0 <= a && a <= 1.0);
|
||||
debug_assert!(
|
||||
0.0 <= l && l <= 1.0,
|
||||
"l should be in the range [0, 1], but was {l}"
|
||||
);
|
||||
debug_assert!(
|
||||
0.0 <= a && a <= 1.0,
|
||||
"a should be in the range [0, 1], but was {a}"
|
||||
);
|
||||
Self([l * a, l * a, l * a, a])
|
||||
}
|
||||
|
||||
/// Transparent black
|
||||
#[inline]
|
||||
pub fn from_black_alpha(a: f32) -> Self {
|
||||
debug_assert!(0.0 <= a && a <= 1.0);
|
||||
debug_assert!(
|
||||
0.0 <= a && a <= 1.0,
|
||||
"a should be in the range [0, 1], but was {a}"
|
||||
);
|
||||
Self([0.0, 0.0, 0.0, a])
|
||||
}
|
||||
|
||||
@@ -211,13 +210,12 @@ impl Rgba {
|
||||
/// unmultiply the alpha
|
||||
#[inline]
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a.abs()),
|
||||
]
|
||||
crate::Color32::from(*self).to_srgba_unmultiplied()
|
||||
}
|
||||
|
||||
/// Blend two colors in linear space, so that `self` is behind the argument.
|
||||
pub fn blend(self, on_top: Self) -> Self {
|
||||
self.multiply(1.0 - on_top.a()) + on_top
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,3 +274,72 @@ impl std::ops::Mul<Rgba> for f32 {
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_rgba() -> impl Iterator<Item = [u8; 4]> {
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 255],
|
||||
[10, 0, 30, 0],
|
||||
[10, 0, 30, 40],
|
||||
[10, 100, 200, 0],
|
||||
[10, 100, 200, 100],
|
||||
[10, 100, 200, 200],
|
||||
[10, 100, 200, 255],
|
||||
[10, 100, 200, 40],
|
||||
[10, 20, 0, 0],
|
||||
[10, 20, 0, 255],
|
||||
[10, 20, 30, 255],
|
||||
[10, 20, 30, 40],
|
||||
[255, 255, 255, 0],
|
||||
[255, 255, 255, 255],
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgba_blend() {
|
||||
let opaque = Rgba::from_rgb(0.4, 0.5, 0.6);
|
||||
let transparent = Rgba::from_rgb(1.0, 0.5, 0.0).multiply(0.3);
|
||||
assert_eq!(
|
||||
transparent.blend(opaque),
|
||||
opaque,
|
||||
"Opaque on top of transparent"
|
||||
);
|
||||
assert_eq!(
|
||||
opaque.blend(transparent),
|
||||
Rgba::from_rgb(
|
||||
0.7 * 0.4 + 0.3 * 1.0,
|
||||
0.7 * 0.5 + 0.3 * 0.5,
|
||||
0.7 * 0.6 + 0.3 * 0.0
|
||||
),
|
||||
"Transparent on top of opaque"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgba_roundtrip() {
|
||||
for in_rgba in test_rgba() {
|
||||
let [r, g, b, a] = in_rgba;
|
||||
if a == 0 {
|
||||
continue;
|
||||
}
|
||||
let rgba = Rgba::from_srgba_unmultiplied(r, g, b, a);
|
||||
let out_rgba = rgba.to_srgba_unmultiplied();
|
||||
|
||||
if a == 255 {
|
||||
assert_eq!(in_rgba, out_rgba);
|
||||
} else {
|
||||
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
||||
// because we multiply and then unmultiply the alpha.
|
||||
for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) {
|
||||
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,73 @@ 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.33.2 - 2025-11-13
|
||||
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
|
||||
* Make sure `native_pixels_per_point` is set during app creation [#7683](https://github.com/emilk/egui/pull/7683) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09
|
||||
### ⭐ Added
|
||||
* Add an option to limit the repaint rate in the web runner [#7482](https://github.com/emilk/egui/pull/7482) by [@s-nie](https://github.com/s-nie)
|
||||
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)
|
||||
* Add support for the safe area on iOS [#7578](https://github.com/emilk/egui/pull/7578) by [@irh](https://github.com/irh)
|
||||
|
||||
### 🔧 Changed
|
||||
* Replace `winapi` with `windows-sys` crate [#7416](https://github.com/emilk/egui/pull/7416) by [@unlimitedsola](https://github.com/unlimitedsola)
|
||||
* Prevent default action on command-comma in eframe web [#7547](https://github.com/emilk/egui/pull/7547) by [@emilk](https://github.com/emilk)
|
||||
* Warn if `DYLD_LIBRARY_PATH` is set and we find no wgpu adapter [#7572](https://github.com/emilk/egui/pull/7572) by [@emilk](https://github.com/emilk)
|
||||
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Properly end winit event loop [#7565](https://github.com/emilk/egui/pull/7565) by [@tye-exe](https://github.com/tye-exe)
|
||||
* Fix eframe window not being focused on mac on startup [#7593](https://github.com/emilk/egui/pull/7593) by [@emilk](https://github.com/emilk)
|
||||
* Fix black flash on start in glow eframe backend [#7616](https://github.com/emilk/egui/pull/7616) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
|
||||
## 0.32.3 - 2025-09-12
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.2 - 2025-09-04
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.1 - 2025-08-15
|
||||
* Enable wgpu default features in eframe / egui_wgpu default features [#7344](https://github.com/emilk/egui/pull/7344) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
* Request a redraw when the url change through the `popstate` event listener [#7403](https://github.com/emilk/egui/pull/7403) by [@irevoire](https://github.com/irevoire)
|
||||
|
||||
|
||||
## 0.32.0 - 2025-07-10
|
||||
### ⭐ Added
|
||||
* Add pointer events and focus handling for apps run in a Shadow DOM [#5627](https://github.com/emilk/egui/pull/5627) by [@xxvvii](https://github.com/xxvvii)
|
||||
* MacOS: Add `movable_by_window_background` option to viewport [#5412](https://github.com/emilk/egui/pull/5412) by [@jim-ec](https://github.com/jim-ec)
|
||||
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
|
||||
* Add external eventloop support [#6750](https://github.com/emilk/egui/pull/6750) by [@wpbrown](https://github.com/wpbrown)
|
||||
|
||||
### 🔧 Changed
|
||||
* Update MSRV to 1.85 [#7279](https://github.com/emilk/egui/pull/7279) by [@emilk](https://github.com/emilk)
|
||||
* Use Rust edition 2024 [#7280](https://github.com/emilk/egui/pull/7280) by [@emilk](https://github.com/emilk)
|
||||
* Rename `should_propagate_event` and add `should_prevent_default` [#5779](https://github.com/emilk/egui/pull/5779) by [@th0rex](https://github.com/th0rex)
|
||||
* Clarify platform-specific details for `Viewport` positioning [#5715](https://github.com/emilk/egui/pull/5715) by [@aspiringLich](https://github.com/aspiringLich)
|
||||
* Enhance stability on Windows [#5723](https://github.com/emilk/egui/pull/5723) by [@rustbasic](https://github.com/rustbasic)
|
||||
* Set `web-sys` min version to `0.3.73` [#5862](https://github.com/emilk/egui/pull/5862) by [@wareya](https://github.com/wareya)
|
||||
* Bump `ron` to `0.10.1` [#6861](https://github.com/emilk/egui/pull/6861) by [@torokati44](https://github.com/torokati44)
|
||||
* Disallow `accesskit` on Android NativeActivity, making `hello_android` working again [#6855](https://github.com/emilk/egui/pull/6855) by [@podusowski](https://github.com/podusowski)
|
||||
* Respect and detect `prefers-color-scheme: no-preference` [#7293](https://github.com/emilk/egui/pull/7293) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Mark all keys as up if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
|
||||
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
|
||||
* Fix text distortion on mobile devices/browsers with `glow` backend [#6893](https://github.com/emilk/egui/pull/6893) by [@wareya](https://github.com/wareya)
|
||||
* Workaround libpng crash on macOS by not creating `NSImage` from png data [#7252](https://github.com/emilk/egui/pull/7252) by [@Wumpf](https://github.com/Wumpf)
|
||||
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
|
||||
* Web: only consume copy/cut events if the canvas has focus [#7270](https://github.com/emilk/egui/pull/7270) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.31.1 - 2025-03-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04
|
||||
* Web: Fix incorrect scale when moving to screen with new DPI [#5631](https://github.com/emilk/egui/pull/5631) by [@emilk](https://github.com/emilk)
|
||||
* Re-enable IME support on Linux [#5198](https://github.com/emilk/egui/pull/5198) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
|
||||
@@ -5,19 +5,13 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "egui framework - write GUI apps that compiles to web and/or natively"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
|
||||
homepage = "https://github.com/emilk/egui/tree/main/crates/eframe"
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["egui", "gui", "gamedev"]
|
||||
include = [
|
||||
"../LICENSE-APACHE",
|
||||
"../LICENSE-MIT",
|
||||
"**/*.rs",
|
||||
"Cargo.toml",
|
||||
"data/icon.png",
|
||||
]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -34,16 +28,15 @@ workspace = true
|
||||
default = [
|
||||
"accesskit",
|
||||
"default_fonts",
|
||||
"glow",
|
||||
"wayland", # Required for Linux support (including CI!)
|
||||
"wayland", # Required for Linux support (including CI!)
|
||||
"web_screen_reader",
|
||||
"wgpu",
|
||||
"winit/default",
|
||||
"x11",
|
||||
"egui-wgpu?/fragile-send-sync-non-atomic-wasm",
|
||||
]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
|
||||
accesskit = ["egui-winit/accesskit"]
|
||||
|
||||
# Allow crates to choose an android-activity backend via Winit
|
||||
# - It's important that most applications should not have to depend on android-activity directly, and can
|
||||
@@ -59,17 +52,15 @@ android-native-activity = ["egui-winit/android-native-activity"]
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = ["egui/default_fonts"]
|
||||
|
||||
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
|
||||
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
|
||||
##
|
||||
## There is generally no need to enable both the `wgpu` and `glow` features,
|
||||
## but if you do you can pick the renderer to use with [`NativeOptions::renderer`]
|
||||
## and `WebOptions::renderer`.
|
||||
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
|
||||
|
||||
## Enable saving app state to disk.
|
||||
persistence = [
|
||||
"dep:home",
|
||||
"egui-winit/serde",
|
||||
"egui/persistence",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
persistence = ["dep:home", "egui-winit/serde", "egui/persistence", "ron", "serde"]
|
||||
|
||||
## Enables wayland support and fixes clipboard issue.
|
||||
##
|
||||
@@ -85,25 +76,30 @@ wayland = [
|
||||
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
|
||||
##
|
||||
## For other platforms, use the `accesskit` feature instead.
|
||||
web_screen_reader = [
|
||||
"web-sys/SpeechSynthesis",
|
||||
"web-sys/SpeechSynthesisUtterance",
|
||||
]
|
||||
web_screen_reader = ["web-sys/SpeechSynthesis", "web-sys/SpeechSynthesisUtterance"]
|
||||
|
||||
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
|
||||
## Enable [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)).
|
||||
##
|
||||
## This overrides the `glow` feature.
|
||||
## There is generally no need to enable both the `wgpu` and `glow` features,
|
||||
## but if you do you can pick the renderer to use with [`NativeOptions::renderer`]
|
||||
## and `WebOptions::renderer`.
|
||||
##
|
||||
## By default, only WebGPU is enabled on web.
|
||||
## If you want to enable WebGL, you need to turn on the `webgl` feature of crate `wgpu`:
|
||||
##
|
||||
## ```toml
|
||||
## wgpu = { version = "*", features = ["webgpu", "webgl"] }
|
||||
## ```
|
||||
## Switching from `wgpu (the default)` to `glow` can significantly reduce your binary size
|
||||
## (including the .wasm of a web app).
|
||||
## See <https://github.com/emilk/egui/issues/5889> for more details.
|
||||
##
|
||||
## By default, eframe will prefer WebGPU over WebGL, but
|
||||
## you can configure this at run-time with [`NativeOptions::wgpu_options`].
|
||||
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
||||
wgpu = ["wgpu_no_default_features", "egui-wgpu/default"]
|
||||
|
||||
## This is exactly like the `wgpu` feature, but does NOT enable the default features of `wgpu` and `egui-wgpu`.
|
||||
##
|
||||
## This means that no `wgpu` backends are enabled. You will need to enable them yourself, e.g. like this:
|
||||
##
|
||||
## ```toml
|
||||
## wgpu = { version = "*", features = ["dx12", "metal", "webgl"] }
|
||||
## ```
|
||||
wgpu_no_default_features = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
||||
|
||||
## Enables compiling for x11.
|
||||
x11 = [
|
||||
@@ -121,10 +117,7 @@ x11 = [
|
||||
__screenshot = []
|
||||
|
||||
[dependencies]
|
||||
egui = { workspace = true, default-features = false, features = [
|
||||
"bytemuck",
|
||||
"log",
|
||||
] }
|
||||
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
|
||||
|
||||
ahash.workspace = true
|
||||
document-features.workspace = true
|
||||
@@ -132,7 +125,7 @@ log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
profiling.workspace = true
|
||||
raw-window-handle.workspace = true
|
||||
static_assertions = "1.1.0"
|
||||
static_assertions.workspace = true
|
||||
web-time.workspace = true
|
||||
|
||||
# Optional dependencies
|
||||
@@ -145,45 +138,35 @@ serde = { workspace = true, optional = true }
|
||||
# -------------------------------------------
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
egui-winit = { workspace = true, default-features = false, features = [
|
||||
"clipboard",
|
||||
"links",
|
||||
] }
|
||||
egui-winit = { workspace = true, default-features = false, features = ["clipboard", "links"] }
|
||||
image = { workspace = true, features = ["png"] } # Needed for app icon
|
||||
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
|
||||
|
||||
# optional native:
|
||||
egui-wgpu = { workspace = true, optional = true, features = [
|
||||
"winit",
|
||||
"capture",
|
||||
] } # 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 = { 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 }
|
||||
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
|
||||
"metal",
|
||||
"webgpu",
|
||||
] }
|
||||
wgpu = { workspace = true, optional = true }
|
||||
|
||||
# mac:
|
||||
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
||||
objc2 = "0.5.1"
|
||||
objc2-foundation = { version = "0.2.0", default-features = false, features = [
|
||||
objc2.workspace = true
|
||||
objc2-foundation = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
"block2",
|
||||
"NSData",
|
||||
"NSString",
|
||||
] }
|
||||
objc2-app-kit = { version = "0.2.0", default-features = false, features = [
|
||||
objc2-app-kit = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
"NSApplication",
|
||||
"NSImage",
|
||||
@@ -194,11 +177,12 @@ objc2-app-kit = { version = "0.2.0", default-features = false, features = [
|
||||
|
||||
# windows:
|
||||
[target.'cfg(any(target_os = "windows"))'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["winuser"] }
|
||||
windows-sys = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
# -------------------------------------------
|
||||
@@ -206,8 +190,8 @@ windows-sys = { workspace = true, features = [
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
bytemuck.workspace = true
|
||||
image = { workspace = true, features = ["png"] } # For copying images
|
||||
js-sys = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
js-sys.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
wasm-bindgen.workspace = true
|
||||
wasm-bindgen-futures.workspace = true
|
||||
web-sys = { workspace = true, features = [
|
||||
@@ -253,6 +237,7 @@ web-sys = { workspace = true, features = [
|
||||
"ResizeObserverEntry",
|
||||
"ResizeObserverOptions",
|
||||
"ResizeObserverSize",
|
||||
"ShadowRoot",
|
||||
"Storage",
|
||||
"Touch",
|
||||
"TouchEvent",
|
||||
@@ -266,13 +251,9 @@ web-sys = { workspace = true, features = [
|
||||
] }
|
||||
|
||||
# optional web:
|
||||
egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit
|
||||
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
|
||||
"webgpu",
|
||||
] }
|
||||
egui-wgpu = { workspace = true, optional = true, features = ["capture"] } # if wgpu is used, use it without (!) winit
|
||||
wgpu = { workspace = true, optional = true }
|
||||
|
||||
# Native dev dependencies for testing
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
directories = "5"
|
||||
directories.workspace = true
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
`eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (for Linux, Mac, Windows, and Android) or as a web app (using [Wasm](https://en.wikipedia.org/wiki/WebAssembly)).
|
||||
|
||||
To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
|
||||
To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
|
||||
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
|
||||
|
||||
There is also a tutorial video at <https://www.youtube.com/watch?v=NtUkr_z7l84>.
|
||||
@@ -16,7 +16,7 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
|
||||
|
||||
---
|
||||
|
||||
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit).
|
||||
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit).
|
||||
|
||||
To use on Linux, first run:
|
||||
|
||||
@@ -24,14 +24,14 @@ To use on Linux, first run:
|
||||
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
|
||||
```
|
||||
|
||||
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
|
||||
You need to either use `edition = "2024"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
|
||||
|
||||
You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
|
||||
You can opt-in to the using [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
|
||||
|
||||
## Alternatives
|
||||
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
|
||||
|
||||
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/crates/egui_glow/examples/pure_glow.rs>.
|
||||
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/main/crates/egui_glow/examples/pure_glow.rs>.
|
||||
|
||||
|
||||
## Limitations when running egui on the web
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use std::any::Any;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use crate::native::winit_integration::UserEvent;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -22,7 +22,7 @@ use raw_window_handle::{
|
||||
use static_assertions::assert_not_impl_any;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
|
||||
|
||||
/// Hook into the building of an event loop before it is run
|
||||
@@ -30,7 +30,7 @@ pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
|
||||
/// You can configure any platform specific details required on top of the default configuration
|
||||
/// done by `EFrame`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
|
||||
|
||||
/// Hook into the building of a the native window.
|
||||
@@ -38,7 +38,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
|
||||
/// You can configure any platform specific details required on top of the default configuration
|
||||
/// done by `eframe`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
|
||||
|
||||
type DynError = Box<dyn std::error::Error + Send + Sync>;
|
||||
@@ -79,7 +79,7 @@ pub struct CreationContext<'s> {
|
||||
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
|
||||
///
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
|
||||
/// Raw platform window handle
|
||||
@@ -91,7 +91,7 @@ pub struct CreationContext<'s> {
|
||||
pub(crate) raw_display_handle: Result<RawDisplayHandle, HandleError>,
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl HasWindowHandle for CreationContext<'_> {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||
@@ -100,7 +100,7 @@ impl HasWindowHandle for CreationContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl HasDisplayHandle for CreationContext<'_> {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||
@@ -121,7 +121,7 @@ impl CreationContext<'_> {
|
||||
gl: None,
|
||||
#[cfg(feature = "glow")]
|
||||
get_proc_address: None,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
raw_window_handle: Err(HandleError::NotSupported),
|
||||
@@ -133,7 +133,7 @@ impl CreationContext<'_> {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
|
||||
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
|
||||
pub trait App {
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
///
|
||||
@@ -317,7 +317,7 @@ pub struct NativeOptions {
|
||||
pub hardware_acceleration: HardwareAcceleration,
|
||||
|
||||
/// What rendering backend to use.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub renderer: Renderer,
|
||||
|
||||
/// This controls what happens when you close the main eframe window.
|
||||
@@ -340,7 +340,7 @@ pub struct NativeOptions {
|
||||
/// event loop before it is run.
|
||||
///
|
||||
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub event_loop_builder: Option<EventLoopBuilderHook>,
|
||||
|
||||
/// Hook into the building of a window.
|
||||
@@ -349,7 +349,7 @@ pub struct NativeOptions {
|
||||
/// window appearance.
|
||||
///
|
||||
/// Note: A [`NativeOptions`] clone will not include any `window_builder` hook.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub window_builder: Option<WindowBuilderHook>,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
@@ -367,7 +367,7 @@ pub struct NativeOptions {
|
||||
pub centered: bool,
|
||||
|
||||
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
||||
|
||||
/// Controls whether or not the native window position and size will be
|
||||
@@ -404,13 +404,13 @@ impl Clone for NativeOptions {
|
||||
Self {
|
||||
viewport: self.viewport.clone(),
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
event_loop_builder: None, // Skip any builder callbacks if cloning
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
window_builder: None, // Skip any builder callbacks if cloning
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: self.wgpu_options.clone(),
|
||||
|
||||
persistence_path: self.persistence_path.clone(),
|
||||
@@ -435,15 +435,15 @@ impl Default for NativeOptions {
|
||||
stencil_buffer: 0,
|
||||
hardware_acceleration: HardwareAcceleration::Preferred,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
renderer: Renderer::default(),
|
||||
|
||||
run_and_return: true,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
event_loop_builder: None,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
window_builder: None,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
@@ -451,7 +451,7 @@ impl Default for NativeOptions {
|
||||
|
||||
centered: false,
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||
|
||||
persist_window: true,
|
||||
@@ -471,6 +471,10 @@ impl Default for NativeOptions {
|
||||
/// Options when using `eframe` in a web page.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct WebOptions {
|
||||
/// What rendering backend to use.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub renderer: Renderer,
|
||||
|
||||
/// Sets the number of bits in the depth buffer.
|
||||
///
|
||||
/// `egui` doesn't need the depth buffer, so the default value is 0.
|
||||
@@ -484,7 +488,7 @@ pub struct WebOptions {
|
||||
pub webgl_context_option: WebGlContextOption,
|
||||
|
||||
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
||||
|
||||
/// Controls whether to apply dithering to minimize banding artifacts.
|
||||
@@ -499,27 +503,43 @@ pub struct WebOptions {
|
||||
/// If the web event corresponding to an egui event should be propagated
|
||||
/// to the rest of the web page.
|
||||
///
|
||||
/// The default is `false`, meaning
|
||||
/// The default is `true`, meaning
|
||||
/// [`stopPropagation`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)
|
||||
/// is called on every event.
|
||||
pub should_propagate_event: Box<dyn Fn(&egui::Event) -> bool>,
|
||||
/// is called on every event, and the event is not propagated to the rest of the web page.
|
||||
pub should_stop_propagation: Box<dyn Fn(&egui::Event) -> bool>,
|
||||
|
||||
/// Whether the web event corresponding to an egui event should have `prevent_default` called
|
||||
/// on it or not.
|
||||
///
|
||||
/// Defaults to true.
|
||||
pub should_prevent_default: Box<dyn Fn(&egui::Event) -> bool>,
|
||||
|
||||
/// Maximum rate at which to repaint. This can be used to artificially reduce the repaint rate below
|
||||
/// vsync in order to save resources.
|
||||
pub max_fps: Option<u32>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl Default for WebOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
renderer: Renderer::default(),
|
||||
|
||||
depth_buffer: 0,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
webgl_context_option: WebGlContextOption::BestFirst,
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||
|
||||
dithering: true,
|
||||
|
||||
should_propagate_event: Box::new(|_| false),
|
||||
should_stop_propagation: Box::new(|_| true),
|
||||
should_prevent_default: Box::new(|_| true),
|
||||
|
||||
max_fps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,7 +568,7 @@ pub enum WebGlContextOption {
|
||||
/// What rendering backend to use.
|
||||
///
|
||||
/// You need to enable the "glow" and "wgpu" features to have a choice.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||
@@ -558,47 +578,49 @@ pub enum Renderer {
|
||||
Glow,
|
||||
|
||||
/// Use [`egui_wgpu`] renderer for [`wgpu`](https://github.com/gfx-rs/wgpu).
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Wgpu,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl Default for Renderer {
|
||||
fn default() -> Self {
|
||||
#[cfg(not(feature = "glow"))]
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
compile_error!("eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'");
|
||||
#[cfg(not(feature = "wgpu_no_default_features"))]
|
||||
compile_error!(
|
||||
"eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"
|
||||
);
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
#[cfg(not(feature = "wgpu_no_default_features"))]
|
||||
return Self::Glow;
|
||||
|
||||
#[cfg(not(feature = "glow"))]
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
return Self::Wgpu;
|
||||
|
||||
// By default, only the `glow` feature is enabled, so if the user added `wgpu` to the feature list
|
||||
// they probably wanted to use wgpu:
|
||||
// It's weird that the user has enabled both glow and wgpu,
|
||||
// but let's pick the better of the two (wgpu):
|
||||
#[cfg(feature = "glow")]
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
return Self::Wgpu;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl std::fmt::Display for Renderer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(feature = "glow")]
|
||||
Self::Glow => "glow".fmt(f),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Self::Wgpu => "wgpu".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl std::str::FromStr for Renderer {
|
||||
type Err = String;
|
||||
|
||||
@@ -607,10 +629,12 @@ impl std::str::FromStr for Renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
"glow" => Ok(Self::Glow),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
"wgpu" => Ok(Self::Wgpu),
|
||||
|
||||
_ => Err(format!("eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."))
|
||||
_ => Err(format!(
|
||||
"eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,7 +662,7 @@ pub struct Frame {
|
||||
Option<Box<dyn FnMut(glow::Texture) -> egui::TextureId>>,
|
||||
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
#[doc(hidden)]
|
||||
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
|
||||
@@ -655,7 +679,7 @@ pub struct Frame {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
assert_not_impl_any!(Frame: Clone);
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl HasWindowHandle for Frame {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||
@@ -664,7 +688,7 @@ impl HasWindowHandle for Frame {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl HasDisplayHandle for Frame {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||
@@ -688,7 +712,7 @@ impl Frame {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
raw_window_handle: Err(HandleError::NotSupported),
|
||||
storage: None,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
}
|
||||
}
|
||||
@@ -696,7 +720,7 @@ impl Frame {
|
||||
/// True if you are in a web environment.
|
||||
///
|
||||
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
||||
#[allow(clippy::unused_self)]
|
||||
#[expect(clippy::unused_self)]
|
||||
pub fn is_web(&self) -> bool {
|
||||
cfg!(target_arch = "wasm32")
|
||||
}
|
||||
@@ -747,7 +771,7 @@ impl Frame {
|
||||
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
|
||||
///
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
|
||||
self.wgpu_render_state.as_ref()
|
||||
}
|
||||
@@ -882,16 +906,15 @@ pub trait Storage {
|
||||
#[cfg(feature = "ron")]
|
||||
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
|
||||
profiling::function_scope!(key);
|
||||
storage
|
||||
.get_string(key)
|
||||
.and_then(|value| match ron::from_str(&value) {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
// This happens on when we break the format, e.g. when updating egui.
|
||||
log::debug!("Failed to decode RON: {err}");
|
||||
None
|
||||
}
|
||||
})
|
||||
let value = storage.get_string(key)?;
|
||||
match ron::from_str(&value) {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
// This happens on when we break the format, e.g. when updating egui.
|
||||
log::debug!("Failed to decode RON: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
|
||||
@@ -900,7 +923,7 @@ pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, valu
|
||||
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),
|
||||
Err(err) => log::error!("eframe failed to encode data using ron: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! If you are planning to write an app for web or native,
|
||||
//! and want to use [`egui`] for everything, then `eframe` is for you!
|
||||
//!
|
||||
//! To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
|
||||
//! To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
|
||||
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
|
||||
//!
|
||||
//! In short, you implement [`App`] (especially [`App::update`]) and then
|
||||
@@ -69,7 +69,7 @@
|
||||
//! #[wasm_bindgen]
|
||||
//! impl WebHandle {
|
||||
//! /// Installs a panic hook, then returns.
|
||||
//! #[allow(clippy::new_without_default)]
|
||||
//! #[expect(clippy::new_without_default)]
|
||||
//! #[wasm_bindgen(constructor)]
|
||||
//! pub fn new() -> Self {
|
||||
//! // Redirect [`log`] message to `console.log` and friends:
|
||||
@@ -144,13 +144,22 @@
|
||||
#![warn(missing_docs)] // let's keep eframe well-documented
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
// Limitation imposed by `accesskit_winit`:
|
||||
// https://github.com/AccessKit/accesskit/tree/accesskit-v0.18.0/platforms/winit#android-activity-compatibility`
|
||||
#[cfg(all(
|
||||
target_os = "android",
|
||||
feature = "accesskit",
|
||||
feature = "android-native-activity"
|
||||
))]
|
||||
compile_error!("`accesskit` feature is only available with `android-game-activity`");
|
||||
|
||||
// Re-export all useful libraries:
|
||||
pub use {egui, egui::emath, egui::epaint};
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub use {egui_glow, glow};
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub use {egui_wgpu, wgpu};
|
||||
|
||||
mod epi;
|
||||
@@ -179,11 +188,19 @@ pub use web::{WebLogger, WebRunner};
|
||||
// When compiling natively
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
mod native;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use native::run::EframeWinitApplication;
|
||||
|
||||
#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use native::run::EframePumpStatus;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[cfg(feature = "persistence")]
|
||||
pub use native::file_storage::storage_dir;
|
||||
|
||||
@@ -192,7 +209,7 @@ pub mod icon_data;
|
||||
|
||||
/// This is how you start a native (desktop) app.
|
||||
///
|
||||
/// The first argument is name of your app, which is a an identifier
|
||||
/// The first argument is name of your app, which is an identifier
|
||||
/// used for the save location of persistence (see [`App::save`]).
|
||||
/// It is also used as the application id on wayland.
|
||||
/// If you set no title on the viewport, the app id will be used
|
||||
@@ -235,13 +252,113 @@ pub mod icon_data;
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
|
||||
pub fn run_native(
|
||||
app_name: &str,
|
||||
mut native_options: NativeOptions,
|
||||
app_creator: AppCreator<'_>,
|
||||
) -> Result {
|
||||
let renderer = init_native(app_name, &mut native_options);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a proxy for your native eframe application to run on your own event loop.
|
||||
///
|
||||
/// See `run_native` for details about `app_name`.
|
||||
///
|
||||
/// Call from `fn main` like this:
|
||||
/// ``` no_run
|
||||
/// use eframe::{egui, UserEvent};
|
||||
/// use winit::event_loop::{ControlFlow, EventLoop};
|
||||
///
|
||||
/// fn main() -> eframe::Result {
|
||||
/// let native_options = eframe::NativeOptions::default();
|
||||
/// let eventloop = EventLoop::<UserEvent>::with_user_event().build()?;
|
||||
/// eventloop.set_control_flow(ControlFlow::Poll);
|
||||
///
|
||||
/// let mut winit_app = eframe::create_native(
|
||||
/// "MyExtApp",
|
||||
/// native_options,
|
||||
/// Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
|
||||
/// &eventloop,
|
||||
/// );
|
||||
///
|
||||
/// eventloop.run_app(&mut winit_app)?;
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct MyEguiApp {}
|
||||
///
|
||||
/// impl MyEguiApp {
|
||||
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
/// Self::default()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl eframe::App for MyEguiApp {
|
||||
/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
/// egui::CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.heading("Hello World!");
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See the `external_eventloop` example for a more complete example.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub fn create_native<'a>(
|
||||
app_name: &str,
|
||||
mut native_options: NativeOptions,
|
||||
app_creator: AppCreator<'a>,
|
||||
event_loop: &winit::event_loop::EventLoop<UserEvent>,
|
||||
) -> EframeWinitApplication<'a> {
|
||||
let renderer = init_native(app_name, &mut native_options);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
EframeWinitApplication::new(native::run::create_glow(
|
||||
app_name,
|
||||
native_options,
|
||||
app_creator,
|
||||
event_loop,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
EframeWinitApplication::new(native::run::create_wgpu(
|
||||
app_name,
|
||||
native_options,
|
||||
app_creator,
|
||||
event_loop,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
|
||||
#[cfg(not(feature = "__screenshot"))]
|
||||
assert!(
|
||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||
@@ -254,28 +371,16 @@ pub fn run_native(
|
||||
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
#[cfg(all(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(all(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
{
|
||||
match renderer {
|
||||
match native_options.renderer {
|
||||
Renderer::Glow => "glow",
|
||||
Renderer::Wgpu => "wgpu",
|
||||
};
|
||||
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
|
||||
}
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
}
|
||||
}
|
||||
renderer
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -315,7 +420,7 @@ pub fn run_native(
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub fn run_simple_native(
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
@@ -367,7 +472,7 @@ pub enum Error {
|
||||
OpenGL(egui_glow::PainterError),
|
||||
|
||||
/// An error from [`wgpu`].
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Wgpu(egui_wgpu::WgpuError),
|
||||
}
|
||||
|
||||
@@ -405,7 +510,7 @@ impl From<egui_glow::PainterError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
impl From<egui_wgpu::WgpuError> for Error {
|
||||
#[inline]
|
||||
fn from(err: egui_wgpu::WgpuError) -> Self {
|
||||
@@ -446,7 +551,7 @@ impl std::fmt::Display for Error {
|
||||
write!(f, "egui_glow: {err}")
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Self::Wgpu(err) => {
|
||||
write!(f, "WGPU error: {err}")
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ pub struct AppTitleIconSetter {
|
||||
|
||||
impl AppTitleIconSetter {
|
||||
pub fn new(title: String, mut icon_data: Option<Arc<IconData>>) -> Self {
|
||||
if let Some(icon) = &icon_data {
|
||||
if **icon == IconData::default() {
|
||||
icon_data = None;
|
||||
}
|
||||
if let Some(icon) = &icon_data
|
||||
&& **icon == IconData::default()
|
||||
{
|
||||
icon_data = None;
|
||||
}
|
||||
|
||||
Self {
|
||||
@@ -47,7 +47,7 @@ enum AppIconStatus {
|
||||
NotSetTryAgain,
|
||||
|
||||
/// We successfully set the icon and it should be visible now.
|
||||
#[allow(dead_code)] // Not used on Linux
|
||||
#[allow(dead_code, clippy::allow_attributes)] // Not used on Linux
|
||||
Set,
|
||||
}
|
||||
|
||||
@@ -71,16 +71,20 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta
|
||||
#[cfg(target_os = "macos")]
|
||||
return set_title_and_icon_mac(_title, _icon_data);
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
#[allow(unreachable_code, clippy::allow_attributes)]
|
||||
AppIconStatus::NotSetIgnored
|
||||
}
|
||||
|
||||
/// Set icon for Windows applications.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
use crate::icon_data::IconDataExt as _;
|
||||
use winapi::um::winuser;
|
||||
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
CreateIconFromResourceEx, GetSystemMetrics, HICON, ICON_BIG, ICON_SMALL, LR_DEFAULTCOLOR,
|
||||
SM_CXICON, SM_CXSMICON, SendMessageW, WM_SETICON,
|
||||
};
|
||||
|
||||
// We would get fairly far already with winit's `set_window_icon` (which is exposed to eframe) actually!
|
||||
// However, it only sets ICON_SMALL, i.e. doesn't allow us to set a higher resolution icon for the task bar.
|
||||
@@ -92,16 +96,13 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
// * using undocumented SetConsoleIcon method (successfully queried via GetProcAddress)
|
||||
|
||||
// SAFETY: WinApi function without side-effects.
|
||||
let window_handle = unsafe { winuser::GetActiveWindow() };
|
||||
let window_handle = unsafe { GetActiveWindow() };
|
||||
if window_handle.is_null() {
|
||||
// The Window isn't available yet. Try again later!
|
||||
return AppIconStatus::NotSetTryAgain;
|
||||
}
|
||||
|
||||
fn create_hicon_with_scale(
|
||||
unscaled_image: &image::RgbaImage,
|
||||
target_size: i32,
|
||||
) -> winapi::shared::windef::HICON {
|
||||
fn create_hicon_with_scale(unscaled_image: &image::RgbaImage, target_size: i32) -> HICON {
|
||||
let image_scaled = image::imageops::resize(
|
||||
unscaled_image,
|
||||
target_size as _,
|
||||
@@ -127,14 +128,14 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
|
||||
// SAFETY: Creating an HICON which should be readonly on our data.
|
||||
unsafe {
|
||||
winuser::CreateIconFromResourceEx(
|
||||
CreateIconFromResourceEx(
|
||||
image_scaled_bytes.as_mut_ptr(),
|
||||
image_scaled_bytes.len() as u32,
|
||||
1, // Means this is an icon, not a cursor.
|
||||
0x00030000, // Version number of the HICON
|
||||
target_size, // Note that this method can scale, but it does so *very* poorly. So let's avoid that!
|
||||
target_size,
|
||||
winuser::LR_DEFAULTCOLOR,
|
||||
LR_DEFAULTCOLOR,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -155,7 +156,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
// Note that ICON_SMALL may be used even if we don't render a title bar as it may be used in alt+tab!
|
||||
{
|
||||
// SAFETY: WinAPI getter function with no known side effects.
|
||||
let icon_size_big = unsafe { winuser::GetSystemMetrics(winuser::SM_CXICON) };
|
||||
let icon_size_big = unsafe { GetSystemMetrics(SM_CXICON) };
|
||||
let icon_big = create_hicon_with_scale(&unscaled_image, icon_size_big);
|
||||
if icon_big.is_null() {
|
||||
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
|
||||
@@ -163,10 +164,10 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
} else {
|
||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||
unsafe {
|
||||
winuser::SendMessageW(
|
||||
SendMessageW(
|
||||
window_handle,
|
||||
winuser::WM_SETICON,
|
||||
winuser::ICON_BIG as usize,
|
||||
WM_SETICON,
|
||||
ICON_BIG as usize,
|
||||
icon_big as isize,
|
||||
);
|
||||
}
|
||||
@@ -174,7 +175,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
}
|
||||
{
|
||||
// SAFETY: WinAPI getter function with no known side effects.
|
||||
let icon_size_small = unsafe { winuser::GetSystemMetrics(winuser::SM_CXSMICON) };
|
||||
let icon_size_small = unsafe { GetSystemMetrics(SM_CXSMICON) };
|
||||
let icon_small = create_hicon_with_scale(&unscaled_image, icon_size_small);
|
||||
if icon_small.is_null() {
|
||||
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
|
||||
@@ -182,10 +183,10 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
} else {
|
||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||
unsafe {
|
||||
winuser::SendMessageW(
|
||||
SendMessageW(
|
||||
window_handle,
|
||||
winuser::WM_SETICON,
|
||||
winuser::ICON_SMALL as usize,
|
||||
WM_SETICON,
|
||||
ICON_SMALL as usize,
|
||||
icon_small as isize,
|
||||
);
|
||||
}
|
||||
@@ -198,20 +199,25 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
|
||||
/// Set icon & app title for `MacOS` applications.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
|
||||
use crate::icon_data::IconDataExt as _;
|
||||
profiling::function_scope!();
|
||||
|
||||
use objc2::ClassType;
|
||||
use objc2::ClassType as _;
|
||||
use objc2_app_kit::{NSApplication, NSImage};
|
||||
use objc2_foundation::{NSData, NSString};
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
let png_bytes = if let Some(icon_data) = icon_data {
|
||||
match icon_data.to_png_bytes() {
|
||||
Ok(png_bytes) => Some(png_bytes),
|
||||
// Do NOT use png even though creating `NSImage` from it is much easier than from raw images data!
|
||||
//
|
||||
// Some MacOS versions have a bug where creating an `NSImage` from a png will cause it to load an arbitrary `libpng.dylib`.
|
||||
// If this dylib isn't the right version, the application will crash with SIGBUS.
|
||||
// For details see https://github.com/emilk/egui/issues/7155
|
||||
let image = if let Some(icon_data) = icon_data {
|
||||
match icon_data.to_image() {
|
||||
Ok(image) => Some(image),
|
||||
Err(err) => {
|
||||
log::warn!("Failed to convert IconData to png: {err}");
|
||||
log::warn!("Failed to read icon data: {err}");
|
||||
return AppIconStatus::NotSetIgnored;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +226,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
|
||||
};
|
||||
|
||||
// TODO(madsmtm): Move this into `objc2-app-kit`
|
||||
extern "C" {
|
||||
unsafe extern "C" {
|
||||
static NSApp: Option<&'static NSApplication>;
|
||||
}
|
||||
|
||||
@@ -231,25 +237,50 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
|
||||
return AppIconStatus::NotSetIgnored;
|
||||
};
|
||||
|
||||
if let Some(png_bytes) = png_bytes {
|
||||
let data = NSData::from_vec(png_bytes);
|
||||
if let Some(image) = image {
|
||||
use objc2_app_kit::{NSBitmapImageRep, NSDeviceRGBColorSpace};
|
||||
use objc2_foundation::NSSize;
|
||||
|
||||
log::trace!("NSImage::initWithData…");
|
||||
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
|
||||
log::trace!(
|
||||
"NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel"
|
||||
);
|
||||
let Some(image_rep) = NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
|
||||
NSBitmapImageRep::alloc(),
|
||||
[image.as_raw().as_ptr().cast_mut()].as_mut_ptr(),
|
||||
image.width() as isize,
|
||||
image.height() as isize,
|
||||
8, // bits per sample
|
||||
4, // samples per pixel
|
||||
true, // has alpha
|
||||
false, // is not planar
|
||||
NSDeviceRGBColorSpace,
|
||||
(image.width() * 4) as isize, // bytes per row
|
||||
32 // bits per pixel
|
||||
) else {
|
||||
log::warn!("Failed to create NSBitmapImageRep from app icon data.");
|
||||
return AppIconStatus::NotSetIgnored;
|
||||
};
|
||||
|
||||
log::trace!("NSImage::initWithSize");
|
||||
let app_icon = NSImage::initWithSize(
|
||||
NSImage::alloc(),
|
||||
NSSize::new(image.width() as f64, image.height() as f64),
|
||||
);
|
||||
log::trace!("NSImage::addRepresentation");
|
||||
app_icon.addRepresentation(&image_rep);
|
||||
|
||||
profiling::scope!("setApplicationIconImage_");
|
||||
log::trace!("setApplicationIconImage…");
|
||||
app.setApplicationIconImage(app_icon.as_deref());
|
||||
app.setApplicationIconImage(Some(&app_icon));
|
||||
}
|
||||
|
||||
// Change the title in the top bar - for python processes this would be again "python" otherwise.
|
||||
if let Some(main_menu) = app.mainMenu() {
|
||||
if let Some(item) = main_menu.itemAtIndex(0) {
|
||||
if let Some(app_menu) = item.submenu() {
|
||||
profiling::scope!("setTitle_");
|
||||
app_menu.setTitle(&NSString::from_str(title));
|
||||
}
|
||||
}
|
||||
if let Some(main_menu) = app.mainMenu()
|
||||
&& let Some(item) = main_menu.itemAtIndex(0)
|
||||
&& let Some(app_menu) = item.submenu()
|
||||
{
|
||||
profiling::scope!("setTitle_");
|
||||
app_menu.setTitle(&NSString::from_str(title));
|
||||
}
|
||||
|
||||
// The title in the Dock apparently can't be changed.
|
||||
|
||||
@@ -52,14 +52,13 @@ pub fn viewport_builder(
|
||||
viewport_builder = viewport_builder.with_position(pos);
|
||||
}
|
||||
|
||||
if clamp_size_to_monitor_size {
|
||||
if let Some(initial_window_size) = viewport_builder.inner_size {
|
||||
let initial_window_size = egui::NumExt::at_most(
|
||||
initial_window_size,
|
||||
largest_monitor_point_size(egui_zoom_factor, event_loop),
|
||||
);
|
||||
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
|
||||
}
|
||||
if clamp_size_to_monitor_size && let Some(initial_window_size) = viewport_builder.inner_size
|
||||
{
|
||||
let initial_window_size = egui::NumExt::at_most(
|
||||
initial_window_size,
|
||||
largest_monitor_point_size(egui_zoom_factor, event_loop),
|
||||
);
|
||||
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
|
||||
}
|
||||
|
||||
viewport_builder.inner_size
|
||||
@@ -136,7 +135,7 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
#[allow(clippy::allow_attributes, clippy::unnecessary_wraps)]
|
||||
pub fn create_storage_with_file(_file: impl Into<PathBuf>) -> Option<Box<dyn epi::Storage>> {
|
||||
#[cfg(feature = "persistence")]
|
||||
return Some(Box::new(
|
||||
@@ -169,7 +168,7 @@ pub struct EpiIntegration {
|
||||
}
|
||||
|
||||
impl EpiIntegration {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::allow_attributes, clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
egui_ctx: egui::Context,
|
||||
window: &winit::window::Window,
|
||||
@@ -180,7 +179,9 @@ impl EpiIntegration {
|
||||
#[cfg(feature = "glow")] glow_register_native_texture: Option<
|
||||
Box<dyn FnMut(glow::Texture) -> egui::TextureId>,
|
||||
>,
|
||||
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
#[cfg(feature = "wgpu_no_default_features")] wgpu_render_state: Option<
|
||||
egui_wgpu::RenderState,
|
||||
>,
|
||||
) -> Self {
|
||||
let frame = epi::Frame {
|
||||
info: epi::IntegrationInfo { cpu_usage: None },
|
||||
@@ -189,7 +190,7 @@ impl EpiIntegration {
|
||||
gl,
|
||||
#[cfg(feature = "glow")]
|
||||
glow_register_native_texture,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
|
||||
@@ -326,30 +327,32 @@ impl EpiIntegration {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
|
||||
pub fn save(&mut self, app: &mut dyn epi::App, window: Option<&winit::window::Window>) {
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
let _ = (self, app, window);
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = self.frame.storage_mut() {
|
||||
profiling::function_scope!();
|
||||
|
||||
if let Some(window) = _window {
|
||||
if self.persist_window {
|
||||
profiling::scope!("native_window");
|
||||
epi::set_value(
|
||||
storage,
|
||||
STORAGE_WINDOW_KEY,
|
||||
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
|
||||
);
|
||||
}
|
||||
if let Some(window) = window
|
||||
&& self.persist_window
|
||||
{
|
||||
profiling::scope!("native_window");
|
||||
epi::set_value(
|
||||
storage,
|
||||
STORAGE_WINDOW_KEY,
|
||||
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
|
||||
);
|
||||
}
|
||||
if _app.persist_egui_memory() {
|
||||
if app.persist_egui_memory() {
|
||||
profiling::scope!("egui_memory");
|
||||
self.egui_ctx
|
||||
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
||||
}
|
||||
{
|
||||
profiling::scope!("App::save");
|
||||
_app.save(storage);
|
||||
app.save(storage);
|
||||
}
|
||||
|
||||
profiling::scope!("Storage::flush");
|
||||
|
||||
@@ -27,7 +27,7 @@ impl Drop for EventLoopGuard {
|
||||
}
|
||||
|
||||
// Helper function to safely use the current event loop
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
pub fn with_current_event_loop<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&ActiveEventLoop) -> R,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::Write,
|
||||
io::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -42,20 +42,20 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
|
||||
// Adapted from
|
||||
// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37
|
||||
#[cfg(all(windows, not(target_vendor = "uwp")))]
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
fn roaming_appdata() -> Option<PathBuf> {
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use std::os::windows::ffi::OsStringExt as _;
|
||||
use std::ptr;
|
||||
use std::slice;
|
||||
|
||||
use windows_sys::Win32::Foundation::S_OK;
|
||||
use windows_sys::Win32::System::Com::CoTaskMemFree;
|
||||
use windows_sys::Win32::UI::Shell::{
|
||||
FOLDERID_RoamingAppData, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY,
|
||||
FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
unsafe extern "C" {
|
||||
fn wcslen(buf: *const u16) -> usize;
|
||||
}
|
||||
let mut path_raw = ptr::null_mut();
|
||||
@@ -72,7 +72,7 @@ fn roaming_appdata() -> Option<PathBuf> {
|
||||
};
|
||||
|
||||
let path = if result == S_OK {
|
||||
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us.
|
||||
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a null-terminated string for us.
|
||||
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
|
||||
Some(PathBuf::from(OsString::from_wide(path_slice)))
|
||||
} else {
|
||||
@@ -118,7 +118,7 @@ impl FileStorage {
|
||||
pub(crate) fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||
profiling::function_scope!();
|
||||
let ron_filepath: PathBuf = ron_filepath.into();
|
||||
log::debug!("Loading app state from {:?}…", ron_filepath);
|
||||
log::debug!("Loading app state from {}…", ron_filepath.display());
|
||||
Self {
|
||||
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
||||
ron_filepath,
|
||||
@@ -133,9 +133,8 @@ impl FileStorage {
|
||||
if let Some(data_dir) = storage_dir(app_id) {
|
||||
if let Err(err) = std::fs::create_dir_all(&data_dir) {
|
||||
log::warn!(
|
||||
"Saving disabled: Failed to create app path at {:?}: {}",
|
||||
data_dir,
|
||||
err
|
||||
"Saving disabled: Failed to create app path at {}: {err}",
|
||||
data_dir.display()
|
||||
);
|
||||
None
|
||||
} else {
|
||||
@@ -193,12 +192,11 @@ impl crate::Storage for FileStorage {
|
||||
fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
|
||||
profiling::function_scope!();
|
||||
|
||||
if let Some(parent_dir) = file_path.parent() {
|
||||
if !parent_dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir_all(parent_dir) {
|
||||
log::warn!("Failed to create directory {parent_dir:?}: {err}");
|
||||
}
|
||||
}
|
||||
if let Some(parent_dir) = file_path.parent()
|
||||
&& !parent_dir.exists()
|
||||
&& let Err(err) = std::fs::create_dir_all(parent_dir)
|
||||
{
|
||||
log::warn!("Failed to create directory {}: {err}", parent_dir.display());
|
||||
}
|
||||
|
||||
match std::fs::File::create(file_path) {
|
||||
@@ -207,16 +205,17 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
|
||||
let config = Default::default();
|
||||
|
||||
profiling::scope!("ron::serialize");
|
||||
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
|
||||
if let Err(err) = ron::Options::default()
|
||||
.to_io_writer_pretty(&mut writer, &kv, config)
|
||||
.and_then(|_| writer.flush().map_err(|err| err.into()))
|
||||
{
|
||||
log::warn!("Failed to serialize app state: {}", err);
|
||||
log::warn!("Failed to serialize app state: {err}");
|
||||
} else {
|
||||
log::trace!("Persisted to {:?}", file_path);
|
||||
log::trace!("Persisted to {}", file_path.display());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Failed to create file {file_path:?}: {err}");
|
||||
log::warn!("Failed to create file {}: {err}", file_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +233,7 @@ where
|
||||
match ron::de::from_reader(reader) {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
log::warn!("Failed to parse RON: {}", err);
|
||||
log::warn!("Failed to parse RON: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,34 +11,34 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
|
||||
|
||||
use egui_winit::ActionRequested;
|
||||
use glutin::{
|
||||
config::GlConfig,
|
||||
context::NotCurrentGlContext,
|
||||
display::GetGlDisplay,
|
||||
prelude::{GlDisplay, PossiblyCurrentGlContext},
|
||||
surface::GlSurface,
|
||||
config::GlConfig as _,
|
||||
context::NotCurrentGlContext as _,
|
||||
display::GetGlDisplay as _,
|
||||
prelude::{GlDisplay as _, PossiblyCurrentGlContext as _},
|
||||
surface::GlSurface as _,
|
||||
};
|
||||
use raw_window_handle::HasWindowHandle;
|
||||
use raw_window_handle::HasWindowHandle as _;
|
||||
use winit::{
|
||||
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
use ahash::{HashMap, HashSet};
|
||||
use ahash::HashMap;
|
||||
use egui::{
|
||||
DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, ViewportClass, ViewportId,
|
||||
ViewportIdMap, ViewportIdPair, ViewportInfo, ViewportOutput,
|
||||
DeferredViewportUiCallback, ImmediateViewport, OrderedViewportIdMap, ViewportBuilder,
|
||||
ViewportClass, ViewportId, ViewportIdPair, ViewportInfo, ViewportOutput,
|
||||
};
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
|
||||
use crate::{
|
||||
native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions,
|
||||
Result, Storage,
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::epi_integration::EpiIntegration,
|
||||
};
|
||||
|
||||
use super::{
|
||||
epi_integration, event_loop_context,
|
||||
winit_integration::{create_egui_context, EventResult, UserEvent, WinitApp},
|
||||
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -94,9 +94,9 @@ struct GlutinWindowContext {
|
||||
current_gl_context: Option<glutin::context::PossiblyCurrentContext>,
|
||||
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
|
||||
|
||||
viewports: ViewportIdMap<Viewport>,
|
||||
viewports: OrderedViewportIdMap<Viewport>,
|
||||
viewport_from_window: HashMap<WindowId, ViewportId>,
|
||||
window_from_viewport: ViewportIdMap<WindowId>,
|
||||
window_from_viewport: OrderedViewportIdMap<WindowId>,
|
||||
|
||||
focused_viewport: Option<ViewportId>,
|
||||
}
|
||||
@@ -107,7 +107,7 @@ struct Viewport {
|
||||
builder: ViewportBuilder,
|
||||
deferred_commands: Vec<egui::viewport::ViewportCommand>,
|
||||
info: ViewportInfo,
|
||||
actions_requested: HashSet<egui_winit::ActionRequested>,
|
||||
actions_requested: Vec<egui_winit::ActionRequested>,
|
||||
|
||||
/// The user-callback that shows the ui.
|
||||
/// None for immediate viewports.
|
||||
@@ -139,7 +139,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
fn create_glutin_windowed_context(
|
||||
egui_ctx: &egui::Context,
|
||||
event_loop: &ActiveEventLoop,
|
||||
@@ -239,7 +239,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
let painter = painter.clone();
|
||||
move |native| painter.borrow_mut().register_native_texture(native)
|
||||
})),
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -272,7 +272,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
..
|
||||
} = viewport
|
||||
{
|
||||
egui_winit.init_accesskit(window, event_loop_proxy);
|
||||
egui_winit.init_accesskit(event_loop, window, event_loop_proxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,10 +281,9 @@ impl<'app> GlowWinitApp<'app> {
|
||||
.viewport
|
||||
.mouse_passthrough
|
||||
.unwrap_or(false)
|
||||
&& let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false)
|
||||
{
|
||||
if let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false) {
|
||||
log::warn!("set_cursor_hittest(false) failed: {err}");
|
||||
}
|
||||
log::warn!("set_cursor_hittest(false) failed: {err}");
|
||||
}
|
||||
|
||||
let app_creator = std::mem::take(&mut self.app_creator)
|
||||
@@ -302,7 +301,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
storage: integration.frame.storage(),
|
||||
gl: Some(gl),
|
||||
get_proc_address: Some(&get_proc_address),
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
|
||||
@@ -336,10 +335,10 @@ impl<'app> GlowWinitApp<'app> {
|
||||
}
|
||||
|
||||
Ok(self.running.insert(GlowWinitRunning {
|
||||
glutin,
|
||||
painter,
|
||||
integration,
|
||||
app,
|
||||
glutin,
|
||||
painter,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -362,8 +361,12 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
|
||||
fn window_id_from_viewport_id(&self, id: ViewportId) -> Option<WindowId> {
|
||||
self.running
|
||||
.as_ref()
|
||||
.and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied())
|
||||
.as_ref()?
|
||||
.glutin
|
||||
.borrow()
|
||||
.window_from_viewport
|
||||
.get(&id)
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn save(&mut self) {
|
||||
@@ -436,20 +439,20 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
_: winit::event::DeviceId,
|
||||
event: winit::event::DeviceEvent,
|
||||
) -> crate::Result<EventResult> {
|
||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
|
||||
if let Some(running) = &mut self.running {
|
||||
let mut glutin = running.glutin.borrow_mut();
|
||||
if let Some(viewport) = glutin
|
||||
.focused_viewport
|
||||
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
}
|
||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event
|
||||
&& let Some(running) = &mut self.running
|
||||
{
|
||||
let mut glutin = running.glutin.borrow_mut();
|
||||
if let Some(viewport) = glutin
|
||||
.focused_viewport
|
||||
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
}
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,7 +469,7 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
if let Some(running) = &mut self.running {
|
||||
Ok(running.on_window_event(window_id, &event))
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
Ok(EventResult::Exit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,16 +479,15 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
|
||||
if let Some(running) = &self.running {
|
||||
let mut glutin = running.glutin.borrow_mut();
|
||||
if let Some(viewport_id) = glutin.viewport_from_window.get(&event.window_id).copied() {
|
||||
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
||||
if let Some(egui_winit) = &mut viewport.egui_winit {
|
||||
return Ok(winit_integration::on_accesskit_window_event(
|
||||
egui_winit,
|
||||
event.window_id,
|
||||
&event.window_event,
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(viewport_id) = glutin.viewport_from_window.get(&event.window_id).copied()
|
||||
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
|
||||
&& let Some(egui_winit) = &mut viewport.egui_winit
|
||||
{
|
||||
return Ok(winit_integration::on_accesskit_window_event(
|
||||
egui_winit,
|
||||
event.window_id,
|
||||
&event.window_event,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,10 +525,10 @@ impl GlowWinitRunning<'_> {
|
||||
if is_immediate && viewport_id != ViewportId::ROOT {
|
||||
// This will only happen if this is an immediate viewport.
|
||||
// That means that the viewport cannot be rendered by itself and needs his parent to be rendered.
|
||||
if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent) {
|
||||
if let Some(window) = parent_viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent)
|
||||
&& let Some(window) = parent_viewport.window.as_ref()
|
||||
{
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
return Ok(EventResult::Wait);
|
||||
}
|
||||
@@ -561,6 +563,12 @@ impl GlowWinitRunning<'_> {
|
||||
(raw_input, viewport_ui_cb)
|
||||
};
|
||||
|
||||
// HACK: In order to get the right clear_color, the system theme needs to be set, which
|
||||
// usually only happens in the `update` call. So we call Options::begin_pass early
|
||||
// to set the right theme. Without this there would be a black flash on the first frame.
|
||||
self.integration
|
||||
.egui_ctx
|
||||
.options_mut(|opt| opt.begin_pass(&raw_input));
|
||||
let clear_color = self
|
||||
.app
|
||||
.clear_color(&self.integration.egui_ctx.style().visuals);
|
||||
@@ -671,7 +679,7 @@ impl GlowWinitRunning<'_> {
|
||||
);
|
||||
|
||||
{
|
||||
for action in viewport.actions_requested.drain() {
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot(user_data) => {
|
||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||
@@ -723,10 +731,10 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
// give it time to settle:
|
||||
#[cfg(feature = "__screenshot")]
|
||||
if integration.egui_ctx.cumulative_pass_nr() == 2 {
|
||||
if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
|
||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||
}
|
||||
if integration.egui_ctx.cumulative_pass_nr() == 2
|
||||
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
|
||||
{
|
||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||
}
|
||||
|
||||
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
|
||||
@@ -743,7 +751,7 @@ impl GlowWinitRunning<'_> {
|
||||
}
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::Exit)
|
||||
Ok(EventResult::CloseRequested)
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
}
|
||||
@@ -773,19 +781,33 @@ impl GlowWinitRunning<'_> {
|
||||
let mut repaint_asap = false;
|
||||
|
||||
match event {
|
||||
winit::event::WindowEvent::Focused(new_focused) => {
|
||||
glutin.focused_viewport = new_focused.then(|| viewport_id).flatten();
|
||||
winit::event::WindowEvent::Focused(focused) => {
|
||||
let focused = if cfg!(target_os = "macos")
|
||||
&& let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = glutin.viewports.get(&viewport_id)
|
||||
&& let Some(window) = &viewport.window
|
||||
{
|
||||
// TODO(emilk): remove this work-around once we update winit
|
||||
// https://github.com/rust-windowing/winit/issues/4371
|
||||
// https://github.com/emilk/egui/issues/7588
|
||||
window.has_focus()
|
||||
} else {
|
||||
*focused
|
||||
};
|
||||
|
||||
glutin.focused_viewport = focused.then_some(viewport_id).flatten();
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::Resized(physical_size) => {
|
||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||
// See: https://github.com/rust-windowing/winit/issues/208
|
||||
// This solves an issue where the app would panic when minimizing on Windows.
|
||||
if 0 < physical_size.width && 0 < physical_size.height {
|
||||
if let Some(viewport_id) = viewport_id {
|
||||
repaint_asap = true;
|
||||
glutin.resize(viewport_id, *physical_size);
|
||||
}
|
||||
if 0 < physical_size.width
|
||||
&& 0 < physical_size.height
|
||||
&& let Some(viewport_id) = viewport_id
|
||||
{
|
||||
repaint_asap = true;
|
||||
glutin.resize(viewport_id, *physical_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,44 +816,31 @@ impl GlowWinitRunning<'_> {
|
||||
log::debug!(
|
||||
"Received WindowEvent::CloseRequested for main viewport - shutting down."
|
||||
);
|
||||
return EventResult::Exit;
|
||||
return EventResult::CloseRequested;
|
||||
}
|
||||
|
||||
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
|
||||
|
||||
if let Some(viewport_id) = viewport_id {
|
||||
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
||||
// Tell viewport it should close:
|
||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
||||
if let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
|
||||
{
|
||||
// Tell viewport it should close:
|
||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
||||
|
||||
// We may need to repaint both us and our parent to close the window,
|
||||
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||
// `request_repaint_of` does a double-repaint though:
|
||||
self.integration.egui_ctx.request_repaint_of(viewport_id);
|
||||
self.integration
|
||||
.egui_ctx
|
||||
.request_repaint_of(viewport.ids.parent);
|
||||
}
|
||||
// We may need to repaint both us and our parent to close the window,
|
||||
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||
// `request_repaint_of` does a double-repaint though:
|
||||
self.integration.egui_ctx.request_repaint_of(viewport_id);
|
||||
self.integration
|
||||
.egui_ctx
|
||||
.request_repaint_of(viewport.ids.parent);
|
||||
}
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::Destroyed => {
|
||||
log::debug!(
|
||||
"Received WindowEvent::Destroyed for viewport {:?}",
|
||||
viewport_id
|
||||
);
|
||||
if viewport_id == Some(ViewportId::ROOT) {
|
||||
return EventResult::Exit;
|
||||
} else {
|
||||
return EventResult::Wait;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.integration.should_close() {
|
||||
return EventResult::Exit;
|
||||
return EventResult::CloseRequested;
|
||||
}
|
||||
|
||||
let mut event_response = egui_winit::EventResponse {
|
||||
@@ -901,7 +910,7 @@ fn change_gl_context(
|
||||
}
|
||||
|
||||
impl GlutinWindowContext {
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
unsafe fn new(
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_builder: ViewportBuilder,
|
||||
@@ -959,7 +968,6 @@ impl GlutinWindowContext {
|
||||
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
|
||||
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
|
||||
egui_ctx,
|
||||
event_loop,
|
||||
viewport_builder.clone(),
|
||||
)));
|
||||
|
||||
@@ -1016,7 +1024,9 @@ impl GlutinWindowContext {
|
||||
let gl_context = match gl_context_result {
|
||||
Ok(it) => it,
|
||||
Err(err) => {
|
||||
log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}");
|
||||
log::warn!(
|
||||
"Failed to create context using default context attributes {context_attributes:?} due to error: {err}"
|
||||
);
|
||||
log::debug!(
|
||||
"Retrying with fallback context attributes: {fallback_context_attributes:?}"
|
||||
);
|
||||
@@ -1030,15 +1040,27 @@ impl GlutinWindowContext {
|
||||
let not_current_gl_context = Some(gl_context);
|
||||
|
||||
let mut viewport_from_window = HashMap::default();
|
||||
let mut window_from_viewport = ViewportIdMap::default();
|
||||
let mut info = ViewportInfo::default();
|
||||
let mut window_from_viewport = OrderedViewportIdMap::default();
|
||||
let mut viewport_info = ViewportInfo::default();
|
||||
if let Some(window) = &window {
|
||||
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
||||
window_from_viewport.insert(ViewportId::ROOT, window.id());
|
||||
egui_winit::update_viewport_info(&mut info, egui_ctx, window, true);
|
||||
egui_winit::update_viewport_info(&mut viewport_info, egui_ctx, window, true);
|
||||
|
||||
// Tell egui right away about native_pixels_per_point etc,
|
||||
// so that the app knows about it during app creation:
|
||||
let pixels_per_point = egui_winit::pixels_per_point(egui_ctx, window);
|
||||
|
||||
egui_ctx.input_mut(|i| {
|
||||
i.raw
|
||||
.viewports
|
||||
.insert(ViewportId::ROOT, viewport_info.clone());
|
||||
|
||||
i.pixels_per_point = pixels_per_point;
|
||||
});
|
||||
}
|
||||
|
||||
let mut viewports = ViewportIdMap::default();
|
||||
let mut viewports = OrderedViewportIdMap::default();
|
||||
viewports.insert(
|
||||
ViewportId::ROOT,
|
||||
Viewport {
|
||||
@@ -1046,7 +1068,7 @@ impl GlutinWindowContext {
|
||||
class: ViewportClass::Root,
|
||||
builder: viewport_builder,
|
||||
deferred_commands: vec![],
|
||||
info,
|
||||
info: viewport_info,
|
||||
actions_requested: Default::default(),
|
||||
viewport_ui_cb: None,
|
||||
gl_surface: None,
|
||||
@@ -1094,7 +1116,7 @@ impl GlutinWindowContext {
|
||||
}
|
||||
|
||||
/// Create a surface, window, and winit integration for the viewport, if missing.
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
pub(crate) fn initialize_window(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
@@ -1113,7 +1135,6 @@ impl GlutinWindowContext {
|
||||
log::debug!("Creating a window for viewport {viewport_id:?}");
|
||||
let window_attributes = egui_winit::create_winit_window_attributes(
|
||||
&self.egui_ctx,
|
||||
event_loop,
|
||||
viewport.builder.clone(),
|
||||
);
|
||||
if window_attributes.transparent()
|
||||
@@ -1241,21 +1262,21 @@ impl GlutinWindowContext {
|
||||
let width_px = NonZeroU32::new(physical_size.width).unwrap_or(NonZeroU32::MIN);
|
||||
let height_px = NonZeroU32::new(physical_size.height).unwrap_or(NonZeroU32::MIN);
|
||||
|
||||
if let Some(viewport) = self.viewports.get(&viewport_id) {
|
||||
if let Some(gl_surface) = &viewport.gl_surface {
|
||||
change_gl_context(
|
||||
&mut self.current_gl_context,
|
||||
&mut self.not_current_gl_context,
|
||||
gl_surface,
|
||||
);
|
||||
gl_surface.resize(
|
||||
self.current_gl_context
|
||||
.as_ref()
|
||||
.expect("failed to get current context to resize surface"),
|
||||
width_px,
|
||||
height_px,
|
||||
);
|
||||
}
|
||||
if let Some(viewport) = self.viewports.get(&viewport_id)
|
||||
&& let Some(gl_surface) = &viewport.gl_surface
|
||||
{
|
||||
change_gl_context(
|
||||
&mut self.current_gl_context,
|
||||
&mut self.not_current_gl_context,
|
||||
gl_surface,
|
||||
);
|
||||
gl_surface.resize(
|
||||
self.current_gl_context
|
||||
.as_ref()
|
||||
.expect("failed to get current context to resize surface"),
|
||||
width_px,
|
||||
height_px,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1265,7 +1286,7 @@ impl GlutinWindowContext {
|
||||
|
||||
pub(crate) fn remove_viewports_not_in(
|
||||
&mut self,
|
||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
) {
|
||||
// GC old viewports
|
||||
self.viewports
|
||||
@@ -1280,7 +1301,7 @@ impl GlutinWindowContext {
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -1337,7 +1358,7 @@ impl GlutinWindowContext {
|
||||
}
|
||||
|
||||
fn initialize_or_update_viewport(
|
||||
viewports: &mut ViewportIdMap<Viewport>,
|
||||
viewports: &mut OrderedViewportIdMap<Viewport>,
|
||||
ids: ViewportIdPair,
|
||||
class: ViewportClass,
|
||||
mut builder: ViewportBuilder,
|
||||
@@ -1345,6 +1366,8 @@ fn initialize_or_update_viewport(
|
||||
) -> &mut Viewport {
|
||||
profiling::function_scope!();
|
||||
|
||||
use std::collections::btree_map::Entry;
|
||||
|
||||
if builder.icon.is_none() {
|
||||
// Inherit icon from parent
|
||||
builder.icon = viewports
|
||||
@@ -1353,7 +1376,7 @@ fn initialize_or_update_viewport(
|
||||
}
|
||||
|
||||
match viewports.entry(ids.this) {
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
Entry::Vacant(entry) => {
|
||||
// New viewport:
|
||||
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
||||
entry.insert(Viewport {
|
||||
@@ -1370,7 +1393,7 @@ fn initialize_or_update_viewport(
|
||||
})
|
||||
}
|
||||
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// Patch an existing viewport:
|
||||
let viewport = entry.get_mut();
|
||||
|
||||
@@ -1566,6 +1589,6 @@ fn save_screenshot_and_exit(
|
||||
});
|
||||
log::info!("Screenshot saved to {path:?}.");
|
||||
|
||||
#[allow(clippy::exit)]
|
||||
#[expect(clippy::exit)]
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ pub(crate) mod winit_integration;
|
||||
#[cfg(feature = "glow")]
|
||||
mod glow_integration;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
mod wgpu_integration;
|
||||
|
||||
@@ -10,9 +10,8 @@ use ahash::HashMap;
|
||||
|
||||
use super::winit_integration::{UserEvent, WinitApp};
|
||||
use crate::{
|
||||
epi,
|
||||
Result, epi,
|
||||
native::{event_loop_context, winit_integration::EventResult},
|
||||
Result,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -93,48 +92,57 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
|
||||
log::trace!("event_result: {event_result:?}");
|
||||
|
||||
let combined_result = event_result.and_then(|event_result| {
|
||||
match event_result {
|
||||
EventResult::Wait => {
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::RepaintNow(window_id) => {
|
||||
log::trace!("RepaintNow of {window_id:?}",);
|
||||
let mut event_result = event_result;
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
||||
self.winit_app.run_ui_and_paint(event_loop, window_id)
|
||||
} else {
|
||||
// Fix for https://github.com/emilk/egui/issues/2425
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
Ok(event_result)
|
||||
}
|
||||
}
|
||||
EventResult::RepaintNext(window_id) => {
|
||||
log::trace!("RepaintNext of {window_id:?}",);
|
||||
if cfg!(target_os = "windows")
|
||||
&& let Ok(EventResult::RepaintNow(window_id)) = event_result
|
||||
{
|
||||
log::trace!("RepaintNow of {window_id:?}");
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
|
||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
||||
event_result = self.winit_app.run_ui_and_paint(event_loop, window_id);
|
||||
}
|
||||
|
||||
let combined_result = event_result.map(|event_result| match event_result {
|
||||
EventResult::Wait => {
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintNow(window_id) => {
|
||||
log::trace!("RepaintNow of {window_id:?}",);
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintNext(window_id) => {
|
||||
log::trace!("RepaintNext of {window_id:?}",);
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintAt(window_id, repaint_time) => {
|
||||
self.windows_next_repaint_times.insert(
|
||||
window_id,
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::RepaintAt(window_id, repaint_time) => {
|
||||
self.windows_next_repaint_times.insert(
|
||||
window_id,
|
||||
self.windows_next_repaint_times
|
||||
.get(&window_id)
|
||||
.map_or(repaint_time, |last| (*last).min(repaint_time)),
|
||||
);
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::Save => {
|
||||
save = true;
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::Exit => {
|
||||
exit = true;
|
||||
Ok(event_result)
|
||||
}
|
||||
.get(&window_id)
|
||||
.map_or(repaint_time, |last| (*last).min(repaint_time)),
|
||||
);
|
||||
event_result
|
||||
}
|
||||
EventResult::Save => {
|
||||
save = true;
|
||||
event_result
|
||||
}
|
||||
EventResult::Exit => {
|
||||
exit = true;
|
||||
event_result
|
||||
}
|
||||
EventResult::CloseRequested => {
|
||||
// The windows need to be dropped whilst the event loop is running to allow for proper cleanup.
|
||||
self.winit_app.save_and_destroy();
|
||||
event_result
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,7 +150,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
log::error!("Exiting because of error: {err}");
|
||||
exit = true;
|
||||
self.return_result = Err(err);
|
||||
};
|
||||
}
|
||||
|
||||
if save {
|
||||
log::debug!("Received an EventResult::Save - saving app state");
|
||||
@@ -159,7 +167,6 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
|
||||
log::debug!("Exiting with return code 0");
|
||||
|
||||
#[allow(clippy::exit)]
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
@@ -174,7 +181,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
.retain(|window_id, repaint_time| {
|
||||
if now < *repaint_time {
|
||||
return true; // not yet ready
|
||||
};
|
||||
}
|
||||
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
@@ -190,7 +197,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
|
||||
if let Some(next_repaint_time) = next_repaint_time {
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +320,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, winit_app: impl WinitApp) -> Result {
|
||||
use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
|
||||
use winit::platform::run_on_demand::EventLoopExtRunOnDemand as _;
|
||||
|
||||
log::trace!("Entering the winit event loop (run_app_on_demand)…");
|
||||
|
||||
@@ -359,9 +366,22 @@ pub fn run_glow(
|
||||
run_and_exit(event_loop, glow_eframe)
|
||||
}
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub fn create_glow<'a>(
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator<'a>,
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
) -> impl ApplicationHandler<UserEvent> + 'a {
|
||||
use super::glow_integration::GlowWinitApp;
|
||||
|
||||
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
WinitAppWrapper::new(glow_eframe, true)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn run_wgpu(
|
||||
app_name: &str,
|
||||
mut native_options: epi::NativeOptions,
|
||||
@@ -383,3 +403,120 @@ pub fn run_wgpu(
|
||||
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
run_and_exit(event_loop, wgpu_eframe)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn create_wgpu<'a>(
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator<'a>,
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
) -> impl ApplicationHandler<UserEvent> + 'a {
|
||||
use super::wgpu_integration::WgpuWinitApp;
|
||||
|
||||
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
WinitAppWrapper::new(wgpu_eframe, true)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A proxy to the eframe application that implements [`ApplicationHandler`].
|
||||
///
|
||||
/// This can be run directly on your own [`EventLoop`] by itself or with other
|
||||
/// windows you manage outside of eframe.
|
||||
pub struct EframeWinitApplication<'a> {
|
||||
wrapper: Box<dyn ApplicationHandler<UserEvent> + 'a>,
|
||||
control_flow: ControlFlow,
|
||||
}
|
||||
|
||||
impl ApplicationHandler<UserEvent> for EframeWinitApplication<'_> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.resumed(event_loop);
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: winit::window::WindowId,
|
||||
event: winit::event::WindowEvent,
|
||||
) {
|
||||
self.wrapper.window_event(event_loop, window_id, event);
|
||||
}
|
||||
|
||||
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
|
||||
self.wrapper.new_events(event_loop, cause);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
||||
self.wrapper.user_event(event_loop, event);
|
||||
}
|
||||
|
||||
fn device_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
device_id: winit::event::DeviceId,
|
||||
event: winit::event::DeviceEvent,
|
||||
) {
|
||||
self.wrapper.device_event(event_loop, device_id, event);
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.about_to_wait(event_loop);
|
||||
self.control_flow = event_loop.control_flow();
|
||||
}
|
||||
|
||||
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.suspended(event_loop);
|
||||
}
|
||||
|
||||
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.exiting(event_loop);
|
||||
}
|
||||
|
||||
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.memory_warning(event_loop);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> EframeWinitApplication<'a> {
|
||||
pub(crate) fn new<T: ApplicationHandler<UserEvent> + 'a>(app: T) -> Self {
|
||||
Self {
|
||||
wrapper: Box::new(app),
|
||||
control_flow: ControlFlow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pump the `EventLoop` to check for and dispatch pending events to this application.
|
||||
///
|
||||
/// Returns either the exit code for the application or the final state of the [`ControlFlow`]
|
||||
/// after all events have been dispatched in this iteration.
|
||||
///
|
||||
/// This is useful when your [`EventLoop`] is not the main event loop for your application.
|
||||
/// See the `external_eventloop_async` example.
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn pump_eframe_app(
|
||||
&mut self,
|
||||
event_loop: &mut EventLoop<UserEvent>,
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> EframePumpStatus {
|
||||
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
|
||||
|
||||
match event_loop.pump_app_events(timeout, self) {
|
||||
PumpStatus::Continue => EframePumpStatus::Continue(self.control_flow),
|
||||
PumpStatus::Exit(code) => EframePumpStatus::Exit(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Either an exit code or a [`ControlFlow`] from the [`ActiveEventLoop`].
|
||||
///
|
||||
/// The result of [`EframeWinitApplication::pump_eframe_app`].
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub enum EframePumpStatus {
|
||||
/// The final state of the [`ControlFlow`] after all events have been dispatched
|
||||
///
|
||||
/// Callers should perform the action that is appropriate for the [`ControlFlow`] value.
|
||||
Continue(ControlFlow),
|
||||
|
||||
/// The exit code for the application
|
||||
Exit(i32),
|
||||
}
|
||||
|
||||
@@ -15,18 +15,19 @@ use winit::{
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
use ahash::{HashMap, HashSet, HashSetExt};
|
||||
use ahash::HashMap;
|
||||
use egui::{
|
||||
DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass,
|
||||
ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput,
|
||||
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
|
||||
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
|
||||
ViewportOutput,
|
||||
};
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
use winit_integration::UserEvent;
|
||||
|
||||
use crate::{
|
||||
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
|
||||
};
|
||||
|
||||
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
|
||||
@@ -70,9 +71,10 @@ pub struct SharedState {
|
||||
painter: egui_wgpu::winit::Painter,
|
||||
viewport_from_window: HashMap<WindowId, ViewportId>,
|
||||
focused_viewport: Option<ViewportId>,
|
||||
resized_viewport: Option<ViewportId>,
|
||||
}
|
||||
|
||||
pub type Viewports = ViewportIdMap<Viewport>;
|
||||
pub type Viewports = egui::OrderedViewportIdMap<Viewport>;
|
||||
|
||||
pub struct Viewport {
|
||||
ids: ViewportIdPair,
|
||||
@@ -80,7 +82,7 @@ pub struct Viewport {
|
||||
builder: ViewportBuilder,
|
||||
deferred_commands: Vec<egui::viewport::ViewportCommand>,
|
||||
info: ViewportInfo,
|
||||
actions_requested: HashSet<ActionRequested>,
|
||||
actions_requested: Vec<ActionRequested>,
|
||||
|
||||
/// `None` for sync viewports.
|
||||
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
||||
@@ -182,19 +184,37 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
builder: ViewportBuilder,
|
||||
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
|
||||
profiling::function_scope!();
|
||||
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
||||
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
|
||||
egui_ctx.clone(),
|
||||
self.native_options.wgpu_options.clone(),
|
||||
self.native_options.multisampling.max(1) as _,
|
||||
egui_wgpu::depth_format_from_bits(
|
||||
self.native_options.depth_buffer,
|
||||
self.native_options.stencil_buffer,
|
||||
),
|
||||
self.native_options.viewport.transparent.unwrap_or(false),
|
||||
self.native_options.dithering,
|
||||
egui_wgpu::RendererOptions {
|
||||
msaa_samples: self.native_options.multisampling as _,
|
||||
depth_stencil_format: egui_wgpu::depth_format_from_bits(
|
||||
self.native_options.depth_buffer,
|
||||
self.native_options.stencil_buffer,
|
||||
),
|
||||
dithering: self.native_options.dithering,
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
|
||||
let mut viewport_info = ViewportInfo::default();
|
||||
egui_winit::update_viewport_info(&mut viewport_info, &egui_ctx, &window, true);
|
||||
|
||||
{
|
||||
// Tell egui right away about native_pixels_per_point etc,
|
||||
// so that the app knows about it during app creation:
|
||||
let pixels_per_point = egui_winit::pixels_per_point(&egui_ctx, &window);
|
||||
|
||||
egui_ctx.input_mut(|i| {
|
||||
i.raw
|
||||
.viewports
|
||||
.insert(ViewportId::ROOT, viewport_info.clone());
|
||||
i.pixels_per_point = pixels_per_point;
|
||||
});
|
||||
}
|
||||
|
||||
let window = Arc::new(window);
|
||||
|
||||
{
|
||||
@@ -236,7 +256,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(unused_mut)] // used for accesskit
|
||||
#[allow(unused_mut, clippy::allow_attributes)] // used for accesskit
|
||||
let mut egui_winit = egui_winit::State::new(
|
||||
egui_ctx.clone(),
|
||||
ViewportId::ROOT,
|
||||
@@ -249,7 +269,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
let event_loop_proxy = self.repaint_proxy.lock().clone();
|
||||
egui_winit.init_accesskit(&window, event_loop_proxy);
|
||||
egui_winit.init_accesskit(event_loop, &window, event_loop_proxy);
|
||||
}
|
||||
|
||||
let app_creator = std::mem::take(&mut self.app_creator)
|
||||
@@ -274,9 +294,6 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
let mut viewport_from_window = HashMap::default();
|
||||
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
||||
|
||||
let mut info = ViewportInfo::default();
|
||||
egui_winit::update_viewport_info(&mut info, &egui_ctx, &window, true);
|
||||
|
||||
let mut viewports = Viewports::default();
|
||||
viewports.insert(
|
||||
ViewportId::ROOT,
|
||||
@@ -285,7 +302,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
class: ViewportClass::Root,
|
||||
builder,
|
||||
deferred_commands: vec![],
|
||||
info,
|
||||
info: viewport_info,
|
||||
actions_requested: Default::default(),
|
||||
viewport_ui_cb: None,
|
||||
window: Some(window),
|
||||
@@ -299,6 +316,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
viewports,
|
||||
painter,
|
||||
focused_viewport: Some(ViewportId::ROOT),
|
||||
resized_viewport: None,
|
||||
}));
|
||||
|
||||
{
|
||||
@@ -333,10 +351,8 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
.as_ref()
|
||||
.and_then(|r| {
|
||||
let shared = r.shared.borrow();
|
||||
shared
|
||||
.viewport_from_window
|
||||
.get(&window_id)
|
||||
.and_then(|id| shared.viewports.get(id).map(|v| v.window.clone()))
|
||||
let id = shared.viewport_from_window.get(&window_id)?;
|
||||
shared.viewports.get(id).map(|v| v.window.clone())
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
@@ -431,20 +447,20 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
_: winit::event::DeviceId,
|
||||
event: winit::event::DeviceEvent,
|
||||
) -> crate::Result<EventResult> {
|
||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
|
||||
if let Some(running) = &mut self.running {
|
||||
let mut shared = running.shared.borrow_mut();
|
||||
if let Some(viewport) = shared
|
||||
.focused_viewport
|
||||
.and_then(|viewport| shared.viewports.get_mut(&viewport))
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
}
|
||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event
|
||||
&& let Some(running) = &mut self.running
|
||||
{
|
||||
let mut shared = running.shared.borrow_mut();
|
||||
if let Some(viewport) = shared
|
||||
.focused_viewport
|
||||
.and_then(|viewport| shared.viewports.get_mut(&viewport))
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
}
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +479,8 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
if let Some(running) = &mut self.running {
|
||||
Ok(running.on_window_event(window_id, &event))
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
// running is removed to get ready for exiting
|
||||
Ok(EventResult::Exit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,14 +496,13 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
if let Some(viewport) = viewport_from_window
|
||||
.get(&event.window_id)
|
||||
.and_then(|id| viewports.get_mut(id))
|
||||
&& let Some(egui_winit) = &mut viewport.egui_winit
|
||||
{
|
||||
if let Some(egui_winit) = &mut viewport.egui_winit {
|
||||
return Ok(winit_integration::on_accesskit_window_event(
|
||||
egui_winit,
|
||||
event.window_id,
|
||||
&event.window_event,
|
||||
));
|
||||
}
|
||||
return Ok(winit_integration::on_accesskit_window_event(
|
||||
egui_winit,
|
||||
event.window_id,
|
||||
&event.window_event,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,10 +580,10 @@ impl WgpuWinitRunning<'_> {
|
||||
if viewport.viewport_ui_cb.is_none() {
|
||||
// This will only happen if this is an immediate viewport.
|
||||
// That means that the viewport cannot be rendered by itself and needs his parent to be rendered.
|
||||
if let Some(viewport) = viewports.get(&viewport.ids.parent) {
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
if let Some(viewport) = viewports.get(&viewport.ids.parent)
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
{
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
return Ok(EventResult::Wait);
|
||||
}
|
||||
@@ -680,7 +696,7 @@ impl WgpuWinitRunning<'_> {
|
||||
screenshot_commands,
|
||||
);
|
||||
|
||||
for action in viewport.actions_requested.drain() {
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot { .. } => {
|
||||
// already handled above
|
||||
@@ -731,17 +747,17 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||
|
||||
if let Some(window) = window {
|
||||
if window.is_minimized() == Some(true) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
if let Some(window) = window
|
||||
&& window.is_minimized() == Some(true)
|
||||
{
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::Exit)
|
||||
Ok(EventResult::CloseRequested)
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
}
|
||||
@@ -762,37 +778,68 @@ impl WgpuWinitRunning<'_> {
|
||||
let viewport_id = shared.viewport_from_window.get(&window_id).copied();
|
||||
|
||||
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the
|
||||
// event handler.
|
||||
//
|
||||
// If this is not done, the compositor will assume that the window does not want to redraw,
|
||||
// and continue ahead.
|
||||
// event handler. If this is not done, the compositor will assume that the window does not want
|
||||
// to redraw and continue ahead.
|
||||
//
|
||||
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
|
||||
// new frames to the compositor in time.
|
||||
//
|
||||
// The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||
// new frames to the compositor in time. The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||
// to resizes anyway, as doing so avoids dropping frames.
|
||||
//
|
||||
// See: https://github.com/emilk/egui/issues/903
|
||||
let mut repaint_asap = false;
|
||||
|
||||
// On MacOS the asap repaint is not enough. The drawn frames must be synchronized with
|
||||
// the CoreAnimation transactions driving the window resize process.
|
||||
//
|
||||
// Thus, Painter, responsible for wgpu surfaces and their resize, has to be notified of the
|
||||
// resize lifecycle, yet winit does not provide any events for that. To work around,
|
||||
// the last resized viewport is tracked until any next non-resize event is received.
|
||||
//
|
||||
// Accidental state change during the resize process due to an unexpected event fire
|
||||
// is ok, state will switch back upon next resize event.
|
||||
//
|
||||
// See: https://github.com/emilk/egui/issues/903
|
||||
if let Some(id) = viewport_id
|
||||
&& shared.resized_viewport == viewport_id
|
||||
{
|
||||
shared.painter.on_window_resize_state_change(id, false);
|
||||
shared.resized_viewport = None;
|
||||
}
|
||||
|
||||
match event {
|
||||
winit::event::WindowEvent::Focused(new_focused) => {
|
||||
shared.focused_viewport = new_focused.then(|| viewport_id).flatten();
|
||||
winit::event::WindowEvent::Focused(focused) => {
|
||||
let focused = if cfg!(target_os = "macos")
|
||||
&& let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = shared.viewports.get(&viewport_id)
|
||||
&& let Some(window) = &viewport.window
|
||||
{
|
||||
// TODO(emilk): remove this work-around once we update winit
|
||||
// https://github.com/rust-windowing/winit/issues/4371
|
||||
// https://github.com/emilk/egui/issues/7588
|
||||
window.has_focus()
|
||||
} else {
|
||||
*focused
|
||||
};
|
||||
|
||||
shared.focused_viewport = focused.then_some(viewport_id).flatten();
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::Resized(physical_size) => {
|
||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||
// See: https://github.com/rust-windowing/winit/issues/208
|
||||
// This solves an issue where the app would panic when minimizing on Windows.
|
||||
if let Some(viewport_id) = viewport_id {
|
||||
if let (Some(width), Some(height)) = (
|
||||
if let Some(id) = viewport_id
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(physical_size.width),
|
||||
NonZeroU32::new(physical_size.height),
|
||||
) {
|
||||
repaint_asap = true;
|
||||
shared.painter.on_window_resized(viewport_id, width, height);
|
||||
)
|
||||
{
|
||||
if shared.resized_viewport != viewport_id {
|
||||
shared.resized_viewport = viewport_id;
|
||||
shared.painter.on_window_resize_state_change(id, true);
|
||||
}
|
||||
shared.painter.on_window_resized(id, width, height);
|
||||
repaint_asap = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,42 +848,41 @@ impl WgpuWinitRunning<'_> {
|
||||
log::debug!(
|
||||
"Received WindowEvent::CloseRequested for main viewport - shutting down."
|
||||
);
|
||||
return EventResult::Exit;
|
||||
return EventResult::CloseRequested;
|
||||
}
|
||||
|
||||
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
|
||||
|
||||
if let Some(viewport_id) = viewport_id {
|
||||
if let Some(viewport) = shared.viewports.get_mut(&viewport_id) {
|
||||
// Tell viewport it should close:
|
||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
||||
if let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = shared.viewports.get_mut(&viewport_id)
|
||||
{
|
||||
// Tell viewport it should close:
|
||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
||||
|
||||
// We may need to repaint both us and our parent to close the window,
|
||||
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||
// `request_repaint_of` does a double-repaint though:
|
||||
integration.egui_ctx.request_repaint_of(viewport_id);
|
||||
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
|
||||
}
|
||||
// We may need to repaint both us and our parent to close the window,
|
||||
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||
// `request_repaint_of` does a double-repaint though:
|
||||
integration.egui_ctx.request_repaint_of(viewport_id);
|
||||
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
let event_response = viewport_id
|
||||
.and_then(|viewport_id| {
|
||||
shared.viewports.get_mut(&viewport_id).and_then(|viewport| {
|
||||
Some(integration.on_window_event(
|
||||
viewport.window.as_deref()?,
|
||||
viewport.egui_winit.as_mut()?,
|
||||
event,
|
||||
))
|
||||
})
|
||||
let viewport = shared.viewports.get_mut(&viewport_id)?;
|
||||
Some(integration.on_window_event(
|
||||
viewport.window.as_deref()?,
|
||||
viewport.egui_winit.as_mut()?,
|
||||
event,
|
||||
))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if integration.should_close() {
|
||||
EventResult::Exit
|
||||
EventResult::CloseRequested
|
||||
} else if event_response.repaint {
|
||||
if repaint_asap {
|
||||
EventResult::RepaintNow(window_id)
|
||||
@@ -1035,10 +1081,10 @@ fn render_immediate_viewport(
|
||||
}
|
||||
|
||||
pub(crate) fn remove_viewports_not_in(
|
||||
viewports: &mut ViewportIdMap<Viewport>,
|
||||
viewports: &mut Viewports,
|
||||
painter: &mut egui_wgpu::winit::Painter,
|
||||
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
) {
|
||||
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
|
||||
|
||||
@@ -1051,8 +1097,8 @@ pub(crate) fn remove_viewports_not_in(
|
||||
/// Add new viewports, and update existing ones:
|
||||
fn handle_viewport_output(
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
||||
viewports: &mut ViewportIdMap<Viewport>,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
viewports: &mut Viewports,
|
||||
painter: &mut egui_wgpu::winit::Painter,
|
||||
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
||||
) {
|
||||
@@ -1089,13 +1135,13 @@ fn handle_viewport_output(
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux") {
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size {
|
||||
if let (Some(width), Some(height)) = (
|
||||
if new_inner_size != old_inner_size
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(new_inner_size.width),
|
||||
NonZeroU32::new(new_inner_size.height),
|
||||
) {
|
||||
painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
)
|
||||
{
|
||||
painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1112,6 +1158,8 @@ 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 {
|
||||
use std::collections::btree_map::Entry;
|
||||
|
||||
profiling::function_scope!();
|
||||
|
||||
if builder.icon.is_none() {
|
||||
@@ -1122,7 +1170,7 @@ fn initialize_or_update_viewport<'a>(
|
||||
}
|
||||
|
||||
match viewports.entry(ids.this) {
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
Entry::Vacant(entry) => {
|
||||
// New viewport:
|
||||
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
||||
entry.insert(Viewport {
|
||||
@@ -1131,14 +1179,14 @@ fn initialize_or_update_viewport<'a>(
|
||||
builder,
|
||||
deferred_commands: vec![],
|
||||
info: Default::default(),
|
||||
actions_requested: HashSet::new(),
|
||||
actions_requested: Vec::new(),
|
||||
viewport_ui_cb,
|
||||
window: None,
|
||||
egui_winit: None,
|
||||
})
|
||||
}
|
||||
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// Patch an existing viewport:
|
||||
let viewport = entry.get_mut();
|
||||
|
||||
|
||||
@@ -124,6 +124,25 @@ pub enum EventResult {
|
||||
/// Causes a save of the client state when the persistence feature is enabled.
|
||||
Save,
|
||||
|
||||
/// Starts the process of ending eframe execution whilst allowing for proper
|
||||
/// clean up of resources.
|
||||
///
|
||||
/// # Warning
|
||||
/// This event **must** occur before [`Exit`] to correctly exit eframe code.
|
||||
/// If in doubt, return this event.
|
||||
///
|
||||
/// [`Exit`]: [EventResult::Exit]
|
||||
CloseRequested,
|
||||
|
||||
/// The event loop will exit, now.
|
||||
/// The correct circumstance to return this event is in response to a winit "Destroyed" event.
|
||||
///
|
||||
/// # Warning
|
||||
/// The [`CloseRequested`] **must** occur before this event to ensure that winit
|
||||
/// is able to remove any open windows. Otherwise the window(s) will remain open
|
||||
/// until the program terminates.
|
||||
///
|
||||
/// [`CloseRequested`]: EventResult::CloseRequested
|
||||
Exit,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ impl Stopwatch {
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
assert!(self.start.is_none());
|
||||
assert!(self.start.is_none(), "Stopwatch already running");
|
||||
self.start = Some(Instant::now());
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Stopwatch {
|
||||
}
|
||||
|
||||
pub fn resume(&mut self) {
|
||||
assert!(self.start.is_none());
|
||||
assert!(self.start.is_none(), "Stopwatch still running");
|
||||
self.start = Some(Instant::now());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use egui::{TexturesDelta, UserData, ViewportCommand};
|
||||
|
||||
use crate::{epi, App};
|
||||
use crate::{App, epi, web::web_painter::WebPainter};
|
||||
|
||||
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint};
|
||||
use super::{NeedRepaint, now_sec, text_agent::TextAgent};
|
||||
|
||||
pub struct AppRunner {
|
||||
#[allow(dead_code)]
|
||||
#[allow(dead_code, clippy::allow_attributes)]
|
||||
pub(crate) web_options: crate::WebOptions,
|
||||
pub(crate) frame: epi::Frame,
|
||||
egui_ctx: egui::Context,
|
||||
painter: super::ActiveWebPainter,
|
||||
painter: Box<dyn WebPainter>,
|
||||
pub(crate) input: super::WebInput,
|
||||
app: Box<dyn epi::App>,
|
||||
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||
@@ -34,6 +34,10 @@ impl Drop for AppRunner {
|
||||
impl AppRunner {
|
||||
/// # Errors
|
||||
/// Failure to initialize WebGL renderer, or failure to create app.
|
||||
#[cfg_attr(
|
||||
not(feature = "wgpu_no_default_features"),
|
||||
expect(clippy::unused_async)
|
||||
)]
|
||||
pub async fn new(
|
||||
canvas: web_sys::HtmlCanvasElement,
|
||||
web_options: crate::WebOptions,
|
||||
@@ -41,7 +45,41 @@ impl AppRunner {
|
||||
text_agent: TextAgent,
|
||||
) -> Result<Self, String> {
|
||||
let egui_ctx = egui::Context::default();
|
||||
let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?;
|
||||
|
||||
#[allow(clippy::allow_attributes, unused_assignments)]
|
||||
#[cfg(feature = "glow")]
|
||||
let mut gl = None;
|
||||
|
||||
#[allow(clippy::allow_attributes, unused_assignments)]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
let mut wgpu_render_state = None;
|
||||
|
||||
let painter = match web_options.renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
epi::Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
let painter = super::web_painter_glow::WebPainterGlow::new(
|
||||
egui_ctx.clone(),
|
||||
canvas,
|
||||
&web_options,
|
||||
)?;
|
||||
gl = Some(painter.gl().clone());
|
||||
Box::new(painter) as Box<dyn WebPainter>
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
epi::Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
let painter = super::web_painter_wgpu::WebPainterWgpu::new(
|
||||
egui_ctx.clone(),
|
||||
canvas,
|
||||
&web_options,
|
||||
)
|
||||
.await?;
|
||||
wgpu_render_state = painter.render_state();
|
||||
Box::new(painter) as Box<dyn WebPainter>
|
||||
}
|
||||
};
|
||||
|
||||
let info = epi::IntegrationInfo {
|
||||
web_info: epi::WebInfo {
|
||||
@@ -65,21 +103,27 @@ impl AppRunner {
|
||||
o.zoom_factor = 1.0;
|
||||
});
|
||||
|
||||
// Tell egui right away about native_pixels_per_point
|
||||
// so that the app knows about it during app creation:
|
||||
egui_ctx.input_mut(|i| {
|
||||
let viewport_info = i.raw.viewports.entry(egui::ViewportId::ROOT).or_default();
|
||||
viewport_info.native_pixels_per_point = Some(super::native_pixels_per_point());
|
||||
i.pixels_per_point = super::native_pixels_per_point();
|
||||
});
|
||||
|
||||
let cc = epi::CreationContext {
|
||||
egui_ctx: egui_ctx.clone(),
|
||||
integration_info: info.clone(),
|
||||
storage: Some(&storage),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
gl: gl.clone(),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
get_proc_address: None,
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: wgpu_render_state.clone(),
|
||||
};
|
||||
let app = app_creator(&cc).map_err(|err| err.to_string())?;
|
||||
|
||||
@@ -88,15 +132,14 @@ impl AppRunner {
|
||||
storage: Some(Box::new(storage)),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
gl,
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state,
|
||||
};
|
||||
|
||||
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
|
||||
let needs_repaint: std::sync::Arc<NeedRepaint> =
|
||||
std::sync::Arc::new(NeedRepaint::new(web_options.max_fps));
|
||||
{
|
||||
let needs_repaint = needs_repaint.clone();
|
||||
egui_ctx.set_request_repaint_callback(move |info| {
|
||||
@@ -304,8 +347,6 @@ impl AppRunner {
|
||||
}
|
||||
|
||||
fn handle_platform_output(&self, platform_output: egui::PlatformOutput) {
|
||||
#![allow(deprecated)]
|
||||
|
||||
#[cfg(feature = "web_screen_reader")]
|
||||
if self.egui_ctx.options(|o| o.screen_reader) {
|
||||
super::screen_reader::speak(&platform_output.events_description());
|
||||
@@ -314,13 +355,10 @@ impl AppRunner {
|
||||
let egui::PlatformOutput {
|
||||
commands,
|
||||
cursor_icon,
|
||||
open_url,
|
||||
copied_text,
|
||||
events: _, // already handled
|
||||
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
|
||||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update: _, // not currently implemented
|
||||
accesskit_update: _, // not currently implemented
|
||||
num_completed_passes: _, // handled by `Context::run`
|
||||
request_discard_reasons: _, // handled by `Context::run`
|
||||
} = platform_output;
|
||||
@@ -341,14 +379,6 @@ impl AppRunner {
|
||||
|
||||
super::set_cursor_icon(cursor_icon);
|
||||
|
||||
if let Some(open) = open_url {
|
||||
super::open_url(&open.url, open.new_tab);
|
||||
}
|
||||
|
||||
if !copied_text.is_empty() {
|
||||
super::set_clipboard_text(&copied_text);
|
||||
}
|
||||
|
||||
if self.has_focus() {
|
||||
// The eframe app has focus.
|
||||
if ime.is_some() {
|
||||
|
||||
@@ -11,9 +11,15 @@ use super::percent_decode;
|
||||
/// Data gathered between frames.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WebInput {
|
||||
/// Required because we don't get a position on touched
|
||||
/// Required because we don't get a position on touchend
|
||||
pub primary_touch: Option<egui::TouchId>,
|
||||
|
||||
/// Helps to track the delta scale from gesture events
|
||||
pub accumulated_scale: f32,
|
||||
|
||||
/// Helps to track the delta rotation from gesture events
|
||||
pub accumulated_rotation: f32,
|
||||
|
||||
/// The raw input to `egui`.
|
||||
pub raw: egui::RawInput,
|
||||
}
|
||||
@@ -50,11 +56,20 @@ impl WebInput {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Stores when to do the next repaint.
|
||||
pub(crate) struct NeedRepaint(Mutex<f64>);
|
||||
pub(crate) struct NeedRepaint {
|
||||
/// Time in seconds when the next repaint should happen.
|
||||
next_repaint: Mutex<f64>,
|
||||
|
||||
impl Default for NeedRepaint {
|
||||
fn default() -> Self {
|
||||
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
|
||||
/// Rate limit for repaint. 0 means "unlimited". The rate may still be limited by vsync.
|
||||
max_fps: u32,
|
||||
}
|
||||
|
||||
impl NeedRepaint {
|
||||
pub fn new(max_fps: Option<u32>) -> Self {
|
||||
Self {
|
||||
next_repaint: Mutex::new(f64::NEG_INFINITY), // start with a repaint
|
||||
max_fps: max_fps.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,25 +77,43 @@ impl NeedRepaint {
|
||||
/// Returns the time (in [`now_sec`] scale) when
|
||||
/// we should next repaint.
|
||||
pub fn when_to_repaint(&self) -> f64 {
|
||||
*self.0.lock()
|
||||
*self.next_repaint.lock()
|
||||
}
|
||||
|
||||
/// Unschedule repainting.
|
||||
pub fn clear(&self) {
|
||||
*self.0.lock() = f64::INFINITY;
|
||||
*self.next_repaint.lock() = f64::INFINITY;
|
||||
}
|
||||
|
||||
pub fn repaint_after(&self, num_seconds: f64) {
|
||||
let mut repaint_time = self.0.lock();
|
||||
*repaint_time = repaint_time.min(super::now_sec() + num_seconds);
|
||||
let mut time = super::now_sec() + num_seconds;
|
||||
time = self.round_repaint_time_to_rate(time);
|
||||
let mut repaint_time = self.next_repaint.lock();
|
||||
*repaint_time = repaint_time.min(time);
|
||||
}
|
||||
|
||||
/// Request a repaint. Depending on the presence of rate limiting, this may not be instant.
|
||||
pub fn repaint(&self) {
|
||||
let time = self.round_repaint_time_to_rate(super::now_sec());
|
||||
let mut repaint_time = self.next_repaint.lock();
|
||||
*repaint_time = repaint_time.min(time);
|
||||
}
|
||||
|
||||
pub fn repaint_asap(&self) {
|
||||
*self.next_repaint.lock() = f64::NEG_INFINITY;
|
||||
}
|
||||
|
||||
pub fn needs_repaint(&self) -> bool {
|
||||
self.when_to_repaint() <= super::now_sec()
|
||||
}
|
||||
|
||||
pub fn repaint_asap(&self) {
|
||||
*self.0.lock() = f64::NEG_INFINITY;
|
||||
fn round_repaint_time_to_rate(&self, time: f64) -> f64 {
|
||||
if self.max_fps == 0 {
|
||||
time
|
||||
} else {
|
||||
let interval = 1.0 / self.max_fps as f64;
|
||||
(time / interval).ceil() * interval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use web_sys::EventTarget;
|
||||
|
||||
use crate::web::string_from_js_value;
|
||||
|
||||
use super::{
|
||||
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
|
||||
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
|
||||
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
|
||||
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast, JsValue, WebRunner,
|
||||
DEBUG_RESIZE,
|
||||
AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event,
|
||||
location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event,
|
||||
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos,
|
||||
push_touches, text_from_keyboard_event, translate_key,
|
||||
};
|
||||
|
||||
use js_sys::Reflect;
|
||||
use web_sys::{Document, EventTarget, ShadowRoot};
|
||||
|
||||
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
|
||||
// than what is probably needed.
|
||||
|
||||
@@ -102,6 +102,7 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
|
||||
install_touchcancel(runner_ref, &canvas)?;
|
||||
|
||||
install_wheel(runner_ref, &canvas)?;
|
||||
install_gesture(runner_ref, &canvas)?;
|
||||
install_drag_and_drop(runner_ref, &canvas)?;
|
||||
install_window_events(runner_ref, &window)?;
|
||||
install_color_scheme_change_event(runner_ref, &window)?;
|
||||
@@ -139,15 +140,20 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
|
||||
{
|
||||
if let Some(text) = text_from_keyboard_event(&event) {
|
||||
let egui_event = egui::Event::Text(text);
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation =
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default =
|
||||
(runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// If this is indeed text, then prevent any other action.
|
||||
event.prevent_default();
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Use web options to tell if the event should be propagated to parent elements.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -158,7 +164,7 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
let has_focus = runner.input.raw.focused;
|
||||
if !has_focus {
|
||||
@@ -184,7 +190,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers,
|
||||
};
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
@@ -201,7 +207,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
|
||||
}
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -221,9 +227,10 @@ fn should_prevent_default_for_key(
|
||||
|
||||
// Prevent cmd/ctrl plus these keys from triggering the default browser action:
|
||||
let keys = [
|
||||
egui::Key::O, // open
|
||||
egui::Key::P, // print (cmd-P is common for command palette)
|
||||
egui::Key::S, // save
|
||||
egui::Key::Comma, // cmd-, opens options on macOS, which egui apps may wanna "steal"
|
||||
egui::Key::O, // open
|
||||
egui::Key::P, // print (cmd-P is common for command palette)
|
||||
egui::Key::S, // save
|
||||
];
|
||||
for key in keys {
|
||||
if egui_key == key && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) {
|
||||
@@ -256,12 +263,12 @@ fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
||||
runner_ref.add_event_listener(target, "keyup", on_keyup)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
let modifiers = modifiers_from_kb_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
|
||||
let mut propagate_event = false;
|
||||
let mut should_stop_propagation = true;
|
||||
|
||||
if let Some(key) = translate_key(&event.key()) {
|
||||
let egui_event = egui::Event::Key {
|
||||
@@ -271,7 +278,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
repeat: false,
|
||||
modifiers,
|
||||
};
|
||||
propagate_event |= (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
}
|
||||
|
||||
@@ -282,6 +289,8 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
// See https://github.com/emilk/egui/issues/4724
|
||||
|
||||
let keys_down = runner.egui_ctx().input(|i| i.keys_down.clone());
|
||||
|
||||
#[expect(clippy::iter_over_hash_type)]
|
||||
for key in keys_down {
|
||||
let egui_event = egui::Event::Key {
|
||||
key,
|
||||
@@ -290,7 +299,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
repeat: false,
|
||||
modifiers,
|
||||
};
|
||||
propagate_event |= (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
}
|
||||
}
|
||||
@@ -299,70 +308,91 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
let has_focus = runner.input.raw.focused;
|
||||
if has_focus && !propagate_event {
|
||||
if has_focus && should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
runner_ref.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, runner| {
|
||||
if !runner.input.raw.focused {
|
||||
return; // The eframe app is not interested
|
||||
}
|
||||
|
||||
if let Some(data) = event.clipboard_data() {
|
||||
if let Ok(text) = data.get_data("text") {
|
||||
let text = text.replace("\r\n", "\n");
|
||||
|
||||
let mut should_propagate = false;
|
||||
if !text.is_empty() && runner.input.raw.focused {
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
if !text.is_empty() {
|
||||
let egui_event = egui::Event::Paste(text);
|
||||
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation =
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
should_prevent_default =
|
||||
(runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
|
||||
if runner.input.raw.focused {
|
||||
runner.input.raw.events.push(egui::Event::Cut);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
if !runner.input.raw.focused {
|
||||
return; // The eframe app is not interested
|
||||
}
|
||||
|
||||
runner.input.raw.events.push(egui::Event::Cut);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !(runner.web_options.should_propagate_event)(&egui::Event::Cut) {
|
||||
if (runner.web_options.should_stop_propagation)(&egui::Event::Cut) {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if (runner.web_options.should_prevent_default)(&egui::Event::Cut) {
|
||||
event.prevent_default();
|
||||
}
|
||||
})?;
|
||||
|
||||
runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| {
|
||||
if runner.input.raw.focused {
|
||||
runner.input.raw.events.push(egui::Event::Copy);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
if !runner.input.raw.focused {
|
||||
return; // The eframe app is not interested
|
||||
}
|
||||
|
||||
runner.input.raw.events.push(egui::Event::Copy);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !(runner.web_options.should_propagate_event)(&egui::Event::Copy) {
|
||||
if (runner.web_options.should_stop_propagation)(&egui::Event::Copy) {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if (runner.web_options.should_prevent_default)(&egui::Event::Copy) {
|
||||
event.prevent_default();
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -380,7 +410,7 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result
|
||||
|
||||
// No need to subscribe to "resize": we already subscribe to the canvas
|
||||
// size using a ResizeObserver, and we also subscribe to DPR changes of the monitor.
|
||||
for event_name in &["load", "pagehide", "pageshow"] {
|
||||
for event_name in &["load", "pagehide", "pageshow", "popstate"] {
|
||||
runner_ref.add_event_listener(window, event_name, move |_: web_sys::Event, runner| {
|
||||
if DEBUG_RESIZE {
|
||||
log::debug!("{event_name:?}");
|
||||
@@ -444,16 +474,19 @@ fn install_color_scheme_change_event(
|
||||
runner_ref: &WebRunner,
|
||||
window: &web_sys::Window,
|
||||
) -> Result<(), JsValue> {
|
||||
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
|
||||
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
|
||||
&media_query_list,
|
||||
"change",
|
||||
|event, runner| {
|
||||
let theme = theme_from_dark_mode(event.matches());
|
||||
runner.input.raw.system_theme = Some(theme);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
for theme in [egui::Theme::Dark, egui::Theme::Light] {
|
||||
if let Some(media_query_list) = prefers_color_scheme(window, theme)? {
|
||||
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
|
||||
&media_query_list,
|
||||
"change",
|
||||
|_event, runner| {
|
||||
if let Some(theme) = super::system_theme() {
|
||||
runner.input.raw.system_theme = Some(theme);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -484,7 +517,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|
||||
|event: web_sys::PointerEvent, runner: &mut AppRunner| {
|
||||
let modifiers = modifiers_from_mouse_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
let mut should_propagate = false;
|
||||
let mut should_stop_propagation = true;
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
|
||||
let modifiers = runner.input.raw.modifiers;
|
||||
@@ -494,7 +527,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|
||||
pressed: true,
|
||||
modifiers,
|
||||
};
|
||||
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
@@ -506,7 +539,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|
||||
}
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
|
||||
@@ -536,7 +569,10 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
pressed: false,
|
||||
modifiers,
|
||||
};
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation =
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default =
|
||||
(runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
|
||||
// Previously on iOS, the canvas would not receive focus on
|
||||
@@ -555,10 +591,12 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
event.prevent_default();
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -570,10 +608,17 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
/// Returns true if the cursor is above the canvas, or if we're dragging something.
|
||||
/// Pass in the position in browser viewport coordinates (usually event.clientX/Y).
|
||||
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let is_hovering_canvas = document
|
||||
.element_from_point(pos.x, pos.y)
|
||||
.is_some_and(|element| element.eq(runner.canvas()));
|
||||
let root_node = runner.canvas().get_root_node();
|
||||
|
||||
let element_at_point = if let Some(document) = root_node.dyn_ref::<Document>() {
|
||||
document.element_from_point(pos.x, pos.y)
|
||||
} else if let Some(shadow) = root_node.dyn_ref::<ShadowRoot>() {
|
||||
shadow.element_from_point(pos.x, pos.y)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let is_hovering_canvas = element_at_point.is_some_and(|element| element.eq(runner.canvas()));
|
||||
let is_pointer_down = runner
|
||||
.egui_ctx()
|
||||
.input(|i| i.pointer.any_down() || i.any_touches());
|
||||
@@ -593,15 +638,19 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
egui::pos2(event.client_x() as f32, event.client_y() as f32),
|
||||
) {
|
||||
let egui_event = egui::Event::PointerMoved(pos);
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
runner.needs_repaint.repaint();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -615,10 +664,13 @@ fn install_mouseleave(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !(runner.web_options.should_propagate_event)(&egui::Event::PointerGone) {
|
||||
if (runner.web_options.should_stop_propagation)(&egui::Event::PointerGone) {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if (runner.web_options.should_prevent_default)(&egui::Event::PointerGone) {
|
||||
event.prevent_default();
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -628,7 +680,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
target,
|
||||
"touchstart",
|
||||
|event: web_sys::TouchEvent, runner| {
|
||||
let mut should_propagate = false;
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
if let Some((pos, _)) = primary_touch_pos(runner, &event) {
|
||||
let egui_event = egui::Event::PointerButton {
|
||||
pos,
|
||||
@@ -636,7 +689,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
pressed: true,
|
||||
modifiers: runner.input.raw.modifiers,
|
||||
};
|
||||
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
}
|
||||
|
||||
@@ -644,10 +698,13 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -660,17 +717,23 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
|
||||
) {
|
||||
let egui_event = egui::Event::PointerMoved(pos);
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation =
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default =
|
||||
(runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
|
||||
push_touches(runner, egui::TouchPhase::Move, &event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
runner.needs_repaint.repaint();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -684,18 +747,23 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
|
||||
) {
|
||||
// First release mouse to click:
|
||||
let mut should_propagate = false;
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
let egui_event = egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: runner.input.raw.modifiers,
|
||||
};
|
||||
should_propagate |= (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation &=
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
// Then remove hover effect:
|
||||
should_propagate |=
|
||||
(runner.web_options.should_propagate_event)(&egui::Event::PointerGone);
|
||||
should_stop_propagation &=
|
||||
(runner.web_options.should_stop_propagation)(&egui::Event::PointerGone);
|
||||
should_prevent_default &=
|
||||
(runner.web_options.should_prevent_default)(&egui::Event::PointerGone);
|
||||
runner.input.raw.events.push(egui::Event::PointerGone);
|
||||
|
||||
push_touches(runner, egui::TouchPhase::End, &event);
|
||||
@@ -703,10 +771,13 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Fix virtual keyboard IOS
|
||||
// Need call focus at the same time of event
|
||||
@@ -748,7 +819,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
||||
|
||||
let egui_event = if modifiers.ctrl && !runner.input.raw.modifiers.ctrl {
|
||||
// The browser is saying the ctrl key is down, but it isn't _really_.
|
||||
// This happens on pinch-to-zoom on a Mac trackpad.
|
||||
// This happens on pinch-to-zoom on multitouch trackpads
|
||||
// egui will treat ctrl+scroll as zoom, so it all works.
|
||||
// However, we explicitly handle it here in order to better match the pinch-to-zoom
|
||||
// speed of a native app, without being sensitive to egui's `scroll_zoom_speed` setting.
|
||||
@@ -760,19 +831,91 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
||||
unit,
|
||||
delta,
|
||||
modifiers,
|
||||
phase: egui::TouchPhase::Move,
|
||||
}
|
||||
};
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
|
||||
runner.needs_repaint.repaint();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn install_gesture(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
runner_ref.add_event_listener(target, "gesturestart", |event: web_sys::Event, runner| {
|
||||
runner.input.accumulated_scale = 1.0;
|
||||
runner.input.accumulated_rotation = 0.0;
|
||||
handle_gesture(event, runner);
|
||||
})?;
|
||||
runner_ref.add_event_listener(target, "gesturechange", handle_gesture)?;
|
||||
runner_ref.add_event_listener(target, "gestureend", |event: web_sys::Event, runner| {
|
||||
handle_gesture(event, runner);
|
||||
runner.input.accumulated_scale = 1.0;
|
||||
runner.input.accumulated_rotation = 0.0;
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
fn handle_gesture(event: web_sys::Event, runner: &mut AppRunner) {
|
||||
// GestureEvent is a non-standard API, so this attempts to get the relevant fields if they exist.
|
||||
let new_scale = Reflect::get(&event, &JsValue::from_str("scale"))
|
||||
.ok()
|
||||
.and_then(|scale| scale.as_f64())
|
||||
.map_or(1.0, |scale| scale as f32);
|
||||
let new_rotation = Reflect::get(&event, &JsValue::from_str("rotation"))
|
||||
.ok()
|
||||
.and_then(|rotation| rotation.as_f64())
|
||||
.map_or(0.0, |rotation| rotation.to_radians() as f32);
|
||||
|
||||
let scale_delta = new_scale / runner.input.accumulated_scale;
|
||||
let rotation_delta = new_rotation - runner.input.accumulated_rotation;
|
||||
runner.input.accumulated_scale *= scale_delta;
|
||||
runner.input.accumulated_rotation += rotation_delta;
|
||||
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
|
||||
if scale_delta != 1.0 {
|
||||
let zoom_event = egui::Event::Zoom(scale_delta);
|
||||
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&zoom_event);
|
||||
should_prevent_default &= (runner.web_options.should_prevent_default)(&zoom_event);
|
||||
runner.input.raw.events.push(zoom_event);
|
||||
}
|
||||
|
||||
if rotation_delta != 0.0 {
|
||||
let rotate_event = egui::Event::Rotate(rotation_delta);
|
||||
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&rotate_event);
|
||||
should_prevent_default &= (runner.web_options.should_prevent_default)(&rotate_event);
|
||||
runner.input.raw.events.push(rotate_event);
|
||||
}
|
||||
|
||||
if scale_delta != 1.0 || rotation_delta != 0.0 {
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
})
|
||||
|
||||
if should_prevent_default {
|
||||
// Prevents a simulated ctrl-scroll event for zoom
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
@@ -856,7 +999,10 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to read file: {:?}", err);
|
||||
log::error!(
|
||||
"Failed to read file: {}",
|
||||
string_from_js_value(&err)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -925,7 +1071,7 @@ impl ResizeObserverContext {
|
||||
// we rely on the resize observer to trigger the first `request_animation_frame`:
|
||||
if let Err(err) = runner_ref.request_animation_frame() {
|
||||
log::error!("{}", super::string_from_js_value(&err));
|
||||
};
|
||||
}
|
||||
} else {
|
||||
log::warn!("ResizeObserverContext callback: failed to lock runner");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{canvas_content_rect, AppRunner};
|
||||
use super::{AppRunner, canvas_content_rect};
|
||||
|
||||
pub fn pos_from_mouse_event(
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
|
||||
@@ -23,25 +23,22 @@ pub use panic_handler::{PanicHandler, PanicSummary};
|
||||
pub use web_logger::WebLogger;
|
||||
pub use web_runner::WebRunner;
|
||||
|
||||
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
|
||||
#[cfg(not(any(feature = "glow", feature = "wgpu_no_default_features")))]
|
||||
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
|
||||
|
||||
mod web_painter;
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
mod web_painter_glow;
|
||||
#[cfg(feature = "glow")]
|
||||
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
mod web_painter_wgpu;
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
||||
|
||||
pub use backend::*;
|
||||
|
||||
use egui::Theme;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::MediaQueryList;
|
||||
use web_sys::{Document, MediaQueryList, Node};
|
||||
|
||||
use input::{
|
||||
button_from_mouse_event, modifiers_from_kb_event, modifiers_from_mouse_event,
|
||||
@@ -64,18 +61,22 @@ pub(crate) fn string_from_js_value(value: &JsValue) -> String {
|
||||
/// - `<a>`/`<area>` with an `href` attribute
|
||||
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
|
||||
/// - any other element with a `tabindex` attribute
|
||||
pub(crate) fn focused_element() -> Option<web_sys::Element> {
|
||||
web_sys::window()?
|
||||
.document()?
|
||||
.active_element()?
|
||||
.dyn_into()
|
||||
.ok()
|
||||
pub(crate) fn focused_element(root: &Node) -> Option<web_sys::Element> {
|
||||
if let Some(document) = root.dyn_ref::<Document>() {
|
||||
document.active_element()
|
||||
} else if let Some(shadow) = root.dyn_ref::<web_sys::ShadowRoot>() {
|
||||
shadow.active_element()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
|
||||
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
|
||||
let element = element.dyn_ref::<web_sys::Element>()?;
|
||||
let focused_element = focused_element()?;
|
||||
let root = element.get_root_node();
|
||||
|
||||
let focused_element = focused_element(&root)?;
|
||||
Some(element == &focused_element)
|
||||
}
|
||||
try_has_focus(element).unwrap_or(false)
|
||||
@@ -109,24 +110,31 @@ pub fn native_pixels_per_point() -> f32 {
|
||||
///
|
||||
/// `None` means unknown.
|
||||
pub fn system_theme() -> Option<egui::Theme> {
|
||||
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
|
||||
.ok()??
|
||||
.matches();
|
||||
Some(theme_from_dark_mode(dark_mode))
|
||||
}
|
||||
|
||||
fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQueryList>, JsValue> {
|
||||
window.match_media("(prefers-color-scheme: dark)")
|
||||
}
|
||||
|
||||
fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme {
|
||||
if dark_mode {
|
||||
egui::Theme::Dark
|
||||
let window = web_sys::window()?;
|
||||
if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) {
|
||||
Some(Theme::Dark)
|
||||
} else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) {
|
||||
Some(Theme::Light)
|
||||
} else {
|
||||
egui::Theme::Light
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn does_prefer_color_scheme(window: &web_sys::Window, theme: Theme) -> Option<bool> {
|
||||
Some(prefers_color_scheme(window, theme).ok()??.matches())
|
||||
}
|
||||
|
||||
fn prefers_color_scheme(
|
||||
window: &web_sys::Window,
|
||||
theme: Theme,
|
||||
) -> Result<Option<MediaQueryList>, JsValue> {
|
||||
let theme = match theme {
|
||||
Theme::Dark => "dark",
|
||||
Theme::Light => "light",
|
||||
};
|
||||
window.match_media(format!("(prefers-color-scheme: {theme})").as_str())
|
||||
}
|
||||
|
||||
/// Returns the canvas in client coordinates.
|
||||
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
|
||||
let bounding_rect = canvas.get_bounding_client_rect();
|
||||
@@ -277,7 +285,7 @@ fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result<web_sys::ClipboardI
|
||||
let items = js_sys::Object::new();
|
||||
|
||||
// SAFETY: I hope so
|
||||
#[allow(unsafe_code, unused_unsafe)] // Weird false positive
|
||||
#[expect(unsafe_code, unused_unsafe)] // Weird false positive
|
||||
unsafe {
|
||||
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Document, Node};
|
||||
|
||||
use super::{AppRunner, WebRunner};
|
||||
|
||||
@@ -14,7 +15,7 @@ pub struct TextAgent {
|
||||
|
||||
impl TextAgent {
|
||||
/// Attach the agent to the document.
|
||||
pub fn attach(runner_ref: &WebRunner) -> Result<Self, JsValue> {
|
||||
pub fn attach(runner_ref: &WebRunner, root: Node) -> Result<Self, JsValue> {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
// create an `<input>` element
|
||||
@@ -37,7 +38,17 @@ impl TextAgent {
|
||||
style.set_property("position", "absolute")?;
|
||||
style.set_property("top", "0")?;
|
||||
style.set_property("left", "0")?;
|
||||
document.body().unwrap().append_child(&input)?;
|
||||
|
||||
if root.has_type::<Document>() {
|
||||
// root object is a document, append to its body
|
||||
root.dyn_into::<Document>()?
|
||||
.body()
|
||||
.unwrap()
|
||||
.append_child(&input)?;
|
||||
} else {
|
||||
// append input into root directly
|
||||
root.append_child(&input)?;
|
||||
}
|
||||
|
||||
// attach event listeners
|
||||
|
||||
@@ -168,7 +179,7 @@ impl TextAgent {
|
||||
|
||||
if let Err(err) = self.input.focus() {
|
||||
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blur(&self) {
|
||||
@@ -180,7 +191,7 @@ impl TextAgent {
|
||||
|
||||
if let Err(err) = self.input.blur() {
|
||||
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ mod console {
|
||||
/// * `tokio-1.24.1/src/runtime/runtime.rs`
|
||||
/// * `rerun/src/main.rs`
|
||||
/// * `core/src/ops/function.rs`
|
||||
#[allow(dead_code)] // only used on web and in tests
|
||||
#[allow(dead_code, clippy::allow_attributes)] // only used on web and in tests
|
||||
fn shorten_file_path(file_path: &str) -> &str {
|
||||
if let Some(i) = file_path.rfind("/src/") {
|
||||
if let Some(prev_slash) = file_path[..i].rfind('/') {
|
||||
@@ -126,12 +126,17 @@ fn shorten_file_path(file_path: &str) -> &str {
|
||||
#[test]
|
||||
fn test_shorten_file_path() {
|
||||
for (before, after) in [
|
||||
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
|
||||
(
|
||||
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
|
||||
"tokio-1.24.1/src/runtime/runtime.rs",
|
||||
),
|
||||
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
|
||||
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
|
||||
(
|
||||
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
|
||||
"core/src/ops/function.rs",
|
||||
),
|
||||
("/weird/path/file.rs", "/weird/path/file.rs"),
|
||||
]
|
||||
{
|
||||
] {
|
||||
assert_eq!(shorten_file_path(before), after);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use egui::{Event, UserData, ViewportId};
|
||||
use egui_glow::glow;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsCast as _;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
@@ -20,14 +20,15 @@ impl WebPainterGlow {
|
||||
self.painter.gl()
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
pub 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)]
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
|
||||
let gl = std::sync::Arc::new(gl);
|
||||
|
||||
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
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};
|
||||
use egui_wgpu::{
|
||||
RenderState, SurfaceErrorAction,
|
||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||
};
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
|
||||
pub(crate) struct WebPainterWgpu {
|
||||
canvas: HtmlCanvasElement,
|
||||
surface: wgpu::Surface<'static>,
|
||||
surface_configuration: wgpu::SurfaceConfiguration,
|
||||
render_state: Option<RenderState>,
|
||||
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_stencil_format: Option<wgpu::TextureFormat>,
|
||||
depth_texture_view: Option<wgpu::TextureView>,
|
||||
screen_capture_state: Option<CaptureState>,
|
||||
capture_tx: CaptureSender,
|
||||
@@ -23,7 +25,6 @@ pub(crate) struct WebPainterWgpu {
|
||||
}
|
||||
|
||||
impl WebPainterWgpu {
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.clone()
|
||||
}
|
||||
@@ -35,7 +36,7 @@ impl WebPainterWgpu {
|
||||
height_in_pixels: u32,
|
||||
) -> Option<wgpu::TextureView> {
|
||||
let device = &render_state.device;
|
||||
self.depth_format.map(|depth_format| {
|
||||
self.depth_stencil_format.map(|depth_stencil_format| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
@@ -47,19 +48,18 @@ impl WebPainterWgpu {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
format: depth_stencil_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[depth_format],
|
||||
view_formats: &[depth_stencil_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub async fn new(
|
||||
ctx: egui::Context,
|
||||
canvas: web_sys::HtmlCanvasElement,
|
||||
options: &WebOptions,
|
||||
options: &crate::WebOptions,
|
||||
) -> Result<Self, String> {
|
||||
log::debug!("Creating wgpu painter");
|
||||
|
||||
@@ -68,15 +68,17 @@ impl WebPainterWgpu {
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
||||
|
||||
let depth_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
|
||||
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
|
||||
|
||||
let render_state = RenderState::create(
|
||||
&options.wgpu_options,
|
||||
&instance,
|
||||
Some(&surface),
|
||||
depth_format,
|
||||
1,
|
||||
options.dithering,
|
||||
egui_wgpu::RendererOptions {
|
||||
dithering: options.dithering,
|
||||
depth_stencil_format,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.to_string())?;
|
||||
@@ -101,7 +103,7 @@ impl WebPainterWgpu {
|
||||
render_state: Some(render_state),
|
||||
surface,
|
||||
surface_configuration,
|
||||
depth_format,
|
||||
depth_stencil_format,
|
||||
depth_texture_view: None,
|
||||
on_surface_error: options.wgpu_options.on_surface_error.clone(),
|
||||
screen_capture_state: None,
|
||||
@@ -236,6 +238,7 @@ impl WebPainter for WebPainterWgpu {
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
@@ -274,18 +277,11 @@ impl WebPainter for WebPainterWgpu {
|
||||
&mut encoder,
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some((output_frame, capture_buffer))
|
||||
};
|
||||
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
render_state
|
||||
.queue
|
||||
@@ -307,6 +303,16 @@ impl WebPainter for WebPainterWgpu {
|
||||
frame.present();
|
||||
}
|
||||
|
||||
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
|
||||
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
|
||||
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{epi, App};
|
||||
use crate::{App, epi};
|
||||
|
||||
use super::{
|
||||
AppRunner, PanicHandler,
|
||||
events::{self, ResizeObserverContext},
|
||||
text_agent::TextAgent,
|
||||
AppRunner, PanicHandler,
|
||||
};
|
||||
|
||||
/// This is how `eframe` runs your web application
|
||||
@@ -37,7 +37,7 @@ pub struct WebRunner {
|
||||
|
||||
impl WebRunner {
|
||||
/// Will install a panic handler that will catch and log any panics
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[expect(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
let panic_handler = PanicHandler::install();
|
||||
|
||||
@@ -73,7 +73,7 @@ impl WebRunner {
|
||||
|
||||
{
|
||||
// First set up the app runner:
|
||||
let text_agent = TextAgent::attach(self)?;
|
||||
let text_agent = TextAgent::attach(self, canvas.get_root_node())?;
|
||||
let app_runner =
|
||||
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
|
||||
self.app_runner.replace(Some(app_runner));
|
||||
@@ -280,7 +280,7 @@ struct TargetEvent {
|
||||
closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[expect(unused)]
|
||||
struct IntervalHandle {
|
||||
handle: i32,
|
||||
closure: Closure<dyn FnMut()>,
|
||||
@@ -289,7 +289,7 @@ struct IntervalHandle {
|
||||
enum EventToUnsubscribe {
|
||||
TargetEvent(TargetEvent),
|
||||
|
||||
#[allow(unused)]
|
||||
#[expect(unused)]
|
||||
IntervalHandle(IntervalHandle),
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,42 @@ 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.33.2 - 2025-11-13
|
||||
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09
|
||||
### 🔧 Changed
|
||||
* Update wgpu to 26 and wasm-bindgen to 0.2.100 [#7540](https://github.com/emilk/egui/pull/7540) by [@Kumpelinus](https://github.com/Kumpelinus)
|
||||
* Warn if `DYLD_LIBRARY_PATH` is set and we find no wgpu adapter [#7572](https://github.com/emilk/egui/pull/7572) by [@emilk](https://github.com/emilk)
|
||||
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
|
||||
* Update wgpu to 27.0.0 [#7580](https://github.com/emilk/egui/pull/7580) by [@Wumpf](https://github.com/Wumpf)
|
||||
* Create `egui_wgpu::RendererOptions` [#7601](https://github.com/emilk/egui/pull/7601) by [@emilk](https://github.com/emilk)
|
||||
* Use software texture filtering in kittest [#7602](https://github.com/emilk/egui/pull/7602) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.32.3 - 2025-09-12
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.2 - 2025-09-04
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.1 - 2025-08-15
|
||||
* Enable wgpu default features in eframe / egui_wgpu default features [#7344](https://github.com/emilk/egui/pull/7344) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
|
||||
## 0.32.0 - 2025-07-10
|
||||
* Update to wgpu 25 [#6744](https://github.com/emilk/egui/pull/6744) by [@torokati44](https://github.com/torokati44)
|
||||
* Free textures after submitting queue instead of before with wgpu renderer on Web [#7291](https://github.com/emilk/egui/pull/7291) by [@Wumpf](https://github.com/Wumpf)
|
||||
* Improve texture filtering by doing it in gamma space [#7311](https://github.com/emilk/egui/pull/7311) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.31.1 - 2025-03-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04
|
||||
* Upgrade to wgpu 24 [#5610](https://github.com/emilk/egui/pull/5610) by [@torokati44](https://github.com/torokati44)
|
||||
* Extend `WgpuSetup`, `egui_kittest` now prefers software rasterizers for testing [#5506](https://github.com/emilk/egui/pull/5506) by [@Wumpf](https://github.com/Wumpf)
|
||||
|
||||
@@ -9,19 +9,13 @@ authors = [
|
||||
]
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
|
||||
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["wgpu", "egui", "gui", "gamedev"]
|
||||
include = [
|
||||
"../LICENSE-APACHE",
|
||||
"../LICENSE-MIT",
|
||||
"**/*.rs",
|
||||
"**/*.wgsl",
|
||||
"Cargo.toml",
|
||||
]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -31,10 +25,13 @@ all-features = true
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
default = ["fragile-send-sync-non-atomic-wasm"]
|
||||
default = ["fragile-send-sync-non-atomic-wasm", "macos-window-resize-jitter-fix", "wgpu/default"]
|
||||
|
||||
## Enables the `capture` module for capturing screenshots.
|
||||
capture = ["dep:egui"]
|
||||
|
||||
## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11`
|
||||
winit = ["dep:winit", "winit/rwh_06"]
|
||||
winit = ["dep:winit", "winit/rwh_06", "dep:egui", "capture"]
|
||||
|
||||
## Enables Wayland support for winit.
|
||||
wayland = ["winit?/wayland"]
|
||||
@@ -43,14 +40,16 @@ wayland = ["winit?/wayland"]
|
||||
x11 = ["winit?/x11"]
|
||||
|
||||
## Make the renderer `Sync` on wasm, exploiting that by default wasm isn't multithreaded.
|
||||
## It may make code easier, expecially when targeting both native and web.
|
||||
## It may make code easier, especially when targeting both native and web.
|
||||
## On native most wgpu objects are send and sync, on the web they are not (by nature of the WebGPU specification).
|
||||
## This is not supported in [multithreaded WASM](https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer).
|
||||
## Thus that usage is guarded against with compiler errors in wgpu.
|
||||
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"]
|
||||
|
||||
## Enables `present_with_transaction` surface flag temporary during window resize on MacOS.
|
||||
macos-window-resize-jitter-fix = ["wgpu/metal"]
|
||||
|
||||
[dependencies]
|
||||
egui = { workspace = true, default-features = false }
|
||||
epaint = { workspace = true, default-features = false, features = ["bytemuck"] }
|
||||
|
||||
ahash.workspace = true
|
||||
@@ -65,4 +64,5 @@ wgpu = { workspace = true, features = ["wgsl"] }
|
||||
|
||||
# Optional dependencies:
|
||||
|
||||
egui = { workspace = true, optional = true, default-features = false }
|
||||
winit = { workspace = true, optional = true, default-features = false }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use egui::{UserData, ViewportId};
|
||||
use epaint::ColorImage;
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::sync::{Arc, mpsc};
|
||||
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
|
||||
@@ -125,7 +126,7 @@ impl CaptureState {
|
||||
// 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)]
|
||||
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
|
||||
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,
|
||||
@@ -159,6 +160,7 @@ impl CaptureState {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
@@ -184,7 +186,7 @@ impl CaptureState {
|
||||
tx: CaptureSender,
|
||||
viewport_id: ViewportId,
|
||||
) {
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
|
||||
let buffer = Arc::new(buffer);
|
||||
let buffer_clone = buffer.clone();
|
||||
let buffer_slice = buffer_clone.slice(..);
|
||||
@@ -195,13 +197,15 @@ impl CaptureState {
|
||||
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);
|
||||
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);
|
||||
log::error!("Failed to map buffer for reading: {err}");
|
||||
return;
|
||||
}
|
||||
let buffer_slice = buffer.slice(..);
|
||||
@@ -226,10 +230,10 @@ impl CaptureState {
|
||||
tx.send((
|
||||
viewport_id,
|
||||
data,
|
||||
ColorImage {
|
||||
size: [tex_extent.width as usize, tex_extent.height as usize],
|
||||
ColorImage::new(
|
||||
[tex_extent.width as usize, tex_extent.height as usize],
|
||||
pixels,
|
||||
},
|
||||
),
|
||||
))
|
||||
.ok();
|
||||
ctx.request_repaint();
|
||||
|
||||
@@ -8,10 +8,13 @@ struct VertexOutput {
|
||||
|
||||
struct Locals {
|
||||
screen_size: vec2<f32>,
|
||||
dithering: u32, // 1 if dithering is enabled, 0 otherwise
|
||||
// Uniform buffers need to be at least 16 bytes in WebGL.
|
||||
// See https://github.com/gfx-rs/wgpu/issues/2072
|
||||
_padding: u32,
|
||||
|
||||
/// 1 if dithering is enabled, 0 otherwise
|
||||
dithering: u32,
|
||||
|
||||
/// 1 to do manual filtering for more predictable kittest snapshot images.
|
||||
/// See also https://github.com/emilk/egui/issues/5295
|
||||
predictable_texture_filtering: u32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> r_locals: Locals;
|
||||
|
||||
@@ -95,11 +98,42 @@ fn vs_main(
|
||||
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
||||
@group(1) @binding(1) var r_tex_sampler: sampler;
|
||||
|
||||
fn sample_texture(in: VertexOutput) -> vec4<f32> {
|
||||
if r_locals.predictable_texture_filtering == 0 {
|
||||
// Hardware filtering: fast, but varies across GPUs and drivers.
|
||||
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
} else {
|
||||
// Manual bilinear filtering with four taps at pixel centers using textureLoad
|
||||
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
|
||||
let texture_size_f = vec2<f32>(texture_size);
|
||||
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
|
||||
let pixel_fract = fract(pixel_coord);
|
||||
let pixel_floor = vec2<i32>(floor(pixel_coord));
|
||||
|
||||
// Manual texture clamping
|
||||
let max_coord = texture_size - vec2<i32>(1, 1);
|
||||
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
|
||||
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
|
||||
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
|
||||
let p11 = clamp(pixel_floor + vec2<i32>(1, 1), vec2<i32>(0, 0), max_coord);
|
||||
|
||||
// Load at pixel centers
|
||||
let tl = textureLoad(r_tex_color, p00, 0);
|
||||
let tr = textureLoad(r_tex_color, p10, 0);
|
||||
let bl = textureLoad(r_tex_color, p01, 0);
|
||||
let br = textureLoad(r_tex_color, p11, 0);
|
||||
|
||||
// Manual bilinear interpolation
|
||||
let top = mix(tl, tr, pixel_fract.x);
|
||||
let bottom = mix(bl, br, pixel_fract.x);
|
||||
return mix(top, bottom, pixel_fract.y);
|
||||
}
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We always have an sRGB aware texture at the moment.
|
||||
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
let tex_gamma = gamma_from_linear_rgba(tex_linear);
|
||||
// We expect "normal" textures that are NOT sRGB-aware.
|
||||
let tex_gamma = sample_texture(in);
|
||||
var out_color_gamma = in.color * tex_gamma;
|
||||
// Dither the float color down to eight bits to reduce banding.
|
||||
// This step is optional for egui backends.
|
||||
@@ -115,9 +149,8 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
|
||||
@fragment
|
||||
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We always have an sRGB aware texture at the moment.
|
||||
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
let tex_gamma = gamma_from_linear_rgba(tex_linear);
|
||||
// We expect "normal" textures that are NOT sRGB-aware.
|
||||
let tex_gamma = sample_texture(in);
|
||||
var out_color_gamma = in.color * tex_gamma;
|
||||
// Dither the float color down to eight bits to reduce banding.
|
||||
// This step is optional for egui backends.
|
||||
|
||||
@@ -29,6 +29,7 @@ pub use renderer::*;
|
||||
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
|
||||
|
||||
/// Helpers for capturing screenshots of the UI.
|
||||
#[cfg(feature = "capture")]
|
||||
pub mod capture;
|
||||
|
||||
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
|
||||
@@ -42,8 +43,11 @@ use epaint::mutex::RwLock;
|
||||
/// An error produced by egui-wgpu.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum WgpuError {
|
||||
#[error("Failed to create wgpu adapter, no suitable adapter found: {0}")]
|
||||
NoSuitableAdapterFound(String),
|
||||
#[error(transparent)]
|
||||
RequestAdapterError(#[from] wgpu::RequestAdapterError),
|
||||
|
||||
#[error("Adapter selection failed: {0}")]
|
||||
CustomNativeAdapterSelectionError(String),
|
||||
|
||||
#[error("There was no valid format for the surface at all.")]
|
||||
NoSurfaceFormatsAvailable,
|
||||
@@ -89,7 +93,7 @@ async fn request_adapter(
|
||||
instance: &wgpu::Instance,
|
||||
power_preference: wgpu::PowerPreference,
|
||||
compatible_surface: Option<&wgpu::Surface<'_>>,
|
||||
_available_adapters: &[wgpu::Adapter],
|
||||
available_adapters: &[wgpu::Adapter],
|
||||
) -> Result<wgpu::Adapter, WgpuError> {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -104,48 +108,58 @@ async fn request_adapter(
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if _available_adapters.is_empty() {
|
||||
log::info!("No wgpu adapters found");
|
||||
} else if _available_adapters.len() == 1 {
|
||||
.inspect_err(|_err| {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
// Nothing to add here
|
||||
} else if available_adapters.is_empty() {
|
||||
if std::env::var("DYLD_LIBRARY_PATH").is_ok() {
|
||||
// DYLD_LIBRARY_PATH can sometimes lead to loading dylibs that cause
|
||||
// us to find zero adapters. Very strange.
|
||||
// I don't want to debug this again.
|
||||
// See https://github.com/rerun-io/rerun/issues/11351 for more
|
||||
log::warn!(
|
||||
"No wgpu adapter found. This could be because DYLD_LIBRARY_PATH causes dylibs to be loaded that interfere with Metal device creation. Try restarting with DYLD_LIBRARY_PATH=''"
|
||||
);
|
||||
} else {
|
||||
log::info!("No wgpu adapter found");
|
||||
}
|
||||
} else if available_adapters.len() == 1 {
|
||||
log::info!(
|
||||
"The only available wgpu adapter was not suitable: {}",
|
||||
adapter_info_summary(&_available_adapters[0].get_info())
|
||||
adapter_info_summary(&available_adapters[0].get_info())
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"No suitable wgpu adapter found out of the {} available ones: {}",
|
||||
_available_adapters.len(),
|
||||
describe_adapters(_available_adapters)
|
||||
available_adapters.len(),
|
||||
describe_adapters(available_adapters)
|
||||
);
|
||||
}
|
||||
|
||||
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
|
||||
})?;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
log::debug!(
|
||||
"Picked wgpu adapter: {}",
|
||||
adapter_info_summary(&adapter.get_info())
|
||||
);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if _available_adapters.len() == 1 {
|
||||
log::debug!(
|
||||
"Picked the only available wgpu adapter: {}",
|
||||
adapter_info_summary(&adapter.get_info())
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"There were {} available wgpu adapters: {}",
|
||||
_available_adapters.len(),
|
||||
describe_adapters(_available_adapters)
|
||||
);
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
log::debug!(
|
||||
"Picked wgpu adapter: {}",
|
||||
adapter_info_summary(&adapter.get_info())
|
||||
);
|
||||
} else {
|
||||
// native:
|
||||
if available_adapters.len() == 1 {
|
||||
log::debug!(
|
||||
"Picked the only available wgpu adapter: {}",
|
||||
adapter_info_summary(&adapter.get_info())
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"There were {} available wgpu adapters: {}",
|
||||
available_adapters.len(),
|
||||
describe_adapters(available_adapters)
|
||||
);
|
||||
log::debug!(
|
||||
"Picked wgpu adapter: {}",
|
||||
adapter_info_summary(&adapter.get_info())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(adapter)
|
||||
@@ -160,9 +174,7 @@ impl RenderState {
|
||||
config: &WgpuConfiguration,
|
||||
instance: &wgpu::Instance,
|
||||
compatible_surface: Option<&wgpu::Surface<'static>>,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
msaa_samples: u32,
|
||||
dithering: bool,
|
||||
options: RendererOptions,
|
||||
) -> Result<Self, WgpuError> {
|
||||
profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
|
||||
|
||||
@@ -184,7 +196,6 @@ impl RenderState {
|
||||
power_preference,
|
||||
native_adapter_selector: _native_adapter_selector,
|
||||
device_descriptor,
|
||||
trace_path,
|
||||
}) => {
|
||||
let adapter = {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -194,7 +205,7 @@ impl RenderState {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(native_adapter_selector) = _native_adapter_selector {
|
||||
native_adapter_selector(&available_adapters, compatible_surface)
|
||||
.map_err(WgpuError::NoSuitableAdapterFound)
|
||||
.map_err(WgpuError::CustomNativeAdapterSelectionError)
|
||||
} else {
|
||||
request_adapter(
|
||||
instance,
|
||||
@@ -209,7 +220,7 @@ impl RenderState {
|
||||
let (device, queue) = {
|
||||
profiling::scope!("request_device");
|
||||
adapter
|
||||
.request_device(&(*device_descriptor)(&adapter), trace_path.as_deref())
|
||||
.request_device(&(*device_descriptor)(&adapter))
|
||||
.await?
|
||||
};
|
||||
|
||||
@@ -232,17 +243,11 @@ impl RenderState {
|
||||
};
|
||||
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
|
||||
|
||||
let renderer = Renderer::new(
|
||||
&device,
|
||||
target_format,
|
||||
depth_format,
|
||||
msaa_samples,
|
||||
dithering,
|
||||
);
|
||||
let renderer = Renderer::new(&device, target_format, options);
|
||||
|
||||
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
|
||||
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
|
||||
Ok(Self {
|
||||
adapter,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -255,7 +260,6 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
|
||||
if adapters.is_empty() {
|
||||
"(none)".to_owned()
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
|
||||
|
||||
use ahash::HashMap;
|
||||
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
|
||||
use bytemuck::Zeroable as _;
|
||||
use epaint::{PaintCallbackInfo, Primitive, Vertex, emath::NumExt as _};
|
||||
|
||||
use wgpu::util::DeviceExt as _;
|
||||
|
||||
@@ -84,7 +85,7 @@ impl Callback {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
|
||||
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/main/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
|
||||
pub trait CallbackTrait: Send + Sync {
|
||||
fn prepare(
|
||||
&self,
|
||||
@@ -140,21 +141,16 @@ impl ScreenDescriptor {
|
||||
}
|
||||
|
||||
/// Uniform buffer used when rendering.
|
||||
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[repr(C)]
|
||||
struct UniformBuffer {
|
||||
screen_size_in_points: [f32; 2],
|
||||
dithering: u32,
|
||||
// Uniform buffers need to be at least 16 bytes in WebGL.
|
||||
// See https://github.com/gfx-rs/wgpu/issues/2072
|
||||
_padding: u32,
|
||||
}
|
||||
|
||||
impl PartialEq for UniformBuffer {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.screen_size_in_points == other.screen_size_in_points
|
||||
&& self.dithering == other.dithering
|
||||
}
|
||||
/// 1 to do manual filtering for more predictable kittest snapshot images.
|
||||
///
|
||||
/// See also <https://github.com/emilk/egui/issues/5295>.
|
||||
predictable_texture_filtering: u32,
|
||||
}
|
||||
|
||||
struct SlicedBuffer {
|
||||
@@ -175,6 +171,69 @@ pub struct Texture {
|
||||
pub options: Option<epaint::textures::TextureOptions>,
|
||||
}
|
||||
|
||||
/// Ways to configure [`Renderer`] during creation.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RendererOptions {
|
||||
/// Set the level of the multisampling anti-aliasing (MSAA).
|
||||
///
|
||||
/// Must be a power-of-two. Higher = more smooth 3D.
|
||||
///
|
||||
/// A value of `0` or `1` turns it off (default).
|
||||
///
|
||||
/// `egui` already performs anti-aliasing via "feathering"
|
||||
/// (controlled by [`egui::epaint::TessellationOptions`]),
|
||||
/// but if you are embedding 3D in egui you may want to turn on multisampling.
|
||||
pub msaa_samples: u32,
|
||||
|
||||
/// What format to use for the depth and stencil buffers,
|
||||
/// e.g. [`wgpu::TextureFormat::Depth32FloatStencil8`].
|
||||
///
|
||||
/// egui doesn't need depth/stencil, so the default value is `None` (no depth or stancil buffers).
|
||||
pub depth_stencil_format: Option<wgpu::TextureFormat>,
|
||||
|
||||
/// Controls whether to apply dithering to minimize banding artifacts.
|
||||
///
|
||||
/// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between
|
||||
/// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space".
|
||||
/// This means that only inputs from texture interpolation and vertex colors should be affected in practice.
|
||||
///
|
||||
/// Defaults to true.
|
||||
pub dithering: bool,
|
||||
|
||||
/// Perform texture filtering in software?
|
||||
///
|
||||
/// This is useful when you want predictable rendering across
|
||||
/// different hardware, e.g. for kittest snapshots.
|
||||
///
|
||||
/// Default is `false`.
|
||||
///
|
||||
/// See also <https://github.com/emilk/egui/issues/5295>.
|
||||
pub predictable_texture_filtering: bool,
|
||||
}
|
||||
|
||||
impl RendererOptions {
|
||||
/// Set options that produce the most predicatable output.
|
||||
///
|
||||
/// Useful for image snapshot tests.
|
||||
pub const PREDICTABLE: Self = Self {
|
||||
msaa_samples: 1,
|
||||
depth_stencil_format: None,
|
||||
dithering: false,
|
||||
predictable_texture_filtering: true,
|
||||
};
|
||||
}
|
||||
|
||||
impl Default for RendererOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
msaa_samples: 0,
|
||||
depth_stencil_format: None,
|
||||
dithering: true,
|
||||
predictable_texture_filtering: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renderer for a egui based GUI.
|
||||
pub struct Renderer {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
@@ -194,7 +253,7 @@ pub struct Renderer {
|
||||
next_user_texture_id: u64,
|
||||
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
|
||||
|
||||
dithering: bool,
|
||||
options: RendererOptions,
|
||||
|
||||
/// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
|
||||
///
|
||||
@@ -210,9 +269,7 @@ impl Renderer {
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
output_color_format: wgpu::TextureFormat,
|
||||
output_depth_format: Option<wgpu::TextureFormat>,
|
||||
msaa_samples: u32,
|
||||
dithering: bool,
|
||||
options: RendererOptions,
|
||||
) -> Self {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -229,8 +286,8 @@ impl Renderer {
|
||||
label: Some("egui_uniform_buffer"),
|
||||
contents: bytemuck::cast_slice(&[UniformBuffer {
|
||||
screen_size_in_points: [0.0, 0.0],
|
||||
dithering: u32::from(dithering),
|
||||
_padding: Default::default(),
|
||||
dithering: u32::from(options.dithering),
|
||||
predictable_texture_filtering: u32::from(options.predictable_texture_filtering),
|
||||
}]),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
@@ -299,13 +356,15 @@ impl Renderer {
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
|
||||
format,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
});
|
||||
let depth_stencil = options
|
||||
.depth_stencil_format
|
||||
.map(|format| wgpu::DepthStencilState {
|
||||
format,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
});
|
||||
|
||||
let pipeline = {
|
||||
profiling::scope!("create_render_pipeline");
|
||||
@@ -337,14 +396,14 @@ impl Renderer {
|
||||
depth_stencil,
|
||||
multisample: wgpu::MultisampleState {
|
||||
alpha_to_coverage_enabled: false,
|
||||
count: msaa_samples,
|
||||
count: options.msaa_samples.max(1),
|
||||
mask: !0,
|
||||
},
|
||||
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some(if output_color_format.is_srgb() {
|
||||
log::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format);
|
||||
log::warn!("Detected a linear (sRGBA aware) framebuffer {output_color_format:?}. egui prefers Rgba8Unorm or Bgra8Unorm");
|
||||
"fs_main_linear_framebuffer"
|
||||
} else {
|
||||
"fs_main_gamma_framebuffer" // this is what we prefer
|
||||
@@ -392,17 +451,13 @@ impl Renderer {
|
||||
},
|
||||
uniform_buffer,
|
||||
// Buffers on wgpu are zero initialized, so this is indeed its current state!
|
||||
previous_uniform_buffer_content: UniformBuffer {
|
||||
screen_size_in_points: [0.0, 0.0],
|
||||
dithering: 0,
|
||||
_padding: 0,
|
||||
},
|
||||
previous_uniform_buffer_content: UniformBuffer::zeroed(),
|
||||
uniform_bind_group,
|
||||
texture_bind_group_layout,
|
||||
textures: HashMap::default(),
|
||||
next_user_texture_id: 0,
|
||||
samplers: HashMap::default(),
|
||||
dithering,
|
||||
options,
|
||||
callback_resources: CallbackResources::default(),
|
||||
}
|
||||
}
|
||||
@@ -564,15 +619,6 @@ impl Renderer {
|
||||
);
|
||||
Cow::Borrowed(&image.pixels)
|
||||
}
|
||||
epaint::ImageData::Font(image) => {
|
||||
assert_eq!(
|
||||
width as usize * height as usize,
|
||||
image.pixels.len(),
|
||||
"Mismatch between texture size and texel count"
|
||||
);
|
||||
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());
|
||||
|
||||
@@ -638,9 +684,9 @@ impl Renderer {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
|
||||
})
|
||||
};
|
||||
let origin = wgpu::Origin3d::ZERO;
|
||||
@@ -699,7 +745,7 @@ impl Renderer {
|
||||
///
|
||||
/// This enables the application to reference the texture inside an image ui element.
|
||||
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
|
||||
/// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
|
||||
/// the texture format [`wgpu::TextureFormat::Rgba8Unorm`].
|
||||
pub fn register_native_texture(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -747,9 +793,9 @@ impl Renderer {
|
||||
/// This allows applications to specify individual minification/magnification filters as well as
|
||||
/// custom mipmap and tiling options.
|
||||
///
|
||||
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
|
||||
/// The texture must have the format [`wgpu::TextureFormat::Rgba8Unorm`].
|
||||
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
|
||||
#[allow(clippy::needless_pass_by_value)] // false positive
|
||||
#[expect(clippy::needless_pass_by_value)] // false positive
|
||||
pub fn register_native_texture_with_sampler_options(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -796,7 +842,7 @@ impl Renderer {
|
||||
/// [`wgpu::SamplerDescriptor`] options.
|
||||
///
|
||||
/// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler options.
|
||||
#[allow(clippy::needless_pass_by_value)] // false positive
|
||||
#[expect(clippy::needless_pass_by_value)] // false positive
|
||||
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -855,8 +901,8 @@ impl Renderer {
|
||||
|
||||
let uniform_buffer_content = UniformBuffer {
|
||||
screen_size_in_points,
|
||||
dithering: u32::from(self.dithering),
|
||||
_padding: Default::default(),
|
||||
dithering: u32::from(self.options.dithering),
|
||||
predictable_texture_filtering: u32::from(self.options.predictable_texture_filtering),
|
||||
};
|
||||
if uniform_buffer_content != self.previous_uniform_buffer_content {
|
||||
profiling::scope!("update uniforms");
|
||||
@@ -882,7 +928,7 @@ impl Renderer {
|
||||
callbacks.push(c.0.as_ref());
|
||||
} else {
|
||||
log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
|
||||
};
|
||||
}
|
||||
acc
|
||||
}
|
||||
}
|
||||
@@ -909,7 +955,11 @@ impl Renderer {
|
||||
);
|
||||
|
||||
let Some(mut index_buffer_staging) = index_buffer_staging else {
|
||||
panic!("Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity);
|
||||
panic!(
|
||||
"Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)",
|
||||
self.index_buffer.buffer.size(),
|
||||
self.index_buffer.capacity
|
||||
);
|
||||
};
|
||||
|
||||
let mut index_offset = 0;
|
||||
@@ -948,7 +998,11 @@ impl Renderer {
|
||||
);
|
||||
|
||||
let Some(mut vertex_buffer_staging) = vertex_buffer_staging else {
|
||||
panic!("Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity);
|
||||
panic!(
|
||||
"Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)",
|
||||
self.vertex_buffer.buffer.size(),
|
||||
self.vertex_buffer.capacity
|
||||
);
|
||||
};
|
||||
|
||||
let mut vertex_offset = 0;
|
||||
|
||||
@@ -48,7 +48,7 @@ impl WgpuSetup {
|
||||
pub async fn new_instance(&self) -> wgpu::Instance {
|
||||
match self {
|
||||
Self::CreateNew(create_new) => {
|
||||
#[allow(unused_mut)]
|
||||
#[allow(unused_mut, clippy::allow_attributes)]
|
||||
let mut backends = create_new.instance_descriptor.backends;
|
||||
|
||||
// Don't try WebGPU if we're not in a secure context.
|
||||
@@ -64,7 +64,7 @@ impl WgpuSetup {
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Creating wgpu instance with backends {:?}", backends);
|
||||
log::debug!("Creating wgpu instance with backends {backends:?}");
|
||||
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
|
||||
.await
|
||||
}
|
||||
@@ -126,13 +126,6 @@ pub struct WgpuSetupCreateNew {
|
||||
/// Configuration passed on device request, given an adapter
|
||||
pub device_descriptor:
|
||||
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
|
||||
|
||||
/// Option path to output a wgpu trace file.
|
||||
///
|
||||
/// This only works if this feature is enabled in `wgpu-core`.
|
||||
/// Does not work when running with WebGPU.
|
||||
/// Defaults to the path set in the `WGPU_TRACE` environment variable.
|
||||
pub trace_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl Clone for WgpuSetupCreateNew {
|
||||
@@ -142,7 +135,6 @@ impl Clone for WgpuSetupCreateNew {
|
||||
power_preference: self.power_preference,
|
||||
native_adapter_selector: self.native_adapter_selector.clone(),
|
||||
device_descriptor: self.device_descriptor.clone(),
|
||||
trace_path: self.trace_path.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +148,6 @@ impl std::fmt::Debug for WgpuSetupCreateNew {
|
||||
"native_adapter_selector",
|
||||
&self.native_adapter_selector.is_some(),
|
||||
)
|
||||
.field("trace_path", &self.trace_path)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -171,6 +162,7 @@ impl Default for WgpuSetupCreateNew {
|
||||
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
|
||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||
backend_options: wgpu::BackendOptions::from_env_or_default(),
|
||||
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||
},
|
||||
|
||||
power_preference: wgpu::PowerPreference::from_env()
|
||||
@@ -187,20 +179,15 @@ impl Default for WgpuSetupCreateNew {
|
||||
|
||||
wgpu::DeviceDescriptor {
|
||||
label: Some("egui wgpu device"),
|
||||
required_features: wgpu::Features::default(),
|
||||
required_limits: wgpu::Limits {
|
||||
// When using a depth buffer, we have to be able to create a texture
|
||||
// large enough for the entire surface, and we want to support 4k+ displays.
|
||||
max_texture_dimension_2d: 8192,
|
||||
..base_limits
|
||||
},
|
||||
memory_hints: wgpu::MemoryHints::default(),
|
||||
..Default::default()
|
||||
}
|
||||
}),
|
||||
|
||||
trace_path: std::env::var("WGPU_TRACE")
|
||||
.ok()
|
||||
.map(std::path::PathBuf::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
|
||||
use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration};
|
||||
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
|
||||
use crate::{
|
||||
RendererOptions,
|
||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||
};
|
||||
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
||||
use std::{num::NonZeroU32, sync::Arc};
|
||||
|
||||
@@ -11,6 +14,7 @@ struct SurfaceState {
|
||||
alpha_mode: wgpu::CompositeAlphaMode,
|
||||
width: u32,
|
||||
height: u32,
|
||||
resizing: bool,
|
||||
}
|
||||
|
||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||
@@ -21,10 +25,8 @@ struct SurfaceState {
|
||||
pub struct Painter {
|
||||
context: Context,
|
||||
configuration: WgpuConfiguration,
|
||||
msaa_samples: u32,
|
||||
options: RendererOptions,
|
||||
support_transparent_backbuffer: bool,
|
||||
dithering: bool,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
screen_capture_state: Option<CaptureState>,
|
||||
|
||||
instance: wgpu::Instance,
|
||||
@@ -54,10 +56,8 @@ impl Painter {
|
||||
pub async fn new(
|
||||
context: Context,
|
||||
configuration: WgpuConfiguration,
|
||||
msaa_samples: u32,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
support_transparent_backbuffer: bool,
|
||||
dithering: bool,
|
||||
options: RendererOptions,
|
||||
) -> Self {
|
||||
let (capture_tx, capture_rx) = capture_channel();
|
||||
let instance = configuration.wgpu_setup.new_instance().await;
|
||||
@@ -65,10 +65,8 @@ impl Painter {
|
||||
Self {
|
||||
context,
|
||||
configuration,
|
||||
msaa_samples,
|
||||
options,
|
||||
support_transparent_backbuffer,
|
||||
dithering,
|
||||
depth_format,
|
||||
screen_capture_state: None,
|
||||
|
||||
instance,
|
||||
@@ -204,9 +202,7 @@ impl Painter {
|
||||
&self.configuration,
|
||||
&self.instance,
|
||||
Some(&surface),
|
||||
self.depth_format,
|
||||
self.msaa_samples,
|
||||
self.dithering,
|
||||
self.options,
|
||||
)
|
||||
.await?;
|
||||
self.render_state.get_or_insert(render_state)
|
||||
@@ -220,7 +216,9 @@ impl Painter {
|
||||
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
|
||||
wgpu::CompositeAlphaMode::PostMultiplied
|
||||
} else {
|
||||
log::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
|
||||
log::warn!(
|
||||
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
|
||||
);
|
||||
wgpu::CompositeAlphaMode::Auto
|
||||
}
|
||||
} else {
|
||||
@@ -233,6 +231,7 @@ impl Painter {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
alpha_mode,
|
||||
resizing: false,
|
||||
},
|
||||
);
|
||||
let Some(width) = NonZeroU32::new(size.width) else {
|
||||
@@ -277,7 +276,7 @@ impl Painter {
|
||||
|
||||
Self::configure_surface(surface_state, render_state, &self.configuration);
|
||||
|
||||
if let Some(depth_format) = self.depth_format {
|
||||
if let Some(depth_format) = self.options.depth_stencil_format {
|
||||
self.depth_texture_view.insert(
|
||||
viewport_id,
|
||||
render_state
|
||||
@@ -290,7 +289,7 @@ impl Painter {
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: self.msaa_samples,
|
||||
sample_count: self.options.msaa_samples.max(1),
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
@@ -301,7 +300,7 @@ impl Painter {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(render_state) = (self.msaa_samples > 1)
|
||||
if let Some(render_state) = (self.options.msaa_samples > 1)
|
||||
.then_some(self.render_state.as_ref())
|
||||
.flatten()
|
||||
{
|
||||
@@ -318,7 +317,7 @@ impl Painter {
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: self.msaa_samples,
|
||||
sample_count: self.options.msaa_samples.max(1),
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: texture_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
@@ -326,7 +325,60 @@ impl Painter {
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles changes of the resizing state.
|
||||
///
|
||||
/// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
|
||||
/// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
|
||||
pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let Some(state) = self.surfaces.get_mut(&viewport_id) else {
|
||||
return;
|
||||
};
|
||||
if state.resizing == resizing {
|
||||
if resizing {
|
||||
log::debug!(
|
||||
"Painter::on_window_resize_state_change() redundant call while resizing"
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Painter::on_window_resize_state_change() redundant call after resizing"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Resizing is a bit tricky on macOS.
|
||||
// It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
|
||||
// flag to avoid jittering during the resize. Even though resize jittering on macOS
|
||||
// is common across rendering backends, the solution for wgpu/metal is known.
|
||||
//
|
||||
// See https://github.com/emilk/egui/issues/903
|
||||
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
|
||||
{
|
||||
// SAFETY: The cast is checked with if condition. If the used backend is not metal
|
||||
// it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping.
|
||||
// This is how wgpu currently exposes this backend-specific flag.
|
||||
unsafe {
|
||||
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
|
||||
let raw =
|
||||
std::ptr::from_ref::<wgpu::hal::metal::Surface>(&*hal_surface).cast_mut();
|
||||
|
||||
(*raw).present_with_transaction = resizing;
|
||||
|
||||
Self::configure_surface(
|
||||
state,
|
||||
self.render_state.as_ref().unwrap(),
|
||||
&self.configuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.resizing = resizing;
|
||||
}
|
||||
|
||||
pub fn on_window_resized(
|
||||
@@ -344,7 +396,9 @@ impl Painter {
|
||||
height_in_pixels,
|
||||
);
|
||||
} else {
|
||||
log::warn!("Ignoring window resize notification with no surface created via Painter::set_window()");
|
||||
log::warn!(
|
||||
"Ignoring window resize notification with no surface created via Painter::set_window()"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,7 +500,7 @@ impl Painter {
|
||||
};
|
||||
let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let (view, resolve_target) = (self.msaa_samples > 1)
|
||||
let (view, resolve_target) = (self.options.msaa_samples > 1)
|
||||
.then_some(self.msaa_texture_view.get(&viewport_id))
|
||||
.flatten()
|
||||
.map_or((&target_view, None), |texture_view| {
|
||||
@@ -467,6 +521,7 @@ impl Painter {
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
@@ -493,14 +548,12 @@ impl Painter {
|
||||
&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,
|
||||
));
|
||||
}
|
||||
if capture && let Some(capture_state) = &mut self.screen_capture_state {
|
||||
capture_buffer = Some(capture_state.copy_textures(
|
||||
&render_state.device,
|
||||
&output_frame,
|
||||
&mut encoder,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,16 +583,16 @@ impl Painter {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
if let Some(capture_buffer) = capture_buffer
|
||||
&& 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,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -575,7 +628,7 @@ impl Painter {
|
||||
.retain(|id, _| active_viewports.contains(id));
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
|
||||
#[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
|
||||
pub fn destroy(&mut self) {
|
||||
// TODO(emilk): something here?
|
||||
}
|
||||
|
||||
@@ -5,6 +5,49 @@ 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.33.2 - 2025-11-13
|
||||
* Don't enable `arboard` on iOS [#7663](https://github.com/emilk/egui/pull/7663) by [@irh](https://github.com/irh)
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09
|
||||
### ⭐ Added
|
||||
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)
|
||||
* Add support for the safe area on iOS [#7578](https://github.com/emilk/egui/pull/7578) by [@irh](https://github.com/irh)
|
||||
|
||||
### 🔧 Changed
|
||||
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
|
||||
* Create `egui_wgpu::RendererOptions` [#7601](https://github.com/emilk/egui/pull/7601) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Fix build error in egui-winit with profiling enabled [#7557](https://github.com/emilk/egui/pull/7557) by [@torokati44](https://github.com/torokati44)
|
||||
* Properly end winit event loop [#7565](https://github.com/emilk/egui/pull/7565) by [@tye-exe](https://github.com/tye-exe)
|
||||
* Fix eframe window not being focused on mac on startup [#7593](https://github.com/emilk/egui/pull/7593) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.32.3 - 2025-09-12
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.2 - 2025-09-04
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.32.1 - 2025-08-15
|
||||
* Update to winit 0.30.12 [#7420](https://github.com/emilk/egui/pull/7420) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.32.0 - 2025-07-10
|
||||
* Mark all keys as released if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
|
||||
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
|
||||
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
|
||||
* Support for back-button on Android [#7073](https://github.com/emilk/egui/pull/7073) by [@ardocrat](https://github.com/ardocrat)
|
||||
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
|
||||
|
||||
|
||||
## 0.31.1 - 2025-03-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.31.0 - 2025-02-04
|
||||
* Re-enable IME support on Linux [#5198](https://github.com/emilk/egui/pull/5198) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
* Update to winit 0.30.7 [#5516](https://github.com/emilk/egui/pull/5516) by [@emilk](https://github.com/emilk)
|
||||
|
||||
@@ -5,10 +5,10 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Bindings for using egui with winit"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
|
||||
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["winit", "egui", "gui", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
@@ -24,7 +24,7 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
default = ["clipboard", "links", "wayland", "winit/default", "x11"]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["dep:accesskit_winit", "egui/accesskit"]
|
||||
accesskit = ["dep:accesskit_winit"]
|
||||
|
||||
# Allow crates to choose an android-activity backend via Winit
|
||||
# - It's important that most applications should not have to depend on android-activity directly, and can
|
||||
@@ -55,9 +55,8 @@ wayland = ["winit/wayland", "bytemuck"]
|
||||
x11 = ["winit/x11", "bytemuck"]
|
||||
|
||||
[dependencies]
|
||||
egui = { workspace = true, default-features = false, features = ["log"] }
|
||||
egui = { workspace = true, default-features = false }
|
||||
|
||||
ahash.workspace = true
|
||||
log.workspace = true
|
||||
profiling.workspace = true
|
||||
raw-window-handle.workspace = true
|
||||
@@ -67,7 +66,7 @@ winit = { workspace = true, default-features = false }
|
||||
#! ### Optional dependencies
|
||||
|
||||
# feature accesskit
|
||||
accesskit_winit = { version = "0.23", optional = true }
|
||||
accesskit_winit = { workspace = true, optional = true }
|
||||
|
||||
bytemuck = { workspace = true, optional = true }
|
||||
|
||||
@@ -75,17 +74,30 @@ bytemuck = { workspace = true, optional = true }
|
||||
document-features = { workspace = true, optional = true }
|
||||
|
||||
serde = { workspace = true, optional = true }
|
||||
webbrowser = { version = "1.0.0", optional = true }
|
||||
webbrowser = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc2.workspace = true
|
||||
objc2-foundation = { workspace = true, features = ["std", "NSThread"] }
|
||||
objc2-ui-kit = { workspace = true, features = [
|
||||
"std",
|
||||
"UIApplication",
|
||||
"UIGeometry",
|
||||
"UIResponder",
|
||||
"UIScene",
|
||||
"UISceneDefinitions",
|
||||
"UIView",
|
||||
"UIWindow",
|
||||
"UIWindowScene",
|
||||
] }
|
||||
|
||||
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
|
||||
smithay-clipboard = { version = "0.7.2", optional = true }
|
||||
smithay-clipboard = { workspace = true, optional = true }
|
||||
|
||||
# The wayland-cursor normally selected doesn't properly enable all the features it uses
|
||||
# and thus doesn't compile as it is used in egui-winit. This is fixed upstream, so force
|
||||
# a slightly newer version. Remove this when winit upgrades past this version.
|
||||
wayland-cursor = { version = "0.31.1", default-features = false, optional = true }
|
||||
wayland-cursor = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
arboard = { version = "3.3", optional = true, default-features = false, features = [
|
||||
"image-data",
|
||||
] }
|
||||
arboard = { workspace = true, optional = true, features = ["image-data"] }
|
||||
|
||||
@@ -5,7 +5,10 @@ use raw_window_handle::RawDisplayHandle;
|
||||
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
|
||||
/// then a fallback clipboard that just works within the same app is used instead.
|
||||
pub struct Clipboard {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
arboard: Option<arboard::Clipboard>,
|
||||
|
||||
#[cfg(all(
|
||||
@@ -28,7 +31,10 @@ impl Clipboard {
|
||||
/// Construct a new instance
|
||||
pub fn new(_raw_display_handle: Option<RawDisplayHandle>) -> Self {
|
||||
Self {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
arboard: init_arboard(),
|
||||
|
||||
#[cfg(all(
|
||||
@@ -68,7 +74,10 @@ impl Clipboard {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
return match clipboard.get_text() {
|
||||
Ok(text) => Some(text),
|
||||
@@ -98,7 +107,10 @@ impl Clipboard {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
if let Err(err) = clipboard.set_text(text) {
|
||||
log::error!("arboard copy/cut error: {err}");
|
||||
@@ -110,7 +122,10 @@ impl Clipboard {
|
||||
}
|
||||
|
||||
pub fn set_image(&mut self, image: &egui::ColorImage) {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
if let Err(err) = clipboard.set_image(arboard::ImageData {
|
||||
width: image.width(),
|
||||
@@ -123,12 +138,17 @@ impl Clipboard {
|
||||
return;
|
||||
}
|
||||
|
||||
log::error!("Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it.");
|
||||
log::error!(
|
||||
"Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it."
|
||||
);
|
||||
_ = image;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
fn init_arboard() -> Option<arboard::Clipboard> {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -161,7 +181,7 @@ fn init_smithay_clipboard(
|
||||
|
||||
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
|
||||
log::trace!("Initializing smithay clipboard…");
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
|
||||
} else {
|
||||
#[cfg(feature = "wayland")]
|
||||
|
||||
@@ -18,11 +18,11 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
|
||||
pub use winit;
|
||||
|
||||
pub mod clipboard;
|
||||
mod safe_area;
|
||||
mod window_settings;
|
||||
|
||||
pub use window_settings::WindowSettings;
|
||||
|
||||
use ahash::HashSet;
|
||||
use raw_window_handle::HasDisplayHandle;
|
||||
|
||||
use winit::{
|
||||
@@ -166,12 +166,14 @@ impl State {
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn init_accesskit<T: From<accesskit_winit::Event> + Send>(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window: &Window,
|
||||
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy(
|
||||
event_loop,
|
||||
window,
|
||||
event_loop_proxy,
|
||||
));
|
||||
@@ -273,6 +275,21 @@ impl State {
|
||||
}
|
||||
|
||||
use winit::event::WindowEvent;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
match &event {
|
||||
WindowEvent::Resized(_)
|
||||
| WindowEvent::ScaleFactorChanged { .. }
|
||||
| WindowEvent::Focused(true)
|
||||
| WindowEvent::Occluded(false) => {
|
||||
// Once winit v0.31 has been released this can be reworked to get the safe area from
|
||||
// `Window::safe_area`, and updated from a new event which is being discussed in
|
||||
// https://github.com/rust-windowing/winit/issues/3911.
|
||||
self.egui_input_mut().safe_area_insets = Some(safe_area::get_safe_area_insets());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match event {
|
||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
let native_pixels_per_point = *scale_factor as f32;
|
||||
@@ -295,8 +312,8 @@ impl State {
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
self.on_mouse_wheel(window, *delta);
|
||||
WindowEvent::MouseWheel { delta, phase, .. } => {
|
||||
self.on_mouse_wheel(window, *delta, *phase);
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
@@ -371,7 +388,7 @@ impl State {
|
||||
winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => {
|
||||
self.ime_event_disable();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
@@ -406,10 +423,19 @@ impl State {
|
||||
}
|
||||
}
|
||||
WindowEvent::Focused(focused) => {
|
||||
self.egui_input.focused = *focused;
|
||||
let focused = if cfg!(target_os = "macos") {
|
||||
// TODO(emilk): remove this work-around once we update winit
|
||||
// https://github.com/rust-windowing/winit/issues/4371
|
||||
// https://github.com/emilk/egui/issues/7588
|
||||
window.has_focus()
|
||||
} else {
|
||||
*focused
|
||||
};
|
||||
|
||||
self.egui_input.focused = focused;
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::WindowFocused(*focused));
|
||||
.push(egui::Event::WindowFocused(focused));
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
@@ -490,9 +516,7 @@ impl State {
|
||||
// Things we completely ignore:
|
||||
WindowEvent::ActivationTokenDone { .. }
|
||||
| WindowEvent::AxisMotion { .. }
|
||||
| WindowEvent::DoubleTapGesture { .. }
|
||||
| WindowEvent::RotationGesture { .. }
|
||||
| WindowEvent::PanGesture { .. } => EventResponse {
|
||||
| WindowEvent::DoubleTapGesture { .. } => EventResponse {
|
||||
repaint: false,
|
||||
consumed: false,
|
||||
},
|
||||
@@ -507,6 +531,34 @@ impl State {
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::RotationGesture { delta, .. } => {
|
||||
// Positive delta values indicate counterclockwise rotation
|
||||
// Negative delta values indicate clockwise rotation
|
||||
// This is opposite of egui's sign convention for angles
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Rotate(-delta.to_radians()));
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PanGesture { delta, phase, .. } => {
|
||||
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||
|
||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
|
||||
phase: to_egui_touch_phase(*phase),
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,41 +600,41 @@ impl State {
|
||||
state: winit::event::ElementState,
|
||||
button: winit::event::MouseButton,
|
||||
) {
|
||||
if let Some(pos) = self.pointer_pos_in_points {
|
||||
if let Some(button) = translate_mouse_button(button) {
|
||||
let pressed = state == winit::event::ElementState::Pressed;
|
||||
if let Some(pos) = self.pointer_pos_in_points
|
||||
&& let Some(button) = translate_mouse_button(button)
|
||||
{
|
||||
let pressed = state == winit::event::ElementState::Pressed;
|
||||
|
||||
self.egui_input.events.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button,
|
||||
pressed,
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
self.egui_input.events.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button,
|
||||
pressed,
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
|
||||
if self.simulate_touch_screen {
|
||||
if pressed {
|
||||
self.any_pointer_button_down = true;
|
||||
if self.simulate_touch_screen {
|
||||
if pressed {
|
||||
self.any_pointer_button_down = true;
|
||||
|
||||
self.egui_input.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(0),
|
||||
id: egui::TouchId(0),
|
||||
phase: egui::TouchPhase::Start,
|
||||
pos,
|
||||
force: None,
|
||||
});
|
||||
} else {
|
||||
self.any_pointer_button_down = false;
|
||||
self.egui_input.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(0),
|
||||
id: egui::TouchId(0),
|
||||
phase: egui::TouchPhase::Start,
|
||||
pos,
|
||||
force: None,
|
||||
});
|
||||
} else {
|
||||
self.any_pointer_button_down = false;
|
||||
|
||||
self.egui_input.events.push(egui::Event::PointerGone);
|
||||
self.egui_input.events.push(egui::Event::PointerGone);
|
||||
|
||||
self.egui_input.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(0),
|
||||
id: egui::TouchId(0),
|
||||
phase: egui::TouchPhase::End,
|
||||
pos,
|
||||
force: None,
|
||||
});
|
||||
};
|
||||
self.egui_input.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(0),
|
||||
id: egui::TouchId(0),
|
||||
phase: egui::TouchPhase::End,
|
||||
pos,
|
||||
force: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -629,12 +681,7 @@ impl State {
|
||||
self.egui_input.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
|
||||
id: egui::TouchId::from(touch.id),
|
||||
phase: match touch.phase {
|
||||
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
|
||||
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
|
||||
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
|
||||
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
|
||||
},
|
||||
phase: to_egui_touch_phase(touch.phase),
|
||||
pos: egui::pos2(
|
||||
touch.location.x as f32 / pixels_per_point,
|
||||
touch.location.y as f32 / pixels_per_point,
|
||||
@@ -687,7 +734,12 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_wheel(&mut self, window: &Window, delta: winit::event::MouseScrollDelta) {
|
||||
fn on_mouse_wheel(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
delta: winit::event::MouseScrollDelta,
|
||||
phase: winit::event::TouchPhase,
|
||||
) {
|
||||
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||
|
||||
{
|
||||
@@ -703,10 +755,12 @@ impl State {
|
||||
egui::vec2(x as f32, y as f32) / pixels_per_point,
|
||||
),
|
||||
};
|
||||
let phase = to_egui_touch_phase(phase);
|
||||
let modifiers = self.egui_input.modifiers;
|
||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||
unit,
|
||||
delta,
|
||||
phase,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
@@ -727,7 +781,7 @@ impl State {
|
||||
// When telling users "Press Ctrl-F to find", this is where we should
|
||||
// look for the "F" key, because they may have a dvorak layout on
|
||||
// a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position.
|
||||
logical_key,
|
||||
logical_key: winit_logical_key,
|
||||
|
||||
text,
|
||||
|
||||
@@ -746,7 +800,7 @@ impl State {
|
||||
None
|
||||
};
|
||||
|
||||
let logical_key = key_from_winit_key(logical_key);
|
||||
let logical_key = key_from_winit_key(winit_logical_key);
|
||||
|
||||
// Helpful logging to enable when adding new key support
|
||||
log::trace!(
|
||||
@@ -789,7 +843,11 @@ impl State {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(text) = &text {
|
||||
if let Some(text) = text
|
||||
.as_ref()
|
||||
.map(|t| t.as_str())
|
||||
.or_else(|| winit_logical_key.to_text())
|
||||
{
|
||||
// Make sure there is text, and that it is not control characters
|
||||
// (e.g. delete is sent as "\u{f728}" on macOS).
|
||||
if !text.is_empty() && text.chars().all(is_printable_char) {
|
||||
@@ -803,7 +861,7 @@ impl State {
|
||||
if pressed && !is_cmd {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Text(text.to_string()));
|
||||
.push(egui::Event::Text(text.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -822,18 +880,14 @@ impl State {
|
||||
window: &Window,
|
||||
platform_output: egui::PlatformOutput,
|
||||
) {
|
||||
#![allow(deprecated)]
|
||||
profiling::function_scope!();
|
||||
|
||||
let egui::PlatformOutput {
|
||||
commands,
|
||||
cursor_icon,
|
||||
open_url,
|
||||
copied_text,
|
||||
events: _, // handled elsewhere
|
||||
mutable_text_under_cursor: _, // only used in eframe web
|
||||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
num_completed_passes: _, // `egui::Context::run` handles this
|
||||
request_discard_reasons: _, // `egui::Context::run` handles this
|
||||
@@ -855,14 +909,6 @@ impl State {
|
||||
|
||||
self.set_cursor_icon(window, cursor_icon);
|
||||
|
||||
if let Some(open_url) = open_url {
|
||||
open_url_in_browser(&open_url.url);
|
||||
}
|
||||
|
||||
if !copied_text.is_empty() {
|
||||
self.clipboard.set_text(copied_text);
|
||||
}
|
||||
|
||||
let allow_ime = ime.is_some();
|
||||
if self.allow_ime != allow_ime {
|
||||
self.allow_ime = allow_ime;
|
||||
@@ -894,12 +940,15 @@ impl State {
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if let Some(accesskit) = self.accesskit.as_mut() {
|
||||
if let Some(update) = accesskit_update {
|
||||
profiling::scope!("accesskit");
|
||||
accesskit.update_if_active(|| update);
|
||||
}
|
||||
if let Some(accesskit) = self.accesskit.as_mut()
|
||||
&& let Some(update) = accesskit_update
|
||||
{
|
||||
profiling::scope!("accesskit");
|
||||
accesskit.update_if_active(|| update);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "accesskit"))]
|
||||
let _ = accesskit_update;
|
||||
}
|
||||
|
||||
fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
|
||||
@@ -926,6 +975,15 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_egui_touch_phase(phase: winit::event::TouchPhase) -> egui::TouchPhase {
|
||||
match phase {
|
||||
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
|
||||
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
|
||||
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
|
||||
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
|
||||
match theme {
|
||||
winit::window::Theme::Dark => Theme::Dark,
|
||||
@@ -1020,7 +1078,7 @@ pub fn update_viewport_info(
|
||||
fn open_url_in_browser(_url: &str) {
|
||||
#[cfg(feature = "webbrowser")]
|
||||
if let Err(err) = webbrowser::open(_url) {
|
||||
log::warn!("Failed to open url: {}", err);
|
||||
log::warn!("Failed to open url: {err}");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "webbrowser"))]
|
||||
@@ -1138,6 +1196,8 @@ fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option<egui::Key>
|
||||
NamedKey::F33 => Key::F33,
|
||||
NamedKey::F34 => Key::F34,
|
||||
NamedKey::F35 => Key::F35,
|
||||
|
||||
NamedKey::BrowserBack => Key::BrowserBack,
|
||||
_ => {
|
||||
log::trace!("Unknown key: {named_key:?}");
|
||||
return None;
|
||||
@@ -1326,7 +1386,7 @@ pub fn process_viewport_commands(
|
||||
info: &mut ViewportInfo,
|
||||
commands: impl IntoIterator<Item = ViewportCommand>,
|
||||
window: &Window,
|
||||
actions_requested: &mut HashSet<ActionRequested>,
|
||||
actions_requested: &mut Vec<ActionRequested>,
|
||||
) {
|
||||
for command in commands {
|
||||
process_viewport_command(egui_ctx, window, command, info, actions_requested);
|
||||
@@ -1338,9 +1398,9 @@ fn process_viewport_command(
|
||||
window: &Window,
|
||||
command: ViewportCommand,
|
||||
info: &mut ViewportInfo,
|
||||
actions_requested: &mut HashSet<ActionRequested>,
|
||||
actions_requested: &mut Vec<ActionRequested>,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
profiling::function_scope!(&format!("{command:?}"));
|
||||
|
||||
use winit::window::ResizeDirection;
|
||||
|
||||
@@ -1357,10 +1417,10 @@ fn process_viewport_command(
|
||||
}
|
||||
ViewportCommand::StartDrag => {
|
||||
// If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed!
|
||||
if window.has_focus() {
|
||||
if let Err(err) = window.drag_window() {
|
||||
log::warn!("{command:?}: {err}");
|
||||
}
|
||||
if window.has_focus()
|
||||
&& let Err(err) = window.drag_window()
|
||||
{
|
||||
log::warn!("{command:?}: {err}");
|
||||
}
|
||||
}
|
||||
ViewportCommand::InnerSize(size) => {
|
||||
@@ -1531,16 +1591,16 @@ fn process_viewport_command(
|
||||
}
|
||||
}
|
||||
ViewportCommand::Screenshot(user_data) => {
|
||||
actions_requested.insert(ActionRequested::Screenshot(user_data));
|
||||
actions_requested.push(ActionRequested::Screenshot(user_data));
|
||||
}
|
||||
ViewportCommand::RequestCut => {
|
||||
actions_requested.insert(ActionRequested::Cut);
|
||||
actions_requested.push(ActionRequested::Cut);
|
||||
}
|
||||
ViewportCommand::RequestCopy => {
|
||||
actions_requested.insert(ActionRequested::Copy);
|
||||
actions_requested.push(ActionRequested::Copy);
|
||||
}
|
||||
ViewportCommand::RequestPaste => {
|
||||
actions_requested.insert(ActionRequested::Paste);
|
||||
actions_requested.push(ActionRequested::Paste);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1558,8 +1618,7 @@ pub fn create_window(
|
||||
) -> Result<Window, winit::error::OsError> {
|
||||
profiling::function_scope!();
|
||||
|
||||
let window_attributes =
|
||||
create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone());
|
||||
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
|
||||
let window = event_loop.create_window(window_attributes)?;
|
||||
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
|
||||
Ok(window)
|
||||
@@ -1567,28 +1626,10 @@ pub fn create_window(
|
||||
|
||||
pub fn create_winit_window_attributes(
|
||||
egui_ctx: &egui::Context,
|
||||
event_loop: &ActiveEventLoop,
|
||||
viewport_builder: ViewportBuilder,
|
||||
) -> winit::window::WindowAttributes {
|
||||
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.
|
||||
// We don't know what monitor the window will appear on though, but
|
||||
// we'll try to fix that after the window is created in the call to `apply_viewport_builder_to_window`.
|
||||
let native_pixels_per_point = event_loop
|
||||
.primary_monitor()
|
||||
.or_else(|| event_loop.available_monitors().next())
|
||||
.map_or_else(
|
||||
|| {
|
||||
log::debug!("Failed to find a monitor - assuming native_pixels_per_point of 1.0");
|
||||
1.0
|
||||
},
|
||||
|m| m.scale_factor() as f32,
|
||||
);
|
||||
let zoom_factor = egui_ctx.zoom_factor();
|
||||
let pixels_per_point = zoom_factor * native_pixels_per_point;
|
||||
|
||||
let ViewportBuilder {
|
||||
title,
|
||||
position,
|
||||
@@ -1610,9 +1651,11 @@ pub fn create_winit_window_attributes(
|
||||
|
||||
// macOS:
|
||||
fullsize_content_view: _fullsize_content_view,
|
||||
movable_by_window_background: _movable_by_window_background,
|
||||
title_shown: _title_shown,
|
||||
titlebar_buttons_shown: _titlebar_buttons_shown,
|
||||
titlebar_shown: _titlebar_shown,
|
||||
has_shadow: _has_shadow,
|
||||
|
||||
// Windows:
|
||||
drag_and_drop: _drag_and_drop,
|
||||
@@ -1662,40 +1705,46 @@ pub fn create_winit_window_attributes(
|
||||
})
|
||||
.with_active(active.unwrap_or(true));
|
||||
|
||||
// Here and below: we create `LogicalSize` / `LogicalPosition` taking
|
||||
// zoom factor into account. We don't have a good way to get physical size here,
|
||||
// and trying to do it anyway leads to weird bugs on Wayland, see:
|
||||
// https://github.com/emilk/egui/issues/7095#issuecomment-2920545377
|
||||
// https://github.com/rust-windowing/winit/issues/4266
|
||||
#[expect(
|
||||
clippy::disallowed_types,
|
||||
reason = "zoom factor is manually accounted for"
|
||||
)]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if let Some(size) = inner_size {
|
||||
window_attributes = window_attributes.with_inner_size(PhysicalSize::new(
|
||||
pixels_per_point * size.x,
|
||||
pixels_per_point * size.y,
|
||||
));
|
||||
}
|
||||
{
|
||||
use winit::dpi::{LogicalPosition, LogicalSize};
|
||||
let zoom_factor = egui_ctx.zoom_factor();
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if let Some(size) = min_inner_size {
|
||||
window_attributes = window_attributes.with_min_inner_size(PhysicalSize::new(
|
||||
pixels_per_point * size.x,
|
||||
pixels_per_point * size.y,
|
||||
));
|
||||
}
|
||||
if let Some(size) = inner_size {
|
||||
window_attributes = window_attributes
|
||||
.with_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if let Some(size) = max_inner_size {
|
||||
window_attributes = window_attributes.with_max_inner_size(PhysicalSize::new(
|
||||
pixels_per_point * size.x,
|
||||
pixels_per_point * size.y,
|
||||
));
|
||||
}
|
||||
if let Some(size) = min_inner_size {
|
||||
window_attributes = window_attributes
|
||||
.with_min_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if let Some(pos) = position {
|
||||
window_attributes = window_attributes.with_position(PhysicalPosition::new(
|
||||
pixels_per_point * pos.x,
|
||||
pixels_per_point * pos.y,
|
||||
));
|
||||
if let Some(size) = max_inner_size {
|
||||
window_attributes = window_attributes
|
||||
.with_max_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
||||
}
|
||||
|
||||
if let Some(pos) = position {
|
||||
window_attributes = window_attributes.with_position(LogicalPosition::new(
|
||||
zoom_factor * pos.x,
|
||||
zoom_factor * pos.y,
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
// Unused:
|
||||
_ = egui_ctx;
|
||||
_ = pixels_per_point;
|
||||
_ = position;
|
||||
_ = inner_size;
|
||||
@@ -1756,7 +1805,9 @@ pub fn create_winit_window_attributes(
|
||||
.with_title_hidden(!_title_shown.unwrap_or(true))
|
||||
.with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
|
||||
.with_titlebar_transparent(!_titlebar_shown.unwrap_or(true))
|
||||
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false));
|
||||
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false))
|
||||
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false))
|
||||
.with_has_shadow(_has_shadow.unwrap_or(true));
|
||||
}
|
||||
|
||||
window_attributes
|
||||
@@ -1783,10 +1834,10 @@ pub fn apply_viewport_builder_to_window(
|
||||
window: &Window,
|
||||
builder: &ViewportBuilder,
|
||||
) {
|
||||
if let Some(mouse_passthrough) = builder.mouse_passthrough {
|
||||
if let Err(err) = window.set_cursor_hittest(!mouse_passthrough) {
|
||||
log::warn!("set_cursor_hittest failed: {err}");
|
||||
}
|
||||
if let Some(mouse_passthrough) = builder.mouse_passthrough
|
||||
&& let Err(err) = window.set_cursor_hittest(!mouse_passthrough)
|
||||
{
|
||||
log::warn!("set_cursor_hittest failed: {err}");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1797,16 +1848,15 @@ pub fn apply_viewport_builder_to_window(
|
||||
|
||||
let pixels_per_point = pixels_per_point(egui_ctx, window);
|
||||
|
||||
if let Some(size) = builder.inner_size {
|
||||
if window
|
||||
if let Some(size) = builder.inner_size
|
||||
&& window
|
||||
.request_inner_size(PhysicalSize::new(
|
||||
pixels_per_point * size.x,
|
||||
pixels_per_point * size.y,
|
||||
))
|
||||
.is_some()
|
||||
{
|
||||
log::debug!("Failed to set window size");
|
||||
}
|
||||
{
|
||||
log::debug!("Failed to set window size");
|
||||
}
|
||||
if let Some(size) = builder.min_inner_size {
|
||||
window.set_min_inner_size(Some(PhysicalSize::new(
|
||||
@@ -1838,8 +1888,8 @@ pub fn short_device_event_description(event: &winit::event::DeviceEvent) -> &'st
|
||||
use winit::event::DeviceEvent;
|
||||
|
||||
match event {
|
||||
DeviceEvent::Added { .. } => "DeviceEvent::Added",
|
||||
DeviceEvent::Removed { .. } => "DeviceEvent::Removed",
|
||||
DeviceEvent::Added => "DeviceEvent::Added",
|
||||
DeviceEvent::Removed => "DeviceEvent::Removed",
|
||||
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
|
||||
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
|
||||
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
|
||||
@@ -1857,11 +1907,11 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
|
||||
WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone",
|
||||
WindowEvent::Resized { .. } => "WindowEvent::Resized",
|
||||
WindowEvent::Moved { .. } => "WindowEvent::Moved",
|
||||
WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested",
|
||||
WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed",
|
||||
WindowEvent::CloseRequested => "WindowEvent::CloseRequested",
|
||||
WindowEvent::Destroyed => "WindowEvent::Destroyed",
|
||||
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
|
||||
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
|
||||
WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled",
|
||||
WindowEvent::HoveredFileCancelled => "WindowEvent::HoveredFileCancelled",
|
||||
WindowEvent::Focused { .. } => "WindowEvent::Focused",
|
||||
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
|
||||
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
|
||||
@@ -1872,7 +1922,7 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
|
||||
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
|
||||
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
|
||||
WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture",
|
||||
WindowEvent::RedrawRequested { .. } => "WindowEvent::RedrawRequested",
|
||||
WindowEvent::RedrawRequested => "WindowEvent::RedrawRequested",
|
||||
WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture",
|
||||
WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture",
|
||||
WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",
|
||||
|
||||
56
crates/egui-winit/src/safe_area.rs
Normal file
56
crates/egui-winit/src/safe_area.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
#[cfg(target_os = "ios")]
|
||||
pub use ios::get_safe_area_insets;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
mod ios {
|
||||
use egui::{SafeAreaInsets, epaint::MarginF32};
|
||||
use objc2::{ClassType, rc::Retained};
|
||||
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
|
||||
use objc2_ui_kit::{UIApplication, UISceneActivationState, UIWindowScene};
|
||||
|
||||
/// Gets the ios safe area insets.
|
||||
///
|
||||
/// A safe area defines the area within a view that isn’t covered by a navigation bar, tab bar,
|
||||
/// toolbar, or other views a window might provide. Safe areas are essential for avoiding a
|
||||
/// device’s interactive and display features, like Dynamic Island on iPhone or the camera
|
||||
/// housing on some Mac models.
|
||||
///
|
||||
/// Once winit v0.31 has been released this can be removed in favor of
|
||||
/// `winit::Window::safe_area`.
|
||||
pub fn get_safe_area_insets() -> SafeAreaInsets {
|
||||
let Some(main_thread_marker) = MainThreadMarker::new() else {
|
||||
log::error!("Getting safe area insets needs to be performed on the main thread");
|
||||
return SafeAreaInsets::default();
|
||||
};
|
||||
|
||||
let app = UIApplication::sharedApplication(main_thread_marker);
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
// Look for the first window scene that's in the foreground
|
||||
for scene in app.connectedScenes() {
|
||||
if scene.isKindOfClass(UIWindowScene::class())
|
||||
&& matches!(
|
||||
scene.activationState(),
|
||||
UISceneActivationState::ForegroundActive
|
||||
| UISceneActivationState::ForegroundInactive
|
||||
)
|
||||
{
|
||||
// Safe to cast, the class kind was checked above
|
||||
let window_scene = Retained::cast::<UIWindowScene>(scene.clone());
|
||||
if let Some(window) = window_scene.keyWindow() {
|
||||
let insets = window.safeAreaInsets();
|
||||
return SafeAreaInsets(MarginF32 {
|
||||
top: insets.top as f32,
|
||||
left: insets.left as f32,
|
||||
right: insets.right as f32,
|
||||
bottom: insets.bottom as f32,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SafeAreaInsets::default()
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,6 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
[features]
|
||||
default = ["default_fonts"]
|
||||
|
||||
## Exposes detailed accessibility implementation required by platform
|
||||
## accessibility APIs. Also requires support in the egui integration.
|
||||
accesskit = ["dep:accesskit"]
|
||||
|
||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`.
|
||||
bytemuck = ["epaint/bytemuck"]
|
||||
|
||||
@@ -44,18 +40,10 @@ cint = ["epaint/cint"]
|
||||
## Enable the [`hex_color`] macro.
|
||||
color-hex = ["epaint/color-hex"]
|
||||
|
||||
## This will automatically detect deadlocks due to double-locking on the same thread.
|
||||
## If your app freezes, you may want to enable this!
|
||||
## Only affects [`epaint::mutex::RwLock`] (which egui uses a lot).
|
||||
deadlock_detection = ["epaint/deadlock_detection"]
|
||||
|
||||
## If set, egui will use `include_bytes!` to bundle some fonts.
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = ["epaint/default_fonts"]
|
||||
|
||||
## Turn on the `log` feature, that makes egui log some errors using the [`log`](https://docs.rs/log) crate.
|
||||
log = ["dep:log", "epaint/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 = ["epaint/mint"]
|
||||
|
||||
@@ -69,7 +57,7 @@ persistence = ["serde", "epaint/serde", "ron"]
|
||||
rayon = ["epaint/rayon"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
|
||||
serde = ["dep:serde", "epaint/serde", "accesskit/serde"]
|
||||
|
||||
## Change Vertex layout to be compatible with unity
|
||||
unity = ["epaint/unity"]
|
||||
@@ -83,19 +71,21 @@ _override_unity = ["epaint/_override_unity"]
|
||||
emath = { workspace = true, default-features = false }
|
||||
epaint = { workspace = true, default-features = false }
|
||||
|
||||
accesskit.workspace = true
|
||||
ahash.workspace = true
|
||||
bitflags.workspace = true
|
||||
log.workspace = true
|
||||
nohash-hasher.workspace = true
|
||||
profiling.workspace = true
|
||||
smallvec.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
accesskit = { version = "0.17.0", optional = true }
|
||||
|
||||
backtrace = { workspace = true, optional = true }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { workspace = true, optional = true }
|
||||
|
||||
log = { workspace = true, optional = true }
|
||||
ron = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
There are no stand-alone egui examples, because egui is not stand-alone!
|
||||
|
||||
See the top-level [examples](https://github.com/emilk/egui/tree/master/examples/) folder instead.
|
||||
See the top-level [examples](https://github.com/emilk/egui/tree/main/examples/) folder instead.
|
||||
|
||||
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/crates/egui_demo_lib>.
|
||||
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/main/crates/egui_demo_lib>.
|
||||
|
||||
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
emath::{remap_clamp, NumExt as _},
|
||||
Id, IdMap, InputState,
|
||||
emath::{NumExt as _, remap_clamp},
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
||||
112
crates/egui/src/atomics/atom.rs
Normal file
112
crates/egui/src/atomics/atom.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use crate::{AtomKind, FontSelection, Id, SizedAtom, Ui};
|
||||
use emath::{NumExt as _, Vec2};
|
||||
use epaint::text::TextWrapMode;
|
||||
|
||||
/// A low-level ui building block.
|
||||
///
|
||||
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
|
||||
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
|
||||
/// ```
|
||||
/// # use egui::{Image, emath::Vec2};
|
||||
/// use egui::AtomExt as _;
|
||||
/// let string_atom = "Hello".atom_grow(true);
|
||||
/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0));
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Atom<'a> {
|
||||
/// See [`crate::AtomExt::atom_size`]
|
||||
pub size: Option<Vec2>,
|
||||
|
||||
/// See [`crate::AtomExt::atom_max_size`]
|
||||
pub max_size: Vec2,
|
||||
|
||||
/// See [`crate::AtomExt::atom_grow`]
|
||||
pub grow: bool,
|
||||
|
||||
/// See [`crate::AtomExt::atom_shrink`]
|
||||
pub shrink: bool,
|
||||
|
||||
/// The atom type
|
||||
pub kind: AtomKind<'a>,
|
||||
}
|
||||
|
||||
impl Default for Atom<'_> {
|
||||
fn default() -> Self {
|
||||
Atom {
|
||||
size: None,
|
||||
max_size: Vec2::INFINITY,
|
||||
grow: false,
|
||||
shrink: false,
|
||||
kind: AtomKind::Empty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Atom<'a> {
|
||||
/// Create an empty [`Atom`] marked as `grow`.
|
||||
///
|
||||
/// This will expand in size, allowing all preceding atoms to be left-aligned,
|
||||
/// and all following atoms to be right-aligned
|
||||
pub fn grow() -> Self {
|
||||
Atom {
|
||||
grow: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`AtomKind::Custom`] with a specific size.
|
||||
pub fn custom(id: Id, size: impl Into<Vec2>) -> Self {
|
||||
Atom {
|
||||
size: Some(size.into()),
|
||||
kind: AtomKind::Custom(id),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn this into a [`SizedAtom`].
|
||||
pub fn into_sized(
|
||||
self,
|
||||
ui: &Ui,
|
||||
mut available_size: Vec2,
|
||||
mut wrap_mode: Option<TextWrapMode>,
|
||||
fallback_font: FontSelection,
|
||||
) -> SizedAtom<'a> {
|
||||
if !self.shrink && self.max_size.x.is_infinite() {
|
||||
wrap_mode = Some(TextWrapMode::Extend);
|
||||
}
|
||||
available_size = available_size.at_most(self.max_size);
|
||||
if let Some(size) = self.size {
|
||||
available_size = available_size.at_most(size);
|
||||
}
|
||||
if self.max_size.x.is_finite() {
|
||||
wrap_mode = Some(TextWrapMode::Truncate);
|
||||
}
|
||||
|
||||
let (intrinsic, kind) = self
|
||||
.kind
|
||||
.into_sized(ui, available_size, wrap_mode, fallback_font);
|
||||
|
||||
let size = self
|
||||
.size
|
||||
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));
|
||||
|
||||
SizedAtom {
|
||||
size,
|
||||
intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()),
|
||||
grow: self.grow,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Atom<'a>
|
||||
where
|
||||
T: Into<AtomKind<'a>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Atom {
|
||||
kind: value.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
107
crates/egui/src/atomics/atom_ext.rs
Normal file
107
crates/egui/src/atomics/atom_ext.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::{Atom, FontSelection, Ui};
|
||||
use emath::Vec2;
|
||||
|
||||
/// A trait for conveniently building [`Atom`]s.
|
||||
///
|
||||
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
|
||||
pub trait AtomExt<'a> {
|
||||
/// Set the atom to a fixed size.
|
||||
///
|
||||
/// If [`Atom::grow`] is `true`, this will be the minimum width.
|
||||
/// If [`Atom::shrink`] is `true`, this will be the maximum width.
|
||||
/// If both are true, the width will have no effect.
|
||||
///
|
||||
/// [`Self::atom_max_size`] will limit size.
|
||||
///
|
||||
/// See [`crate::AtomKind`] docs to see how the size affects the different types.
|
||||
fn atom_size(self, size: Vec2) -> Atom<'a>;
|
||||
|
||||
/// Grow this atom to the available space.
|
||||
///
|
||||
/// This will affect the size of the [`Atom`] in the main direction. Since
|
||||
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
|
||||
///
|
||||
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
|
||||
/// remaining space.
|
||||
fn atom_grow(self, grow: bool) -> Atom<'a>;
|
||||
|
||||
/// Shrink this atom if there isn't enough space.
|
||||
///
|
||||
/// This will affect the size of the [`Atom`] in the main direction. Since
|
||||
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
|
||||
///
|
||||
/// NOTE: Only a single [`Atom`] may shrink for each widget.
|
||||
///
|
||||
/// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first
|
||||
/// `AtomKind::Text` is set to shrink.
|
||||
fn atom_shrink(self, shrink: bool) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum size of this atom.
|
||||
///
|
||||
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
|
||||
/// equally to fill the available space).
|
||||
fn atom_max_size(self, max_size: Vec2) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum width of this atom.
|
||||
///
|
||||
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
|
||||
/// equally to fill the available space).
|
||||
fn atom_max_width(self, max_width: f32) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum height of this atom.
|
||||
fn atom_max_height(self, max_height: f32) -> Atom<'a>;
|
||||
|
||||
/// Set the max height of this atom to match the font size.
|
||||
///
|
||||
/// This is useful for e.g. limiting the height of icons in buttons.
|
||||
fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let font_selection = FontSelection::default();
|
||||
let font_id = font_selection.resolve(ui.style());
|
||||
let height = ui.fonts_mut(|f| f.row_height(&font_id));
|
||||
self.atom_max_height(height)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> AtomExt<'a> for T
|
||||
where
|
||||
T: Into<Atom<'a>> + Sized,
|
||||
{
|
||||
fn atom_size(self, size: Vec2) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.size = Some(size);
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_grow(self, grow: bool) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.grow = grow;
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_shrink(self, shrink: bool) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.shrink = shrink;
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_max_size(self, max_size: Vec2) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.max_size = max_size;
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_max_width(self, max_width: f32) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.max_size.x = max_width;
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_max_height(self, max_height: f32) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.max_size.y = max_height;
|
||||
atom
|
||||
}
|
||||
}
|
||||
119
crates/egui/src/atomics/atom_kind.rs
Normal file
119
crates/egui/src/atomics/atom_kind.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use crate::{FontSelection, Id, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
|
||||
use emath::Vec2;
|
||||
use epaint::text::TextWrapMode;
|
||||
|
||||
/// The different kinds of [`crate::Atom`]s.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub enum AtomKind<'a> {
|
||||
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
|
||||
#[default]
|
||||
Empty,
|
||||
|
||||
/// Text atom.
|
||||
///
|
||||
/// Truncation within [`crate::AtomLayout`] works like this:
|
||||
/// -
|
||||
/// - if `wrap_mode` is not Extend
|
||||
/// - if no atom is `shrink`
|
||||
/// - the first text atom is selected and will be marked as `shrink`
|
||||
/// - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode
|
||||
/// - any other text atoms will have `wrap_mode` extend
|
||||
/// - if `wrap_mode` is extend, Text will extend as expected.
|
||||
///
|
||||
/// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or
|
||||
/// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom
|
||||
/// that is not `shrink` will have unexpected results.
|
||||
///
|
||||
/// The size is determined by converting the [`WidgetText`] into a galley and using the galleys
|
||||
/// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`]
|
||||
/// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`.
|
||||
/// [`crate::AtomExt::atom_max_height`] has no effect on text.
|
||||
Text(WidgetText),
|
||||
|
||||
/// Image atom.
|
||||
///
|
||||
/// By default the size is determined via [`Image::calc_size`].
|
||||
/// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size.
|
||||
/// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the
|
||||
/// default font height, which is convenient for icons.
|
||||
Image(Image<'a>),
|
||||
|
||||
/// For custom rendering.
|
||||
///
|
||||
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
|
||||
/// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui};
|
||||
/// # use emath::Vec2;
|
||||
/// # __run_test_ui(|ui| {
|
||||
/// let id = Id::new("my_button");
|
||||
/// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui);
|
||||
///
|
||||
/// let rect = response.rect(id);
|
||||
/// if let Some(rect) = rect {
|
||||
/// ui.place(rect, Button::new("⏵"));
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
Custom(Id),
|
||||
}
|
||||
|
||||
impl<'a> AtomKind<'a> {
|
||||
pub fn text(text: impl Into<WidgetText>) -> Self {
|
||||
AtomKind::Text(text.into())
|
||||
}
|
||||
|
||||
pub fn image(image: impl Into<Image<'a>>) -> Self {
|
||||
AtomKind::Image(image.into())
|
||||
}
|
||||
|
||||
/// Turn this [`AtomKind`] into a [`SizedAtomKind`].
|
||||
///
|
||||
/// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
|
||||
/// The first returned argument is the preferred size.
|
||||
pub fn into_sized(
|
||||
self,
|
||||
ui: &Ui,
|
||||
available_size: Vec2,
|
||||
wrap_mode: Option<TextWrapMode>,
|
||||
fallback_font: FontSelection,
|
||||
) -> (Vec2, SizedAtomKind<'a>) {
|
||||
match self {
|
||||
AtomKind::Text(text) => {
|
||||
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
|
||||
let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font);
|
||||
(galley.intrinsic_size(), SizedAtomKind::Text(galley))
|
||||
}
|
||||
AtomKind::Image(image) => {
|
||||
let size = image.load_and_calc_size(ui, available_size);
|
||||
let size = size.unwrap_or(Vec2::ZERO);
|
||||
(size, SizedAtomKind::Image(image, size))
|
||||
}
|
||||
AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)),
|
||||
AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ImageSource<'a>> for AtomKind<'a> {
|
||||
fn from(value: ImageSource<'a>) -> Self {
|
||||
AtomKind::Image(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Image<'a>> for AtomKind<'a> {
|
||||
fn from(value: Image<'a>) -> Self {
|
||||
AtomKind::Image(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for AtomKind<'_>
|
||||
where
|
||||
T: Into<WidgetText>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
AtomKind::Text(value.into())
|
||||
}
|
||||
}
|
||||
515
crates/egui/src/atomics/atom_layout.rs
Normal file
515
crates/egui/src/atomics/atom_layout.rs
Normal file
@@ -0,0 +1,515 @@
|
||||
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
|
||||
use crate::{
|
||||
AtomKind, Atoms, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom,
|
||||
SizedAtomKind, Ui, Widget,
|
||||
};
|
||||
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
|
||||
use epaint::text::TextWrapMode;
|
||||
use epaint::{Color32, Galley};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Intra-widget layout utility.
|
||||
///
|
||||
/// Used to lay out and paint [`crate::Atom`]s.
|
||||
/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`].
|
||||
/// You can use it to make your own widgets.
|
||||
///
|
||||
/// Painting the atoms can be split in two phases:
|
||||
/// - [`AtomLayout::allocate`]
|
||||
/// - calculates sizes
|
||||
/// - converts texts to [`Galley`]s
|
||||
/// - allocates a [`Response`]
|
||||
/// - returns a [`AllocatedAtomLayout`]
|
||||
/// - [`AllocatedAtomLayout::paint`]
|
||||
/// - paints the [`Frame`]
|
||||
/// - calculates individual [`crate::Atom`] positions
|
||||
/// - paints each single atom
|
||||
///
|
||||
/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
|
||||
/// [`AllocatedAtomLayout`] for interaction styling.
|
||||
pub struct AtomLayout<'a> {
|
||||
id: Option<Id>,
|
||||
pub atoms: Atoms<'a>,
|
||||
gap: Option<f32>,
|
||||
pub(crate) frame: Frame,
|
||||
pub(crate) sense: Sense,
|
||||
fallback_text_color: Option<Color32>,
|
||||
fallback_font: Option<FontSelection>,
|
||||
min_size: Vec2,
|
||||
wrap_mode: Option<TextWrapMode>,
|
||||
align2: Option<Align2>,
|
||||
}
|
||||
|
||||
impl Default for AtomLayout<'_> {
|
||||
fn default() -> Self {
|
||||
Self::new(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AtomLayout<'a> {
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
atoms: atoms.into_atoms(),
|
||||
gap: None,
|
||||
frame: Frame::default(),
|
||||
sense: Sense::hover(),
|
||||
fallback_text_color: None,
|
||||
fallback_font: None,
|
||||
min_size: Vec2::ZERO,
|
||||
wrap_mode: None,
|
||||
align2: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the gap between atoms.
|
||||
///
|
||||
/// Default: `Spacing::icon_spacing`
|
||||
#[inline]
|
||||
pub fn gap(mut self, gap: f32) -> Self {
|
||||
self.gap = Some(gap);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Frame`].
|
||||
#[inline]
|
||||
pub fn frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = frame;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Sense`] used when allocating the [`Response`].
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the fallback (default) text color.
|
||||
///
|
||||
/// Default: [`crate::Visuals::text_color`]
|
||||
#[inline]
|
||||
pub fn fallback_text_color(mut self, color: Color32) -> Self {
|
||||
self.fallback_text_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the fallback (default) font.
|
||||
#[inline]
|
||||
pub fn fallback_font(mut self, font: impl Into<FontSelection>) -> Self {
|
||||
self.fallback_font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the minimum size of the Widget.
|
||||
///
|
||||
/// This will find and expand atoms with `grow: true`.
|
||||
/// If there are no growable atoms then everything will be left-aligned.
|
||||
#[inline]
|
||||
pub fn min_size(mut self, size: Vec2) -> Self {
|
||||
self.min_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Id`] used to allocate a [`Response`].
|
||||
#[inline]
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
|
||||
///
|
||||
/// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
|
||||
/// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most)
|
||||
/// [`AtomKind::Text`] will be set to shrink.
|
||||
#[inline]
|
||||
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
|
||||
self.wrap_mode = Some(wrap_mode);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Align2`].
|
||||
///
|
||||
/// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`].
|
||||
///
|
||||
/// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See
|
||||
/// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png)
|
||||
/// for info on how the [`crate::Layout`] affects the alignment.
|
||||
#[inline]
|
||||
pub fn align2(mut self, align2: Align2) -> Self {
|
||||
self.align2 = Some(align2);
|
||||
self
|
||||
}
|
||||
|
||||
/// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go.
|
||||
pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
|
||||
self.allocate(ui).paint(ui)
|
||||
}
|
||||
|
||||
/// Calculate sizes, create [`Galley`]s and allocate a [`Response`].
|
||||
///
|
||||
/// Use the returned [`AllocatedAtomLayout`] for painting.
|
||||
pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
|
||||
let Self {
|
||||
id,
|
||||
mut atoms,
|
||||
gap,
|
||||
frame,
|
||||
sense,
|
||||
fallback_text_color,
|
||||
min_size,
|
||||
wrap_mode,
|
||||
align2,
|
||||
fallback_font,
|
||||
} = self;
|
||||
|
||||
let fallback_font = fallback_font.unwrap_or_default();
|
||||
|
||||
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
|
||||
|
||||
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
|
||||
// If none is found, mark the first text item as `shrink`.
|
||||
if wrap_mode != TextWrapMode::Extend {
|
||||
let any_shrink = atoms.iter().any(|a| a.shrink);
|
||||
if !any_shrink {
|
||||
let first_text = atoms
|
||||
.iter_mut()
|
||||
.find(|a| matches!(a.kind, AtomKind::Text(..)));
|
||||
if let Some(atom) = first_text {
|
||||
atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id = id.unwrap_or_else(|| ui.next_auto_id());
|
||||
|
||||
let fallback_text_color =
|
||||
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
|
||||
let gap = gap.unwrap_or(ui.spacing().icon_spacing);
|
||||
|
||||
// The size available for the content
|
||||
let available_inner_size = ui.available_size() - frame.total_margin().sum();
|
||||
|
||||
let mut desired_width = 0.0;
|
||||
|
||||
// intrinsic width / height is the ideal size of the widget, e.g. the size where the
|
||||
// text is not wrapped. Used to set Response::intrinsic_size.
|
||||
let mut intrinsic_width = 0.0;
|
||||
let mut intrinsic_height = 0.0;
|
||||
|
||||
let mut height: f32 = 0.0;
|
||||
|
||||
let mut sized_items = SmallVec::new();
|
||||
|
||||
let mut grow_count = 0;
|
||||
|
||||
let mut shrink_item = None;
|
||||
|
||||
let align2 = align2.unwrap_or_else(|| {
|
||||
Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()])
|
||||
});
|
||||
|
||||
if atoms.len() > 1 {
|
||||
let gap_space = gap * (atoms.len() as f32 - 1.0);
|
||||
desired_width += gap_space;
|
||||
intrinsic_width += gap_space;
|
||||
}
|
||||
|
||||
for (idx, item) in atoms.into_iter().enumerate() {
|
||||
if item.grow {
|
||||
grow_count += 1;
|
||||
}
|
||||
if item.shrink {
|
||||
debug_assert!(
|
||||
shrink_item.is_none(),
|
||||
"Only one atomic may be marked as shrink. {item:?}"
|
||||
);
|
||||
if shrink_item.is_none() {
|
||||
shrink_item = Some((idx, item));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let sized = item.into_sized(
|
||||
ui,
|
||||
available_inner_size,
|
||||
Some(wrap_mode),
|
||||
fallback_font.clone(),
|
||||
);
|
||||
let size = sized.size;
|
||||
|
||||
desired_width += size.x;
|
||||
intrinsic_width += sized.intrinsic_size.x;
|
||||
|
||||
height = height.at_least(size.y);
|
||||
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
|
||||
|
||||
sized_items.push(sized);
|
||||
}
|
||||
|
||||
if let Some((index, item)) = shrink_item {
|
||||
// The `shrink` item gets the remaining space
|
||||
let available_size_for_shrink_item = Vec2::new(
|
||||
available_inner_size.x - desired_width,
|
||||
available_inner_size.y,
|
||||
);
|
||||
|
||||
let sized = item.into_sized(
|
||||
ui,
|
||||
available_size_for_shrink_item,
|
||||
Some(wrap_mode),
|
||||
fallback_font,
|
||||
);
|
||||
let size = sized.size;
|
||||
|
||||
desired_width += size.x;
|
||||
intrinsic_width += sized.intrinsic_size.x;
|
||||
|
||||
height = height.at_least(size.y);
|
||||
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
|
||||
|
||||
sized_items.insert(index, sized);
|
||||
}
|
||||
|
||||
let margin = frame.total_margin();
|
||||
let desired_size = Vec2::new(desired_width, height);
|
||||
let frame_size = (desired_size + margin.sum()).at_least(min_size);
|
||||
|
||||
let (_, rect) = ui.allocate_space(frame_size);
|
||||
let mut response = ui.interact(rect, id, sense);
|
||||
|
||||
response.intrinsic_size =
|
||||
Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));
|
||||
|
||||
AllocatedAtomLayout {
|
||||
sized_atoms: sized_items,
|
||||
frame,
|
||||
fallback_text_color,
|
||||
response,
|
||||
grow_count,
|
||||
desired_size,
|
||||
align2,
|
||||
gap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Instructions for painting an [`AtomLayout`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AllocatedAtomLayout<'a> {
|
||||
pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>,
|
||||
pub frame: Frame,
|
||||
pub fallback_text_color: Color32,
|
||||
pub response: Response,
|
||||
grow_count: usize,
|
||||
// The size of the inner content, before any growing.
|
||||
desired_size: Vec2,
|
||||
align2: Align2,
|
||||
gap: f32,
|
||||
}
|
||||
|
||||
impl<'atom> AllocatedAtomLayout<'atom> {
|
||||
pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
|
||||
self.sized_atoms.iter().map(|atom| &atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
|
||||
self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let SizedAtomKind::Image(image, _) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let SizedAtomKind::Image(image, _) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let SizedAtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let SizedAtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_kind<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
|
||||
{
|
||||
for kind in self.iter_kinds_mut() {
|
||||
*kind = f(std::mem::take(kind));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_images<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(Image<'atom>) -> Image<'atom>,
|
||||
{
|
||||
self.map_kind(|kind| {
|
||||
if let SizedAtomKind::Image(image, size) = kind {
|
||||
SizedAtomKind::Image(f(image), size)
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Paint the [`Frame`] and individual [`crate::Atom`]s.
|
||||
pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
|
||||
let Self {
|
||||
sized_atoms,
|
||||
frame,
|
||||
fallback_text_color,
|
||||
response,
|
||||
grow_count,
|
||||
desired_size,
|
||||
align2,
|
||||
gap,
|
||||
} = self;
|
||||
|
||||
let inner_rect = response.rect - self.frame.total_margin();
|
||||
|
||||
ui.painter().add(frame.paint(inner_rect));
|
||||
|
||||
let width_to_fill = inner_rect.width();
|
||||
let extra_space = f32::max(width_to_fill - desired_size.x, 0.0);
|
||||
let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
|
||||
|
||||
let aligned_rect = if grow_count > 0 {
|
||||
align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect)
|
||||
} else {
|
||||
align2.align_size_within_rect(desired_size, inner_rect)
|
||||
};
|
||||
|
||||
let mut cursor = aligned_rect.left();
|
||||
|
||||
let mut response = AtomLayoutResponse::empty(response);
|
||||
|
||||
for sized in sized_atoms {
|
||||
let size = sized.size;
|
||||
// TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
|
||||
// https://github.com/emilk/egui/pull/5830#discussion_r2079627864
|
||||
let growth = if sized.is_grow() { grow_width } else { 0.0 };
|
||||
|
||||
let frame = aligned_rect
|
||||
.with_min_x(cursor)
|
||||
.with_max_x(cursor + size.x + growth);
|
||||
cursor = frame.right() + gap;
|
||||
|
||||
let align = Align2::CENTER_CENTER;
|
||||
let rect = align.align_size_within_rect(size, frame);
|
||||
|
||||
match sized.kind {
|
||||
SizedAtomKind::Text(galley) => {
|
||||
ui.painter().galley(rect.min, galley, fallback_text_color);
|
||||
}
|
||||
SizedAtomKind::Image(image, _) => {
|
||||
image.paint_at(ui, rect);
|
||||
}
|
||||
SizedAtomKind::Custom(id) => {
|
||||
debug_assert!(
|
||||
!response.custom_rects.iter().any(|(i, _)| *i == id),
|
||||
"Duplicate custom id"
|
||||
);
|
||||
response.custom_rects.push((id, rect));
|
||||
}
|
||||
SizedAtomKind::Empty => {}
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
|
||||
///
|
||||
/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AtomLayoutResponse {
|
||||
pub response: Response,
|
||||
// There should rarely be more than one custom rect.
|
||||
custom_rects: SmallVec<[(Id, Rect); 1]>,
|
||||
}
|
||||
|
||||
impl AtomLayoutResponse {
|
||||
pub fn empty(response: Response) -> Self {
|
||||
Self {
|
||||
response,
|
||||
custom_rects: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
|
||||
self.custom_rects.iter().copied()
|
||||
}
|
||||
|
||||
/// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets.
|
||||
///
|
||||
/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
|
||||
pub fn rect(&self, id: Id) -> Option<Rect> {
|
||||
self.custom_rects
|
||||
.iter()
|
||||
.find_map(|(i, r)| if *i == id { Some(*r) } else { None })
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AtomLayout<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
self.show(ui).response
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for AtomLayout<'a> {
|
||||
type Target = Atoms<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for AtomLayout<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for AllocatedAtomLayout<'a> {
|
||||
type Target = [SizedAtom<'a>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.sized_atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for AllocatedAtomLayout<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.sized_atoms
|
||||
}
|
||||
}
|
||||
259
crates/egui/src/atomics/atoms.rs
Normal file
259
crates/egui/src/atomics/atoms.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use crate::{Atom, AtomKind, Image, WidgetText};
|
||||
use smallvec::SmallVec;
|
||||
use std::borrow::Cow;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
// Rarely there should be more than 2 atoms in one Widget.
|
||||
// I guess it could happen in a menu button with Image and right text...
|
||||
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
|
||||
|
||||
/// A list of [`Atom`]s.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
|
||||
|
||||
impl<'a> Atoms<'a> {
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
atoms.into_atoms()
|
||||
}
|
||||
|
||||
/// Insert a new [`Atom`] at the end of the list (right side).
|
||||
pub fn push_right(&mut self, atom: impl Into<Atom<'a>>) {
|
||||
self.0.push(atom.into());
|
||||
}
|
||||
|
||||
/// Insert a new [`Atom`] at the beginning of the list (left side).
|
||||
pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
|
||||
self.0.insert(0, atom.into());
|
||||
}
|
||||
|
||||
/// Concatenate and return the text contents.
|
||||
// TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g.
|
||||
// in a submenu button there is a right text '⏵' which is now passed to the screen reader.
|
||||
pub fn text(&self) -> Option<Cow<'_, str>> {
|
||||
let mut string: Option<Cow<'_, str>> = None;
|
||||
for atom in &self.0 {
|
||||
if let AtomKind::Text(text) = &atom.kind {
|
||||
if let Some(string) = &mut string {
|
||||
let string = string.to_mut();
|
||||
string.push(' ');
|
||||
string.push_str(text.text());
|
||||
} else {
|
||||
string = Some(Cow::Borrowed(text.text()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no text, try to find an image with alt text.
|
||||
if string.is_none() {
|
||||
string = self.iter().find_map(|a| match &a.kind {
|
||||
AtomKind::Image(image) => image.alt_text.as_deref().map(Cow::Borrowed),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
|
||||
string
|
||||
}
|
||||
|
||||
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
|
||||
self.0.iter().map(|atom| &atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut AtomKind<'a>> {
|
||||
self.0.iter_mut().map(|atom| &mut atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'a>> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'a>> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts(&self) -> impl Iterator<Item = &WidgetText> + use<'_, 'a> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut WidgetText> + use<'a, '_> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
|
||||
self.iter_mut()
|
||||
.for_each(|atom| *atom = f(std::mem::take(atom)));
|
||||
}
|
||||
|
||||
pub fn map_kind<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
|
||||
{
|
||||
for kind in self.iter_kinds_mut() {
|
||||
*kind = f(std::mem::take(kind));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_images<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(Image<'a>) -> Image<'a>,
|
||||
{
|
||||
self.map_kind(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
AtomKind::Image(f(image))
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn map_texts<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(WidgetText) -> WidgetText,
|
||||
{
|
||||
self.map_kind(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
AtomKind::Text(f(text))
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Atoms<'a> {
|
||||
type Item = Atom<'a>;
|
||||
type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait to convert a tuple of atoms into [`Atoms`].
|
||||
///
|
||||
/// ```
|
||||
/// use egui::{Atoms, Image, IntoAtoms, RichText};
|
||||
/// let atoms: Atoms = (
|
||||
/// "Some text",
|
||||
/// RichText::new("Some RichText"),
|
||||
/// Image::new("some_image_url"),
|
||||
/// ).into_atoms();
|
||||
/// ```
|
||||
impl<'a, T> IntoAtoms<'a> for T
|
||||
where
|
||||
T: Into<Atom<'a>>,
|
||||
{
|
||||
fn collect(self, atoms: &mut Atoms<'a>) {
|
||||
atoms.push_right(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
|
||||
pub trait IntoAtoms<'a> {
|
||||
fn collect(self, atoms: &mut Atoms<'a>);
|
||||
|
||||
fn into_atoms(self) -> Atoms<'a>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut atoms = Atoms::default();
|
||||
self.collect(&mut atoms);
|
||||
atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAtoms<'a> for Atoms<'a> {
|
||||
fn collect(self, atoms: &mut Self) {
|
||||
atoms.0.extend(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! all_the_atoms {
|
||||
($($T:ident),*) => {
|
||||
impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*)
|
||||
where
|
||||
$($T: IntoAtoms<'a>),*
|
||||
{
|
||||
fn collect(self, _atoms: &mut Atoms<'a>) {
|
||||
#[allow(clippy::allow_attributes)]
|
||||
#[allow(non_snake_case)]
|
||||
let ($($T),*) = self;
|
||||
$($T.collect(_atoms);)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
all_the_atoms!();
|
||||
all_the_atoms!(T0, T1);
|
||||
all_the_atoms!(T0, T1, T2);
|
||||
all_the_atoms!(T0, T1, T2, T3);
|
||||
all_the_atoms!(T0, T1, T2, T3, T4);
|
||||
all_the_atoms!(T0, T1, T2, T3, T4, T5);
|
||||
|
||||
impl<'a> Deref for Atoms<'a> {
|
||||
type Target = [Atom<'a>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Atoms<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Into<Atom<'a>>> From<Vec<T>> for Atoms<'a> {
|
||||
fn from(vec: Vec<T>) -> Self {
|
||||
Atoms(vec.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Into<Atom<'a>> + Clone> From<&[T]> for Atoms<'a> {
|
||||
fn from(slice: &[T]) -> Self {
|
||||
Atoms(slice.iter().cloned().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Item: Into<Atom<'a>>> FromIterator<Item> for Atoms<'a> {
|
||||
fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
|
||||
Atoms(iter.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Atoms;
|
||||
|
||||
#[test]
|
||||
fn collect_atoms() {
|
||||
let _: Atoms<'_> = ["Hello", "World"].into_iter().collect();
|
||||
let _ = Atoms::from(vec!["Hi"]);
|
||||
let _ = Atoms::from(["Hi"].as_slice());
|
||||
}
|
||||
}
|
||||
15
crates/egui/src/atomics/mod.rs
Normal file
15
crates/egui/src/atomics/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
mod atom;
|
||||
mod atom_ext;
|
||||
mod atom_kind;
|
||||
mod atom_layout;
|
||||
mod atoms;
|
||||
mod sized_atom;
|
||||
mod sized_atom_kind;
|
||||
|
||||
pub use atom::*;
|
||||
pub use atom_ext::*;
|
||||
pub use atom_kind::*;
|
||||
pub use atom_layout::*;
|
||||
pub use atoms::*;
|
||||
pub use sized_atom::*;
|
||||
pub use sized_atom_kind::*;
|
||||
26
crates/egui/src/atomics/sized_atom.rs
Normal file
26
crates/egui/src/atomics/sized_atom.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use crate::SizedAtomKind;
|
||||
use emath::Vec2;
|
||||
|
||||
/// A [`crate::Atom`] which has been sized.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SizedAtom<'a> {
|
||||
pub(crate) grow: bool,
|
||||
|
||||
/// The size of the atom.
|
||||
///
|
||||
/// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by
|
||||
/// size.x + gap.
|
||||
pub size: Vec2,
|
||||
|
||||
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
|
||||
pub intrinsic_size: Vec2,
|
||||
|
||||
pub kind: SizedAtomKind<'a>,
|
||||
}
|
||||
|
||||
impl SizedAtom<'_> {
|
||||
/// Was this [`crate::Atom`] marked as `grow`?
|
||||
pub fn is_grow(&self) -> bool {
|
||||
self.grow
|
||||
}
|
||||
}
|
||||
25
crates/egui/src/atomics/sized_atom_kind.rs
Normal file
25
crates/egui/src/atomics/sized_atom_kind.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::{Id, Image};
|
||||
use emath::Vec2;
|
||||
use epaint::Galley;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A sized [`crate::AtomKind`].
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub enum SizedAtomKind<'a> {
|
||||
#[default]
|
||||
Empty,
|
||||
Text(Arc<Galley>),
|
||||
Image(Image<'a>, Vec2),
|
||||
Custom(Id),
|
||||
}
|
||||
|
||||
impl SizedAtomKind<'_> {
|
||||
/// Get the calculated size.
|
||||
pub fn size(&self) -> Vec2 {
|
||||
match self {
|
||||
SizedAtomKind::Text(galley) => galley.size(),
|
||||
SizedAtomKind::Image(_, size) => *size,
|
||||
SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
2
crates/egui/src/cache/cache_trait.rs
vendored
2
crates/egui/src/cache/cache_trait.rs
vendored
@@ -1,5 +1,5 @@
|
||||
/// A cache, storing some value for some length of time.
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
#[expect(clippy::len_without_is_empty)]
|
||||
pub trait CacheTrait: 'static + Send + Sync {
|
||||
/// Call once per frame to evict cache.
|
||||
fn update(&mut self);
|
||||
|
||||
@@ -20,10 +20,10 @@ pub fn capture() -> String {
|
||||
backtrace::resolve_frame(frame, |symbol| {
|
||||
let mut file_and_line = symbol.filename().map(shorten_source_file_path);
|
||||
|
||||
if let Some(file_and_line) = &mut file_and_line {
|
||||
if let Some(line_nr) = symbol.lineno() {
|
||||
file_and_line.push_str(&format!(":{line_nr}"));
|
||||
}
|
||||
if let Some(file_and_line) = &mut file_and_line
|
||||
&& let Some(line_nr) = symbol.lineno()
|
||||
{
|
||||
file_and_line.push_str(&format!(":{line_nr}"));
|
||||
}
|
||||
let file_and_line = file_and_line.unwrap_or_default();
|
||||
|
||||
@@ -204,12 +204,17 @@ fn shorten_source_file_path(path: &std::path::Path) -> String {
|
||||
#[test]
|
||||
fn test_shorten_path() {
|
||||
for (before, after) in [
|
||||
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
|
||||
(
|
||||
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
|
||||
"tokio-1.24.1/src/runtime/runtime.rs",
|
||||
),
|
||||
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
|
||||
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
|
||||
(
|
||||
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
|
||||
"core/src/ops/function.rs",
|
||||
),
|
||||
("/weird/path/file.rs", "/weird/path/file.rs"),
|
||||
]
|
||||
{
|
||||
] {
|
||||
use std::str::FromStr as _;
|
||||
let before = std::path::PathBuf::from_str(before).unwrap();
|
||||
assert_eq!(shorten_source_file_path(&before), after);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
use emath::GuiRounding as _;
|
||||
|
||||
use crate::{
|
||||
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
|
||||
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
|
||||
Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
|
||||
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
|
||||
};
|
||||
|
||||
/// State of an [`Area`] that is persisted between frames.
|
||||
@@ -103,10 +103,10 @@ impl AreaState {
|
||||
///
|
||||
/// The previous rectangle used by this area can be obtained through [`crate::Memory::area_rect()`].
|
||||
#[must_use = "You should call .show()"]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Area {
|
||||
pub(crate) id: Id,
|
||||
kind: UiKind,
|
||||
info: UiStackInfo,
|
||||
sense: Option<Sense>,
|
||||
movable: bool,
|
||||
interactable: bool,
|
||||
@@ -120,6 +120,8 @@ pub struct Area {
|
||||
anchor: Option<(Align2, Vec2)>,
|
||||
new_pos: Option<Pos2>,
|
||||
fade_in: bool,
|
||||
layout: Layout,
|
||||
sizing_pass: bool,
|
||||
}
|
||||
|
||||
impl WidgetWithState for Area {
|
||||
@@ -131,7 +133,7 @@ impl Area {
|
||||
pub fn new(id: Id) -> Self {
|
||||
Self {
|
||||
id,
|
||||
kind: UiKind::GenericArea,
|
||||
info: UiStackInfo::new(UiKind::GenericArea),
|
||||
sense: None,
|
||||
movable: true,
|
||||
interactable: true,
|
||||
@@ -145,6 +147,8 @@ impl Area {
|
||||
pivot: Align2::LEFT_TOP,
|
||||
anchor: None,
|
||||
fade_in: true,
|
||||
layout: Layout::default(),
|
||||
sizing_pass: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +166,16 @@ impl Area {
|
||||
/// Default to [`UiKind::GenericArea`].
|
||||
#[inline]
|
||||
pub fn kind(mut self, kind: UiKind) -> Self {
|
||||
self.kind = kind;
|
||||
self.info = UiStackInfo::new(kind);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`UiStackInfo`] of the area's [`Ui`].
|
||||
///
|
||||
/// Default to [`UiStackInfo::new(UiKind::GenericArea)`].
|
||||
#[inline]
|
||||
pub fn info(mut self, info: UiStackInfo) -> Self {
|
||||
self.info = info;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -339,10 +352,38 @@ impl Area {
|
||||
self.fade_in = fade_in;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the layout for the child Ui.
|
||||
#[inline]
|
||||
pub fn layout(mut self, layout: Layout) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// While true, a sizing pass will be done. This means the area will be invisible
|
||||
/// and the contents will be laid out to estimate the proper containing size of the area.
|
||||
/// If false, there will be no change to the default area behavior. This is useful if the
|
||||
/// area contents area dynamic and you need to need to make sure the area adjusts its size
|
||||
/// accordingly.
|
||||
///
|
||||
/// This should only be set to true during the specific frames you want force a sizing pass.
|
||||
/// Do NOT hard-code this as `.sizing_pass(true)`, as it will cause the area to never be
|
||||
/// visible.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - resize: If true, the area will be resized to fit its contents. False will keep the
|
||||
/// default area resizing behavior.
|
||||
///
|
||||
/// Default: `false`.
|
||||
#[inline]
|
||||
pub fn sizing_pass(mut self, resize: bool) -> Self {
|
||||
self.sizing_pass = resize;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Prepared {
|
||||
kind: UiKind,
|
||||
info: Option<UiStackInfo>,
|
||||
layer_id: LayerId,
|
||||
state: AreaState,
|
||||
move_response: Response,
|
||||
@@ -358,6 +399,7 @@ pub(crate) struct Prepared {
|
||||
sizing_pass: bool,
|
||||
|
||||
fade_in: bool,
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
@@ -366,7 +408,7 @@ impl Area {
|
||||
ctx: &Context,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let prepared = self.begin(ctx);
|
||||
let mut prepared = self.begin(ctx);
|
||||
let mut content_ui = prepared.content_ui(ctx);
|
||||
let inner = add_contents(&mut content_ui);
|
||||
let response = prepared.end(ctx, content_ui);
|
||||
@@ -376,7 +418,7 @@ impl Area {
|
||||
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
|
||||
let Self {
|
||||
id,
|
||||
kind,
|
||||
info,
|
||||
sense,
|
||||
movable,
|
||||
order,
|
||||
@@ -390,9 +432,11 @@ impl Area {
|
||||
constrain,
|
||||
constrain_rect,
|
||||
fade_in,
|
||||
layout,
|
||||
sizing_pass: force_sizing_pass,
|
||||
} = self;
|
||||
|
||||
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
|
||||
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.content_rect());
|
||||
|
||||
let layer_id = LayerId::new(order, id);
|
||||
|
||||
@@ -405,6 +449,10 @@ impl Area {
|
||||
interactable,
|
||||
last_became_visible_at: None,
|
||||
});
|
||||
if force_sizing_pass {
|
||||
sizing_pass = true;
|
||||
state.size = None;
|
||||
}
|
||||
state.pivot = pivot;
|
||||
state.interactable = interactable;
|
||||
if let Some(new_pos) = new_pos {
|
||||
@@ -477,10 +525,21 @@ impl Area {
|
||||
true,
|
||||
);
|
||||
|
||||
if movable && move_response.dragged() {
|
||||
if let Some(pivot_pos) = &mut state.pivot_pos {
|
||||
*pivot_pos += move_response.drag_delta();
|
||||
}
|
||||
// Used to prevent drift
|
||||
let pivot_at_start_of_drag_id = id.with("pivot_at_drag_start");
|
||||
|
||||
if movable
|
||||
&& move_response.dragged()
|
||||
&& let Some(pivot_pos) = &mut state.pivot_pos
|
||||
{
|
||||
let pivot_at_start_of_drag = ctx.data_mut(|data| {
|
||||
*data.get_temp_mut_or::<Pos2>(pivot_at_start_of_drag_id, *pivot_pos)
|
||||
});
|
||||
|
||||
*pivot_pos =
|
||||
pivot_at_start_of_drag + move_response.total_drag_delta().unwrap_or_default();
|
||||
} else {
|
||||
ctx.data_mut(|data| data.remove::<Pos2>(pivot_at_start_of_drag_id));
|
||||
}
|
||||
|
||||
if (move_response.dragged() || move_response.clicked())
|
||||
@@ -494,20 +553,21 @@ impl Area {
|
||||
move_response
|
||||
};
|
||||
|
||||
if constrain {
|
||||
state.set_left_top_pos(
|
||||
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
|
||||
);
|
||||
}
|
||||
|
||||
state.set_left_top_pos(state.left_top_pos());
|
||||
state.set_left_top_pos(round_area_position(
|
||||
ctx,
|
||||
if constrain {
|
||||
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
|
||||
} else {
|
||||
state.left_top_pos()
|
||||
},
|
||||
));
|
||||
|
||||
// Update response with possibly moved/constrained rect:
|
||||
move_response.rect = state.rect();
|
||||
move_response.interact_rect = state.rect();
|
||||
|
||||
Prepared {
|
||||
kind,
|
||||
info: Some(info),
|
||||
layer_id,
|
||||
state,
|
||||
move_response,
|
||||
@@ -516,10 +576,21 @@ impl Area {
|
||||
constrain_rect,
|
||||
sizing_pass,
|
||||
fade_in,
|
||||
layout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
|
||||
// We round a lot of rendering to pixels, so we round the whole
|
||||
// area positions to pixels too, so avoid widgets appearing to float
|
||||
// around independently of each other when the area is dragged.
|
||||
// But just in case pixels_per_point is irrational,
|
||||
// we then also round to ui coordinates:
|
||||
|
||||
pos.round_to_pixels(ctx.pixels_per_point()).round_ui()
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
pub(crate) fn state(&self) -> &AreaState {
|
||||
&self.state
|
||||
@@ -537,13 +608,16 @@ impl Prepared {
|
||||
self.constrain_rect
|
||||
}
|
||||
|
||||
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
|
||||
pub(crate) fn content_ui(&mut self, ctx: &Context) -> Ui {
|
||||
let max_rect = self.state.rect();
|
||||
|
||||
let mut ui_builder = UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(self.kind))
|
||||
.ui_stack_info(self.info.take().unwrap_or_default())
|
||||
.layer_id(self.layer_id)
|
||||
.max_rect(max_rect);
|
||||
.max_rect(max_rect)
|
||||
.layout(self.layout)
|
||||
.accessibility_parent(self.move_response.id)
|
||||
.closable();
|
||||
|
||||
if !self.enabled {
|
||||
ui_builder = ui_builder.disabled();
|
||||
@@ -555,16 +629,16 @@ impl Prepared {
|
||||
let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
|
||||
ui.set_clip_rect(self.constrain_rect); // Don't paint outside our bounds
|
||||
|
||||
if self.fade_in {
|
||||
if let Some(last_became_visible_at) = self.state.last_became_visible_at {
|
||||
let age =
|
||||
ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
|
||||
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
|
||||
let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
|
||||
ui.multiply_opacity(opacity);
|
||||
if opacity < 1.0 {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
if self.fade_in
|
||||
&& let Some(last_became_visible_at) = self.state.last_became_visible_at
|
||||
{
|
||||
let age =
|
||||
ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
|
||||
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
|
||||
let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
|
||||
ui.multiply_opacity(opacity);
|
||||
if opacity < 1.0 {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,10 +653,10 @@ impl Prepared {
|
||||
self.move_response.id
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
|
||||
#[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
|
||||
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
|
||||
let Self {
|
||||
kind: _,
|
||||
info: _,
|
||||
layer_id,
|
||||
mut state,
|
||||
move_response: mut response,
|
||||
@@ -598,6 +672,12 @@ impl Prepared {
|
||||
response.rect = final_rect;
|
||||
response.interact_rect = final_rect;
|
||||
|
||||
// TODO(lucasmerlin): Can the area response be based on Ui::response? Then this won't be needed
|
||||
// Bubble up the close event
|
||||
if content_ui.should_close() {
|
||||
response.set_close();
|
||||
}
|
||||
|
||||
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
|
||||
|
||||
if sizing_pass {
|
||||
@@ -648,7 +728,7 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
|
||||
let current_column_bb = column_bbs.last_mut().unwrap();
|
||||
if rect.left() < current_column_bb.right() {
|
||||
// same column
|
||||
*current_column_bb = current_column_bb.union(rect);
|
||||
*current_column_bb |= rect;
|
||||
} else {
|
||||
// new column
|
||||
column_bbs.push(rect);
|
||||
|
||||
28
crates/egui/src/containers/close_tag.rs
Normal file
28
crates/egui/src/containers/close_tag.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
#[expect(unused_imports)]
|
||||
use crate::{Ui, UiBuilder};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
/// A tag to mark a container as closable.
|
||||
///
|
||||
/// Usually set via [`UiBuilder::closable`].
|
||||
///
|
||||
/// [`Ui::close`] will find the closest parent [`ClosableTag`] and set its `close` field to `true`.
|
||||
/// Use [`Ui::should_close`] to check if close has been called.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ClosableTag {
|
||||
pub close: AtomicBool,
|
||||
}
|
||||
|
||||
impl ClosableTag {
|
||||
pub const NAME: &'static str = "egui_close_tag";
|
||||
|
||||
/// Set close to `true`
|
||||
pub fn set_close(&self) {
|
||||
self.close.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Returns `true` if [`ClosableTag::set_close`] has been called.
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.close.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate::{
|
||||
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
|
||||
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle,
|
||||
TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
emath, epaint, pos2, remap, remap_clamp, vec2,
|
||||
};
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::{Shape, StrokeKind};
|
||||
@@ -203,11 +204,16 @@ impl CollapsingState {
|
||||
add_body: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let openness = self.openness(ui.ctx());
|
||||
|
||||
let builder = UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(UiKind::Collapsible))
|
||||
.closable();
|
||||
|
||||
if openness <= 0.0 {
|
||||
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
|
||||
None
|
||||
} else if openness < 1.0 {
|
||||
Some(ui.scope(|child_ui| {
|
||||
Some(ui.scope_builder(builder, |child_ui| {
|
||||
let max_height = if self.state.open && self.state.open_height.is_none() {
|
||||
// First frame of expansion.
|
||||
// We don't know full height yet, but we will next frame.
|
||||
@@ -226,6 +232,9 @@ impl CollapsingState {
|
||||
|
||||
let mut min_rect = child_ui.min_rect();
|
||||
self.state.open_height = Some(min_rect.height());
|
||||
if child_ui.should_close() {
|
||||
self.state.open = false;
|
||||
}
|
||||
self.store(child_ui.ctx()); // remember the height
|
||||
|
||||
// Pretend children took up at most `max_height` space:
|
||||
@@ -234,7 +243,10 @@ impl CollapsingState {
|
||||
ret
|
||||
}))
|
||||
} else {
|
||||
let ret_response = ui.scope(add_body);
|
||||
let ret_response = ui.scope_builder(builder, add_body);
|
||||
if ret_response.response.should_close() {
|
||||
self.state.open = false;
|
||||
}
|
||||
let full_size = ret_response.response.rect.size();
|
||||
self.state.open_height = Some(full_size.y);
|
||||
self.store(ui.ctx()); // remember the height
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
use epaint::Shape;
|
||||
|
||||
use crate::{
|
||||
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter,
|
||||
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
|
||||
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
Align2, Context, Id, InnerResponse, NumExt as _, Painter, Popup, PopupCloseBehavior, Rect,
|
||||
Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo,
|
||||
WidgetText, WidgetType, epaint, style::StyleModifier, style::WidgetVisuals, vec2,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)] // Documentation
|
||||
#[expect(unused_imports)] // Documentation
|
||||
use crate::style::Spacing;
|
||||
|
||||
/// Indicate whether a popup will be shown above or below the box.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum AboveOrBelow {
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// A function that paints the [`ComboBox`] icon
|
||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
|
||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
|
||||
|
||||
/// A drop-down selection menu with a descriptive label.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # #[derive(Debug, PartialEq)]
|
||||
/// # #[derive(Debug, PartialEq, Copy, Clone)]
|
||||
/// # enum Enum { First, Second, Third }
|
||||
/// # let mut selected = Enum::First;
|
||||
/// let before = selected;
|
||||
/// egui::ComboBox::from_label("Select one!")
|
||||
/// .selected_text(format!("{:?}", selected))
|
||||
/// .show_ui(ui, |ui| {
|
||||
@@ -34,6 +28,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBe
|
||||
/// ui.selectable_value(&mut selected, Enum::Third, "Third");
|
||||
/// }
|
||||
/// );
|
||||
///
|
||||
/// if selected != before {
|
||||
/// // Handle selection change
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[must_use = "You should call .show*"]
|
||||
@@ -46,6 +44,7 @@ pub struct ComboBox {
|
||||
icon: Option<IconPainter>,
|
||||
wrap_mode: Option<TextWrapMode>,
|
||||
close_behavior: Option<PopupCloseBehavior>,
|
||||
popup_style: StyleModifier,
|
||||
}
|
||||
|
||||
impl ComboBox {
|
||||
@@ -60,6 +59,7 @@ impl ComboBox {
|
||||
icon: None,
|
||||
wrap_mode: None,
|
||||
close_behavior: None,
|
||||
popup_style: StyleModifier::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ impl ComboBox {
|
||||
icon: None,
|
||||
wrap_mode: None,
|
||||
close_behavior: None,
|
||||
popup_style: StyleModifier::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,11 +90,12 @@ impl ComboBox {
|
||||
icon: None,
|
||||
wrap_mode: None,
|
||||
close_behavior: None,
|
||||
popup_style: StyleModifier::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Without label.
|
||||
#[deprecated = "Renamed id_salt"]
|
||||
#[deprecated = "Renamed from_id_salt"]
|
||||
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
|
||||
Self::from_id_salt(id_salt)
|
||||
}
|
||||
@@ -135,7 +137,6 @@ impl ComboBox {
|
||||
/// rect: egui::Rect,
|
||||
/// visuals: &egui::style::WidgetVisuals,
|
||||
/// _is_open: bool,
|
||||
/// _above_or_below: egui::AboveOrBelow,
|
||||
/// ) {
|
||||
/// let rect = egui::Rect::from_center_size(
|
||||
/// rect.center(),
|
||||
@@ -154,10 +155,8 @@ impl ComboBox {
|
||||
/// .show_ui(ui, |_ui| {});
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn icon(
|
||||
mut self,
|
||||
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
|
||||
) -> Self {
|
||||
#[inline]
|
||||
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self {
|
||||
self.icon = Some(Box::new(icon_fn));
|
||||
self
|
||||
}
|
||||
@@ -196,6 +195,16 @@ impl ComboBox {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the popup menu.
|
||||
///
|
||||
/// Could for example be used with [`crate::containers::menu::menu_style`] to get the frame-less
|
||||
/// menu button style.
|
||||
#[inline]
|
||||
pub fn popup_style(mut self, popup_style: StyleModifier) -> Self {
|
||||
self.popup_style = popup_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the combo box, with the given ui code for the menu contents.
|
||||
///
|
||||
/// Returns `InnerResponse { inner: None }` if the combo box is closed.
|
||||
@@ -221,6 +230,7 @@ impl ComboBox {
|
||||
icon,
|
||||
wrap_mode,
|
||||
close_behavior,
|
||||
popup_style,
|
||||
} = self;
|
||||
|
||||
let button_id = ui.make_persistent_id(id_salt);
|
||||
@@ -229,21 +239,24 @@ impl ComboBox {
|
||||
let mut ir = combo_box_dyn(
|
||||
ui,
|
||||
button_id,
|
||||
selected_text,
|
||||
selected_text.clone(),
|
||||
menu_contents,
|
||||
icon,
|
||||
wrap_mode,
|
||||
close_behavior,
|
||||
popup_style,
|
||||
(width, height),
|
||||
);
|
||||
ir.response.widget_info(|| {
|
||||
let mut info = WidgetInfo::new(WidgetType::ComboBox);
|
||||
info.enabled = ui.is_enabled();
|
||||
info.current_text_value = Some(selected_text.text().to_owned());
|
||||
info
|
||||
});
|
||||
if let Some(label) = label {
|
||||
ir.response.widget_info(|| {
|
||||
WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text())
|
||||
});
|
||||
ir.response |= ui.label(label);
|
||||
} else {
|
||||
ir.response
|
||||
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), ""));
|
||||
let label_response = ui.label(label);
|
||||
ir.response = ir.response.labelled_by(label_response.id);
|
||||
ir.response |= label_response;
|
||||
}
|
||||
ir
|
||||
})
|
||||
@@ -298,7 +311,7 @@ impl ComboBox {
|
||||
|
||||
/// Check if the [`ComboBox`] with the given id has its popup menu currently opened.
|
||||
pub fn is_open(ctx: &Context, id: Id) -> bool {
|
||||
ctx.memory(|m| m.is_popup_open(Self::widget_to_popup_id(id)))
|
||||
Popup::is_id_open(ctx, Self::widget_to_popup_id(id))
|
||||
}
|
||||
|
||||
/// Convert a [`ComboBox`] id to the id used to store it's popup state.
|
||||
@@ -307,7 +320,7 @@ impl ComboBox {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn combo_box_dyn<'c, R>(
|
||||
ui: &mut Ui,
|
||||
button_id: Id,
|
||||
@@ -316,27 +329,12 @@ fn combo_box_dyn<'c, R>(
|
||||
icon: Option<IconPainter>,
|
||||
wrap_mode: Option<TextWrapMode>,
|
||||
close_behavior: Option<PopupCloseBehavior>,
|
||||
popup_style: StyleModifier,
|
||||
(width, height): (Option<f32>, Option<f32>),
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let popup_id = ComboBox::widget_to_popup_id(button_id);
|
||||
|
||||
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
|
||||
|
||||
let popup_height = ui.memory(|m| {
|
||||
m.areas()
|
||||
.get(popup_id)
|
||||
.and_then(|state| state.size)
|
||||
.map_or(100.0, |size| size.y)
|
||||
});
|
||||
|
||||
let above_or_below =
|
||||
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
|
||||
< ui.ctx().screen_rect().bottom()
|
||||
{
|
||||
AboveOrBelow::Below
|
||||
} else {
|
||||
AboveOrBelow::Above
|
||||
};
|
||||
let is_popup_open = Popup::is_id_open(ui.ctx(), popup_id);
|
||||
|
||||
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
|
||||
|
||||
@@ -385,15 +383,9 @@ fn combo_box_dyn<'c, R>(
|
||||
icon_rect.expand(visuals.expansion),
|
||||
visuals,
|
||||
is_popup_open,
|
||||
above_or_below,
|
||||
);
|
||||
} else {
|
||||
paint_default_icon(
|
||||
ui.painter(),
|
||||
icon_rect.expand(visuals.expansion),
|
||||
visuals,
|
||||
above_or_below,
|
||||
);
|
||||
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
||||
}
|
||||
|
||||
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
||||
@@ -402,19 +394,16 @@ fn combo_box_dyn<'c, R>(
|
||||
}
|
||||
});
|
||||
|
||||
if button_response.clicked() {
|
||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
}
|
||||
|
||||
let height = height.unwrap_or_else(|| ui.spacing().combo_height);
|
||||
|
||||
let inner = crate::popup::popup_above_or_below_widget(
|
||||
ui,
|
||||
popup_id,
|
||||
&button_response,
|
||||
above_or_below,
|
||||
close_behavior,
|
||||
|ui| {
|
||||
let inner = Popup::menu(&button_response)
|
||||
.id(popup_id)
|
||||
.width(button_response.rect.width())
|
||||
.close_behavior(close_behavior)
|
||||
.style(popup_style)
|
||||
.show(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
|
||||
ScrollArea::vertical()
|
||||
.max_height(height)
|
||||
.show(ui, |ui| {
|
||||
@@ -427,8 +416,8 @@ fn combo_box_dyn<'c, R>(
|
||||
menu_contents(ui)
|
||||
})
|
||||
.inner
|
||||
},
|
||||
);
|
||||
})
|
||||
.map(|r| r.inner);
|
||||
|
||||
InnerResponse {
|
||||
inner,
|
||||
@@ -484,33 +473,19 @@ fn button_frame(
|
||||
response
|
||||
}
|
||||
|
||||
fn paint_default_icon(
|
||||
painter: &Painter,
|
||||
rect: Rect,
|
||||
visuals: &WidgetVisuals,
|
||||
above_or_below: AboveOrBelow,
|
||||
) {
|
||||
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
|
||||
let rect = Rect::from_center_size(
|
||||
rect.center(),
|
||||
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
||||
);
|
||||
|
||||
match above_or_below {
|
||||
AboveOrBelow::Above => {
|
||||
// Upward pointing triangle
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
AboveOrBelow::Below => {
|
||||
// Downward pointing triangle
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
}
|
||||
// Downward pointing triangle
|
||||
// Previously, we would show an up arrow when we expected the popup to open upwards
|
||||
// (due to lack of space below the button), but this could look weird in edge cases, so this
|
||||
// feature was removed. (See https://github.com/emilk/egui/pull/5713#issuecomment-2654420245)
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Frame container
|
||||
|
||||
use crate::{
|
||||
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
|
||||
UiStackInfo,
|
||||
InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, epaint,
|
||||
layers::ShapeIdx,
|
||||
};
|
||||
use epaint::{Color32, CornerRadius, Margin, Marginf, Rect, Shadow, Shape, Stroke};
|
||||
use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stroke};
|
||||
|
||||
/// A frame around some content, including margin, colors, etc.
|
||||
///
|
||||
@@ -46,7 +46,7 @@ use epaint::{Color32, CornerRadius, Margin, Marginf, Rect, Shadow, Shape, Stroke
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// egui::Frame::none()
|
||||
/// egui::Frame::NONE
|
||||
/// .fill(egui::Color32::RED)
|
||||
/// .show(ui, |ui| {
|
||||
/// ui.label("Label with red background");
|
||||
@@ -143,7 +143,8 @@ pub struct Frame {
|
||||
#[test]
|
||||
fn frame_size() {
|
||||
assert_eq!(
|
||||
std::mem::size_of::<Frame>(), 32,
|
||||
std::mem::size_of::<Frame>(),
|
||||
32,
|
||||
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
|
||||
);
|
||||
assert!(
|
||||
@@ -337,10 +338,10 @@ impl Frame {
|
||||
///
|
||||
/// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`].
|
||||
#[inline]
|
||||
pub fn total_margin(&self) -> Marginf {
|
||||
Marginf::from(self.inner_margin)
|
||||
+ Marginf::from(self.stroke.width)
|
||||
+ Marginf::from(self.outer_margin)
|
||||
pub fn total_margin(&self) -> MarginF32 {
|
||||
MarginF32::from(self.inner_margin)
|
||||
+ MarginF32::from(self.stroke.width)
|
||||
+ MarginF32::from(self.outer_margin)
|
||||
}
|
||||
|
||||
/// Calculate the `fill_rect` from the `content_rect`.
|
||||
@@ -354,14 +355,14 @@ impl Frame {
|
||||
///
|
||||
/// This is the visible and interactive rectangle.
|
||||
pub fn widget_rect(&self, content_rect: Rect) -> Rect {
|
||||
content_rect + self.inner_margin + Marginf::from(self.stroke.width)
|
||||
content_rect + self.inner_margin + MarginF32::from(self.stroke.width)
|
||||
}
|
||||
|
||||
/// Calculate the `outer_rect` from the `content_rect`.
|
||||
///
|
||||
/// This is what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`].
|
||||
pub fn outer_rect(&self, content_rect: Rect) -> Rect {
|
||||
content_rect + self.inner_margin + Marginf::from(self.stroke.width) + self.outer_margin
|
||||
content_rect + self.inner_margin + MarginF32::from(self.stroke.width) + self.outer_margin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +464,7 @@ impl Prepared {
|
||||
let content_rect = self.content_ui.min_rect();
|
||||
content_rect
|
||||
+ self.frame.inner_margin
|
||||
+ Marginf::from(self.frame.stroke.width)
|
||||
+ MarginF32::from(self.frame.stroke.width)
|
||||
+ self.frame.outer_margin
|
||||
}
|
||||
|
||||
|
||||
592
crates/egui/src/containers/menu.rs
Normal file
592
crates/egui/src/containers/menu.rs
Normal file
@@ -0,0 +1,592 @@
|
||||
//! Popup menus, context menus and menu bars.
|
||||
//!
|
||||
//! Show menus via
|
||||
//! - [`Popup::menu`] and [`Popup::context_menu`]
|
||||
//! - [`Ui::menu_button`], [`MenuButton`] and [`SubMenuButton`]
|
||||
//! - [`MenuBar`]
|
||||
//! - [`Response::context_menu`]
|
||||
//!
|
||||
//! See [`MenuBar`] for an example.
|
||||
|
||||
use crate::style::StyleModifier;
|
||||
use crate::{
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
|
||||
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
|
||||
};
|
||||
use emath::{Align, RectAlign, Vec2, vec2};
|
||||
use epaint::Stroke;
|
||||
|
||||
/// Apply a menu style to the [`Style`].
|
||||
///
|
||||
/// Mainly removes the background stroke and the inactive background fill.
|
||||
pub fn menu_style(style: &mut Style) {
|
||||
style.spacing.button_padding = vec2(2.0, 0.0);
|
||||
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
|
||||
style.visuals.widgets.open.bg_stroke = Stroke::NONE;
|
||||
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
|
||||
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
||||
}
|
||||
|
||||
/// Find the root [`UiStack`] of the menu.
|
||||
pub fn find_menu_root(ui: &Ui) -> &UiStack {
|
||||
ui.stack()
|
||||
.iter()
|
||||
.find(|stack| {
|
||||
stack.is_root_ui()
|
||||
|| [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind())
|
||||
|| stack.info.tags.contains(MenuConfig::MENU_CONFIG_TAG)
|
||||
})
|
||||
.expect("We should always find the root")
|
||||
}
|
||||
|
||||
/// Is this Ui part of a menu?
|
||||
///
|
||||
/// Returns `false` if this is a menu bar.
|
||||
/// Should be used to determine if we should show a menu button or submenu button.
|
||||
pub fn is_in_menu(ui: &Ui) -> bool {
|
||||
for stack in ui.stack().iter() {
|
||||
if let Some(config) = stack
|
||||
.info
|
||||
.tags
|
||||
.get_downcast::<MenuConfig>(MenuConfig::MENU_CONFIG_TAG)
|
||||
{
|
||||
return !config.bar;
|
||||
}
|
||||
if [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Configuration and style for menus.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MenuConfig {
|
||||
/// Is this a menu bar?
|
||||
bar: bool,
|
||||
|
||||
/// If the user clicks, should we close the menu?
|
||||
pub close_behavior: PopupCloseBehavior,
|
||||
|
||||
/// Override the menu style.
|
||||
///
|
||||
/// Default is [`menu_style`].
|
||||
pub style: StyleModifier,
|
||||
}
|
||||
|
||||
impl Default for MenuConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
close_behavior: PopupCloseBehavior::default(),
|
||||
bar: false,
|
||||
style: menu_style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MenuConfig {
|
||||
/// The tag used to store the menu config in the [`UiStack`].
|
||||
pub const MENU_CONFIG_TAG: &'static str = "egui_menu_config";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// If the user clicks, should we close the menu?
|
||||
#[inline]
|
||||
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
|
||||
self.close_behavior = close_behavior;
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the menu style.
|
||||
///
|
||||
/// Default is [`menu_style`].
|
||||
#[inline]
|
||||
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn from_stack(stack: &UiStack) -> Self {
|
||||
stack
|
||||
.info
|
||||
.tags
|
||||
.get_downcast(Self::MENU_CONFIG_TAG)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Find the config for the current menu.
|
||||
///
|
||||
/// Returns the default config if no config is found.
|
||||
pub fn find(ui: &Ui) -> Self {
|
||||
find_menu_root(ui)
|
||||
.info
|
||||
.tags
|
||||
.get_downcast(Self::MENU_CONFIG_TAG)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the state of the menu.
|
||||
#[derive(Clone)]
|
||||
pub struct MenuState {
|
||||
/// The currently open sub menu in this menu.
|
||||
pub open_item: Option<Id>,
|
||||
last_visible_pass: u64,
|
||||
}
|
||||
|
||||
impl MenuState {
|
||||
pub const ID: &'static str = "menu_state";
|
||||
|
||||
/// Find the root of the menu and get the state
|
||||
pub fn from_ui<R>(ui: &Ui, f: impl FnOnce(&mut Self, &UiStack) -> R) -> R {
|
||||
let stack = find_menu_root(ui);
|
||||
Self::from_id(ui.ctx(), stack.id, |state| f(state, stack))
|
||||
}
|
||||
|
||||
/// Get the state via the menus root [`Ui`] id
|
||||
pub fn from_id<R>(ctx: &Context, id: Id, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
let pass_nr = ctx.cumulative_pass_nr();
|
||||
ctx.data_mut(|data| {
|
||||
let state_id = id.with(Self::ID);
|
||||
let mut state = data.get_temp(state_id).unwrap_or(Self {
|
||||
open_item: None,
|
||||
last_visible_pass: pass_nr,
|
||||
});
|
||||
// If the menu was closed for at least a frame, reset the open item
|
||||
if state.last_visible_pass + 1 < pass_nr {
|
||||
state.open_item = None;
|
||||
}
|
||||
if let Some(item) = state.open_item {
|
||||
if data
|
||||
.get_temp(item.with(Self::ID))
|
||||
.is_none_or(|item: Self| item.last_visible_pass + 1 < pass_nr)
|
||||
{
|
||||
// If the open item wasn't shown for at least a frame, reset the open item
|
||||
state.open_item = None;
|
||||
}
|
||||
}
|
||||
let r = f(&mut state);
|
||||
data.insert_temp(state_id, state);
|
||||
r
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mark_shown(ctx: &Context, id: Id) {
|
||||
let pass_nr = ctx.cumulative_pass_nr();
|
||||
Self::from_id(ctx, id, |state| {
|
||||
state.last_visible_pass = pass_nr;
|
||||
});
|
||||
}
|
||||
|
||||
/// Is the menu with this id the deepest sub menu? (-> no child sub menu is open)
|
||||
///
|
||||
/// Note: This only returns correct results if called after the menu contents were shown.
|
||||
pub fn is_deepest_open_sub_menu(ctx: &Context, id: Id) -> bool {
|
||||
let pass_nr = ctx.cumulative_pass_nr();
|
||||
let open_item = Self::from_id(ctx, id, |state| state.open_item);
|
||||
// If we have some open item, check if that was actually shown this frame
|
||||
open_item.is_none_or(|submenu_id| {
|
||||
Self::from_id(ctx, submenu_id, |state| state.last_visible_pass != pass_nr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal menu bar where you can add [`MenuButton`]s.
|
||||
///
|
||||
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
|
||||
/// but can also be placed in a [`crate::Window`].
|
||||
/// In the latter case you may want to wrap it in [`Frame`].
|
||||
///
|
||||
/// ### Example:
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// egui::MenuBar::new().ui(ui, |ui| {
|
||||
/// ui.menu_button("File", |ui| {
|
||||
/// if ui.button("Quit").clicked() {
|
||||
/// ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
/// }
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MenuBar {
|
||||
config: MenuConfig,
|
||||
style: StyleModifier,
|
||||
}
|
||||
|
||||
#[deprecated = "Renamed to `egui::MenuBar`"]
|
||||
pub type Bar = MenuBar;
|
||||
|
||||
impl Default for MenuBar {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: MenuConfig::default(),
|
||||
style: menu_style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MenuBar {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the style for buttons in the menu bar.
|
||||
///
|
||||
/// Doesn't affect the style of submenus, use [`MenuConfig::style`] for that.
|
||||
/// Default is [`menu_style`].
|
||||
#[inline]
|
||||
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the config for submenus.
|
||||
///
|
||||
/// Note: The config will only be passed when using [`MenuButton`], not via [`Popup::menu`].
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the menu bar.
|
||||
#[inline]
|
||||
pub fn ui<R>(self, ui: &mut Ui, content: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
let Self { mut config, style } = self;
|
||||
config.bar = true;
|
||||
// TODO(lucasmerlin): It'd be nice if we had a ui.horizontal_builder or something
|
||||
// So we don't need the nested scope here
|
||||
ui.horizontal(|ui| {
|
||||
ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.layout(Layout::left_to_right(Align::Center))
|
||||
.ui_stack_info(
|
||||
UiStackInfo::new(UiKind::Menu)
|
||||
.with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
|
||||
),
|
||||
|ui| {
|
||||
style.apply(ui.style_mut());
|
||||
|
||||
// Take full width and fixed height:
|
||||
let height = ui.spacing().interact_size.y;
|
||||
ui.set_min_size(vec2(ui.available_width(), height));
|
||||
|
||||
content(ui)
|
||||
},
|
||||
)
|
||||
.inner
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A thin wrapper around a [`Button`] that shows a [`Popup::menu`] when clicked.
|
||||
///
|
||||
/// The only thing this does is search for the current menu config (if set via [`MenuBar`]).
|
||||
/// If your menu button is not in a [`MenuBar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
|
||||
/// directly.
|
||||
pub struct MenuButton<'a> {
|
||||
pub button: Button<'a>,
|
||||
pub config: Option<MenuConfig>,
|
||||
}
|
||||
|
||||
impl<'a> MenuButton<'a> {
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self::from_button(Button::new(atoms.into_atoms()))
|
||||
}
|
||||
|
||||
/// Set the config for the menu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new menu button from a [`Button`].
|
||||
#[inline]
|
||||
pub fn from_button(button: Button<'a>) -> Self {
|
||||
Self {
|
||||
button,
|
||||
config: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the menu button.
|
||||
pub fn ui<R>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
content: impl FnOnce(&mut Ui) -> R,
|
||||
) -> (Response, Option<InnerResponse<R>>) {
|
||||
let response = self.button.ui(ui);
|
||||
let mut config = self.config.unwrap_or_else(|| MenuConfig::find(ui));
|
||||
config.bar = false;
|
||||
let inner = Popup::menu(&response)
|
||||
.close_behavior(config.close_behavior)
|
||||
.style(config.style.clone())
|
||||
.info(
|
||||
UiStackInfo::new(UiKind::Menu).with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
|
||||
)
|
||||
.show(content);
|
||||
(response, inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A submenu button that shows a [`SubMenu`] if a [`Button`] is hovered.
|
||||
pub struct SubMenuButton<'a> {
|
||||
pub button: Button<'a>,
|
||||
pub sub_menu: SubMenu,
|
||||
}
|
||||
|
||||
impl<'a> SubMenuButton<'a> {
|
||||
/// The default right arrow symbol: `"⏵"`
|
||||
pub const RIGHT_ARROW: &'static str = "⏵";
|
||||
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self::from_button(Button::new(atoms.into_atoms()).right_text("⏵"))
|
||||
}
|
||||
|
||||
/// Create a new submenu button from a [`Button`].
|
||||
///
|
||||
/// Use [`Button::right_text`] and [`SubMenuButton::RIGHT_ARROW`] to add the default right
|
||||
/// arrow symbol.
|
||||
pub fn from_button(button: Button<'a>) -> Self {
|
||||
Self {
|
||||
button,
|
||||
sub_menu: SubMenu::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the config for the submenu.
|
||||
///
|
||||
/// The close behavior will not affect the current button, but the buttons in the submenu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.sub_menu.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the submenu button.
|
||||
pub fn ui<R>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
content: impl FnOnce(&mut Ui) -> R,
|
||||
) -> (Response, Option<InnerResponse<R>>) {
|
||||
let my_id = ui.next_auto_id();
|
||||
let open = MenuState::from_ui(ui, |state, _| {
|
||||
state.open_item == Some(SubMenu::id_from_widget_id(my_id))
|
||||
});
|
||||
let inactive = ui.style().visuals.widgets.inactive;
|
||||
// TODO(lucasmerlin) add `open` function to `Button`
|
||||
if open {
|
||||
ui.style_mut().visuals.widgets.inactive = ui.style().visuals.widgets.open;
|
||||
}
|
||||
let response = self.button.ui(ui);
|
||||
ui.style_mut().visuals.widgets.inactive = inactive;
|
||||
|
||||
let popup_response = self.sub_menu.show(ui, &response, content);
|
||||
|
||||
(response, popup_response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a submenu in a menu.
|
||||
///
|
||||
/// Useful if you want to make custom menu buttons.
|
||||
/// Usually, just use [`MenuButton`] or [`SubMenuButton`] instead.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SubMenu {
|
||||
config: Option<MenuConfig>,
|
||||
}
|
||||
|
||||
impl SubMenu {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the config for the submenu.
|
||||
///
|
||||
/// The close behavior will not affect the current button, but the buttons in the submenu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the id for the submenu from the widget/response id.
|
||||
pub fn id_from_widget_id(widget_id: Id) -> Id {
|
||||
widget_id.with("submenu")
|
||||
}
|
||||
|
||||
/// Show the submenu.
|
||||
///
|
||||
/// This does some heuristics to check if the `button_response` was the last thing in the
|
||||
/// menu that was hovered/clicked, and if so, shows the submenu.
|
||||
pub fn show<R>(
|
||||
self,
|
||||
ui: &Ui,
|
||||
button_response: &Response,
|
||||
content: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
let frame = Frame::menu(ui.style());
|
||||
|
||||
let id = Self::id_from_widget_id(button_response.id);
|
||||
|
||||
// Get the state from the parent menu
|
||||
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
|
||||
(state.open_item, stack.id, MenuConfig::from_stack(stack))
|
||||
});
|
||||
|
||||
let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone());
|
||||
menu_config.bar = false;
|
||||
|
||||
let menu_root_response = ui
|
||||
.ctx()
|
||||
.read_response(menu_id)
|
||||
// Since we are a child of that ui, this should always exist
|
||||
.unwrap();
|
||||
|
||||
let hover_pos = ui.ctx().pointer_hover_pos();
|
||||
|
||||
// We don't care if the user is hovering over the border
|
||||
let menu_rect = menu_root_response.rect - frame.total_margin();
|
||||
let is_hovering_menu = hover_pos.is_some_and(|pos| {
|
||||
ui.ctx().layer_id_at(pos) == Some(menu_root_response.layer_id)
|
||||
&& menu_rect.contains(pos)
|
||||
});
|
||||
|
||||
let is_any_open = open_item.is_some();
|
||||
let mut is_open = open_item == Some(id);
|
||||
let mut set_open = None;
|
||||
|
||||
// We expand the button rect so there is no empty space where no menu is shown
|
||||
// TODO(lucasmerlin): Instead, maybe make item_spacing.y 0.0?
|
||||
let button_rect = button_response
|
||||
.rect
|
||||
.expand2(ui.style().spacing.item_spacing / 2.0);
|
||||
|
||||
// In theory some other widget could cover the button and this check would still pass
|
||||
// But since we check if no other menu is open, nothing should be able to cover the button
|
||||
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
|
||||
|
||||
// The clicked handler is there for accessibility (keyboard navigation)
|
||||
let should_open =
|
||||
ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open));
|
||||
if should_open {
|
||||
set_open = Some(true);
|
||||
is_open = true;
|
||||
// Ensure that all other sub menus are closed when we open the menu
|
||||
MenuState::from_id(ui.ctx(), menu_id, |state| {
|
||||
state.open_item = None;
|
||||
});
|
||||
}
|
||||
|
||||
let gap = frame.total_margin().sum().x / 2.0 + 2.0;
|
||||
|
||||
let mut response = button_response.clone();
|
||||
// Expand the button rect so that the button and the first item in the submenu are aligned
|
||||
let expand = Vec2::new(0.0, frame.total_margin().sum().y / 2.0);
|
||||
response.interact_rect = response.interact_rect.expand2(expand);
|
||||
|
||||
let popup_response = Popup::from_response(&response)
|
||||
.id(id)
|
||||
.open(is_open)
|
||||
.align(RectAlign::RIGHT_START)
|
||||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.gap(gap)
|
||||
.style(menu_config.style.clone())
|
||||
.frame(frame)
|
||||
// The close behavior is handled by the menu (see below)
|
||||
.close_behavior(PopupCloseBehavior::IgnoreClicks)
|
||||
.info(
|
||||
UiStackInfo::new(UiKind::Menu)
|
||||
.with_tag_value(MenuConfig::MENU_CONFIG_TAG, menu_config.clone()),
|
||||
)
|
||||
.show(|ui| {
|
||||
// Ensure our layer stays on top when the button is clicked
|
||||
if button_response.clicked() || button_response.is_pointer_button_down_on() {
|
||||
ui.ctx().move_to_top(ui.layer_id());
|
||||
}
|
||||
content(ui)
|
||||
});
|
||||
|
||||
if let Some(popup_response) = &popup_response {
|
||||
// If no child sub menu is open means we must be the deepest child sub menu.
|
||||
let is_deepest_submenu = MenuState::is_deepest_open_sub_menu(ui.ctx(), id);
|
||||
|
||||
// If the user clicks and the cursor is not hovering over our menu rect, it's
|
||||
// safe to assume they clicked outside the menu, so we close everything.
|
||||
// If they were to hover some other parent submenu we wouldn't be open.
|
||||
// Only edge case is the user hovering this submenu's button, so we also check
|
||||
// if we clicked outside the parent menu (which we luckily have access to here).
|
||||
let clicked_outside = is_deepest_submenu
|
||||
&& popup_response.response.clicked_elsewhere()
|
||||
&& menu_root_response.clicked_elsewhere();
|
||||
|
||||
// We never automatically close when a submenu button is clicked, (so menus work
|
||||
// on touch devices)
|
||||
// Luckily we will always be the deepest submenu when a submenu button is clicked,
|
||||
// so the following check is enough.
|
||||
let submenu_button_clicked = button_response.clicked();
|
||||
|
||||
let clicked_inside = is_deepest_submenu
|
||||
&& !submenu_button_clicked
|
||||
&& response.ctx.input(|i| i.pointer.any_click())
|
||||
&& hover_pos.is_some_and(|pos| popup_response.response.interact_rect.contains(pos));
|
||||
|
||||
let click_close = match menu_config.close_behavior {
|
||||
PopupCloseBehavior::CloseOnClick => clicked_outside || clicked_inside,
|
||||
PopupCloseBehavior::CloseOnClickOutside => clicked_outside,
|
||||
PopupCloseBehavior::IgnoreClicks => false,
|
||||
};
|
||||
|
||||
if click_close {
|
||||
set_open = Some(false);
|
||||
ui.close();
|
||||
}
|
||||
|
||||
let is_moving_towards_rect = ui.input(|i| {
|
||||
i.pointer
|
||||
.is_moving_towards_rect(&popup_response.response.rect)
|
||||
});
|
||||
if is_moving_towards_rect {
|
||||
// We need to repaint while this is true, so we can detect when
|
||||
// the pointer is no longer moving towards the rect
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
let hovering_other_menu_entry = is_open
|
||||
&& !is_hovered
|
||||
&& !popup_response.response.contains_pointer()
|
||||
&& !is_moving_towards_rect
|
||||
&& is_hovering_menu;
|
||||
|
||||
let close_called = popup_response.response.should_close();
|
||||
|
||||
// Close the parent ui to e.g. close the popup from where the submenu was opened
|
||||
if close_called {
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if hovering_other_menu_entry {
|
||||
set_open = Some(false);
|
||||
}
|
||||
|
||||
if ui.will_parent_close() {
|
||||
ui.data_mut(|data| data.remove_by_type::<MenuState>());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(set_open) = set_open {
|
||||
MenuState::from_id(ui.ctx(), menu_id, |state| {
|
||||
state.open_item = set_open.then_some(id);
|
||||
});
|
||||
}
|
||||
|
||||
popup_response
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,36 @@
|
||||
//! For instance, a [`Frame`] adds a frame and background to some contained UI.
|
||||
|
||||
pub(crate) mod area;
|
||||
mod close_tag;
|
||||
pub mod collapsing_header;
|
||||
mod combo_box;
|
||||
pub mod frame;
|
||||
pub mod menu;
|
||||
pub mod modal;
|
||||
pub mod old_popup;
|
||||
pub mod panel;
|
||||
pub mod popup;
|
||||
mod popup;
|
||||
pub(crate) mod resize;
|
||||
mod scene;
|
||||
pub mod scroll_area;
|
||||
mod sides;
|
||||
mod tooltip;
|
||||
pub(crate) mod window;
|
||||
|
||||
pub use {
|
||||
area::{Area, AreaState},
|
||||
close_tag::ClosableTag,
|
||||
collapsing_header::{CollapsingHeader, CollapsingResponse},
|
||||
combo_box::*,
|
||||
frame::Frame,
|
||||
modal::{Modal, ModalResponse},
|
||||
panel::{CentralPanel, Panel, PanelSide, VerticalSide, HorizontalSide},
|
||||
old_popup::*,
|
||||
panel::{CentralPanel, HorizontalSide, Panel, PanelSide, VerticalSide},
|
||||
popup::*,
|
||||
resize::Resize,
|
||||
scene::Scene,
|
||||
scene::{DragPanButtons, Scene},
|
||||
scroll_area::ScrollArea,
|
||||
sides::Sides,
|
||||
tooltip::*,
|
||||
window::Window,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use emath::{Align2, Vec2};
|
||||
|
||||
use crate::{
|
||||
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
|
||||
};
|
||||
use emath::{Align2, Vec2};
|
||||
|
||||
/// A modal dialog.
|
||||
///
|
||||
/// Similar to a [`crate::Window`] but centered and with a backdrop that
|
||||
/// blocks input to the rest of the UI.
|
||||
///
|
||||
/// You can show multiple modals on top of each other. The topmost modal will always be
|
||||
/// the most recently shown one.
|
||||
/// If multiple modals are newly shown in the same frame, the order of the modals is undefined
|
||||
/// (either first or second could be top).
|
||||
pub struct Modal {
|
||||
pub area: Area,
|
||||
pub backdrop_color: Color32,
|
||||
@@ -16,7 +20,9 @@ pub struct Modal {
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
/// Create a new Modal. The id is passed to the area.
|
||||
/// Create a new Modal.
|
||||
///
|
||||
/// The id is passed to the area.
|
||||
pub fn new(id: Id) -> Self {
|
||||
Self {
|
||||
area: Self::default_area(id),
|
||||
@@ -26,6 +32,7 @@ impl Modal {
|
||||
}
|
||||
|
||||
/// Returns an area customized for a modal.
|
||||
///
|
||||
/// Makes these changes to the default area:
|
||||
/// - sense: hover
|
||||
/// - anchor: center
|
||||
@@ -74,18 +81,16 @@ impl Modal {
|
||||
frame,
|
||||
} = self;
|
||||
|
||||
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
|
||||
let is_top_modal = ctx.memory_mut(|mem| {
|
||||
mem.set_modal_layer(area.layer());
|
||||
(
|
||||
mem.top_modal_layer() == Some(area.layer()),
|
||||
mem.any_popup_open(),
|
||||
)
|
||||
mem.top_modal_layer() == Some(area.layer())
|
||||
});
|
||||
let any_popup_open = crate::Popup::is_any_open(ctx);
|
||||
let InnerResponse {
|
||||
inner: (inner, backdrop_response),
|
||||
response,
|
||||
} = area.show(ctx, |ui| {
|
||||
let bg_rect = ui.ctx().screen_rect();
|
||||
let bg_rect = ui.ctx().content_rect();
|
||||
let bg_sense = Sense::CLICK | Sense::DRAG;
|
||||
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
|
||||
backdrop.set_min_size(bg_rect.size());
|
||||
@@ -150,7 +155,10 @@ impl<T> ModalResponse<T> {
|
||||
let escape_clicked =
|
||||
|| ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
|
||||
|
||||
let ui_close_called = self.response.should_close();
|
||||
|
||||
self.backdrop_response.clicked()
|
||||
|| ui_close_called
|
||||
|| (self.is_top_modal && !self.any_popup_open && escape_clicked())
|
||||
}
|
||||
}
|
||||
|
||||
212
crates/egui/src/containers/old_popup.rs
Normal file
212
crates/egui/src/containers/old_popup.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
//! Old and deprecated API for popups. Use [`Popup`] instead.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use crate::containers::tooltip::Tooltip;
|
||||
use crate::{
|
||||
Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect,
|
||||
Response, Ui, Widget as _, WidgetText,
|
||||
};
|
||||
use emath::RectAlign;
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Show a tooltip at the current pointer position (if any).
|
||||
///
|
||||
/// Most of the time it is easier to use [`Response::on_hover_ui`].
|
||||
///
|
||||
/// See also [`show_tooltip_text`].
|
||||
///
|
||||
/// Returns `None` if the tooltip could not be placed.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # #[expect(deprecated)]
|
||||
/// if ui.ui_contains_pointer() {
|
||||
/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
|
||||
/// ui.label("Helpful text");
|
||||
/// });
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||
pub fn show_tooltip<R>(
|
||||
ctx: &Context,
|
||||
parent_layer: LayerId,
|
||||
widget_id: Id,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
|
||||
}
|
||||
|
||||
/// Show a tooltip at the current pointer position (if any).
|
||||
///
|
||||
/// Most of the time it is easier to use [`Response::on_hover_ui`].
|
||||
///
|
||||
/// See also [`show_tooltip_text`].
|
||||
///
|
||||
/// Returns `None` if the tooltip could not be placed.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if ui.ui_contains_pointer() {
|
||||
/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
|
||||
/// ui.label("Helpful text");
|
||||
/// });
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||
pub fn show_tooltip_at_pointer<R>(
|
||||
ctx: &Context,
|
||||
parent_layer: LayerId,
|
||||
widget_id: Id,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
|
||||
.gap(12.0)
|
||||
.show(add_contents)
|
||||
.map(|response| response.inner)
|
||||
}
|
||||
|
||||
/// Show a tooltip under the given area.
|
||||
///
|
||||
/// If the tooltip does not fit under the area, it tries to place it above it instead.
|
||||
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||
pub fn show_tooltip_for<R>(
|
||||
ctx: &Context,
|
||||
parent_layer: LayerId,
|
||||
widget_id: Id,
|
||||
widget_rect: &Rect,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, *widget_rect)
|
||||
.show(add_contents)
|
||||
.map(|response| response.inner)
|
||||
}
|
||||
|
||||
/// Show a tooltip at the given position.
|
||||
///
|
||||
/// Returns `None` if the tooltip could not be placed.
|
||||
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||
pub fn show_tooltip_at<R>(
|
||||
ctx: &Context,
|
||||
parent_layer: LayerId,
|
||||
widget_id: Id,
|
||||
suggested_position: Pos2,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, suggested_position)
|
||||
.show(add_contents)
|
||||
.map(|response| response.inner)
|
||||
}
|
||||
|
||||
/// Show some text at the current pointer position (if any).
|
||||
///
|
||||
/// Most of the time it is easier to use [`Response::on_hover_text`].
|
||||
///
|
||||
/// See also [`show_tooltip`].
|
||||
///
|
||||
/// Returns `None` if the tooltip could not be placed.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if ui.ui_contains_pointer() {
|
||||
/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text");
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||
pub fn show_tooltip_text(
|
||||
ctx: &Context,
|
||||
parent_layer: LayerId,
|
||||
widget_id: Id,
|
||||
text: impl Into<WidgetText>,
|
||||
) -> Option<()> {
|
||||
show_tooltip(ctx, parent_layer, widget_id, |ui| {
|
||||
crate::widgets::Label::new(text).ui(ui);
|
||||
})
|
||||
}
|
||||
|
||||
/// Was this tooltip visible last frame?
|
||||
#[deprecated = "Use `Tooltip::was_tooltip_open_last_frame` instead"]
|
||||
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
|
||||
Tooltip::was_tooltip_open_last_frame(ctx, widget_id)
|
||||
}
|
||||
|
||||
/// Indicate whether a popup will be shown above or below the box.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum AboveOrBelow {
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// Helper for [`popup_above_or_below_widget`].
|
||||
#[deprecated = "Use `egui::Popup` instead"]
|
||||
pub fn popup_below_widget<R>(
|
||||
ui: &Ui,
|
||||
popup_id: Id,
|
||||
widget_response: &Response,
|
||||
close_behavior: PopupCloseBehavior,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
popup_above_or_below_widget(
|
||||
ui,
|
||||
popup_id,
|
||||
widget_response,
|
||||
AboveOrBelow::Below,
|
||||
close_behavior,
|
||||
add_contents,
|
||||
)
|
||||
}
|
||||
|
||||
/// Shows a popup above or below another widget.
|
||||
///
|
||||
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
|
||||
///
|
||||
/// The opened popup will have a minimum width matching its parent.
|
||||
///
|
||||
/// You must open the popup with [`crate::Memory::open_popup`] or [`crate::Memory::toggle_popup`].
|
||||
///
|
||||
/// Returns `None` if the popup is not open.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// let response = ui.button("Open popup");
|
||||
/// let popup_id = ui.make_persistent_id("my_unique_id");
|
||||
/// if response.clicked() {
|
||||
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
/// }
|
||||
/// let below = egui::AboveOrBelow::Below;
|
||||
/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside;
|
||||
/// # #[expect(deprecated)]
|
||||
/// egui::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| {
|
||||
/// ui.set_min_width(200.0); // if you want to control the size
|
||||
/// ui.label("Some more info, or things you can select:");
|
||||
/// ui.label("…");
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
#[deprecated = "Use `egui::Popup` instead"]
|
||||
pub fn popup_above_or_below_widget<R>(
|
||||
_parent_ui: &Ui,
|
||||
popup_id: Id,
|
||||
widget_response: &Response,
|
||||
above_or_below: AboveOrBelow,
|
||||
close_behavior: PopupCloseBehavior,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let response = Popup::from_response(widget_response)
|
||||
.layout(Layout::top_down_justified(Align::LEFT))
|
||||
.open_memory(None)
|
||||
.close_behavior(close_behavior)
|
||||
.id(popup_id)
|
||||
.align(match above_or_below {
|
||||
AboveOrBelow::Above => RectAlign::TOP_START,
|
||||
AboveOrBelow::Below => RectAlign::BOTTOM_START,
|
||||
})
|
||||
.width(widget_response.rect.width())
|
||||
.show(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
add_contents(ui)
|
||||
})?;
|
||||
Some(response.inner)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt, Rect, Response, Sense, Shape, Ui,
|
||||
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
|
||||
Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense, Shape, Ui,
|
||||
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -220,7 +220,7 @@ impl Resize {
|
||||
.at_least(self.min_size)
|
||||
.at_most(self.max_size)
|
||||
.at_most(
|
||||
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
||||
ui.ctx().content_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
||||
)
|
||||
.round_ui();
|
||||
|
||||
@@ -241,14 +241,12 @@ impl Resize {
|
||||
|
||||
let corner_id = self.resizable.any().then(|| id.with("__resize_corner"));
|
||||
|
||||
if let Some(corner_id) = corner_id {
|
||||
if let Some(corner_response) = ui.ctx().read_response(corner_id) {
|
||||
if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
|
||||
// Respond to the interaction early to avoid frame delay.
|
||||
user_requested_size =
|
||||
Some(pointer_pos - position + 0.5 * corner_response.rect.size());
|
||||
}
|
||||
}
|
||||
if let Some(corner_id) = corner_id
|
||||
&& let Some(corner_response) = ui.ctx().read_response(corner_id)
|
||||
&& let Some(pointer_pos) = corner_response.interact_pointer_pos()
|
||||
{
|
||||
// Respond to the interaction early to avoid frame delay.
|
||||
user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size());
|
||||
}
|
||||
|
||||
if let Some(user_requested_size) = user_requested_size {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use core::f32;
|
||||
|
||||
use emath::{GuiRounding, Pos2};
|
||||
use emath::{GuiRounding as _, Pos2};
|
||||
|
||||
use crate::{
|
||||
emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
|
||||
InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
|
||||
emath::TSTransform,
|
||||
};
|
||||
|
||||
/// Creates a transformation that fits a given scene rectangle into the available screen size.
|
||||
@@ -44,14 +45,41 @@ fn fit_to_rect_in_scene(
|
||||
#[must_use = "You should call .show()"]
|
||||
pub struct Scene {
|
||||
zoom_range: Rangef,
|
||||
sense: Sense,
|
||||
max_inner_size: Vec2,
|
||||
drag_pan_buttons: DragPanButtons,
|
||||
}
|
||||
|
||||
/// Specifies which pointer buttons can be used to pan the scene by dragging.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct DragPanButtons(u8);
|
||||
|
||||
bitflags::bitflags! {
|
||||
impl DragPanButtons: u8 {
|
||||
/// [PointerButton::Primary]
|
||||
const PRIMARY = 1 << 0;
|
||||
|
||||
/// [PointerButton::Secondary]
|
||||
const SECONDARY = 1 << 1;
|
||||
|
||||
/// [PointerButton::Middle]
|
||||
const MIDDLE = 1 << 2;
|
||||
|
||||
/// [PointerButton::Extra1]
|
||||
const EXTRA_1 = 1 << 3;
|
||||
|
||||
/// [PointerButton::Extra2]
|
||||
const EXTRA_2 = 1 << 4;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Scene {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
zoom_range: Rangef::new(f32::EPSILON, 1.0),
|
||||
sense: Sense::click_and_drag(),
|
||||
max_inner_size: Vec2::splat(1000.0),
|
||||
drag_pan_buttons: DragPanButtons::all(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +90,17 @@ impl Scene {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Specify what type of input the scene should respond to.
|
||||
///
|
||||
/// The default is `Sense::click_and_drag()`.
|
||||
///
|
||||
/// Set this to `Sense::hover()` to disable panning via clicking and dragging.
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the allowed zoom range.
|
||||
///
|
||||
/// The default zoom range is `0.0..=1.0`,
|
||||
@@ -82,6 +121,15 @@ impl Scene {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify which pointer buttons can be used to pan by clicking and dragging.
|
||||
///
|
||||
/// By default, this is `DragPanButtons::all()`.
|
||||
#[inline]
|
||||
pub fn drag_pan_buttons(mut self, flags: DragPanButtons) -> Self {
|
||||
self.drag_pan_buttons = flags;
|
||||
self
|
||||
}
|
||||
|
||||
/// `scene_rect` contains the view bounds of the inner [`Ui`].
|
||||
///
|
||||
/// `scene_rect` will be mutated by any panning/zooming done by the user.
|
||||
@@ -118,8 +166,10 @@ impl Scene {
|
||||
}
|
||||
|
||||
if !scene_rect_was_good {
|
||||
// Auto-reset if the trsnsformation goes bad somehow (or started bad).
|
||||
*scene_rect = inner_rect;
|
||||
// Auto-reset if the transformation goes bad somehow (or started bad).
|
||||
// Recalculates transform based on inner_rect, resulting in a rect that's the full size of outer_rect but centered on inner_rect.
|
||||
let to_global = fit_to_rect_in_scene(outer_rect, inner_rect, self.zoom_range);
|
||||
*scene_rect = to_global.inverse() * outer_rect;
|
||||
}
|
||||
|
||||
ret
|
||||
@@ -147,7 +197,7 @@ impl Scene {
|
||||
UiBuilder::new()
|
||||
.layer_id(scene_layer_id)
|
||||
.max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size))
|
||||
.sense(Sense::click_and_drag()),
|
||||
.sense(self.sense),
|
||||
);
|
||||
|
||||
let mut pan_response = local_ui.response();
|
||||
@@ -158,17 +208,17 @@ impl Scene {
|
||||
// Set a correct global clip rect:
|
||||
local_ui.set_clip_rect(to_global.inverse() * outer_rect);
|
||||
|
||||
// Tell egui to apply the transform on the layer:
|
||||
local_ui
|
||||
.ctx()
|
||||
.set_transform_layer(scene_layer_id, *to_global);
|
||||
|
||||
// Add the actual contents to the area:
|
||||
let ret = add_contents(&mut local_ui);
|
||||
|
||||
// This ensures we catch clicks/drags/pans anywhere on the background.
|
||||
local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui());
|
||||
|
||||
// Tell egui to apply the transform on the layer:
|
||||
local_ui
|
||||
.ctx()
|
||||
.set_transform_layer(scene_layer_id, *to_global);
|
||||
|
||||
InnerResponse {
|
||||
response: pan_response,
|
||||
inner: ret,
|
||||
@@ -177,43 +227,51 @@ impl Scene {
|
||||
|
||||
/// Helper function to handle pan and zoom interactions on a response.
|
||||
pub fn register_pan_and_zoom(&self, ui: &Ui, resp: &mut Response, to_global: &mut TSTransform) {
|
||||
if resp.dragged() {
|
||||
let dragged = self.drag_pan_buttons.iter().any(|button| match button {
|
||||
DragPanButtons::PRIMARY => resp.dragged_by(PointerButton::Primary),
|
||||
DragPanButtons::SECONDARY => resp.dragged_by(PointerButton::Secondary),
|
||||
DragPanButtons::MIDDLE => resp.dragged_by(PointerButton::Middle),
|
||||
DragPanButtons::EXTRA_1 => resp.dragged_by(PointerButton::Extra1),
|
||||
DragPanButtons::EXTRA_2 => resp.dragged_by(PointerButton::Extra2),
|
||||
_ => false,
|
||||
});
|
||||
if dragged {
|
||||
to_global.translation += to_global.scaling * resp.drag_delta();
|
||||
resp.mark_changed();
|
||||
}
|
||||
|
||||
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos()) {
|
||||
if resp.contains_pointer() {
|
||||
let pointer_in_scene = to_global.inverse() * mouse_pos;
|
||||
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
|
||||
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
|
||||
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos())
|
||||
&& resp.contains_pointer()
|
||||
{
|
||||
let pointer_in_scene = to_global.inverse() * mouse_pos;
|
||||
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
|
||||
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta());
|
||||
|
||||
// Most of the time we can return early. This is also important to
|
||||
// avoid `ui_from_scene` to change slightly due to floating point errors.
|
||||
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
|
||||
return;
|
||||
}
|
||||
|
||||
if zoom_delta != 1.0 {
|
||||
// Zoom in on pointer, but only if we are not zoomed in or out too far.
|
||||
let zoom_delta = zoom_delta.clamp(
|
||||
self.zoom_range.min / to_global.scaling,
|
||||
self.zoom_range.max / to_global.scaling,
|
||||
);
|
||||
|
||||
*to_global = *to_global
|
||||
* TSTransform::from_translation(pointer_in_scene.to_vec2())
|
||||
* TSTransform::from_scaling(zoom_delta)
|
||||
* TSTransform::from_translation(-pointer_in_scene.to_vec2());
|
||||
|
||||
// Clamp to exact zoom range.
|
||||
to_global.scaling = self.zoom_range.clamp(to_global.scaling);
|
||||
}
|
||||
|
||||
// Pan:
|
||||
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
|
||||
resp.mark_changed();
|
||||
// Most of the time we can return early. This is also important to
|
||||
// avoid `ui_from_scene` to change slightly due to floating point errors.
|
||||
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
|
||||
return;
|
||||
}
|
||||
|
||||
if zoom_delta != 1.0 {
|
||||
// Zoom in on pointer, but only if we are not zoomed in or out too far.
|
||||
let zoom_delta = zoom_delta.clamp(
|
||||
self.zoom_range.min / to_global.scaling,
|
||||
self.zoom_range.max / to_global.scaling,
|
||||
);
|
||||
|
||||
*to_global = *to_global
|
||||
* TSTransform::from_translation(pointer_in_scene.to_vec2())
|
||||
* TSTransform::from_scaling(zoom_delta)
|
||||
* TSTransform::from_translation(-pointer_in_scene.to_vec2());
|
||||
|
||||
// Clamp to exact zoom range.
|
||||
to_global.scaling = self.zoom_range.clamp(to_global.scaling);
|
||||
}
|
||||
|
||||
// Pan:
|
||||
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
|
||||
resp.mark_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
||||
|
||||
use emath::GuiRounding as _;
|
||||
|
||||
use crate::{
|
||||
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2,
|
||||
Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
|
||||
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
|
||||
UiKind, UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -133,6 +137,113 @@ impl ScrollBarVisibility {
|
||||
];
|
||||
}
|
||||
|
||||
/// What is the source of scrolling for a [`ScrollArea`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ScrollSource {
|
||||
/// Scroll the area by dragging a scroll bar.
|
||||
///
|
||||
/// By default the scroll bars remain visible to show current position.
|
||||
/// To hide them use [`ScrollArea::scroll_bar_visibility()`].
|
||||
pub scroll_bar: bool,
|
||||
|
||||
/// Scroll the area by dragging the contents.
|
||||
pub drag: bool,
|
||||
|
||||
/// Scroll the area by scrolling (or shift scrolling) the mouse wheel with
|
||||
/// the mouse cursor over the [`ScrollArea`].
|
||||
pub mouse_wheel: bool,
|
||||
}
|
||||
|
||||
impl Default for ScrollSource {
|
||||
fn default() -> Self {
|
||||
Self::ALL
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollSource {
|
||||
pub const NONE: Self = Self {
|
||||
scroll_bar: false,
|
||||
drag: false,
|
||||
mouse_wheel: false,
|
||||
};
|
||||
pub const ALL: Self = Self {
|
||||
scroll_bar: true,
|
||||
drag: true,
|
||||
mouse_wheel: true,
|
||||
};
|
||||
pub const SCROLL_BAR: Self = Self {
|
||||
scroll_bar: true,
|
||||
drag: false,
|
||||
mouse_wheel: false,
|
||||
};
|
||||
pub const DRAG: Self = Self {
|
||||
scroll_bar: false,
|
||||
drag: true,
|
||||
mouse_wheel: false,
|
||||
};
|
||||
pub const MOUSE_WHEEL: Self = Self {
|
||||
scroll_bar: false,
|
||||
drag: false,
|
||||
mouse_wheel: true,
|
||||
};
|
||||
|
||||
/// Is everything disabled?
|
||||
#[inline]
|
||||
pub fn is_none(&self) -> bool {
|
||||
self == &Self::NONE
|
||||
}
|
||||
|
||||
/// Is anything enabled?
|
||||
#[inline]
|
||||
pub fn any(&self) -> bool {
|
||||
self.scroll_bar | self.drag | self.mouse_wheel
|
||||
}
|
||||
|
||||
/// Is everything enabled?
|
||||
#[inline]
|
||||
pub fn is_all(&self) -> bool {
|
||||
self.scroll_bar & self.drag & self.mouse_wheel
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr for ScrollSource {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
scroll_bar: self.scroll_bar | rhs.scroll_bar,
|
||||
drag: self.drag | rhs.drag,
|
||||
mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::suspicious_arithmetic_impl)]
|
||||
impl Add for ScrollSource {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
self | rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOrAssign for ScrollSource {
|
||||
#[inline]
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
*self = *self | rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for ScrollSource {
|
||||
#[inline]
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
*self = *self + rhs;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
|
||||
///
|
||||
/// By default, scroll bars only show up when needed, i.e. when the contents
|
||||
@@ -168,7 +279,7 @@ impl ScrollBarVisibility {
|
||||
#[must_use = "You should call .show()"]
|
||||
pub struct ScrollArea {
|
||||
/// Do we have horizontal/vertical scrolling enabled?
|
||||
scroll_enabled: Vec2b,
|
||||
direction_enabled: Vec2b,
|
||||
|
||||
auto_shrink: Vec2b,
|
||||
max_size: Vec2,
|
||||
@@ -178,10 +289,10 @@ pub struct ScrollArea {
|
||||
id_salt: Option<Id>,
|
||||
offset_x: Option<f32>,
|
||||
offset_y: Option<f32>,
|
||||
|
||||
/// If false, we ignore scroll events.
|
||||
scrolling_enabled: bool,
|
||||
drag_to_scroll: bool,
|
||||
on_hover_cursor: Option<CursorIcon>,
|
||||
on_drag_cursor: Option<CursorIcon>,
|
||||
scroll_source: ScrollSource,
|
||||
wheel_scroll_multiplier: Vec2,
|
||||
|
||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||
/// end position until user manually changes position. It will become true
|
||||
@@ -220,9 +331,9 @@ impl ScrollArea {
|
||||
|
||||
/// Create a scroll area where you decide which axis has scrolling enabled.
|
||||
/// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling.
|
||||
pub fn new(scroll_enabled: impl Into<Vec2b>) -> Self {
|
||||
pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
|
||||
Self {
|
||||
scroll_enabled: scroll_enabled.into(),
|
||||
direction_enabled: direction_enabled.into(),
|
||||
auto_shrink: Vec2b::TRUE,
|
||||
max_size: Vec2::INFINITY,
|
||||
min_scrolled_size: Vec2::splat(64.0),
|
||||
@@ -231,8 +342,10 @@ impl ScrollArea {
|
||||
id_salt: None,
|
||||
offset_x: None,
|
||||
offset_y: None,
|
||||
scrolling_enabled: true,
|
||||
drag_to_scroll: true,
|
||||
on_hover_cursor: None,
|
||||
on_drag_cursor: None,
|
||||
scroll_source: ScrollSource::default(),
|
||||
wheel_scroll_multiplier: Vec2::splat(1.0),
|
||||
stick_to_end: Vec2b::FALSE,
|
||||
animated: true,
|
||||
}
|
||||
@@ -355,17 +468,41 @@ impl ScrollArea {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the cursor used when the mouse pointer is hovering over the [`ScrollArea`].
|
||||
///
|
||||
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
|
||||
///
|
||||
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
|
||||
/// override this setting.
|
||||
#[inline]
|
||||
pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
|
||||
self.on_hover_cursor = Some(cursor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the cursor used when the [`ScrollArea`] is being dragged.
|
||||
///
|
||||
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
|
||||
///
|
||||
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
|
||||
/// override this setting.
|
||||
#[inline]
|
||||
pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
|
||||
self.on_drag_cursor = Some(cursor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Turn on/off scrolling on the horizontal axis.
|
||||
#[inline]
|
||||
pub fn hscroll(mut self, hscroll: bool) -> Self {
|
||||
self.scroll_enabled[0] = hscroll;
|
||||
self.direction_enabled[0] = hscroll;
|
||||
self
|
||||
}
|
||||
|
||||
/// Turn on/off scrolling on the vertical axis.
|
||||
#[inline]
|
||||
pub fn vscroll(mut self, vscroll: bool) -> Self {
|
||||
self.scroll_enabled[1] = vscroll;
|
||||
self.direction_enabled[1] = vscroll;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -373,16 +510,8 @@ impl ScrollArea {
|
||||
///
|
||||
/// You can pass in `false`, `true`, `[false, true]` etc.
|
||||
#[inline]
|
||||
pub fn scroll(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
|
||||
self.scroll_enabled = scroll_enabled.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Turn on/off scrolling on the horizontal/vertical axes.
|
||||
#[deprecated = "Renamed to `scroll`"]
|
||||
#[inline]
|
||||
pub fn scroll2(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
|
||||
self.scroll_enabled = scroll_enabled.into();
|
||||
pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
|
||||
self.direction_enabled = direction_enabled.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -395,9 +524,14 @@ impl ScrollArea {
|
||||
/// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
|
||||
///
|
||||
/// This controls both scrolling directions.
|
||||
#[deprecated = "Use `ScrollArea::scroll_source()"]
|
||||
#[inline]
|
||||
pub fn enable_scrolling(mut self, enable: bool) -> Self {
|
||||
self.scrolling_enabled = enable;
|
||||
self.scroll_source = if enable {
|
||||
ScrollSource::ALL
|
||||
} else {
|
||||
ScrollSource::NONE
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
@@ -408,9 +542,28 @@ impl ScrollArea {
|
||||
/// If `true`, the [`ScrollArea`] will sense drags.
|
||||
///
|
||||
/// Default: `true`.
|
||||
#[deprecated = "Use `ScrollArea::scroll_source()"]
|
||||
#[inline]
|
||||
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
|
||||
self.drag_to_scroll = drag_to_scroll;
|
||||
self.scroll_source.drag = drag_to_scroll;
|
||||
self
|
||||
}
|
||||
|
||||
/// What sources does the [`ScrollArea`] use for scrolling the contents.
|
||||
#[inline]
|
||||
pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
|
||||
self.scroll_source = scroll_source;
|
||||
self
|
||||
}
|
||||
|
||||
/// The scroll amount caused by a mouse wheel scroll is multiplied by this amount.
|
||||
///
|
||||
/// Independent for each scroll direction. Defaults to `Vec2{x: 1.0, y: 1.0}`.
|
||||
///
|
||||
/// This can invert or effectively disable mouse scrolling.
|
||||
#[inline]
|
||||
pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
|
||||
self.wheel_scroll_multiplier = multiplier;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -437,7 +590,7 @@ impl ScrollArea {
|
||||
|
||||
/// Is any scrolling enabled?
|
||||
pub(crate) fn is_any_scroll_enabled(&self) -> bool {
|
||||
self.scroll_enabled[0] || self.scroll_enabled[1]
|
||||
self.direction_enabled[0] || self.direction_enabled[1]
|
||||
}
|
||||
|
||||
/// The scroll handle will stick to the rightmost position even while the content size
|
||||
@@ -472,7 +625,7 @@ struct Prepared {
|
||||
auto_shrink: Vec2b,
|
||||
|
||||
/// Does this `ScrollArea` have horizontal/vertical scrolling enabled?
|
||||
scroll_enabled: Vec2b,
|
||||
direction_enabled: Vec2b,
|
||||
|
||||
/// Smoothly interpolated boolean of whether or not to show the scroll bars.
|
||||
show_bars_factor: Vec2,
|
||||
@@ -500,20 +653,24 @@ struct Prepared {
|
||||
/// `viewport.min == ZERO` means we scrolled to the top.
|
||||
viewport: Rect,
|
||||
|
||||
scrolling_enabled: bool,
|
||||
scroll_source: ScrollSource,
|
||||
wheel_scroll_multiplier: Vec2,
|
||||
stick_to_end: Vec2b,
|
||||
|
||||
/// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
|
||||
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
|
||||
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
|
||||
|
||||
/// The response from dragging the background (if enabled)
|
||||
background_drag_response: Option<Response>,
|
||||
|
||||
animated: bool,
|
||||
}
|
||||
|
||||
impl ScrollArea {
|
||||
fn begin(self, ui: &mut Ui) -> Prepared {
|
||||
let Self {
|
||||
scroll_enabled,
|
||||
direction_enabled,
|
||||
auto_shrink,
|
||||
max_size,
|
||||
min_scrolled_size,
|
||||
@@ -522,14 +679,15 @@ impl ScrollArea {
|
||||
id_salt,
|
||||
offset_x,
|
||||
offset_y,
|
||||
scrolling_enabled,
|
||||
drag_to_scroll,
|
||||
on_hover_cursor,
|
||||
on_drag_cursor,
|
||||
scroll_source,
|
||||
wheel_scroll_multiplier,
|
||||
stick_to_end,
|
||||
animated,
|
||||
} = self;
|
||||
|
||||
let ctx = ui.ctx().clone();
|
||||
let scrolling_enabled = scrolling_enabled && ui.is_enabled();
|
||||
|
||||
let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
|
||||
let id = ui.make_persistent_id(id_salt);
|
||||
@@ -546,7 +704,7 @@ impl ScrollArea {
|
||||
let show_bars: Vec2b = match scroll_bar_visibility {
|
||||
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
|
||||
ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
|
||||
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
|
||||
ScrollBarVisibility::AlwaysVisible => direction_enabled,
|
||||
};
|
||||
|
||||
let show_bars_factor = Vec2::new(
|
||||
@@ -568,7 +726,7 @@ impl ScrollArea {
|
||||
// one shouldn't collapse into nothingness.
|
||||
// See https://github.com/emilk/egui/issues/1097
|
||||
for d in 0..2 {
|
||||
if scroll_enabled[d] {
|
||||
if direction_enabled[d] {
|
||||
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
|
||||
}
|
||||
}
|
||||
@@ -585,13 +743,19 @@ impl ScrollArea {
|
||||
} else {
|
||||
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
|
||||
for d in 0..2 {
|
||||
if scroll_enabled[d] {
|
||||
if direction_enabled[d] {
|
||||
content_max_size[d] = f32::INFINITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
|
||||
|
||||
// Round to pixels to avoid widgets appearing to "float" when scrolling fractional amounts:
|
||||
let content_max_rect = content_max_rect
|
||||
.round_to_pixels(ui.pixels_per_point())
|
||||
.round_ui();
|
||||
|
||||
let mut content_ui = ui.new_child(
|
||||
UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
|
||||
@@ -603,7 +767,7 @@ impl ScrollArea {
|
||||
let clip_rect_margin = ui.visuals().clip_rect_margin;
|
||||
let mut content_clip_rect = ui.clip_rect();
|
||||
for d in 0..2 {
|
||||
if scroll_enabled[d] {
|
||||
if direction_enabled[d] {
|
||||
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
|
||||
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
||||
} else {
|
||||
@@ -619,56 +783,72 @@ impl ScrollArea {
|
||||
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
|
||||
|
||||
if (scrolling_enabled && drag_to_scroll)
|
||||
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
|
||||
{
|
||||
// Drag contents to scroll (for touch screens mostly).
|
||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||
// or we will steal input from the widgets we contain.
|
||||
let content_response_option = state
|
||||
.interact_rect
|
||||
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
|
||||
let background_drag_response =
|
||||
if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
|
||||
// Drag contents to scroll (for touch screens mostly).
|
||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||
// or we will steal input from the widgets we contain.
|
||||
let content_response_option = state
|
||||
.interact_rect
|
||||
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
|
||||
|
||||
if content_response_option
|
||||
.as_ref()
|
||||
.is_some_and(|response| response.dragged())
|
||||
{
|
||||
for d in 0..2 {
|
||||
if scroll_enabled[d] {
|
||||
ui.input(|input| {
|
||||
state.offset[d] -= input.pointer.delta()[d];
|
||||
});
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
state.offset_target[d] = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Apply the cursor velocity to the scroll area when the user releases the drag.
|
||||
if content_response_option
|
||||
.as_ref()
|
||||
.is_some_and(|response| response.drag_stopped())
|
||||
.is_some_and(|response| response.dragged())
|
||||
{
|
||||
state.vel =
|
||||
scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
|
||||
}
|
||||
for d in 0..2 {
|
||||
// Kinetic scrolling
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
for d in 0..2 {
|
||||
if direction_enabled[d] {
|
||||
ui.input(|input| {
|
||||
state.offset[d] -= input.pointer.delta()[d];
|
||||
});
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
state.offset_target[d] = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Apply the cursor velocity to the scroll area when the user releases the drag.
|
||||
if content_response_option
|
||||
.as_ref()
|
||||
.is_some_and(|response| response.drag_stopped())
|
||||
{
|
||||
state.vel = direction_enabled.to_vec2()
|
||||
* ui.input(|input| input.pointer.velocity());
|
||||
}
|
||||
for d in 0..2 {
|
||||
// Kinetic scrolling
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
||||
state.vel[d] = 0.0;
|
||||
} else {
|
||||
state.vel[d] -= friction * state.vel[d].signum();
|
||||
// Offset has an inverted coordinate system compared to
|
||||
// the velocity, so we subtract it instead of adding it
|
||||
state.offset[d] -= state.vel[d] * dt;
|
||||
ctx.request_repaint();
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
||||
state.vel[d] = 0.0;
|
||||
} else {
|
||||
state.vel[d] -= friction * state.vel[d].signum();
|
||||
// Offset has an inverted coordinate system compared to
|
||||
// the velocity, so we subtract it instead of adding it
|
||||
state.offset[d] -= state.vel[d] * dt;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the desired mouse cursors.
|
||||
if let Some(response) = &content_response_option {
|
||||
if response.dragged()
|
||||
&& let Some(cursor) = on_drag_cursor
|
||||
{
|
||||
ui.ctx().set_cursor_icon(cursor);
|
||||
} else if response.hovered()
|
||||
&& let Some(cursor) = on_hover_cursor
|
||||
{
|
||||
ui.ctx().set_cursor_icon(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
content_response_option
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
|
||||
// above).
|
||||
@@ -709,7 +889,7 @@ impl ScrollArea {
|
||||
id,
|
||||
state,
|
||||
auto_shrink,
|
||||
scroll_enabled,
|
||||
direction_enabled,
|
||||
show_bars_factor,
|
||||
current_bar_use,
|
||||
scroll_bar_visibility,
|
||||
@@ -717,9 +897,11 @@ impl ScrollArea {
|
||||
inner_rect,
|
||||
content_ui,
|
||||
viewport,
|
||||
scrolling_enabled,
|
||||
scroll_source,
|
||||
wheel_scroll_multiplier,
|
||||
stick_to_end,
|
||||
saved_scroll_target,
|
||||
background_drag_response,
|
||||
animated,
|
||||
}
|
||||
}
|
||||
@@ -776,7 +958,7 @@ impl ScrollArea {
|
||||
|
||||
let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
|
||||
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |viewport_ui| {
|
||||
ui.scope_builder(UiBuilder::new().max_rect(rect), |viewport_ui| {
|
||||
viewport_ui.skip_ahead_auto_ids(min_row); // Make sure we get consistent IDs.
|
||||
add_contents(viewport_ui, min_row..max_row)
|
||||
})
|
||||
@@ -824,16 +1006,18 @@ impl Prepared {
|
||||
mut state,
|
||||
inner_rect,
|
||||
auto_shrink,
|
||||
scroll_enabled,
|
||||
direction_enabled,
|
||||
mut show_bars_factor,
|
||||
current_bar_use,
|
||||
scroll_bar_visibility,
|
||||
scroll_bar_rect,
|
||||
content_ui,
|
||||
viewport: _,
|
||||
scrolling_enabled,
|
||||
scroll_source,
|
||||
wheel_scroll_multiplier,
|
||||
stick_to_end,
|
||||
saved_scroll_target,
|
||||
background_drag_response,
|
||||
animated,
|
||||
} = self;
|
||||
|
||||
@@ -854,7 +1038,7 @@ impl Prepared {
|
||||
.ctx()
|
||||
.pass_state_mut(|state| state.scroll_target[d].take());
|
||||
|
||||
if scroll_enabled[d] {
|
||||
if direction_enabled[d] {
|
||||
if let Some(target) = scroll_target {
|
||||
let pass_state::ScrollTarget {
|
||||
range,
|
||||
@@ -890,7 +1074,7 @@ impl Prepared {
|
||||
|
||||
delta += delta_update;
|
||||
animation = animation_update;
|
||||
};
|
||||
}
|
||||
|
||||
if delta != 0.0 {
|
||||
let target_offset = state.offset[d] + delta;
|
||||
@@ -921,7 +1105,7 @@ impl Prepared {
|
||||
for d in 0..2 {
|
||||
if saved_scroll_target[d].is_some() {
|
||||
state.scroll_target[d] = saved_scroll_target[d].clone();
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -930,7 +1114,7 @@ impl Prepared {
|
||||
let mut inner_size = inner_rect.size();
|
||||
|
||||
for d in 0..2 {
|
||||
inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) {
|
||||
inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
|
||||
(true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
|
||||
(true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
|
||||
(false, true) => content_size[d], // Follow the content (expand/contract to fit it).
|
||||
@@ -944,25 +1128,35 @@ impl Prepared {
|
||||
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
|
||||
|
||||
let content_is_too_large = Vec2b::new(
|
||||
scroll_enabled[0] && inner_rect.width() < content_size.x,
|
||||
scroll_enabled[1] && inner_rect.height() < content_size.y,
|
||||
direction_enabled[0] && inner_rect.width() < content_size.x,
|
||||
direction_enabled[1] && inner_rect.height() < content_size.y,
|
||||
);
|
||||
|
||||
let max_offset = content_size - inner_rect.size();
|
||||
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
|
||||
if scrolling_enabled && is_hovering_outer_rect {
|
||||
|
||||
// Drag-to-scroll?
|
||||
let is_dragging_background = background_drag_response
|
||||
.as_ref()
|
||||
.is_some_and(|r| r.dragged());
|
||||
|
||||
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
|
||||
&& ui.ctx().dragged_id().is_none()
|
||||
|| is_dragging_background;
|
||||
|
||||
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
|
||||
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
|
||||
&& scroll_enabled[0] != scroll_enabled[1];
|
||||
&& direction_enabled[0] != direction_enabled[1];
|
||||
for d in 0..2 {
|
||||
if scroll_enabled[d] {
|
||||
let scroll_delta = ui.ctx().input_mut(|input| {
|
||||
if direction_enabled[d] {
|
||||
let scroll_delta = ui.ctx().input(|input| {
|
||||
if always_scroll_enabled_direction {
|
||||
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
|
||||
input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
|
||||
input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
|
||||
} else {
|
||||
input.smooth_scroll_delta[d]
|
||||
input.smooth_scroll_delta()[d]
|
||||
}
|
||||
});
|
||||
let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
|
||||
|
||||
let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
|
||||
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
|
||||
@@ -973,10 +1167,10 @@ impl Prepared {
|
||||
// Clear scroll delta so no parent scroll will use it:
|
||||
ui.ctx().input_mut(|input| {
|
||||
if always_scroll_enabled_direction {
|
||||
input.smooth_scroll_delta[0] = 0.0;
|
||||
input.smooth_scroll_delta[1] = 0.0;
|
||||
input.smooth_scroll_delta()[0] = 0.0;
|
||||
input.smooth_scroll_delta()[1] = 0.0;
|
||||
} else {
|
||||
input.smooth_scroll_delta[d] = 0.0;
|
||||
input.smooth_scroll_delta()[d] = 0.0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -990,7 +1184,7 @@ impl Prepared {
|
||||
let show_scroll_this_frame = match scroll_bar_visibility {
|
||||
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
|
||||
ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
|
||||
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
|
||||
ScrollBarVisibility::AlwaysVisible => direction_enabled,
|
||||
};
|
||||
|
||||
// Avoid frame delay; start showing scroll bar right away:
|
||||
@@ -1034,6 +1228,7 @@ impl Prepared {
|
||||
|
||||
let is_hovering_bar_area = is_hovering_outer_rect
|
||||
&& ui.rect_contains_pointer(max_bar_rect)
|
||||
&& !is_dragging_background
|
||||
|| state.scroll_bar_interaction[d];
|
||||
|
||||
let is_hovering_bar_area_t = ui
|
||||
@@ -1090,23 +1285,37 @@ impl Prepared {
|
||||
)
|
||||
};
|
||||
|
||||
let handle_rect = if d == 0 {
|
||||
Rect::from_min_max(
|
||||
pos2(from_content(state.offset.x), cross.min),
|
||||
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
|
||||
)
|
||||
} else {
|
||||
Rect::from_min_max(
|
||||
pos2(cross.min, from_content(state.offset.y)),
|
||||
pos2(
|
||||
cross.max,
|
||||
from_content(state.offset.y + inner_rect.height()),
|
||||
),
|
||||
)
|
||||
let calculate_handle_rect = |d, offset: &Vec2| {
|
||||
let handle_size = if d == 0 {
|
||||
from_content(offset.x + inner_rect.width()) - from_content(offset.x)
|
||||
} else {
|
||||
from_content(offset.y + inner_rect.height()) - from_content(offset.y)
|
||||
}
|
||||
.max(scroll_style.handle_min_length);
|
||||
|
||||
let handle_start_point = remap_clamp(
|
||||
offset[d],
|
||||
0.0..=max_offset[d],
|
||||
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
|
||||
);
|
||||
|
||||
if d == 0 {
|
||||
Rect::from_min_max(
|
||||
pos2(handle_start_point, cross.min),
|
||||
pos2(handle_start_point + handle_size, cross.max),
|
||||
)
|
||||
} else {
|
||||
Rect::from_min_max(
|
||||
pos2(cross.min, handle_start_point),
|
||||
pos2(cross.max, handle_start_point + handle_size),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let handle_rect = calculate_handle_rect(d, &state.offset);
|
||||
|
||||
let interact_id = id.with(d);
|
||||
let sense = if self.scrolling_enabled {
|
||||
let sense = if scroll_source.scroll_bar && ui.is_enabled() {
|
||||
Sense::click_and_drag()
|
||||
} else {
|
||||
Sense::hover()
|
||||
@@ -1131,11 +1340,13 @@ impl Prepared {
|
||||
});
|
||||
|
||||
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
|
||||
state.offset[d] = remap(
|
||||
new_handle_top,
|
||||
scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
|
||||
0.0..=content_size[d],
|
||||
);
|
||||
let handle_travel =
|
||||
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
|
||||
state.offset[d] = if handle_travel.start() == handle_travel.end() {
|
||||
0.0
|
||||
} else {
|
||||
remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
|
||||
};
|
||||
|
||||
// some manual action taken, scroll not stuck
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
@@ -1154,33 +1365,9 @@ impl Prepared {
|
||||
|
||||
if ui.is_rect_visible(outer_scroll_bar_rect) {
|
||||
// Avoid frame-delay by calculating a new handle rect:
|
||||
let mut handle_rect = if d == 0 {
|
||||
Rect::from_min_max(
|
||||
pos2(from_content(state.offset.x), cross.min),
|
||||
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
|
||||
)
|
||||
} else {
|
||||
Rect::from_min_max(
|
||||
pos2(cross.min, from_content(state.offset.y)),
|
||||
pos2(
|
||||
cross.max,
|
||||
from_content(state.offset.y + inner_rect.height()),
|
||||
),
|
||||
)
|
||||
};
|
||||
let min_handle_size = scroll_style.handle_min_length;
|
||||
if handle_rect.size()[d] < min_handle_size {
|
||||
handle_rect = Rect::from_center_size(
|
||||
handle_rect.center(),
|
||||
if d == 0 {
|
||||
vec2(min_handle_size, handle_rect.size().y)
|
||||
} else {
|
||||
vec2(handle_rect.size().x, min_handle_size)
|
||||
},
|
||||
);
|
||||
}
|
||||
let handle_rect = calculate_handle_rect(d, &state.offset);
|
||||
|
||||
let visuals = if scrolling_enabled {
|
||||
let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
|
||||
// Pick visuals based on interaction with the handle.
|
||||
// Remember that the response is for the whole scroll bar!
|
||||
let is_hovering_handle = response.hovered()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user