mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 22:00:03 -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
|
* text=auto eol=lf
|
||||||
Cargo.lock linguist-generated=false
|
Cargo.lock linguist-generated=false
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
# Exclude some small files from LFS:
|
# Exclude some small files from LFS:
|
||||||
crates/eframe/data/* !filter !diff !merge text=auto eol=lf
|
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 👍
|
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]
|
[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:
|
# 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**
|
**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.
|
* Keep your PR:s small and focused.
|
||||||
* The PR title is what ends up in the changelog, so make it descriptive!
|
* 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:
|
jobs:
|
||||||
cargo-machete:
|
cargo-machete:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
steps:
|
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
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Machete
|
- name: Machete Check
|
||||||
run: cargo install cargo-machete --locked && cargo machete
|
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
|
name: Deploy web demo
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# We only run this on merges to master
|
# We only run this on merges to main
|
||||||
push:
|
push:
|
||||||
branches: ["master"]
|
branches: ["main"]
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
|
# 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:
|
# release:
|
||||||
# types: ["published"]
|
# types: ["published"]
|
||||||
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # for committing to gh-pages branch
|
contents: write # for committing to gh-pages branch
|
||||||
|
|
||||||
@@ -29,8 +28,8 @@ jobs:
|
|||||||
# Single deploy job since we're just deploying
|
# Single deploy job since we're just deploying
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy web demo
|
name: Deploy web demo
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -39,16 +38,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
target: wasm32-unknown-unknown
|
target: wasm32-unknown-unknown
|
||||||
toolchain: 1.81.0
|
toolchain: 1.88.0
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
prefix-key: "web-demo-"
|
prefix-key: "web-demo-"
|
||||||
|
|
||||||
- name: "Install wasmopt / binaryen"
|
- name: Install wasm-opt
|
||||||
run: |
|
uses: sigoden/install-binary@v1
|
||||||
sudo apt-get update && sudo apt-get install binaryen
|
with:
|
||||||
|
repo: WebAssembly/binaryen
|
||||||
|
tag: version_123
|
||||||
|
name: wasm-opt
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
scripts/build_demo_web.sh --release
|
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:
|
jobs:
|
||||||
label:
|
label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check for a "do-not-merge" label
|
- name: Check for a "do-not-merge" label
|
||||||
uses: mheap/github-action-required-labels@v3
|
uses: mheap/github-action-required-labels@v3
|
||||||
@@ -29,4 +30,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
mode: minimum
|
mode: minimum
|
||||||
count: 1
|
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:
|
jobs:
|
||||||
check-binary-files:
|
check-binary-files:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -25,7 +26,7 @@ jobs:
|
|||||||
exclude_pattern=$(printf "|^%s" "${exclude_paths[@]}" | sed 's/^|//')
|
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
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
17
.github/workflows/preview_build.yml
vendored
17
.github/workflows/preview_build.yml
vendored
@@ -15,16 +15,23 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- run: rustup toolchain install stable --profile minimal --target wasm32-unknown-unknown
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: 1.88.0
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
prefix-key: "pr-preview-"
|
prefix-key: "pr-preview-"
|
||||||
|
|
||||||
- name: "Install wasmopt / binaryen"
|
- name: Install wasm-opt
|
||||||
run: |
|
uses: sigoden/install-binary@v1
|
||||||
sudo apt-get update && sudo apt-get install binaryen
|
with:
|
||||||
|
repo: WebAssembly/binaryen
|
||||||
|
tag: version_123
|
||||||
|
name: wasm-opt
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
scripts/build_demo_web.sh --release
|
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:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
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:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -60,5 +61,7 @@ jobs:
|
|||||||
message: |
|
message: |
|
||||||
Preview available at https://egui-pr-preview.github.io/pr/${{ env.URL_SLUG }}
|
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.
|
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 }}
|
pr_number: ${{ env.PR_NUMBER }}
|
||||||
comment_tag: 'egui-preview'
|
comment_tag: 'egui-preview'
|
||||||
|
|||||||
100
.github/workflows/rust.yml
vendored
100
.github/workflows/rust.yml
vendored
@@ -5,20 +5,21 @@ name: Rust
|
|||||||
env:
|
env:
|
||||||
RUSTFLAGS: -D warnings
|
RUSTFLAGS: -D warnings
|
||||||
RUSTDOCFLAGS: -D warnings
|
RUSTDOCFLAGS: -D warnings
|
||||||
NIGHTLY_VERSION: nightly-2024-09-11
|
NIGHTLY_VERSION: nightly-2025-09-16
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fmt-crank-check-test:
|
fmt-crank-check-test:
|
||||||
name: Format + check
|
name: Format + check
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: 1.81.0
|
toolchain: 1.88.0
|
||||||
|
|
||||||
- name: Install packages (Linux)
|
- name: Install packages (Linux)
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
@@ -37,39 +38,28 @@ jobs:
|
|||||||
- name: Lint vertical spacing
|
- name: Lint vertical spacing
|
||||||
run: ./scripts/lint.py
|
run: ./scripts/lint.py
|
||||||
|
|
||||||
- name: check --all-features
|
- run: cargo clippy --locked --all-features --all-targets
|
||||||
run: cargo check --locked --all-features --all-targets
|
|
||||||
|
|
||||||
- name: check egui_extras --all-features
|
- run: cargo clippy --locked --all-features -p egui_extras
|
||||||
run: cargo check --locked --all-features -p egui_extras
|
|
||||||
|
|
||||||
- name: check default features
|
- run: cargo clippy --locked --all-targets
|
||||||
run: cargo check --locked --all-targets
|
|
||||||
|
|
||||||
- name: check --no-default-features
|
- run: cargo clippy --locked --no-default-features --lib --all-targets
|
||||||
run: cargo check --locked --no-default-features --lib --all-targets
|
|
||||||
|
|
||||||
- name: check eframe --no-default-features
|
- run: cargo clippy --locked --no-default-features --lib -p eframe --features x11
|
||||||
run: cargo check --locked --no-default-features --features x11 --lib -p eframe
|
|
||||||
|
|
||||||
- name: check egui_extras --no-default-features
|
- run: cargo clippy --locked --no-default-features --lib -p eframe --features x11,wgpu_no_default_features
|
||||||
run: cargo check --locked --no-default-features --lib -p egui_extras
|
|
||||||
|
|
||||||
- name: check epaint --no-default-features
|
- run: cargo clippy --locked --no-default-features --lib -p egui_extras
|
||||||
run: cargo check --locked --no-default-features --lib -p epaint
|
|
||||||
|
- run: cargo clippy --locked --no-default-features --lib -p epaint
|
||||||
|
|
||||||
# Regression test for https://github.com/emilk/egui/issues/4771
|
# Regression test for https://github.com/emilk/egui/issues/4771
|
||||||
- name: cargo check -p test_egui_extras_compilation
|
- run: cargo clippy -p test_egui_extras_compilation
|
||||||
run: cargo check -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
|
||||||
run: cargo doc --document-private-items --no-deps --all-features
|
|
||||||
|
|
||||||
- name: clippy
|
|
||||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
|
||||||
|
|
||||||
- name: clippy release
|
- name: clippy release
|
||||||
run: cargo clippy --all-targets --all-features --release -- -D warnings
|
run: cargo clippy --all-targets --all-features --release -- -D warnings
|
||||||
@@ -79,11 +69,12 @@ jobs:
|
|||||||
check_wasm:
|
check_wasm:
|
||||||
name: Check wasm32 + wasm-bindgen
|
name: Check wasm32 + wasm-bindgen
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: 1.81.0
|
toolchain: 1.88.0
|
||||||
targets: wasm32-unknown-unknown
|
targets: wasm32-unknown-unknown
|
||||||
|
|
||||||
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
|
- 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
|
- name: Set up cargo cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Check wasm32 egui_demo_app
|
- name: clippy wasm32 egui_demo_app
|
||||||
run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown
|
run: cargo clippy -p egui_demo_app --lib --target wasm32-unknown-unknown
|
||||||
|
|
||||||
- name: Check wasm32 egui_demo_app --all-features
|
- name: clippy wasm32 egui_demo_app --all-features
|
||||||
run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
|
run: cargo clippy -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
|
||||||
|
|
||||||
- name: Check wasm32 eframe
|
- name: clippy wasm32 eframe
|
||||||
run: cargo check -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
|
run: cargo clippy -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
|
||||||
|
|
||||||
- name: wasm-bindgen
|
- name: wasm-bindgen
|
||||||
uses: jetli/wasm-bindgen-action@v0.1.0
|
uses: jetli/wasm-bindgen-action@v0.1.0
|
||||||
with:
|
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
|
- run: ./scripts/wasm_bindgen_check.sh --skip-setup
|
||||||
|
|
||||||
@@ -114,20 +105,21 @@ jobs:
|
|||||||
check_wasm_atomics:
|
check_wasm_atomics:
|
||||||
name: Check wasm32+atomics
|
name: Check wasm32+atomics
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
|
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
|
||||||
|
|
||||||
- name: Set up cargo cache
|
- name: Set up cargo cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: ${{env.NIGHTLY_VERSION}}
|
toolchain: ${{env.NIGHTLY_VERSION}}
|
||||||
targets: wasm32-unknown-unknown
|
targets: wasm32-unknown-unknown
|
||||||
components: rust-src
|
components: rust-src
|
||||||
|
|
||||||
- name: Check wasm32+atomics eframe with wgpu
|
- 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 }}
|
name: cargo-deny ${{ matrix.target }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
with:
|
with:
|
||||||
rust-version: "1.81.0"
|
rust-version: "1.88.0"
|
||||||
log-level: error
|
log-level: error
|
||||||
command: check
|
command: check
|
||||||
arguments: --target ${{ matrix.target }}
|
arguments: --target ${{ matrix.target }}
|
||||||
@@ -165,18 +158,21 @@ jobs:
|
|||||||
android:
|
android:
|
||||||
name: android
|
name: android
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: 1.81.0
|
toolchain: 1.88.0
|
||||||
targets: aarch64-linux-android
|
targets: aarch64-linux-android
|
||||||
|
|
||||||
- name: Set up cargo cache
|
- name: Set up cargo cache
|
||||||
uses: Swatinem/rust-cache@v2
|
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
|
working-directory: crates/eframe
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -184,12 +180,13 @@ jobs:
|
|||||||
ios:
|
ios:
|
||||||
name: ios
|
name: ios
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: 1.81.0
|
toolchain: 1.88.0
|
||||||
targets: aarch64-apple-ios
|
targets: aarch64-apple-ios
|
||||||
|
|
||||||
- name: Set up cargo cache
|
- name: Set up cargo cache
|
||||||
@@ -204,20 +201,19 @@ jobs:
|
|||||||
windows:
|
windows:
|
||||||
name: Check Windows
|
name: Check Windows
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: 1.81.0
|
toolchain: 1.88.0
|
||||||
|
|
||||||
- name: Set up cargo cache
|
- name: Set up cargo cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Check all
|
- run: cargo clippy --all-targets --all-features
|
||||||
run: cargo check --all-targets --all-features
|
|
||||||
|
|
||||||
- name: Check hello_world
|
- run: cargo clippy -p hello_world
|
||||||
run: cargo check -p hello_world
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -225,14 +221,14 @@ jobs:
|
|||||||
name: Run tests
|
name: Run tests
|
||||||
# We run the tests on macOS because it will run with an actual GPU
|
# We run the tests on macOS because it will run with an actual GPU
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: 1.81.0
|
toolchain: 1.88.0
|
||||||
|
|
||||||
- name: Set up cargo cache
|
- name: Set up cargo cache
|
||||||
uses: Swatinem/rust-cache@v2
|
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
|
# install and run locally: cargo install typos-cli && typos
|
||||||
name: typos
|
name: typos
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Actions Repository
|
- name: Checkout Actions Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check spelling of entire workspace
|
- name: Check spelling of entire workspace
|
||||||
uses: crate-ci/typos@master
|
uses: crate-ci/typos@v1.38.0
|
||||||
|
|
||||||
linkinator:
|
lychee:
|
||||||
name: linkinator
|
name: lychee
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: jprochazk/linkinator-action@main
|
- name: Don't check CHANGELOG.md files
|
||||||
with:
|
# This is really stupid but lychee doesn't have a way of excluding files via GLOB:
|
||||||
linksToSkip: "https://crates.io/crates/.*, http://localhost:.*" # Avoid crates.io rate-limiting
|
# https://github.com/lycheeverse/lychee/issues/1608
|
||||||
retry: true
|
|
||||||
retryErrors: true
|
# We need to exclude CHANGELOG.md since we don't want to have a CI failure everytime some contributor decides
|
||||||
retryErrorsCount: 5
|
# to change their username.
|
||||||
retryErrorsJitter: 2000
|
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
|
# run: typos
|
||||||
|
|
||||||
[default.extend-words]
|
[default.extend-words]
|
||||||
ime = "ime" # Input Method Editor
|
ime = "ime" # Input Method Editor
|
||||||
nknown = "nknown" # part of @55nknown username
|
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
|
# I mistype these so often
|
||||||
tesalator = "tessellator"
|
tesalator = "tessellator"
|
||||||
@@ -17,5 +20,133 @@ teselation = "tessellation"
|
|||||||
tessalation = "tessellation"
|
tessalation = "tessellation"
|
||||||
tesselation = "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]
|
[files]
|
||||||
extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated
|
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_wasm/**": true,
|
||||||
"target/**": 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.command": "clippy",
|
||||||
"rust-analyzer.check.overrideCommand": [
|
// Whether `--workspace` should be passed to `cargo clippy`. If false, `-p <package>` will be passed instead.
|
||||||
"cargo",
|
"rust-analyzer.check.workspace": false,
|
||||||
"clippy",
|
"rust-analyzer.cargo.allTargets": true,
|
||||||
"--target-dir=target_ra",
|
"rust-analyzer.cargo.features": "all",
|
||||||
"--workspace",
|
// Use a separate target directory for Rust Analyzer so it doesn't prevent cargo/clippy from doing things.
|
||||||
"--message-format=json",
|
"rust-analyzer.cargo.targetDir": "target_ra",
|
||||||
"--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.showUnlinkedFileNotification": false,
|
"rust-analyzer.showUnlinkedFileNotification": false,
|
||||||
|
|
||||||
// Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`.
|
// 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.
|
// Don't forget to put it in a comment again before committing.
|
||||||
// "rust-analyzer.cargo.target": "wasm32-unknown-unknown",
|
// "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`.
|
Depends on `egui_demo_lib` + `eframe`.
|
||||||
|
|
||||||
### `egui_kittest`
|
### `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
|
### 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.
|
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
|
## 0.31.0 - 2025-02-04 - Scene container, improved rendering quality
|
||||||
|
|
||||||
### Highlights ✨
|
### 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))
|
* 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`
|
#### `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),
|
`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).
|
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!).
|
`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))
|
* 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))
|
* 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))
|
* `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))
|
* `DragValue::clamp_range` renamed to `range` (([#4728](https://github.com/emilk/egui/pull/4728))
|
||||||
|
|
||||||
### ⭐ Added
|
### ⭐ Added
|
||||||
@@ -937,7 +1325,7 @@ egui_extras::install_image_loaders(egui_ctx);
|
|||||||
## 0.18.0 - 2022-04-30
|
## 0.18.0 - 2022-04-30
|
||||||
|
|
||||||
### ⭐ Added
|
### ⭐ 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)).
|
* 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)).
|
* `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)).
|
* 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`.
|
You can test your code locally by running `./scripts/check.sh`.
|
||||||
There are snapshots test that might need to be updated.
|
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.
|
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).
|
For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md).
|
||||||
|
Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info.
|
||||||
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 .`.
|
|
||||||
|
|
||||||
If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs.
|
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!
|
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.
|
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
|
## PR review
|
||||||
|
|
||||||
Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs!
|
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 {}`
|
* Flip `if !condition {} else {}`
|
||||||
* Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`)
|
* 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 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
|
* 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]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/ecolor",
|
"crates/ecolor",
|
||||||
"crates/egui_demo_app",
|
"crates/egui_demo_app",
|
||||||
"crates/egui_demo_lib",
|
"crates/egui_demo_lib",
|
||||||
"crates/egui_extras",
|
"crates/egui_extras",
|
||||||
"crates/egui_glow",
|
"crates/egui_glow",
|
||||||
"crates/egui_kittest",
|
"crates/egui_kittest",
|
||||||
"crates/egui-wgpu",
|
"crates/egui-wgpu",
|
||||||
"crates/egui-winit",
|
"crates/egui-winit",
|
||||||
"crates/egui",
|
"crates/egui",
|
||||||
"crates/emath",
|
"crates/emath",
|
||||||
"crates/epaint",
|
"crates/epaint",
|
||||||
"crates/epaint_default_fonts",
|
"crates/epaint_default_fonts",
|
||||||
|
|
||||||
"examples/*",
|
"examples/*",
|
||||||
"tests/*",
|
"tests/*",
|
||||||
|
|
||||||
"xtask",
|
"xtask",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
rust-version = "1.81"
|
rust-version = "1.88"
|
||||||
version = "0.31.0"
|
version = "0.33.2"
|
||||||
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -55,54 +55,95 @@ opt-level = 2
|
|||||||
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
emath = { version = "0.31.0", path = "crates/emath", default-features = false }
|
emath = { version = "0.33.2", path = "crates/emath", default-features = false }
|
||||||
ecolor = { version = "0.31.0", path = "crates/ecolor", default-features = false }
|
ecolor = { version = "0.33.2", path = "crates/ecolor", default-features = false }
|
||||||
epaint = { version = "0.31.0", path = "crates/epaint", default-features = false }
|
epaint = { version = "0.33.2", path = "crates/epaint", default-features = false }
|
||||||
epaint_default_fonts = { version = "0.31.0", path = "crates/epaint_default_fonts" }
|
epaint_default_fonts = { version = "0.33.2", path = "crates/epaint_default_fonts" }
|
||||||
egui = { version = "0.31.0", path = "crates/egui", default-features = false }
|
egui = { version = "0.33.2", path = "crates/egui", default-features = false }
|
||||||
egui-winit = { version = "0.31.0", path = "crates/egui-winit", default-features = false }
|
egui-winit = { version = "0.33.2", path = "crates/egui-winit", default-features = false }
|
||||||
egui_extras = { version = "0.31.0", path = "crates/egui_extras", default-features = false }
|
egui_extras = { version = "0.33.2", path = "crates/egui_extras", default-features = false }
|
||||||
egui-wgpu = { version = "0.31.0", path = "crates/egui-wgpu", default-features = false }
|
egui-wgpu = { version = "0.33.2", path = "crates/egui-wgpu", default-features = false }
|
||||||
egui_demo_lib = { version = "0.31.0", path = "crates/egui_demo_lib", default-features = false }
|
egui_demo_lib = { version = "0.33.2", path = "crates/egui_demo_lib", default-features = false }
|
||||||
egui_glow = { version = "0.31.0", path = "crates/egui_glow", default-features = false }
|
egui_glow = { version = "0.33.2", path = "crates/egui_glow", default-features = false }
|
||||||
egui_kittest = { version = "0.31.0", path = "crates/egui_kittest", default-features = false }
|
egui_kittest = { version = "0.33.2", path = "crates/egui_kittest", default-features = false }
|
||||||
eframe = { version = "0.31.0", path = "crates/eframe", default-features = false }
|
eframe = { version = "0.33.2", path = "crates/eframe", default-features = false }
|
||||||
|
|
||||||
ahash = { version = "0.8.11", default-features = false, features = [
|
accesskit = "0.21.1"
|
||||||
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
accesskit_consumer = "0.30.1"
|
||||||
"std",
|
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"
|
android_logger = "0.15.1"
|
||||||
bitflags = "2.6"
|
arboard = { version = "3.6.1", default-features = false }
|
||||||
bytemuck = "1.7.2"
|
backtrace = "0.3.76"
|
||||||
criterion = { version = "0.5.1", default-features = false }
|
bitflags = "2.9.4"
|
||||||
dify = { version = "0.7", default-features = false }
|
bytemuck = "1.24.0"
|
||||||
document-features = "0.2.10"
|
chrono = { version = "0.4.42", default-features = false }
|
||||||
glow = "0.16"
|
cint = "0.3.1"
|
||||||
glutin = { version = "0.32.0", default-features = false }
|
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 }
|
glutin-winit = { version = "0.5.0", default-features = false }
|
||||||
home = "0.5.9"
|
home = "0.5.9"
|
||||||
image = { version = "0.25", default-features = false }
|
image = { version = "0.25.6", default-features = false }
|
||||||
kittest = { version = "0.1" }
|
js-sys = "0.3.77"
|
||||||
log = { version = "0.4", features = ["std"] }
|
kittest = { version = "0.3.0" }
|
||||||
nohash-hasher = "0.2"
|
log = { version = "0.4.28", features = ["std"] }
|
||||||
parking_lot = "0.12"
|
memoffset = "0.9.1"
|
||||||
pollster = "0.4"
|
mimalloc = "0.1.48"
|
||||||
profiling = { version = "1.0.16", default-features = false }
|
mime_guess2 = { version = "2.3.1", default-features = false }
|
||||||
puffin = "0.19"
|
mint = "0.5.9"
|
||||||
puffin_http = "0.16"
|
nohash-hasher = "0.2.0"
|
||||||
raw-window-handle = "0.6.0"
|
objc2 = "0.5.2"
|
||||||
ron = "0.8"
|
objc2-app-kit = { version = "0.2.2", default-features = false }
|
||||||
serde = { version = "1", features = ["derive"] }
|
objc2-foundation = { version = "0.2.2", default-features = false }
|
||||||
thiserror = "1.0.37"
|
objc2-ui-kit = { version = "0.2.2", default-features = false }
|
||||||
type-map = "0.5.0"
|
open = "5.3.2"
|
||||||
wasm-bindgen = "0.2"
|
parking_lot = "0.12.5"
|
||||||
wasm-bindgen-futures = "0.4"
|
percent-encoding = "2.3.2"
|
||||||
web-sys = "0.3.70"
|
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
|
web-time = "1.1.0" # Timekeeping for native and web
|
||||||
wgpu = { version = "24.0.0", default-features = false }
|
webbrowser = "1.0.5"
|
||||||
windows-sys = "0.59"
|
wgpu = { version = "27.0.1", default-features = false, features = ["std"] }
|
||||||
winit = { version = "0.30.7", default-features = false }
|
windows-sys = "0.61.2"
|
||||||
|
winit = { version = "0.30.12", default-features = false }
|
||||||
|
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "deny"
|
unsafe_code = "deny"
|
||||||
@@ -115,7 +156,7 @@ rust_2021_prelude_collisions = "warn"
|
|||||||
semicolon_in_expressions_from_macros = "warn"
|
semicolon_in_expressions_from_macros = "warn"
|
||||||
trivial_numeric_casts = "warn"
|
trivial_numeric_casts = "warn"
|
||||||
unexpected_cfgs = "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_extern_crates = "warn"
|
||||||
unused_import_braces = "warn"
|
unused_import_braces = "warn"
|
||||||
unused_lifetimes = "warn"
|
unused_lifetimes = "warn"
|
||||||
@@ -130,25 +171,33 @@ broken_intra_doc_links = "warn"
|
|||||||
|
|
||||||
# See also clippy.toml
|
# See also clippy.toml
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
|
all = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
|
allow_attributes = "warn"
|
||||||
as_ptr_cast_mut = "warn"
|
as_ptr_cast_mut = "warn"
|
||||||
await_holding_lock = "warn"
|
await_holding_lock = "warn"
|
||||||
bool_to_int_with_if = "warn"
|
bool_to_int_with_if = "warn"
|
||||||
|
branches_sharing_code = "warn"
|
||||||
char_lit_as_u8 = "warn"
|
char_lit_as_u8 = "warn"
|
||||||
checked_conversions = "warn"
|
checked_conversions = "warn"
|
||||||
clear_with_drain = "warn"
|
clear_with_drain = "warn"
|
||||||
cloned_instead_of_copied = "warn"
|
cloned_instead_of_copied = "warn"
|
||||||
dbg_macro = "warn"
|
dbg_macro = "warn"
|
||||||
debug_assert_with_mut_call = "warn"
|
debug_assert_with_mut_call = "warn"
|
||||||
|
default_union_representation = "warn"
|
||||||
derive_partial_eq_without_eq = "warn"
|
derive_partial_eq_without_eq = "warn"
|
||||||
disallowed_macros = "warn" # See clippy.toml
|
disallowed_macros = "warn" # See clippy.toml
|
||||||
disallowed_methods = "warn" # See clippy.toml
|
disallowed_methods = "warn" # See clippy.toml
|
||||||
disallowed_names = "warn" # See clippy.toml
|
disallowed_names = "warn" # See clippy.toml
|
||||||
disallowed_script_idents = "warn" # See clippy.toml
|
disallowed_script_idents = "warn" # See clippy.toml
|
||||||
disallowed_types = "warn" # See clippy.toml
|
disallowed_types = "warn" # See clippy.toml
|
||||||
|
doc_comment_double_space_linebreaks = "warn"
|
||||||
doc_link_with_quotes = "warn"
|
doc_link_with_quotes = "warn"
|
||||||
doc_markdown = "warn"
|
doc_markdown = "warn"
|
||||||
|
elidable_lifetime_names = "warn"
|
||||||
empty_enum = "warn"
|
empty_enum = "warn"
|
||||||
empty_enum_variants_with_brackets = "warn"
|
empty_enum_variants_with_brackets = "warn"
|
||||||
|
empty_line_after_outer_attr = "warn"
|
||||||
enum_glob_use = "warn"
|
enum_glob_use = "warn"
|
||||||
equatable_if_let = "warn"
|
equatable_if_let = "warn"
|
||||||
exit = "warn"
|
exit = "warn"
|
||||||
@@ -165,9 +214,11 @@ fn_to_numeric_cast_any = "warn"
|
|||||||
from_iter_instead_of_collect = "warn"
|
from_iter_instead_of_collect = "warn"
|
||||||
get_unwrap = "warn"
|
get_unwrap = "warn"
|
||||||
if_let_mutex = "warn"
|
if_let_mutex = "warn"
|
||||||
|
ignore_without_reason = "warn"
|
||||||
implicit_clone = "warn"
|
implicit_clone = "warn"
|
||||||
implied_bounds_in_impls = "warn"
|
implied_bounds_in_impls = "warn"
|
||||||
imprecise_flops = "warn"
|
imprecise_flops = "warn"
|
||||||
|
inconsistent_struct_constructor = "warn"
|
||||||
index_refutable_slice = "warn"
|
index_refutable_slice = "warn"
|
||||||
inefficient_to_string = "warn"
|
inefficient_to_string = "warn"
|
||||||
infinite_loop = "warn"
|
infinite_loop = "warn"
|
||||||
@@ -178,33 +229,37 @@ iter_filter_is_some = "warn"
|
|||||||
iter_not_returning_iterator = "warn"
|
iter_not_returning_iterator = "warn"
|
||||||
iter_on_empty_collections = "warn"
|
iter_on_empty_collections = "warn"
|
||||||
iter_on_single_items = "warn"
|
iter_on_single_items = "warn"
|
||||||
|
iter_over_hash_type = "warn"
|
||||||
iter_without_into_iter = "warn"
|
iter_without_into_iter = "warn"
|
||||||
large_digit_groups = "warn"
|
large_digit_groups = "warn"
|
||||||
|
large_futures = "warn"
|
||||||
large_include_file = "warn"
|
large_include_file = "warn"
|
||||||
large_stack_arrays = "warn"
|
large_stack_arrays = "warn"
|
||||||
large_stack_frames = "warn"
|
large_stack_frames = "warn"
|
||||||
large_types_passed_by_value = "warn"
|
large_types_passed_by_value = "warn"
|
||||||
let_unit_value = "warn"
|
let_unit_value = "warn"
|
||||||
linkedlist = "warn"
|
linkedlist = "warn"
|
||||||
|
literal_string_with_formatting_args = "warn"
|
||||||
lossy_float_literal = "warn"
|
lossy_float_literal = "warn"
|
||||||
macro_use_imports = "warn"
|
macro_use_imports = "warn"
|
||||||
manual_assert = "warn"
|
manual_assert = "warn"
|
||||||
manual_clamp = "warn"
|
manual_clamp = "warn"
|
||||||
manual_instant_elapsed = "warn"
|
manual_instant_elapsed = "warn"
|
||||||
|
manual_is_power_of_two = "warn"
|
||||||
manual_is_variant_and = "warn"
|
manual_is_variant_and = "warn"
|
||||||
manual_let_else = "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_ok_or = "warn"
|
||||||
manual_string_new = "warn"
|
manual_string_new = "warn"
|
||||||
map_err_ignore = "warn"
|
map_err_ignore = "warn"
|
||||||
map_flatten = "warn"
|
map_flatten = "warn"
|
||||||
map_unwrap_or = "warn"
|
|
||||||
match_bool = "warn"
|
match_bool = "warn"
|
||||||
match_on_vec_items = "warn"
|
|
||||||
match_same_arms = "warn"
|
match_same_arms = "warn"
|
||||||
match_wild_err_arm = "warn"
|
match_wild_err_arm = "warn"
|
||||||
match_wildcard_for_single_variants = "warn"
|
match_wildcard_for_single_variants = "warn"
|
||||||
mem_forget = "warn"
|
mem_forget = "warn"
|
||||||
mismatching_type_param_order = "warn"
|
mismatching_type_param_order = "warn"
|
||||||
|
missing_assert_message = "warn"
|
||||||
missing_enforced_import_renames = "warn"
|
missing_enforced_import_renames = "warn"
|
||||||
missing_errors_doc = "warn"
|
missing_errors_doc = "warn"
|
||||||
missing_safety_doc = "warn"
|
missing_safety_doc = "warn"
|
||||||
@@ -217,11 +272,16 @@ needless_for_each = "warn"
|
|||||||
needless_pass_by_ref_mut = "warn"
|
needless_pass_by_ref_mut = "warn"
|
||||||
needless_pass_by_value = "warn"
|
needless_pass_by_value = "warn"
|
||||||
negative_feature_names = "warn"
|
negative_feature_names = "warn"
|
||||||
|
non_std_lazy_statics = "warn"
|
||||||
|
non_zero_suggestions = "warn"
|
||||||
nonstandard_macro_braces = "warn"
|
nonstandard_macro_braces = "warn"
|
||||||
option_as_ref_cloned = "warn"
|
option_as_ref_cloned = "warn"
|
||||||
option_option = "warn"
|
option_option = "warn"
|
||||||
path_buf_push_overwrite = "warn"
|
path_buf_push_overwrite = "warn"
|
||||||
|
pathbuf_init_then_push = "warn"
|
||||||
|
precedence_bits = "warn"
|
||||||
print_stderr = "warn"
|
print_stderr = "warn"
|
||||||
|
print_stdout = "warn"
|
||||||
ptr_as_ptr = "warn"
|
ptr_as_ptr = "warn"
|
||||||
ptr_cast_constness = "warn"
|
ptr_cast_constness = "warn"
|
||||||
pub_underscore_fields = "warn"
|
pub_underscore_fields = "warn"
|
||||||
@@ -233,10 +293,13 @@ ref_as_ptr = "warn"
|
|||||||
ref_option_ref = "warn"
|
ref_option_ref = "warn"
|
||||||
ref_patterns = "warn"
|
ref_patterns = "warn"
|
||||||
rest_pat_in_fully_bound_structs = "warn"
|
rest_pat_in_fully_bound_structs = "warn"
|
||||||
|
return_and_then = "warn"
|
||||||
same_functions_in_if_condition = "warn"
|
same_functions_in_if_condition = "warn"
|
||||||
semicolon_if_nothing_returned = "warn"
|
semicolon_if_nothing_returned = "warn"
|
||||||
|
set_contains_or_insert = "warn"
|
||||||
single_char_pattern = "warn"
|
single_char_pattern = "warn"
|
||||||
single_match_else = "warn"
|
single_match_else = "warn"
|
||||||
|
single_option_map = "warn"
|
||||||
str_split_at_newline = "warn"
|
str_split_at_newline = "warn"
|
||||||
str_to_string = "warn"
|
str_to_string = "warn"
|
||||||
string_add = "warn"
|
string_add = "warn"
|
||||||
@@ -247,8 +310,10 @@ string_to_string = "warn"
|
|||||||
suspicious_command_arg_space = "warn"
|
suspicious_command_arg_space = "warn"
|
||||||
suspicious_xor_used_as_pow = "warn"
|
suspicious_xor_used_as_pow = "warn"
|
||||||
todo = "warn"
|
todo = "warn"
|
||||||
|
too_long_first_doc_paragraph = "warn"
|
||||||
trailing_empty_array = "warn"
|
trailing_empty_array = "warn"
|
||||||
trait_duplication_in_bounds = "warn"
|
trait_duplication_in_bounds = "warn"
|
||||||
|
transmute_ptr_to_ptr = "warn"
|
||||||
tuple_array_conversions = "warn"
|
tuple_array_conversions = "warn"
|
||||||
unchecked_duration_subtraction = "warn"
|
unchecked_duration_subtraction = "warn"
|
||||||
undocumented_unsafe_blocks = "warn"
|
undocumented_unsafe_blocks = "warn"
|
||||||
@@ -256,14 +321,22 @@ unimplemented = "warn"
|
|||||||
uninhabited_references = "warn"
|
uninhabited_references = "warn"
|
||||||
uninlined_format_args = "warn"
|
uninlined_format_args = "warn"
|
||||||
unnecessary_box_returns = "warn"
|
unnecessary_box_returns = "warn"
|
||||||
|
unnecessary_debug_formatting = "warn"
|
||||||
|
unnecessary_literal_bound = "warn"
|
||||||
|
unnecessary_safety_comment = "warn"
|
||||||
unnecessary_safety_doc = "warn"
|
unnecessary_safety_doc = "warn"
|
||||||
|
unnecessary_self_imports = "warn"
|
||||||
|
unnecessary_semicolon = "warn"
|
||||||
unnecessary_struct_initialization = "warn"
|
unnecessary_struct_initialization = "warn"
|
||||||
unnecessary_wraps = "warn"
|
unnecessary_wraps = "warn"
|
||||||
unnested_or_patterns = "warn"
|
unnested_or_patterns = "warn"
|
||||||
|
unused_async = "warn"
|
||||||
unused_peekable = "warn"
|
unused_peekable = "warn"
|
||||||
unused_rounding = "warn"
|
unused_rounding = "warn"
|
||||||
unused_self = "warn"
|
unused_self = "warn"
|
||||||
|
unused_trait_names = "warn"
|
||||||
use_self = "warn"
|
use_self = "warn"
|
||||||
|
useless_let_if_seq = "warn"
|
||||||
useless_transmute = "warn"
|
useless_transmute = "warn"
|
||||||
verbose_file_reads = "warn"
|
verbose_file_reads = "warn"
|
||||||
wildcard_dependencies = "warn"
|
wildcard_dependencies = "warn"
|
||||||
@@ -271,17 +344,17 @@ zero_sized_map_values = "warn"
|
|||||||
|
|
||||||
|
|
||||||
# TODO(emilk): maybe enable more of these lints?
|
# TODO(emilk): maybe enable more of these lints?
|
||||||
iter_over_hash_type = "allow"
|
comparison_chain = "allow"
|
||||||
missing_assert_message = "allow"
|
|
||||||
should_panic_without_expect = "allow"
|
should_panic_without_expect = "allow"
|
||||||
too_many_lines = "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:
|
# These are meh:
|
||||||
assigning_clones = "allow" # No please
|
assigning_clones = "allow" # No please
|
||||||
let_underscore_must_use = "allow"
|
let_underscore_must_use = "allow"
|
||||||
let_underscore_untyped = "allow"
|
let_underscore_untyped = "allow"
|
||||||
manual_range_contains = "allow" # this one is just worse imho
|
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
|
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
|
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://crates.io/crates/egui)
|
||||||
[](https://docs.rs/egui)
|
[](https://docs.rs/egui)
|
||||||
[](https://github.com/rust-secure-code/safety-dance/)
|
[](https://github.com/rust-secure-code/safety-dance/)
|
||||||
[](https://github.com/emilk/egui/actions?workflow=CI)
|
[](https://github.com/emilk/egui/actions/workflows/rust.yml)
|
||||||
[](https://github.com/emilk/egui/blob/master/LICENSE-MIT)
|
[](https://github.com/emilk/egui/blob/main/LICENSE-MIT)
|
||||||
[](https://github.com/emilk/egui/blob/master/LICENSE-APACHE)
|
[](https://github.com/emilk/egui/blob/main/LICENSE-APACHE)
|
||||||
[](https://discord.gg/JFcEma9bJq)
|
[](https://discord.gg/JFcEma9bJq)
|
||||||
|
|
||||||
|
|
||||||
<div align="center">
|
<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>
|
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
|
||||||
an SDK for visualizing streams of multimodal data.
|
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.
|
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
|
## Example
|
||||||
@@ -46,7 +46,7 @@ ui.label(format!("Hello '{name}', age {age}"));
|
|||||||
ui.image(egui::include_image!("ferris.png"));
|
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:
|
## Sections:
|
||||||
|
|
||||||
@@ -68,19 +68,19 @@ ui.image(egui::include_image!("ferris.png"));
|
|||||||
|
|
||||||
## Quick start
|
## 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 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
|
## 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`.
|
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`
|
`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
|
* Easy to integrate into any environment
|
||||||
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
|
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
|
||||||
* Pure immediate mode: no callbacks
|
* 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
|
* 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
|
* Safe: there is no `unsafe` code in egui
|
||||||
* Minimal dependencies
|
* 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
|
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.
|
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:
|
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
|
## Dependencies
|
||||||
`egui` has a minimal set of default dependencies:
|
`egui` has a minimal set of default dependencies.
|
||||||
|
|
||||||
* [`ab_glyph`](https://crates.io/crates/ab_glyph)
|
|
||||||
* [`ahash`](https://crates.io/crates/ahash)
|
|
||||||
* [`bitflags`](https://crates.io/crates/bitflags)
|
|
||||||
* [`nohash-hasher`](https://crates.io/crates/nohash-hasher)
|
|
||||||
* [`parking_lot`](https://crates.io/crates/parking_lot)
|
|
||||||
|
|
||||||
Heavier dependencies are kept out of `egui`, even as opt-in.
|
Heavier dependencies are kept out of `egui`, even as opt-in.
|
||||||
All code in `egui` is Wasm-friendly (even outside a browser).
|
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?
|
## 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
|
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
|
||||||
* Call into the application GUI code
|
* Call into the application GUI code
|
||||||
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
|
* **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
|
### Official integrations
|
||||||
|
|
||||||
These are the official egui 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`
|
* [`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/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps
|
* [`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/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API)
|
* [`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/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit)
|
* [`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
|
### 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):
|
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`?
|
### 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:
|
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.
|
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` 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.
|
`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`
|
#### `Shape::Callback`
|
||||||
Example:
|
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
|
#### 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.
|
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:
|
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 [`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
|
## 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)
|
* [@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)
|
* [@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)
|
* [@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)
|
* [@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)
|
* [@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)
|
* [@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)
|
* [@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).
|
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
|
||||||
|
|
||||||
@@ -376,7 +369,7 @@ Default fonts:
|
|||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<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>
|
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
|
||||||
an SDK for visualizing streams of multimodal data.
|
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 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
|
## 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.
|
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
|
# Release process
|
||||||
## Patch release
|
* [ ] copy this checklist to a new egui issue, called "Release 0.xx.y"
|
||||||
* [ ] Make a branch off of the latest release
|
* [ ] 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
|
* [ ] cherry-pick what you want to release
|
||||||
* [ ] run `cargo semver-checks`
|
* [ ] 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
|
* [ ] check that CI is green
|
||||||
|
|
||||||
## Preparation
|
## 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
|
* [ ] run `scripts/generate_example_screenshots.sh` if needed
|
||||||
* [ ] write a short release note that fits in a bluesky post
|
* [ ] write a short release note that fits in a bluesky post
|
||||||
* [ ] record gif for `CHANGELOG.md` release note (and later 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`
|
* [ ] update changelogs
|
||||||
* [ ] bump version numbers in workspace `Cargo.toml`
|
* [ ] 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
|
## 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.
|
* [ ] bump version numbers in workspace `Cargo.toml`
|
||||||
|
* [ ] check that CI for the PR is green
|
||||||
* [ ] Run `typos`
|
* [ ] publish the crates by running `scripts/publish_crates.sh`
|
||||||
* [ ] `git commit -m 'Release 0.x.0 - <release title>'`
|
|
||||||
* [ ] `cargo publish` (see below)
|
|
||||||
* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - <release title>'`
|
* [ ] `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`
|
* [ ] `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`
|
* [ ] merge release PR as `Release 0.x.0 - <release title>`
|
||||||
* [ ] check that CI is green
|
* [ ] check that CI for `main` is green
|
||||||
* [ ] do a GitHub release: https://github.com/emilk/egui/releases/new
|
* [ ] do a GitHub release: https://github.com/emilk/egui/releases/new
|
||||||
* Follow the format of the last release
|
* follow the format of the last release
|
||||||
* [ ] wait for documentation to build: https://docs.rs/releases/queue
|
* [ ] 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
|
## Announcements
|
||||||
* [ ] [Bluesky](https://bsky.app/profile/ernerfeldt.bsky.social)
|
* [ ] [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/)
|
* [ ] [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)
|
* [ ] [This Week in Rust](https://github.com/rust-lang/this-week-in-rust/pull/5167)
|
||||||
|
|
||||||
|
|
||||||
## After release
|
## After release
|
||||||
* [ ] publish new `eframe_template`
|
* [ ] update `eframe_template`
|
||||||
* [ ] publish new `egui_plot`
|
* [ ] publish new `egui_plot`
|
||||||
* [ ] publish new `egui_table`
|
* [ ] publish new `egui_table`
|
||||||
* [ ] publish new `egui_tiles`
|
* [ ] publish new `egui_tiles`
|
||||||
* [ ] make a PR to `egui_commonmark`
|
* [ ] make a PR to `egui_commonmark`
|
||||||
* [ ] make a PR to `rerun`
|
* [ ] 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:
|
# Section identical to scripts/clippy_wasm/clippy.toml:
|
||||||
|
|
||||||
msrv = "1.81"
|
msrv = "1.88"
|
||||||
|
|
||||||
allow-unwrap-in-tests = true
|
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
|
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros
|
||||||
disallowed-macros = [
|
disallowed-macros = [
|
||||||
'dbg',
|
'std::dbg',
|
||||||
'std::unimplemented',
|
'std::unimplemented',
|
||||||
|
|
||||||
# TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses
|
# TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses
|
||||||
# 'std::eprint',
|
# 'std::eprint',
|
||||||
# 'std::eprintln',
|
# 'std::eprintln',
|
||||||
# 'std::print',
|
# 'std::print',
|
||||||
# 'std::println',
|
# 'std::println',
|
||||||
]
|
]
|
||||||
|
|
||||||
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods
|
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods
|
||||||
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,
|
{ path = "std::env::temp_dir", readon = "Use the tempfile crate instead" },
|
||||||
# but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406)
|
{ path = "std::panic::catch_unwind", reason = "We compile with `panic = abort" },
|
||||||
# so we do that in `clipppy_wasm.toml` instead.
|
{ path = "std::thread::spawn", readon = "Use `std::thread::Builder` and name the thread" },
|
||||||
|
|
||||||
"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"`
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names
|
# 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
|
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
|
||||||
disallowed-types = [
|
disallowed-types = [
|
||||||
# Use the faster & simpler non-poisonable primitives in `parking_lot` instead
|
{ path = "std::sync::Condvar", reason = "Use parking_lot instead" },
|
||||||
"std::sync::Mutex",
|
{ path = "std::sync::Mutex", reason = "Use epaint::mutex instead" },
|
||||||
"std::sync::RwLock",
|
{ path = "std::sync::RwLock", reason = "Use epaint::mutex instead" },
|
||||||
"std::sync::Condvar",
|
{ path = "winit::dpi::LogicalPosition", 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
|
{ 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
|
||||||
"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
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
|
# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
|
||||||
doc-valid-idents = [
|
doc-valid-idents = [
|
||||||
# You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
|
# You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
|
||||||
"AccessKit",
|
"AccessKit",
|
||||||
"WebGL",
|
"WebGL",
|
||||||
"WebGL1",
|
"WebGL1",
|
||||||
"WebGL2",
|
"WebGL2",
|
||||||
"WebGPU",
|
"WebGPU",
|
||||||
"VirtualBox",
|
"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.
|
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
|
## 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)
|
* 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]
|
[package]
|
||||||
name = "ecolor"
|
name = "ecolor"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
authors = [
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>", "Andreas Reich <reichandreas@gmx.de>"]
|
||||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
|
||||||
"Andreas Reich <reichandreas@gmx.de>",
|
|
||||||
]
|
|
||||||
description = "Color structs and color conversion utilities"
|
description = "Color structs and color conversion utilities"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@@ -39,10 +36,10 @@ emath.workspace = true
|
|||||||
bytemuck = { workspace = true, optional = true, features = ["derive"] }
|
bytemuck = { workspace = true, optional = true, features = ["derive"] }
|
||||||
|
|
||||||
## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries.
|
## [`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.
|
## Enable the [`hex_color`] macro.
|
||||||
color-hex = { version = "0.2.0", optional = true }
|
color-hex = { workspace = true, optional = true }
|
||||||
|
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { workspace = true, optional = true }
|
document-features = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -8,4 +8,6 @@
|
|||||||
|
|
||||||
A simple color storage and conversion library.
|
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};
|
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
|
||||||
|
|
||||||
// ---- Color32 ----
|
// ---- 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).
|
/// This format is used for space-efficient color representation (32 bits).
|
||||||
///
|
///
|
||||||
/// Instead of manipulating this directly it is often better
|
/// Instead of manipulating this directly it is often better
|
||||||
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
|
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
|
||||||
///
|
///
|
||||||
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
|
/// Internally this uses 0-255 gamma space `sRGBA` color with _premultiplied alpha_.
|
||||||
/// Alpha channel is in linear space.
|
|
||||||
///
|
///
|
||||||
/// 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(C)]
|
||||||
|
#[repr(align(4))]
|
||||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||||
pub struct Color32(pub(crate) [u8; 4]);
|
pub struct Color32(pub(crate) [u8; 4]);
|
||||||
|
|
||||||
impl std::fmt::Debug for Color32 {
|
impl std::fmt::Debug for Color32 {
|
||||||
|
/// Prints the contents with premultiplied alpha!
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let [r, g, b, a] = self.0;
|
let [r, g, b, a] = self.0;
|
||||||
write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
|
write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
|
||||||
@@ -87,44 +103,49 @@ impl Color32 {
|
|||||||
/// i.e. often taken to mean "no color".
|
/// i.e. often taken to mean "no color".
|
||||||
pub const PLACEHOLDER: Self = Self::from_rgba_premultiplied(64, 254, 0, 128);
|
pub const PLACEHOLDER: Self = Self::from_rgba_premultiplied(64, 254, 0, 128);
|
||||||
|
|
||||||
#[deprecated = "Renamed to PLACEHOLDER"]
|
/// From RGB with alpha of 255 (opaque).
|
||||||
pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER;
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
||||||
Self([r, g, b, 255])
|
Self([r, g, b, 255])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// From RGB into an additive color (will make everything it blend with brighter).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
|
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
|
||||||
Self([r, g, b, 0])
|
Self([r, g, b, 0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// From `sRGBA` with premultiplied alpha.
|
/// From `sRGBA` with premultiplied alpha.
|
||||||
|
///
|
||||||
|
/// You likely want to use [`Self::from_rgba_unmultiplied`] instead.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
Self([r, g, b, a])
|
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]
|
#[inline]
|
||||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
match a {
|
match a {
|
||||||
// common-case optimization
|
// common-case optimization:
|
||||||
0 => Self::TRANSPARENT,
|
0 => Self::TRANSPARENT,
|
||||||
// common-case optimization
|
|
||||||
|
// common-case optimization:
|
||||||
255 => Self::from_rgb(r, g, b),
|
255 => Self::from_rgb(r, g, b),
|
||||||
|
|
||||||
a => {
|
a => {
|
||||||
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
|
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
|
||||||
let lut = LOOKUP_TABLE.get_or_init(|| {
|
let lut = LOOKUP_TABLE.get_or_init(|| {
|
||||||
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8};
|
|
||||||
(0..=u16::MAX)
|
(0..=u16::MAX)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let [value, alpha] = i.to_ne_bytes();
|
let [value, alpha] = i.to_ne_bytes();
|
||||||
let value_lin = linear_f32_from_gamma_u8(value);
|
fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
|
||||||
let alpha_lin = linear_f32_from_linear_u8(alpha);
|
|
||||||
gamma_u8_from_linear_f32(value_lin * alpha_lin)
|
|
||||||
})
|
})
|
||||||
.collect()
|
.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")]
|
#[doc(alias = "from_grey")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn from_gray(l: u8) -> Self {
|
pub const fn from_gray(l: u8) -> Self {
|
||||||
Self([l, l, l, 255])
|
Self([l, l, l, 255])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Black with the given opacity.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn from_black_alpha(a: u8) -> Self {
|
pub const fn from_black_alpha(a: u8) -> Self {
|
||||||
Self([0, 0, 0, a])
|
Self([0, 0, 0, a])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// White with the given opacity.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_white_alpha(a: u8) -> Self {
|
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]
|
#[inline]
|
||||||
pub const fn from_additive_luminance(l: u8) -> Self {
|
pub const fn from_additive_luminance(l: u8) -> Self {
|
||||||
Self([l, l, l, 0])
|
Self([l, l, l, 0])
|
||||||
@@ -162,21 +208,25 @@ impl Color32 {
|
|||||||
self.a() == 255
|
self.a() == 255
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Red component multiplied by alpha.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn r(&self) -> u8 {
|
pub const fn r(&self) -> u8 {
|
||||||
self.0[0]
|
self.0[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Green component multiplied by alpha.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn g(&self) -> u8 {
|
pub const fn g(&self) -> u8 {
|
||||||
self.0[1]
|
self.0[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Blue component multiplied by alpha.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn b(&self) -> u8 {
|
pub const fn b(&self) -> u8 {
|
||||||
self.0[2]
|
self.0[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Alpha (opacity).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn a(&self) -> u8 {
|
pub const fn a(&self) -> u8 {
|
||||||
self.0[3]
|
self.0[3]
|
||||||
@@ -213,9 +263,26 @@ impl Color32 {
|
|||||||
(self.r(), self.g(), self.b(), self.a())
|
(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]
|
#[inline]
|
||||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
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.
|
/// 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`].
|
/// This is perceptually even, and faster that [`Self::linear_multiply`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn gamma_multiply(self, factor: f32) -> Self {
|
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;
|
let Self([r, g, b, a]) = self;
|
||||||
Self([
|
Self([
|
||||||
(r as f32 * factor + 0.5) as u8,
|
(r as f32 * factor + 0.5) as u8,
|
||||||
@@ -258,7 +328,10 @@ impl Color32 {
|
|||||||
/// You likely want to use [`Self::gamma_multiply`] instead.
|
/// You likely want to use [`Self::gamma_multiply`] instead.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn linear_multiply(self, factor: f32) -> Self {
|
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
|
// As an unfortunate side-effect of using premultiplied alpha
|
||||||
// we need a somewhat expensive conversion to linear space and back.
|
// we need a somewhat expensive conversion to linear space and back.
|
||||||
Rgba::from(self).multiply(factor).into()
|
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 {
|
pub fn blend(self, on_top: Self) -> Self {
|
||||||
self.gamma_multiply_u8(255 - on_top.a()) + on_top
|
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 {
|
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");
|
/// 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");
|
/// const COLOR: ecolor::Color32 = ecolor::hex_color!("#202122");
|
||||||
|
/// assert_eq!(COLOR, ecolor::Color32::from_rgb(0x20, 0x21, 0x22));
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! hex_color {
|
macro_rules! hex_color {
|
||||||
@@ -42,7 +43,7 @@ macro_rules! hex_color {
|
|||||||
let array = $crate::color_hex::color_from_hex!($s);
|
let array = $crate::color_hex::color_from_hex!($s);
|
||||||
match array.as_slice() {
|
match array.as_slice() {
|
||||||
[r, g, b] => $crate::Color32::from_rgb(*r, *g, *b),
|
[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"),
|
_ => 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)
|
let [r, gb] = u16::from_str_radix(s, 16)
|
||||||
.map_err(ParseHexColorError::InvalidInt)?
|
.map_err(ParseHexColorError::InvalidInt)?
|
||||||
.to_be_bytes();
|
.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)))
|
Ok(Self::Hex3(Color32::from_rgb(r, g, b)))
|
||||||
}
|
}
|
||||||
4 => {
|
4 => {
|
||||||
let [r_g, b_a] = u16::from_str_radix(s, 16)
|
let [r_g, b_a] = u16::from_str_radix(s, 16)
|
||||||
.map_err(ParseHexColorError::InvalidInt)?
|
.map_err(ParseHexColorError::InvalidInt)?
|
||||||
.to_be_bytes();
|
.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)))
|
Ok(Self::Hex4(Color32::from_rgba_unmultiplied(r, g, b, a)))
|
||||||
}
|
}
|
||||||
6 => {
|
6 => {
|
||||||
@@ -207,17 +208,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hex_string_round_trip() {
|
fn hex_string_round_trip() {
|
||||||
use Color32 as C;
|
|
||||||
let cases = [
|
let cases = [
|
||||||
C::from_rgba_unmultiplied(10, 20, 30, 0),
|
[0, 20, 30, 0],
|
||||||
C::from_rgba_unmultiplied(10, 20, 30, 40),
|
[10, 0, 30, 40],
|
||||||
C::from_rgba_unmultiplied(10, 20, 30, 255),
|
[10, 100, 200, 0],
|
||||||
C::from_rgba_unmultiplied(0, 20, 30, 0),
|
[10, 100, 200, 100],
|
||||||
C::from_rgba_unmultiplied(10, 0, 30, 40),
|
[10, 100, 200, 200],
|
||||||
C::from_rgba_unmultiplied(10, 20, 0, 255),
|
[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 {
|
for [r, g, b, a] in cases {
|
||||||
assert_eq!(C::from_hex(color.to_hex().as_str()), Ok(color));
|
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::{
|
use crate::{
|
||||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
Color32, Rgba, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32,
|
||||||
linear_u8_from_linear_f32, Color32, Rgba,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Hue, saturation, value, alpha. All in the range [0, 1].
|
/// Hue, saturation, value, alpha. All in the range [0, 1].
|
||||||
/// No premultiplied alpha.
|
/// No premultiplied alpha.
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
pub struct Hsva {
|
pub struct Hsva {
|
||||||
/// hue 0-1
|
/// hue 0-1
|
||||||
@@ -29,30 +29,20 @@ impl Hsva {
|
|||||||
/// From `sRGBA` with premultiplied alpha
|
/// From `sRGBA` with premultiplied alpha
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self {
|
pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self {
|
||||||
Self::from_rgba_premultiplied(
|
Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
|
||||||
linear_f32_from_gamma_u8(r),
|
|
||||||
linear_f32_from_gamma_u8(g),
|
|
||||||
linear_f32_from_gamma_u8(b),
|
|
||||||
linear_f32_from_linear_u8(a),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// From `sRGBA` without premultiplied alpha
|
/// From `sRGBA` without premultiplied alpha
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self {
|
pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self {
|
||||||
Self::from_rgba_unmultiplied(
|
Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
|
||||||
linear_f32_from_gamma_u8(r),
|
|
||||||
linear_f32_from_gamma_u8(g),
|
|
||||||
linear_f32_from_gamma_u8(b),
|
|
||||||
linear_f32_from_linear_u8(a),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// From linear RGBA with premultiplied alpha
|
/// From linear RGBA with premultiplied alpha
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||||
#![allow(clippy::many_single_char_names)]
|
#![allow(clippy::many_single_char_names)]
|
||||||
if a == 0.0 {
|
if a <= 0.0 {
|
||||||
if r == 0.0 && b == 0.0 && a == 0.0 {
|
if r == 0.0 && b == 0.0 && a == 0.0 {
|
||||||
Self::default()
|
Self::default()
|
||||||
} else {
|
} else {
|
||||||
@@ -152,13 +142,7 @@ impl Hsva {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
|
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
|
||||||
let [r, g, b, a] = self.to_rgba_premultiplied();
|
Color32::from(*self).to_array()
|
||||||
[
|
|
||||||
gamma_u8_from_linear_f32(r),
|
|
||||||
gamma_u8_from_linear_f32(g),
|
|
||||||
gamma_u8_from_linear_f32(b),
|
|
||||||
linear_u8_from_linear_f32(a),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// To gamma-space 0-255.
|
/// To gamma-space 0-255.
|
||||||
@@ -250,7 +234,7 @@ pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore] // a bit expensive
|
#[ignore = "too expensive"]
|
||||||
fn test_hsv_roundtrip() {
|
fn test_hsv_roundtrip() {
|
||||||
for r in 0..=255 {
|
for r in 0..=255 {
|
||||||
for g 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
|
/// Like Hsva but with the `v` value (brightness) being gamma corrected
|
||||||
/// so that it is somewhat perceptually even.
|
/// so that it is somewhat perceptually even.
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
//! Color conversions and types.
|
//! 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 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`].
|
//! 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
|
//! ## Feature flags
|
||||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||||
//!
|
//!
|
||||||
@@ -39,23 +50,46 @@ pub use hex_color_runtime::*;
|
|||||||
|
|
||||||
impl From<Color32> for Rgba {
|
impl From<Color32> for Rgba {
|
||||||
fn from(srgba: Color32) -> Self {
|
fn from(srgba: Color32) -> Self {
|
||||||
Self([
|
let [r, g, b, a] = srgba.to_array();
|
||||||
linear_f32_from_gamma_u8(srgba.0[0]),
|
if a == 0 {
|
||||||
linear_f32_from_gamma_u8(srgba.0[1]),
|
// Additive, or completely transparent
|
||||||
linear_f32_from_gamma_u8(srgba.0[2]),
|
Self([
|
||||||
linear_f32_from_linear_u8(srgba.0[3]),
|
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 {
|
impl From<Rgba> for Color32 {
|
||||||
fn from(rgba: Rgba) -> Self {
|
fn from(rgba: Rgba) -> Self {
|
||||||
Self([
|
let [r, g, b, a] = rgba.to_array();
|
||||||
gamma_u8_from_linear_f32(rgba.0[0]),
|
if a == 0.0 {
|
||||||
gamma_u8_from_linear_f32(rgba.0[1]),
|
// Additive, or completely transparent
|
||||||
gamma_u8_from_linear_f32(rgba.0[2]),
|
Self([
|
||||||
linear_u8_from_linear_f32(rgba.0[3]),
|
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].
|
/// linear [0, 255] -> linear [0, 1].
|
||||||
/// Useful for alpha-channel.
|
/// Useful for alpha-channel.
|
||||||
#[inline(always)]
|
#[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
|
a as f32 / 255.0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +130,7 @@ pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
|
|||||||
fast_round(a * 255.0)
|
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
|
(r + 0.5) as _ // rust does a saturating cast since 1.45
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use crate::{
|
use crate::Color32;
|
||||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
|
||||||
linear_u8_from_linear_f32,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// 0-1 linear space `RGBA` color with premultiplied alpha.
|
/// 0-1 linear space `RGBA` color with premultiplied alpha.
|
||||||
|
///
|
||||||
|
/// See [`crate::Color32`] for explanation of what "premultiplied alpha" means.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[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() {
|
} else if f.is_nan() {
|
||||||
state.write_u8(1);
|
state.write_u8(1);
|
||||||
} else {
|
} else {
|
||||||
use std::hash::Hash;
|
use std::hash::Hash as _;
|
||||||
f.to_bits().hash(state);
|
f.to_bits().hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
|
||||||
impl std::hash::Hash for Rgba {
|
impl std::hash::Hash for Rgba {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
@@ -70,20 +68,12 @@ impl Rgba {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
let r = linear_f32_from_gamma_u8(r);
|
Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
let r = linear_f32_from_gamma_u8(r);
|
Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -99,15 +89,24 @@ impl Rgba {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
|
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
|
||||||
debug_assert!(0.0 <= l && l <= 1.0);
|
debug_assert!(
|
||||||
debug_assert!(0.0 <= a && a <= 1.0);
|
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])
|
Self([l * a, l * a, l * a, a])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transparent black
|
/// Transparent black
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_black_alpha(a: f32) -> Self {
|
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])
|
Self([0.0, 0.0, 0.0, a])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,13 +210,12 @@ impl Rgba {
|
|||||||
/// unmultiply the alpha
|
/// unmultiply the alpha
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
crate::Color32::from(*self).to_srgba_unmultiplied()
|
||||||
[
|
}
|
||||||
gamma_u8_from_linear_f32(r),
|
|
||||||
gamma_u8_from_linear_f32(g),
|
/// Blend two colors in linear space, so that `self` is behind the argument.
|
||||||
gamma_u8_from_linear_f32(b),
|
pub fn blend(self, on_top: Self) -> Self {
|
||||||
linear_u8_from_linear_f32(a.abs()),
|
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.
|
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
|
## 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)
|
* 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)
|
* 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"
|
description = "egui framework - write GUI apps that compiles to web and/or natively"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.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
|
license.workspace = true
|
||||||
readme = "README.md"
|
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"]
|
categories = ["gui", "game-development"]
|
||||||
keywords = ["egui", "gui", "gamedev"]
|
keywords = ["egui", "gui", "gamedev"]
|
||||||
include = [
|
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
|
||||||
"../LICENSE-APACHE",
|
|
||||||
"../LICENSE-MIT",
|
|
||||||
"**/*.rs",
|
|
||||||
"Cargo.toml",
|
|
||||||
"data/icon.png",
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
@@ -34,16 +28,15 @@ workspace = true
|
|||||||
default = [
|
default = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"default_fonts",
|
"default_fonts",
|
||||||
"glow",
|
"wayland", # Required for Linux support (including CI!)
|
||||||
"wayland", # Required for Linux support (including CI!)
|
|
||||||
"web_screen_reader",
|
"web_screen_reader",
|
||||||
|
"wgpu",
|
||||||
"winit/default",
|
"winit/default",
|
||||||
"x11",
|
"x11",
|
||||||
"egui-wgpu?/fragile-send-sync-non-atomic-wasm",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
## 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
|
# 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
|
# - 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.
|
## If you plan on specifying your own fonts you may disable this feature.
|
||||||
default_fonts = ["egui/default_fonts"]
|
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"]
|
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
|
||||||
|
|
||||||
## Enable saving app state to disk.
|
## Enable saving app state to disk.
|
||||||
persistence = [
|
persistence = ["dep:home", "egui-winit/serde", "egui/persistence", "ron", "serde"]
|
||||||
"dep:home",
|
|
||||||
"egui-winit/serde",
|
|
||||||
"egui/persistence",
|
|
||||||
"ron",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
## Enables wayland support and fixes clipboard issue.
|
## 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.
|
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
|
||||||
##
|
##
|
||||||
## For other platforms, use the `accesskit` feature instead.
|
## For other platforms, use the `accesskit` feature instead.
|
||||||
web_screen_reader = [
|
web_screen_reader = ["web-sys/SpeechSynthesis", "web-sys/SpeechSynthesisUtterance"]
|
||||||
"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.
|
## Switching from `wgpu (the default)` to `glow` can significantly reduce your binary size
|
||||||
## If you want to enable WebGL, you need to turn on the `webgl` feature of crate `wgpu`:
|
## (including the .wasm of a web app).
|
||||||
##
|
## See <https://github.com/emilk/egui/issues/5889> for more details.
|
||||||
## ```toml
|
|
||||||
## wgpu = { version = "*", features = ["webgpu", "webgl"] }
|
|
||||||
## ```
|
|
||||||
##
|
##
|
||||||
## By default, eframe will prefer WebGPU over WebGL, but
|
## By default, eframe will prefer WebGPU over WebGL, but
|
||||||
## you can configure this at run-time with [`NativeOptions::wgpu_options`].
|
## you can configure this at run-time with [`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.
|
## Enables compiling for x11.
|
||||||
x11 = [
|
x11 = [
|
||||||
@@ -121,10 +117,7 @@ x11 = [
|
|||||||
__screenshot = []
|
__screenshot = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { workspace = true, default-features = false, features = [
|
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
|
||||||
"bytemuck",
|
|
||||||
"log",
|
|
||||||
] }
|
|
||||||
|
|
||||||
ahash.workspace = true
|
ahash.workspace = true
|
||||||
document-features.workspace = true
|
document-features.workspace = true
|
||||||
@@ -132,7 +125,7 @@ log.workspace = true
|
|||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
profiling.workspace = true
|
profiling.workspace = true
|
||||||
raw-window-handle.workspace = true
|
raw-window-handle.workspace = true
|
||||||
static_assertions = "1.1.0"
|
static_assertions.workspace = true
|
||||||
web-time.workspace = true
|
web-time.workspace = true
|
||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
@@ -145,45 +138,35 @@ serde = { workspace = true, optional = true }
|
|||||||
# -------------------------------------------
|
# -------------------------------------------
|
||||||
# native:
|
# native:
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
egui-winit = { workspace = true, default-features = false, features = [
|
egui-winit = { workspace = true, default-features = false, features = ["clipboard", "links"] }
|
||||||
"clipboard",
|
|
||||||
"links",
|
|
||||||
] }
|
|
||||||
image = { workspace = true, features = ["png"] } # Needed for app icon
|
image = { workspace = true, features = ["png"] } # Needed for app icon
|
||||||
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
|
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
|
||||||
|
|
||||||
# optional native:
|
# optional native:
|
||||||
egui-wgpu = { workspace = true, optional = true, features = [
|
egui-wgpu = { workspace = true, optional = true, features = [
|
||||||
"winit",
|
"winit",
|
||||||
|
"capture",
|
||||||
] } # if wgpu is used, use it with winit
|
] } # if wgpu is used, use it with winit
|
||||||
pollster = { workspace = true, optional = true } # needed for wgpu
|
pollster = { workspace = true, optional = true } # needed for wgpu
|
||||||
|
|
||||||
glutin = { workspace = true, optional = true, default-features = false, features = [
|
glutin = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] }
|
||||||
"egl",
|
|
||||||
"wgl",
|
|
||||||
] }
|
|
||||||
glutin-winit = { workspace = true, optional = true, default-features = false, features = [
|
glutin-winit = { workspace = true, optional = true, default-features = false, features = [
|
||||||
"egl",
|
"egl",
|
||||||
"wgl",
|
"wgl",
|
||||||
] }
|
] }
|
||||||
home = { workspace = true, optional = true }
|
home = { workspace = true, optional = true }
|
||||||
wgpu = { workspace = true, optional = true, features = [
|
wgpu = { workspace = true, optional = true }
|
||||||
# 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",
|
|
||||||
] }
|
|
||||||
|
|
||||||
# mac:
|
# mac:
|
||||||
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
||||||
objc2 = "0.5.1"
|
objc2.workspace = true
|
||||||
objc2-foundation = { version = "0.2.0", default-features = false, features = [
|
objc2-foundation = { workspace = true, default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"block2",
|
"block2",
|
||||||
"NSData",
|
"NSData",
|
||||||
"NSString",
|
"NSString",
|
||||||
] }
|
] }
|
||||||
objc2-app-kit = { version = "0.2.0", default-features = false, features = [
|
objc2-app-kit = { workspace = true, default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"NSApplication",
|
"NSApplication",
|
||||||
"NSImage",
|
"NSImage",
|
||||||
@@ -194,11 +177,12 @@ objc2-app-kit = { version = "0.2.0", default-features = false, features = [
|
|||||||
|
|
||||||
# windows:
|
# windows:
|
||||||
[target.'cfg(any(target_os = "windows"))'.dependencies]
|
[target.'cfg(any(target_os = "windows"))'.dependencies]
|
||||||
winapi = { version = "0.3.9", features = ["winuser"] }
|
|
||||||
windows-sys = { workspace = true, features = [
|
windows-sys = { workspace = true, features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_UI_Shell",
|
|
||||||
"Win32_System_Com",
|
"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]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
bytemuck.workspace = true
|
bytemuck.workspace = true
|
||||||
image = { workspace = true, features = ["png"] } # For copying images
|
image = { workspace = true, features = ["png"] } # For copying images
|
||||||
js-sys = "0.3"
|
js-sys.workspace = true
|
||||||
percent-encoding = "2.1"
|
percent-encoding.workspace = true
|
||||||
wasm-bindgen.workspace = true
|
wasm-bindgen.workspace = true
|
||||||
wasm-bindgen-futures.workspace = true
|
wasm-bindgen-futures.workspace = true
|
||||||
web-sys = { workspace = true, features = [
|
web-sys = { workspace = true, features = [
|
||||||
@@ -253,6 +237,7 @@ web-sys = { workspace = true, features = [
|
|||||||
"ResizeObserverEntry",
|
"ResizeObserverEntry",
|
||||||
"ResizeObserverOptions",
|
"ResizeObserverOptions",
|
||||||
"ResizeObserverSize",
|
"ResizeObserverSize",
|
||||||
|
"ShadowRoot",
|
||||||
"Storage",
|
"Storage",
|
||||||
"Touch",
|
"Touch",
|
||||||
"TouchEvent",
|
"TouchEvent",
|
||||||
@@ -266,13 +251,9 @@ web-sys = { workspace = true, features = [
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
# optional web:
|
# optional web:
|
||||||
egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit
|
egui-wgpu = { workspace = true, optional = true, features = ["capture"] } # if wgpu is used, use it without (!) winit
|
||||||
wgpu = { workspace = true, optional = true, features = [
|
wgpu = { workspace = true, optional = true }
|
||||||
# Let's enable some backends so that users can use `eframe` out-of-the-box
|
|
||||||
# without having to explicitly opt-in to backends
|
|
||||||
"webgpu",
|
|
||||||
] }
|
|
||||||
|
|
||||||
# Native dev dependencies for testing
|
# Native dev dependencies for testing
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
[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)).
|
`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!
|
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>.
|
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:
|
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
|
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
|
## 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.
|
`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
|
## Limitations when running egui on the web
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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;
|
pub use crate::native::winit_integration::UserEvent;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -22,7 +22,7 @@ use raw_window_handle::{
|
|||||||
use static_assertions::assert_not_impl_any;
|
use static_assertions::assert_not_impl_any;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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};
|
pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
|
||||||
|
|
||||||
/// Hook into the building of an event loop before it is run
|
/// 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
|
/// You can configure any platform specific details required on top of the default configuration
|
||||||
/// done by `EFrame`.
|
/// done by `EFrame`.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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>)>;
|
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
|
||||||
|
|
||||||
/// Hook into the building of a the native window.
|
/// 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
|
/// You can configure any platform specific details required on top of the default configuration
|
||||||
/// done by `eframe`.
|
/// done by `eframe`.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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>;
|
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
|
||||||
|
|
||||||
type DynError = Box<dyn std::error::Error + Send + Sync>;
|
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`].
|
/// 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.
|
/// 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>,
|
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||||
|
|
||||||
/// Raw platform window handle
|
/// Raw platform window handle
|
||||||
@@ -91,7 +91,7 @@ pub struct CreationContext<'s> {
|
|||||||
pub(crate) raw_display_handle: Result<RawDisplayHandle, HandleError>,
|
pub(crate) raw_display_handle: Result<RawDisplayHandle, HandleError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl HasWindowHandle for CreationContext<'_> {
|
impl HasWindowHandle for CreationContext<'_> {
|
||||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl HasDisplayHandle for CreationContext<'_> {
|
impl HasDisplayHandle for CreationContext<'_> {
|
||||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||||
@@ -121,7 +121,7 @@ impl CreationContext<'_> {
|
|||||||
gl: None,
|
gl: None,
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
get_proc_address: None,
|
get_proc_address: None,
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_render_state: None,
|
wgpu_render_state: None,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
raw_window_handle: Err(HandleError::NotSupported),
|
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 {
|
pub trait App {
|
||||||
/// Called each time the UI needs repainting, which may be many times per second.
|
/// 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,
|
pub hardware_acceleration: HardwareAcceleration,
|
||||||
|
|
||||||
/// What rendering backend to use.
|
/// What rendering backend to use.
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
pub renderer: Renderer,
|
pub renderer: Renderer,
|
||||||
|
|
||||||
/// This controls what happens when you close the main eframe window.
|
/// This controls what happens when you close the main eframe window.
|
||||||
@@ -340,7 +340,7 @@ pub struct NativeOptions {
|
|||||||
/// event loop before it is run.
|
/// event loop before it is run.
|
||||||
///
|
///
|
||||||
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
|
/// 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>,
|
pub event_loop_builder: Option<EventLoopBuilderHook>,
|
||||||
|
|
||||||
/// Hook into the building of a window.
|
/// Hook into the building of a window.
|
||||||
@@ -349,7 +349,7 @@ pub struct NativeOptions {
|
|||||||
/// window appearance.
|
/// window appearance.
|
||||||
///
|
///
|
||||||
/// Note: A [`NativeOptions`] clone will not include any `window_builder` hook.
|
/// 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>,
|
pub window_builder: Option<WindowBuilderHook>,
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
@@ -367,7 +367,7 @@ pub struct NativeOptions {
|
|||||||
pub centered: bool,
|
pub centered: bool,
|
||||||
|
|
||||||
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
||||||
|
|
||||||
/// Controls whether or not the native window position and size will be
|
/// Controls whether or not the native window position and size will be
|
||||||
@@ -404,13 +404,13 @@ impl Clone for NativeOptions {
|
|||||||
Self {
|
Self {
|
||||||
viewport: self.viewport.clone(),
|
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
|
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
|
window_builder: None, // Skip any builder callbacks if cloning
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_options: self.wgpu_options.clone(),
|
wgpu_options: self.wgpu_options.clone(),
|
||||||
|
|
||||||
persistence_path: self.persistence_path.clone(),
|
persistence_path: self.persistence_path.clone(),
|
||||||
@@ -435,15 +435,15 @@ impl Default for NativeOptions {
|
|||||||
stencil_buffer: 0,
|
stencil_buffer: 0,
|
||||||
hardware_acceleration: HardwareAcceleration::Preferred,
|
hardware_acceleration: HardwareAcceleration::Preferred,
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
renderer: Renderer::default(),
|
renderer: Renderer::default(),
|
||||||
|
|
||||||
run_and_return: true,
|
run_and_return: true,
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
event_loop_builder: None,
|
event_loop_builder: None,
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
window_builder: None,
|
window_builder: None,
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
@@ -451,7 +451,7 @@ impl Default for NativeOptions {
|
|||||||
|
|
||||||
centered: false,
|
centered: false,
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||||
|
|
||||||
persist_window: true,
|
persist_window: true,
|
||||||
@@ -471,6 +471,10 @@ impl Default for NativeOptions {
|
|||||||
/// Options when using `eframe` in a web page.
|
/// Options when using `eframe` in a web page.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub struct WebOptions {
|
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.
|
/// Sets the number of bits in the depth buffer.
|
||||||
///
|
///
|
||||||
/// `egui` doesn't need the depth buffer, so the default value is 0.
|
/// `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,
|
pub webgl_context_option: WebGlContextOption,
|
||||||
|
|
||||||
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
||||||
|
|
||||||
/// Controls whether to apply dithering to minimize banding artifacts.
|
/// 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
|
/// If the web event corresponding to an egui event should be propagated
|
||||||
/// to the rest of the web page.
|
/// 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)
|
/// [`stopPropagation`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)
|
||||||
/// is called on every event.
|
/// is called on every event, and the event is not propagated to the rest of the web page.
|
||||||
pub should_propagate_event: Box<dyn Fn(&egui::Event) -> bool>,
|
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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
impl Default for WebOptions {
|
impl Default for WebOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
|
renderer: Renderer::default(),
|
||||||
|
|
||||||
depth_buffer: 0,
|
depth_buffer: 0,
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
webgl_context_option: WebGlContextOption::BestFirst,
|
webgl_context_option: WebGlContextOption::BestFirst,
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||||
|
|
||||||
dithering: true,
|
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.
|
/// What rendering backend to use.
|
||||||
///
|
///
|
||||||
/// You need to enable the "glow" and "wgpu" features to have a choice.
|
/// 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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||||
@@ -558,47 +578,49 @@ pub enum Renderer {
|
|||||||
Glow,
|
Glow,
|
||||||
|
|
||||||
/// Use [`egui_wgpu`] renderer for [`wgpu`](https://github.com/gfx-rs/wgpu).
|
/// Use [`egui_wgpu`] renderer for [`wgpu`](https://github.com/gfx-rs/wgpu).
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
Wgpu,
|
Wgpu,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
impl Default for Renderer {
|
impl Default for Renderer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
#[cfg(not(feature = "glow"))]
|
#[cfg(not(feature = "glow"))]
|
||||||
#[cfg(not(feature = "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'");
|
compile_error!(
|
||||||
|
"eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
#[cfg(not(feature = "wgpu"))]
|
#[cfg(not(feature = "wgpu_no_default_features"))]
|
||||||
return Self::Glow;
|
return Self::Glow;
|
||||||
|
|
||||||
#[cfg(not(feature = "glow"))]
|
#[cfg(not(feature = "glow"))]
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
return Self::Wgpu;
|
return Self::Wgpu;
|
||||||
|
|
||||||
// By default, only the `glow` feature is enabled, so if the user added `wgpu` to the feature list
|
// It's weird that the user has enabled both glow and wgpu,
|
||||||
// they probably wanted to use wgpu:
|
// but let's pick the better of the two (wgpu):
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
return Self::Wgpu;
|
return Self::Wgpu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
impl std::fmt::Display for Renderer {
|
impl std::fmt::Display for Renderer {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
Self::Glow => "glow".fmt(f),
|
Self::Glow => "glow".fmt(f),
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
Self::Wgpu => "wgpu".fmt(f),
|
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 {
|
impl std::str::FromStr for Renderer {
|
||||||
type Err = String;
|
type Err = String;
|
||||||
|
|
||||||
@@ -607,10 +629,12 @@ impl std::str::FromStr for Renderer {
|
|||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
"glow" => Ok(Self::Glow),
|
"glow" => Ok(Self::Glow),
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
"wgpu" => Ok(Self::Wgpu),
|
"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>>,
|
Option<Box<dyn FnMut(glow::Texture) -> egui::TextureId>>,
|
||||||
|
|
||||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
/// 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)]
|
#[doc(hidden)]
|
||||||
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||||
|
|
||||||
@@ -655,7 +679,7 @@ pub struct Frame {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
assert_not_impl_any!(Frame: Clone);
|
assert_not_impl_any!(Frame: Clone);
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl HasWindowHandle for Frame {
|
impl HasWindowHandle for Frame {
|
||||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl HasDisplayHandle for Frame {
|
impl HasDisplayHandle for Frame {
|
||||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||||
@@ -688,7 +712,7 @@ impl Frame {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
raw_window_handle: Err(HandleError::NotSupported),
|
raw_window_handle: Err(HandleError::NotSupported),
|
||||||
storage: None,
|
storage: None,
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_render_state: None,
|
wgpu_render_state: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,7 +720,7 @@ impl Frame {
|
|||||||
/// True if you are in a web environment.
|
/// True if you are in a web environment.
|
||||||
///
|
///
|
||||||
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
||||||
#[allow(clippy::unused_self)]
|
#[expect(clippy::unused_self)]
|
||||||
pub fn is_web(&self) -> bool {
|
pub fn is_web(&self) -> bool {
|
||||||
cfg!(target_arch = "wasm32")
|
cfg!(target_arch = "wasm32")
|
||||||
}
|
}
|
||||||
@@ -747,7 +771,7 @@ impl Frame {
|
|||||||
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
|
/// 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.
|
/// 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> {
|
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
|
||||||
self.wgpu_render_state.as_ref()
|
self.wgpu_render_state.as_ref()
|
||||||
}
|
}
|
||||||
@@ -882,16 +906,15 @@ pub trait Storage {
|
|||||||
#[cfg(feature = "ron")]
|
#[cfg(feature = "ron")]
|
||||||
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
|
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
|
||||||
profiling::function_scope!(key);
|
profiling::function_scope!(key);
|
||||||
storage
|
let value = storage.get_string(key)?;
|
||||||
.get_string(key)
|
match ron::from_str(&value) {
|
||||||
.and_then(|value| match ron::from_str(&value) {
|
Ok(value) => Some(value),
|
||||||
Ok(value) => Some(value),
|
Err(err) => {
|
||||||
Err(err) => {
|
// This happens on when we break the format, e.g. when updating egui.
|
||||||
// This happens on when we break the format, e.g. when updating egui.
|
log::debug!("Failed to decode RON: {err}");
|
||||||
log::debug!("Failed to decode RON: {err}");
|
None
|
||||||
None
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
|
/// 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);
|
profiling::function_scope!(key);
|
||||||
match ron::ser::to_string(value) {
|
match ron::ser::to_string(value) {
|
||||||
Ok(string) => storage.set_string(key, string),
|
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,
|
//! If you are planning to write an app for web or native,
|
||||||
//! and want to use [`egui`] for everything, then `eframe` is for you!
|
//! 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!
|
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
|
||||||
//!
|
//!
|
||||||
//! In short, you implement [`App`] (especially [`App::update`]) and then
|
//! In short, you implement [`App`] (especially [`App::update`]) and then
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
//! #[wasm_bindgen]
|
//! #[wasm_bindgen]
|
||||||
//! impl WebHandle {
|
//! impl WebHandle {
|
||||||
//! /// Installs a panic hook, then returns.
|
//! /// Installs a panic hook, then returns.
|
||||||
//! #[allow(clippy::new_without_default)]
|
//! #[expect(clippy::new_without_default)]
|
||||||
//! #[wasm_bindgen(constructor)]
|
//! #[wasm_bindgen(constructor)]
|
||||||
//! pub fn new() -> Self {
|
//! pub fn new() -> Self {
|
||||||
//! // Redirect [`log`] message to `console.log` and friends:
|
//! // Redirect [`log`] message to `console.log` and friends:
|
||||||
@@ -144,13 +144,22 @@
|
|||||||
#![warn(missing_docs)] // let's keep eframe well-documented
|
#![warn(missing_docs)] // let's keep eframe well-documented
|
||||||
#![allow(clippy::needless_doctest_main)]
|
#![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:
|
// Re-export all useful libraries:
|
||||||
pub use {egui, egui::emath, egui::epaint};
|
pub use {egui, egui::emath, egui::epaint};
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
pub use {egui_glow, glow};
|
pub use {egui_glow, glow};
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
pub use {egui_wgpu, wgpu};
|
pub use {egui_wgpu, wgpu};
|
||||||
|
|
||||||
mod epi;
|
mod epi;
|
||||||
@@ -179,11 +188,19 @@ pub use web::{WebLogger, WebRunner};
|
|||||||
// When compiling natively
|
// When compiling natively
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
mod native;
|
mod native;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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")]
|
#[cfg(feature = "persistence")]
|
||||||
pub use native::file_storage::storage_dir;
|
pub use native::file_storage::storage_dir;
|
||||||
|
|
||||||
@@ -192,7 +209,7 @@ pub mod icon_data;
|
|||||||
|
|
||||||
/// This is how you start a native (desktop) app.
|
/// 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`]).
|
/// used for the save location of persistence (see [`App::save`]).
|
||||||
/// It is also used as the application id on wayland.
|
/// It is also used as the application id on wayland.
|
||||||
/// If you set no title on the viewport, the app id will be used
|
/// If you set no title on the viewport, the app id will be used
|
||||||
@@ -235,13 +252,113 @@ pub mod icon_data;
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// This function can fail if we fail to set up a graphics context.
|
/// This function can fail if we fail to set up a graphics context.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
|
||||||
pub fn run_native(
|
pub fn run_native(
|
||||||
app_name: &str,
|
app_name: &str,
|
||||||
mut native_options: NativeOptions,
|
mut native_options: NativeOptions,
|
||||||
app_creator: AppCreator<'_>,
|
app_creator: AppCreator<'_>,
|
||||||
) -> Result {
|
) -> 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"))]
|
#[cfg(not(feature = "__screenshot"))]
|
||||||
assert!(
|
assert!(
|
||||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||||
@@ -254,28 +371,16 @@ pub fn run_native(
|
|||||||
|
|
||||||
let renderer = native_options.renderer;
|
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::Glow => "glow",
|
||||||
Renderer::Wgpu => "wgpu",
|
Renderer::Wgpu => "wgpu",
|
||||||
};
|
};
|
||||||
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
|
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
match renderer {
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@@ -315,7 +420,7 @@ pub fn run_native(
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// This function can fail if we fail to set up a graphics context.
|
/// This function can fail if we fail to set up a graphics context.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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(
|
pub fn run_simple_native(
|
||||||
app_name: &str,
|
app_name: &str,
|
||||||
native_options: NativeOptions,
|
native_options: NativeOptions,
|
||||||
@@ -367,7 +472,7 @@ pub enum Error {
|
|||||||
OpenGL(egui_glow::PainterError),
|
OpenGL(egui_glow::PainterError),
|
||||||
|
|
||||||
/// An error from [`wgpu`].
|
/// An error from [`wgpu`].
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
Wgpu(egui_wgpu::WgpuError),
|
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 {
|
impl From<egui_wgpu::WgpuError> for Error {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(err: egui_wgpu::WgpuError) -> Self {
|
fn from(err: egui_wgpu::WgpuError) -> Self {
|
||||||
@@ -446,7 +551,7 @@ impl std::fmt::Display for Error {
|
|||||||
write!(f, "egui_glow: {err}")
|
write!(f, "egui_glow: {err}")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
Self::Wgpu(err) => {
|
Self::Wgpu(err) => {
|
||||||
write!(f, "WGPU error: {err}")
|
write!(f, "WGPU error: {err}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ pub struct AppTitleIconSetter {
|
|||||||
|
|
||||||
impl AppTitleIconSetter {
|
impl AppTitleIconSetter {
|
||||||
pub fn new(title: String, mut icon_data: Option<Arc<IconData>>) -> Self {
|
pub fn new(title: String, mut icon_data: Option<Arc<IconData>>) -> Self {
|
||||||
if let Some(icon) = &icon_data {
|
if let Some(icon) = &icon_data
|
||||||
if **icon == IconData::default() {
|
&& **icon == IconData::default()
|
||||||
icon_data = None;
|
{
|
||||||
}
|
icon_data = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -47,7 +47,7 @@ enum AppIconStatus {
|
|||||||
NotSetTryAgain,
|
NotSetTryAgain,
|
||||||
|
|
||||||
/// We successfully set the icon and it should be visible now.
|
/// 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,
|
Set,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,16 +71,20 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
return set_title_and_icon_mac(_title, _icon_data);
|
return set_title_and_icon_mac(_title, _icon_data);
|
||||||
|
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code, clippy::allow_attributes)]
|
||||||
AppIconStatus::NotSetIgnored
|
AppIconStatus::NotSetIgnored
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set icon for Windows applications.
|
/// Set icon for Windows applications.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||||
use crate::icon_data::IconDataExt as _;
|
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!
|
// 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.
|
// 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)
|
// * using undocumented SetConsoleIcon method (successfully queried via GetProcAddress)
|
||||||
|
|
||||||
// SAFETY: WinApi function without side-effects.
|
// SAFETY: WinApi function without side-effects.
|
||||||
let window_handle = unsafe { winuser::GetActiveWindow() };
|
let window_handle = unsafe { GetActiveWindow() };
|
||||||
if window_handle.is_null() {
|
if window_handle.is_null() {
|
||||||
// The Window isn't available yet. Try again later!
|
// The Window isn't available yet. Try again later!
|
||||||
return AppIconStatus::NotSetTryAgain;
|
return AppIconStatus::NotSetTryAgain;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_hicon_with_scale(
|
fn create_hicon_with_scale(unscaled_image: &image::RgbaImage, target_size: i32) -> HICON {
|
||||||
unscaled_image: &image::RgbaImage,
|
|
||||||
target_size: i32,
|
|
||||||
) -> winapi::shared::windef::HICON {
|
|
||||||
let image_scaled = image::imageops::resize(
|
let image_scaled = image::imageops::resize(
|
||||||
unscaled_image,
|
unscaled_image,
|
||||||
target_size as _,
|
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.
|
// SAFETY: Creating an HICON which should be readonly on our data.
|
||||||
unsafe {
|
unsafe {
|
||||||
winuser::CreateIconFromResourceEx(
|
CreateIconFromResourceEx(
|
||||||
image_scaled_bytes.as_mut_ptr(),
|
image_scaled_bytes.as_mut_ptr(),
|
||||||
image_scaled_bytes.len() as u32,
|
image_scaled_bytes.len() as u32,
|
||||||
1, // Means this is an icon, not a cursor.
|
1, // Means this is an icon, not a cursor.
|
||||||
0x00030000, // Version number of the HICON
|
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, // Note that this method can scale, but it does so *very* poorly. So let's avoid that!
|
||||||
target_size,
|
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!
|
// 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.
|
// 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);
|
let icon_big = create_hicon_with_scale(&unscaled_image, icon_size_big);
|
||||||
if icon_big.is_null() {
|
if icon_big.is_null() {
|
||||||
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
|
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 {
|
} else {
|
||||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||||
unsafe {
|
unsafe {
|
||||||
winuser::SendMessageW(
|
SendMessageW(
|
||||||
window_handle,
|
window_handle,
|
||||||
winuser::WM_SETICON,
|
WM_SETICON,
|
||||||
winuser::ICON_BIG as usize,
|
ICON_BIG as usize,
|
||||||
icon_big as isize,
|
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.
|
// 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);
|
let icon_small = create_hicon_with_scale(&unscaled_image, icon_size_small);
|
||||||
if icon_small.is_null() {
|
if icon_small.is_null() {
|
||||||
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
|
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 {
|
} else {
|
||||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||||
unsafe {
|
unsafe {
|
||||||
winuser::SendMessageW(
|
SendMessageW(
|
||||||
window_handle,
|
window_handle,
|
||||||
winuser::WM_SETICON,
|
WM_SETICON,
|
||||||
winuser::ICON_SMALL as usize,
|
ICON_SMALL as usize,
|
||||||
icon_small as isize,
|
icon_small as isize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -198,20 +199,25 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
|||||||
|
|
||||||
/// Set icon & app title for `MacOS` applications.
|
/// Set icon & app title for `MacOS` applications.
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
|
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
|
||||||
use crate::icon_data::IconDataExt as _;
|
use crate::icon_data::IconDataExt as _;
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
use objc2::ClassType;
|
use objc2::ClassType as _;
|
||||||
use objc2_app_kit::{NSApplication, NSImage};
|
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 {
|
// Do NOT use png even though creating `NSImage` from it is much easier than from raw images data!
|
||||||
match icon_data.to_png_bytes() {
|
//
|
||||||
Ok(png_bytes) => Some(png_bytes),
|
// 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) => {
|
Err(err) => {
|
||||||
log::warn!("Failed to convert IconData to png: {err}");
|
log::warn!("Failed to read icon data: {err}");
|
||||||
return AppIconStatus::NotSetIgnored;
|
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`
|
// TODO(madsmtm): Move this into `objc2-app-kit`
|
||||||
extern "C" {
|
unsafe extern "C" {
|
||||||
static NSApp: Option<&'static NSApplication>;
|
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;
|
return AppIconStatus::NotSetIgnored;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(png_bytes) = png_bytes {
|
if let Some(image) = image {
|
||||||
let data = NSData::from_vec(png_bytes);
|
use objc2_app_kit::{NSBitmapImageRep, NSDeviceRGBColorSpace};
|
||||||
|
use objc2_foundation::NSSize;
|
||||||
|
|
||||||
log::trace!("NSImage::initWithData…");
|
log::trace!(
|
||||||
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
|
"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_");
|
profiling::scope!("setApplicationIconImage_");
|
||||||
log::trace!("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.
|
// 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(main_menu) = app.mainMenu()
|
||||||
if let Some(item) = main_menu.itemAtIndex(0) {
|
&& let Some(item) = main_menu.itemAtIndex(0)
|
||||||
if let Some(app_menu) = item.submenu() {
|
&& let Some(app_menu) = item.submenu()
|
||||||
profiling::scope!("setTitle_");
|
{
|
||||||
app_menu.setTitle(&NSString::from_str(title));
|
profiling::scope!("setTitle_");
|
||||||
}
|
app_menu.setTitle(&NSString::from_str(title));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The title in the Dock apparently can't be changed.
|
// 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);
|
viewport_builder = viewport_builder.with_position(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
if clamp_size_to_monitor_size {
|
if clamp_size_to_monitor_size && let Some(initial_window_size) = viewport_builder.inner_size
|
||||||
if let Some(initial_window_size) = viewport_builder.inner_size {
|
{
|
||||||
let initial_window_size = egui::NumExt::at_most(
|
let initial_window_size = egui::NumExt::at_most(
|
||||||
initial_window_size,
|
initial_window_size,
|
||||||
largest_monitor_point_size(egui_zoom_factor, event_loop),
|
largest_monitor_point_size(egui_zoom_factor, event_loop),
|
||||||
);
|
);
|
||||||
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
|
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewport_builder.inner_size
|
viewport_builder.inner_size
|
||||||
@@ -136,7 +135,7 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
|
|||||||
None
|
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>> {
|
pub fn create_storage_with_file(_file: impl Into<PathBuf>) -> Option<Box<dyn epi::Storage>> {
|
||||||
#[cfg(feature = "persistence")]
|
#[cfg(feature = "persistence")]
|
||||||
return Some(Box::new(
|
return Some(Box::new(
|
||||||
@@ -169,7 +168,7 @@ pub struct EpiIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EpiIntegration {
|
impl EpiIntegration {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::allow_attributes, clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
egui_ctx: egui::Context,
|
egui_ctx: egui::Context,
|
||||||
window: &winit::window::Window,
|
window: &winit::window::Window,
|
||||||
@@ -180,7 +179,9 @@ impl EpiIntegration {
|
|||||||
#[cfg(feature = "glow")] glow_register_native_texture: Option<
|
#[cfg(feature = "glow")] glow_register_native_texture: Option<
|
||||||
Box<dyn FnMut(glow::Texture) -> egui::TextureId>,
|
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 {
|
) -> Self {
|
||||||
let frame = epi::Frame {
|
let frame = epi::Frame {
|
||||||
info: epi::IntegrationInfo { cpu_usage: None },
|
info: epi::IntegrationInfo { cpu_usage: None },
|
||||||
@@ -189,7 +190,7 @@ impl EpiIntegration {
|
|||||||
gl,
|
gl,
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
glow_register_native_texture,
|
glow_register_native_texture,
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_render_state,
|
wgpu_render_state,
|
||||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||||
raw_window_handle: window.window_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")]
|
#[cfg(feature = "persistence")]
|
||||||
if let Some(storage) = self.frame.storage_mut() {
|
if let Some(storage) = self.frame.storage_mut() {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
if let Some(window) = _window {
|
if let Some(window) = window
|
||||||
if self.persist_window {
|
&& self.persist_window
|
||||||
profiling::scope!("native_window");
|
{
|
||||||
epi::set_value(
|
profiling::scope!("native_window");
|
||||||
storage,
|
epi::set_value(
|
||||||
STORAGE_WINDOW_KEY,
|
storage,
|
||||||
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
|
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");
|
profiling::scope!("egui_memory");
|
||||||
self.egui_ctx
|
self.egui_ctx
|
||||||
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
profiling::scope!("App::save");
|
profiling::scope!("App::save");
|
||||||
_app.save(storage);
|
app.save(storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
profiling::scope!("Storage::flush");
|
profiling::scope!("Storage::flush");
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ impl Drop for EventLoopGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to safely use the current event loop
|
// 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>
|
pub fn with_current_event_loop<F, R>(f: F) -> Option<R>
|
||||||
where
|
where
|
||||||
F: FnOnce(&ActiveEventLoop) -> R,
|
F: FnOnce(&ActiveEventLoop) -> R,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::Write,
|
io::Write as _,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,20 +42,20 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
|
|||||||
// Adapted from
|
// Adapted from
|
||||||
// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37
|
// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37
|
||||||
#[cfg(all(windows, not(target_vendor = "uwp")))]
|
#[cfg(all(windows, not(target_vendor = "uwp")))]
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
fn roaming_appdata() -> Option<PathBuf> {
|
fn roaming_appdata() -> Option<PathBuf> {
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::os::windows::ffi::OsStringExt;
|
use std::os::windows::ffi::OsStringExt as _;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
use std::slice;
|
use std::slice;
|
||||||
|
|
||||||
use windows_sys::Win32::Foundation::S_OK;
|
use windows_sys::Win32::Foundation::S_OK;
|
||||||
use windows_sys::Win32::System::Com::CoTaskMemFree;
|
use windows_sys::Win32::System::Com::CoTaskMemFree;
|
||||||
use windows_sys::Win32::UI::Shell::{
|
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;
|
fn wcslen(buf: *const u16) -> usize;
|
||||||
}
|
}
|
||||||
let mut path_raw = ptr::null_mut();
|
let mut path_raw = ptr::null_mut();
|
||||||
@@ -72,7 +72,7 @@ fn roaming_appdata() -> Option<PathBuf> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let path = if result == S_OK {
|
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)) };
|
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
|
||||||
Some(PathBuf::from(OsString::from_wide(path_slice)))
|
Some(PathBuf::from(OsString::from_wide(path_slice)))
|
||||||
} else {
|
} else {
|
||||||
@@ -118,7 +118,7 @@ impl FileStorage {
|
|||||||
pub(crate) fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
pub(crate) fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
let ron_filepath: PathBuf = ron_filepath.into();
|
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 {
|
Self {
|
||||||
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
||||||
ron_filepath,
|
ron_filepath,
|
||||||
@@ -133,9 +133,8 @@ impl FileStorage {
|
|||||||
if let Some(data_dir) = storage_dir(app_id) {
|
if let Some(data_dir) = storage_dir(app_id) {
|
||||||
if let Err(err) = std::fs::create_dir_all(&data_dir) {
|
if let Err(err) = std::fs::create_dir_all(&data_dir) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Saving disabled: Failed to create app path at {:?}: {}",
|
"Saving disabled: Failed to create app path at {}: {err}",
|
||||||
data_dir,
|
data_dir.display()
|
||||||
err
|
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -193,12 +192,11 @@ impl crate::Storage for FileStorage {
|
|||||||
fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
|
fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
if let Some(parent_dir) = file_path.parent() {
|
if let Some(parent_dir) = file_path.parent()
|
||||||
if !parent_dir.exists() {
|
&& !parent_dir.exists()
|
||||||
if let Err(err) = std::fs::create_dir_all(parent_dir) {
|
&& let Err(err) = std::fs::create_dir_all(parent_dir)
|
||||||
log::warn!("Failed to create directory {parent_dir:?}: {err}");
|
{
|
||||||
}
|
log::warn!("Failed to create directory {}: {err}", parent_dir.display());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match std::fs::File::create(file_path) {
|
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();
|
let config = Default::default();
|
||||||
|
|
||||||
profiling::scope!("ron::serialize");
|
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()))
|
.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 {
|
} else {
|
||||||
log::trace!("Persisted to {:?}", file_path);
|
log::trace!("Persisted to {}", file_path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
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) {
|
match ron::de::from_reader(reader) {
|
||||||
Ok(value) => Some(value),
|
Ok(value) => Some(value),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("Failed to parse RON: {}", err);
|
log::warn!("Failed to parse RON: {err}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,34 +11,34 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
|
|||||||
|
|
||||||
use egui_winit::ActionRequested;
|
use egui_winit::ActionRequested;
|
||||||
use glutin::{
|
use glutin::{
|
||||||
config::GlConfig,
|
config::GlConfig as _,
|
||||||
context::NotCurrentGlContext,
|
context::NotCurrentGlContext as _,
|
||||||
display::GetGlDisplay,
|
display::GetGlDisplay as _,
|
||||||
prelude::{GlDisplay, PossiblyCurrentGlContext},
|
prelude::{GlDisplay as _, PossiblyCurrentGlContext as _},
|
||||||
surface::GlSurface,
|
surface::GlSurface as _,
|
||||||
};
|
};
|
||||||
use raw_window_handle::HasWindowHandle;
|
use raw_window_handle::HasWindowHandle as _;
|
||||||
use winit::{
|
use winit::{
|
||||||
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
|
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
|
||||||
window::{Window, WindowId},
|
window::{Window, WindowId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ahash::{HashMap, HashSet};
|
use ahash::HashMap;
|
||||||
use egui::{
|
use egui::{
|
||||||
DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, ViewportClass, ViewportId,
|
DeferredViewportUiCallback, ImmediateViewport, OrderedViewportIdMap, ViewportBuilder,
|
||||||
ViewportIdMap, ViewportIdPair, ViewportInfo, ViewportOutput,
|
ViewportClass, ViewportId, ViewportIdPair, ViewportInfo, ViewportOutput,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
use egui_winit::accesskit_winit;
|
use egui_winit::accesskit_winit;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions,
|
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||||
Result, Storage,
|
native::epi_integration::EpiIntegration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
epi_integration, event_loop_context,
|
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>,
|
current_gl_context: Option<glutin::context::PossiblyCurrentContext>,
|
||||||
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
|
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
|
||||||
|
|
||||||
viewports: ViewportIdMap<Viewport>,
|
viewports: OrderedViewportIdMap<Viewport>,
|
||||||
viewport_from_window: HashMap<WindowId, ViewportId>,
|
viewport_from_window: HashMap<WindowId, ViewportId>,
|
||||||
window_from_viewport: ViewportIdMap<WindowId>,
|
window_from_viewport: OrderedViewportIdMap<WindowId>,
|
||||||
|
|
||||||
focused_viewport: Option<ViewportId>,
|
focused_viewport: Option<ViewportId>,
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ struct Viewport {
|
|||||||
builder: ViewportBuilder,
|
builder: ViewportBuilder,
|
||||||
deferred_commands: Vec<egui::viewport::ViewportCommand>,
|
deferred_commands: Vec<egui::viewport::ViewportCommand>,
|
||||||
info: ViewportInfo,
|
info: ViewportInfo,
|
||||||
actions_requested: HashSet<egui_winit::ActionRequested>,
|
actions_requested: Vec<egui_winit::ActionRequested>,
|
||||||
|
|
||||||
/// The user-callback that shows the ui.
|
/// The user-callback that shows the ui.
|
||||||
/// None for immediate viewports.
|
/// None for immediate viewports.
|
||||||
@@ -139,7 +139,7 @@ impl<'app> GlowWinitApp<'app> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
fn create_glutin_windowed_context(
|
fn create_glutin_windowed_context(
|
||||||
egui_ctx: &egui::Context,
|
egui_ctx: &egui::Context,
|
||||||
event_loop: &ActiveEventLoop,
|
event_loop: &ActiveEventLoop,
|
||||||
@@ -239,7 +239,7 @@ impl<'app> GlowWinitApp<'app> {
|
|||||||
let painter = painter.clone();
|
let painter = painter.clone();
|
||||||
move |native| painter.borrow_mut().register_native_texture(native)
|
move |native| painter.borrow_mut().register_native_texture(native)
|
||||||
})),
|
})),
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ impl<'app> GlowWinitApp<'app> {
|
|||||||
..
|
..
|
||||||
} = viewport
|
} = 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
|
.viewport
|
||||||
.mouse_passthrough
|
.mouse_passthrough
|
||||||
.unwrap_or(false)
|
.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)
|
let app_creator = std::mem::take(&mut self.app_creator)
|
||||||
@@ -302,7 +301,7 @@ impl<'app> GlowWinitApp<'app> {
|
|||||||
storage: integration.frame.storage(),
|
storage: integration.frame.storage(),
|
||||||
gl: Some(gl),
|
gl: Some(gl),
|
||||||
get_proc_address: Some(&get_proc_address),
|
get_proc_address: Some(&get_proc_address),
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_render_state: None,
|
wgpu_render_state: None,
|
||||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||||
raw_window_handle: window.window_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 {
|
Ok(self.running.insert(GlowWinitRunning {
|
||||||
glutin,
|
|
||||||
painter,
|
|
||||||
integration,
|
integration,
|
||||||
app,
|
app,
|
||||||
|
glutin,
|
||||||
|
painter,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,8 +361,12 @@ impl WinitApp for GlowWinitApp<'_> {
|
|||||||
|
|
||||||
fn window_id_from_viewport_id(&self, id: ViewportId) -> Option<WindowId> {
|
fn window_id_from_viewport_id(&self, id: ViewportId) -> Option<WindowId> {
|
||||||
self.running
|
self.running
|
||||||
.as_ref()
|
.as_ref()?
|
||||||
.and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied())
|
.glutin
|
||||||
|
.borrow()
|
||||||
|
.window_from_viewport
|
||||||
|
.get(&id)
|
||||||
|
.copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&mut self) {
|
fn save(&mut self) {
|
||||||
@@ -436,20 +439,20 @@ impl WinitApp for GlowWinitApp<'_> {
|
|||||||
_: winit::event::DeviceId,
|
_: winit::event::DeviceId,
|
||||||
event: winit::event::DeviceEvent,
|
event: winit::event::DeviceEvent,
|
||||||
) -> crate::Result<EventResult> {
|
) -> crate::Result<EventResult> {
|
||||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
|
if let winit::event::DeviceEvent::MouseMotion { delta } = event
|
||||||
if let Some(running) = &mut self.running {
|
&& let Some(running) = &mut self.running
|
||||||
let mut glutin = running.glutin.borrow_mut();
|
{
|
||||||
if let Some(viewport) = glutin
|
let mut glutin = running.glutin.borrow_mut();
|
||||||
.focused_viewport
|
if let Some(viewport) = glutin
|
||||||
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
|
.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(egui_winit) = viewport.egui_winit.as_mut() {
|
||||||
}
|
egui_winit.on_mouse_motion(delta);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(window) = viewport.window.as_ref() {
|
if let Some(window) = viewport.window.as_ref() {
|
||||||
return Ok(EventResult::RepaintNext(window.id()));
|
return Ok(EventResult::RepaintNext(window.id()));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,7 +469,7 @@ impl WinitApp for GlowWinitApp<'_> {
|
|||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
Ok(running.on_window_event(window_id, &event))
|
Ok(running.on_window_event(window_id, &event))
|
||||||
} else {
|
} else {
|
||||||
Ok(EventResult::Wait)
|
Ok(EventResult::Exit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,16 +479,15 @@ impl WinitApp for GlowWinitApp<'_> {
|
|||||||
|
|
||||||
if let Some(running) = &self.running {
|
if let Some(running) = &self.running {
|
||||||
let mut glutin = running.glutin.borrow_mut();
|
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_id) = glutin.viewport_from_window.get(&event.window_id).copied()
|
||||||
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
|
||||||
if let Some(egui_winit) = &mut viewport.egui_winit {
|
&& let Some(egui_winit) = &mut viewport.egui_winit
|
||||||
return Ok(winit_integration::on_accesskit_window_event(
|
{
|
||||||
egui_winit,
|
return Ok(winit_integration::on_accesskit_window_event(
|
||||||
event.window_id,
|
egui_winit,
|
||||||
&event.window_event,
|
event.window_id,
|
||||||
));
|
&event.window_event,
|
||||||
}
|
));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,10 +525,10 @@ impl GlowWinitRunning<'_> {
|
|||||||
if is_immediate && viewport_id != ViewportId::ROOT {
|
if is_immediate && viewport_id != ViewportId::ROOT {
|
||||||
// This will only happen if this is an immediate viewport.
|
// 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.
|
// 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(parent_viewport) = glutin.viewports.get(&viewport.ids.parent)
|
||||||
if let Some(window) = parent_viewport.window.as_ref() {
|
&& let Some(window) = parent_viewport.window.as_ref()
|
||||||
return Ok(EventResult::RepaintNext(window.id()));
|
{
|
||||||
}
|
return Ok(EventResult::RepaintNext(window.id()));
|
||||||
}
|
}
|
||||||
return Ok(EventResult::Wait);
|
return Ok(EventResult::Wait);
|
||||||
}
|
}
|
||||||
@@ -561,6 +563,12 @@ impl GlowWinitRunning<'_> {
|
|||||||
(raw_input, viewport_ui_cb)
|
(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
|
let clear_color = self
|
||||||
.app
|
.app
|
||||||
.clear_color(&self.integration.egui_ctx.style().visuals);
|
.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 {
|
match action {
|
||||||
ActionRequested::Screenshot(user_data) => {
|
ActionRequested::Screenshot(user_data) => {
|
||||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||||
@@ -723,10 +731,10 @@ impl GlowWinitRunning<'_> {
|
|||||||
|
|
||||||
// give it time to settle:
|
// give it time to settle:
|
||||||
#[cfg(feature = "__screenshot")]
|
#[cfg(feature = "__screenshot")]
|
||||||
if integration.egui_ctx.cumulative_pass_nr() == 2 {
|
if integration.egui_ctx.cumulative_pass_nr() == 2
|
||||||
if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
|
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
|
||||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
{
|
||||||
}
|
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
|
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
|
||||||
@@ -743,7 +751,7 @@ impl GlowWinitRunning<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if integration.should_close() {
|
if integration.should_close() {
|
||||||
Ok(EventResult::Exit)
|
Ok(EventResult::CloseRequested)
|
||||||
} else {
|
} else {
|
||||||
Ok(EventResult::Wait)
|
Ok(EventResult::Wait)
|
||||||
}
|
}
|
||||||
@@ -773,19 +781,33 @@ impl GlowWinitRunning<'_> {
|
|||||||
let mut repaint_asap = false;
|
let mut repaint_asap = false;
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
winit::event::WindowEvent::Focused(new_focused) => {
|
winit::event::WindowEvent::Focused(focused) => {
|
||||||
glutin.focused_viewport = new_focused.then(|| viewport_id).flatten();
|
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) => {
|
winit::event::WindowEvent::Resized(physical_size) => {
|
||||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
// 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
|
// See: https://github.com/rust-windowing/winit/issues/208
|
||||||
// This solves an issue where the app would panic when minimizing on Windows.
|
// This solves an issue where the app would panic when minimizing on Windows.
|
||||||
if 0 < physical_size.width && 0 < physical_size.height {
|
if 0 < physical_size.width
|
||||||
if let Some(viewport_id) = viewport_id {
|
&& 0 < physical_size.height
|
||||||
repaint_asap = true;
|
&& let Some(viewport_id) = viewport_id
|
||||||
glutin.resize(viewport_id, *physical_size);
|
{
|
||||||
}
|
repaint_asap = true;
|
||||||
|
glutin.resize(viewport_id, *physical_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,44 +816,31 @@ impl GlowWinitRunning<'_> {
|
|||||||
log::debug!(
|
log::debug!(
|
||||||
"Received WindowEvent::CloseRequested for main viewport - shutting down."
|
"Received WindowEvent::CloseRequested for main viewport - shutting down."
|
||||||
);
|
);
|
||||||
return EventResult::Exit;
|
return EventResult::CloseRequested;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
|
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
|
||||||
|
|
||||||
if let Some(viewport_id) = viewport_id {
|
if let Some(viewport_id) = viewport_id
|
||||||
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
|
||||||
// Tell viewport it should close:
|
{
|
||||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
// 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,
|
// 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).
|
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||||
// `request_repaint_of` does a double-repaint though:
|
// `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_id);
|
||||||
self.integration
|
self.integration
|
||||||
.egui_ctx
|
.egui_ctx
|
||||||
.request_repaint_of(viewport.ids.parent);
|
.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() {
|
if self.integration.should_close() {
|
||||||
return EventResult::Exit;
|
return EventResult::CloseRequested;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut event_response = egui_winit::EventResponse {
|
let mut event_response = egui_winit::EventResponse {
|
||||||
@@ -901,7 +910,7 @@ fn change_gl_context(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GlutinWindowContext {
|
impl GlutinWindowContext {
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
unsafe fn new(
|
unsafe fn new(
|
||||||
egui_ctx: &egui::Context,
|
egui_ctx: &egui::Context,
|
||||||
viewport_builder: ViewportBuilder,
|
viewport_builder: ViewportBuilder,
|
||||||
@@ -959,7 +968,6 @@ impl GlutinWindowContext {
|
|||||||
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
|
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
|
||||||
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
|
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
|
||||||
egui_ctx,
|
egui_ctx,
|
||||||
event_loop,
|
|
||||||
viewport_builder.clone(),
|
viewport_builder.clone(),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
@@ -1016,7 +1024,9 @@ impl GlutinWindowContext {
|
|||||||
let gl_context = match gl_context_result {
|
let gl_context = match gl_context_result {
|
||||||
Ok(it) => it,
|
Ok(it) => it,
|
||||||
Err(err) => {
|
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!(
|
log::debug!(
|
||||||
"Retrying with fallback context attributes: {fallback_context_attributes:?}"
|
"Retrying with fallback context attributes: {fallback_context_attributes:?}"
|
||||||
);
|
);
|
||||||
@@ -1030,15 +1040,27 @@ impl GlutinWindowContext {
|
|||||||
let not_current_gl_context = Some(gl_context);
|
let not_current_gl_context = Some(gl_context);
|
||||||
|
|
||||||
let mut viewport_from_window = HashMap::default();
|
let mut viewport_from_window = HashMap::default();
|
||||||
let mut window_from_viewport = ViewportIdMap::default();
|
let mut window_from_viewport = OrderedViewportIdMap::default();
|
||||||
let mut info = ViewportInfo::default();
|
let mut viewport_info = ViewportInfo::default();
|
||||||
if let Some(window) = &window {
|
if let Some(window) = &window {
|
||||||
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
||||||
window_from_viewport.insert(ViewportId::ROOT, window.id());
|
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(
|
viewports.insert(
|
||||||
ViewportId::ROOT,
|
ViewportId::ROOT,
|
||||||
Viewport {
|
Viewport {
|
||||||
@@ -1046,7 +1068,7 @@ impl GlutinWindowContext {
|
|||||||
class: ViewportClass::Root,
|
class: ViewportClass::Root,
|
||||||
builder: viewport_builder,
|
builder: viewport_builder,
|
||||||
deferred_commands: vec![],
|
deferred_commands: vec![],
|
||||||
info,
|
info: viewport_info,
|
||||||
actions_requested: Default::default(),
|
actions_requested: Default::default(),
|
||||||
viewport_ui_cb: None,
|
viewport_ui_cb: None,
|
||||||
gl_surface: None,
|
gl_surface: None,
|
||||||
@@ -1094,7 +1116,7 @@ impl GlutinWindowContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a surface, window, and winit integration for the viewport, if missing.
|
/// Create a surface, window, and winit integration for the viewport, if missing.
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
pub(crate) fn initialize_window(
|
pub(crate) fn initialize_window(
|
||||||
&mut self,
|
&mut self,
|
||||||
viewport_id: ViewportId,
|
viewport_id: ViewportId,
|
||||||
@@ -1113,7 +1135,6 @@ impl GlutinWindowContext {
|
|||||||
log::debug!("Creating a window for viewport {viewport_id:?}");
|
log::debug!("Creating a window for viewport {viewport_id:?}");
|
||||||
let window_attributes = egui_winit::create_winit_window_attributes(
|
let window_attributes = egui_winit::create_winit_window_attributes(
|
||||||
&self.egui_ctx,
|
&self.egui_ctx,
|
||||||
event_loop,
|
|
||||||
viewport.builder.clone(),
|
viewport.builder.clone(),
|
||||||
);
|
);
|
||||||
if window_attributes.transparent()
|
if window_attributes.transparent()
|
||||||
@@ -1241,21 +1262,21 @@ impl GlutinWindowContext {
|
|||||||
let width_px = NonZeroU32::new(physical_size.width).unwrap_or(NonZeroU32::MIN);
|
let width_px = NonZeroU32::new(physical_size.width).unwrap_or(NonZeroU32::MIN);
|
||||||
let height_px = NonZeroU32::new(physical_size.height).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(viewport) = self.viewports.get(&viewport_id)
|
||||||
if let Some(gl_surface) = &viewport.gl_surface {
|
&& let Some(gl_surface) = &viewport.gl_surface
|
||||||
change_gl_context(
|
{
|
||||||
&mut self.current_gl_context,
|
change_gl_context(
|
||||||
&mut self.not_current_gl_context,
|
&mut self.current_gl_context,
|
||||||
gl_surface,
|
&mut self.not_current_gl_context,
|
||||||
);
|
gl_surface,
|
||||||
gl_surface.resize(
|
);
|
||||||
self.current_gl_context
|
gl_surface.resize(
|
||||||
.as_ref()
|
self.current_gl_context
|
||||||
.expect("failed to get current context to resize surface"),
|
.as_ref()
|
||||||
width_px,
|
.expect("failed to get current context to resize surface"),
|
||||||
height_px,
|
width_px,
|
||||||
);
|
height_px,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,7 +1286,7 @@ impl GlutinWindowContext {
|
|||||||
|
|
||||||
pub(crate) fn remove_viewports_not_in(
|
pub(crate) fn remove_viewports_not_in(
|
||||||
&mut self,
|
&mut self,
|
||||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||||
) {
|
) {
|
||||||
// GC old viewports
|
// GC old viewports
|
||||||
self.viewports
|
self.viewports
|
||||||
@@ -1280,7 +1301,7 @@ impl GlutinWindowContext {
|
|||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &ActiveEventLoop,
|
event_loop: &ActiveEventLoop,
|
||||||
egui_ctx: &egui::Context,
|
egui_ctx: &egui::Context,
|
||||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||||
) {
|
) {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
@@ -1337,7 +1358,7 @@ impl GlutinWindowContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn initialize_or_update_viewport(
|
fn initialize_or_update_viewport(
|
||||||
viewports: &mut ViewportIdMap<Viewport>,
|
viewports: &mut OrderedViewportIdMap<Viewport>,
|
||||||
ids: ViewportIdPair,
|
ids: ViewportIdPair,
|
||||||
class: ViewportClass,
|
class: ViewportClass,
|
||||||
mut builder: ViewportBuilder,
|
mut builder: ViewportBuilder,
|
||||||
@@ -1345,6 +1366,8 @@ fn initialize_or_update_viewport(
|
|||||||
) -> &mut Viewport {
|
) -> &mut Viewport {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
|
use std::collections::btree_map::Entry;
|
||||||
|
|
||||||
if builder.icon.is_none() {
|
if builder.icon.is_none() {
|
||||||
// Inherit icon from parent
|
// Inherit icon from parent
|
||||||
builder.icon = viewports
|
builder.icon = viewports
|
||||||
@@ -1353,7 +1376,7 @@ fn initialize_or_update_viewport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match viewports.entry(ids.this) {
|
match viewports.entry(ids.this) {
|
||||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
// New viewport:
|
// New viewport:
|
||||||
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
||||||
entry.insert(Viewport {
|
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:
|
// Patch an existing viewport:
|
||||||
let viewport = entry.get_mut();
|
let viewport = entry.get_mut();
|
||||||
|
|
||||||
@@ -1566,6 +1589,6 @@ fn save_screenshot_and_exit(
|
|||||||
});
|
});
|
||||||
log::info!("Screenshot saved to {path:?}.");
|
log::info!("Screenshot saved to {path:?}.");
|
||||||
|
|
||||||
#[allow(clippy::exit)]
|
#[expect(clippy::exit)]
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ pub(crate) mod winit_integration;
|
|||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
mod glow_integration;
|
mod glow_integration;
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
mod wgpu_integration;
|
mod wgpu_integration;
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ use ahash::HashMap;
|
|||||||
|
|
||||||
use super::winit_integration::{UserEvent, WinitApp};
|
use super::winit_integration::{UserEvent, WinitApp};
|
||||||
use crate::{
|
use crate::{
|
||||||
epi,
|
Result, epi,
|
||||||
native::{event_loop_context, winit_integration::EventResult},
|
native::{event_loop_context, winit_integration::EventResult},
|
||||||
Result,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@@ -93,48 +92,57 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
|||||||
|
|
||||||
log::trace!("event_result: {event_result:?}");
|
log::trace!("event_result: {event_result:?}");
|
||||||
|
|
||||||
let combined_result = event_result.and_then(|event_result| {
|
let mut event_result = 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:?}",);
|
|
||||||
|
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows")
|
||||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
&& let Ok(EventResult::RepaintNow(window_id)) = event_result
|
||||||
self.winit_app.run_ui_and_paint(event_loop, window_id)
|
{
|
||||||
} else {
|
log::trace!("RepaintNow of {window_id:?}");
|
||||||
// Fix for https://github.com/emilk/egui/issues/2425
|
self.windows_next_repaint_times
|
||||||
self.windows_next_repaint_times
|
.insert(window_id, Instant::now());
|
||||||
.insert(window_id, Instant::now());
|
|
||||||
Ok(event_result)
|
// 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);
|
||||||
}
|
}
|
||||||
EventResult::RepaintNext(window_id) => {
|
|
||||||
log::trace!("RepaintNext of {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
|
self.windows_next_repaint_times
|
||||||
.insert(window_id, Instant::now());
|
.get(&window_id)
|
||||||
Ok(event_result)
|
.map_or(repaint_time, |last| (*last).min(repaint_time)),
|
||||||
}
|
);
|
||||||
EventResult::RepaintAt(window_id, repaint_time) => {
|
event_result
|
||||||
self.windows_next_repaint_times.insert(
|
}
|
||||||
window_id,
|
EventResult::Save => {
|
||||||
self.windows_next_repaint_times
|
save = true;
|
||||||
.get(&window_id)
|
event_result
|
||||||
.map_or(repaint_time, |last| (*last).min(repaint_time)),
|
}
|
||||||
);
|
EventResult::Exit => {
|
||||||
Ok(event_result)
|
exit = true;
|
||||||
}
|
event_result
|
||||||
EventResult::Save => {
|
}
|
||||||
save = true;
|
EventResult::CloseRequested => {
|
||||||
Ok(event_result)
|
// The windows need to be dropped whilst the event loop is running to allow for proper cleanup.
|
||||||
}
|
self.winit_app.save_and_destroy();
|
||||||
EventResult::Exit => {
|
event_result
|
||||||
exit = true;
|
|
||||||
Ok(event_result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,7 +150,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
|||||||
log::error!("Exiting because of error: {err}");
|
log::error!("Exiting because of error: {err}");
|
||||||
exit = true;
|
exit = true;
|
||||||
self.return_result = Err(err);
|
self.return_result = Err(err);
|
||||||
};
|
}
|
||||||
|
|
||||||
if save {
|
if save {
|
||||||
log::debug!("Received an EventResult::Save - saving app state");
|
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");
|
log::debug!("Exiting with return code 0");
|
||||||
|
|
||||||
#[allow(clippy::exit)]
|
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +181,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
|||||||
.retain(|window_id, repaint_time| {
|
.retain(|window_id, repaint_time| {
|
||||||
if now < *repaint_time {
|
if now < *repaint_time {
|
||||||
return true; // not yet ready
|
return true; // not yet ready
|
||||||
};
|
}
|
||||||
|
|
||||||
event_loop.set_control_flow(ControlFlow::Poll);
|
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();
|
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
|
||||||
if let Some(next_repaint_time) = next_repaint_time {
|
if let Some(next_repaint_time) = next_repaint_time {
|
||||||
event_loop.set_control_flow(ControlFlow::WaitUntil(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"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, winit_app: impl WinitApp) -> Result {
|
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)…");
|
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)
|
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(
|
pub fn run_wgpu(
|
||||||
app_name: &str,
|
app_name: &str,
|
||||||
mut native_options: epi::NativeOptions,
|
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);
|
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||||
run_and_exit(event_loop, wgpu_eframe)
|
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},
|
window::{Window, WindowId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ahash::{HashMap, HashSet, HashSetExt};
|
use ahash::HashMap;
|
||||||
use egui::{
|
use egui::{
|
||||||
DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass,
|
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
|
||||||
ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput,
|
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
|
||||||
|
ViewportOutput,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
use egui_winit::accesskit_winit;
|
use egui_winit::accesskit_winit;
|
||||||
use winit_integration::UserEvent;
|
use winit_integration::UserEvent;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
|
|
||||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
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};
|
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
|
||||||
@@ -70,9 +71,10 @@ pub struct SharedState {
|
|||||||
painter: egui_wgpu::winit::Painter,
|
painter: egui_wgpu::winit::Painter,
|
||||||
viewport_from_window: HashMap<WindowId, ViewportId>,
|
viewport_from_window: HashMap<WindowId, ViewportId>,
|
||||||
focused_viewport: Option<ViewportId>,
|
focused_viewport: Option<ViewportId>,
|
||||||
|
resized_viewport: Option<ViewportId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Viewports = ViewportIdMap<Viewport>;
|
pub type Viewports = egui::OrderedViewportIdMap<Viewport>;
|
||||||
|
|
||||||
pub struct Viewport {
|
pub struct Viewport {
|
||||||
ids: ViewportIdPair,
|
ids: ViewportIdPair,
|
||||||
@@ -80,7 +82,7 @@ pub struct Viewport {
|
|||||||
builder: ViewportBuilder,
|
builder: ViewportBuilder,
|
||||||
deferred_commands: Vec<egui::viewport::ViewportCommand>,
|
deferred_commands: Vec<egui::viewport::ViewportCommand>,
|
||||||
info: ViewportInfo,
|
info: ViewportInfo,
|
||||||
actions_requested: HashSet<ActionRequested>,
|
actions_requested: Vec<ActionRequested>,
|
||||||
|
|
||||||
/// `None` for sync viewports.
|
/// `None` for sync viewports.
|
||||||
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
||||||
@@ -182,19 +184,37 @@ impl<'app> WgpuWinitApp<'app> {
|
|||||||
builder: ViewportBuilder,
|
builder: ViewportBuilder,
|
||||||
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
|
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
|
||||||
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
|
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
|
||||||
egui_ctx.clone(),
|
egui_ctx.clone(),
|
||||||
self.native_options.wgpu_options.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.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);
|
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(
|
let mut egui_winit = egui_winit::State::new(
|
||||||
egui_ctx.clone(),
|
egui_ctx.clone(),
|
||||||
ViewportId::ROOT,
|
ViewportId::ROOT,
|
||||||
@@ -249,7 +269,7 @@ impl<'app> WgpuWinitApp<'app> {
|
|||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
{
|
{
|
||||||
let event_loop_proxy = self.repaint_proxy.lock().clone();
|
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)
|
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();
|
let mut viewport_from_window = HashMap::default();
|
||||||
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
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();
|
let mut viewports = Viewports::default();
|
||||||
viewports.insert(
|
viewports.insert(
|
||||||
ViewportId::ROOT,
|
ViewportId::ROOT,
|
||||||
@@ -285,7 +302,7 @@ impl<'app> WgpuWinitApp<'app> {
|
|||||||
class: ViewportClass::Root,
|
class: ViewportClass::Root,
|
||||||
builder,
|
builder,
|
||||||
deferred_commands: vec![],
|
deferred_commands: vec![],
|
||||||
info,
|
info: viewport_info,
|
||||||
actions_requested: Default::default(),
|
actions_requested: Default::default(),
|
||||||
viewport_ui_cb: None,
|
viewport_ui_cb: None,
|
||||||
window: Some(window),
|
window: Some(window),
|
||||||
@@ -299,6 +316,7 @@ impl<'app> WgpuWinitApp<'app> {
|
|||||||
viewports,
|
viewports,
|
||||||
painter,
|
painter,
|
||||||
focused_viewport: Some(ViewportId::ROOT),
|
focused_viewport: Some(ViewportId::ROOT),
|
||||||
|
resized_viewport: None,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -333,10 +351,8 @@ impl WinitApp for WgpuWinitApp<'_> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|r| {
|
.and_then(|r| {
|
||||||
let shared = r.shared.borrow();
|
let shared = r.shared.borrow();
|
||||||
shared
|
let id = shared.viewport_from_window.get(&window_id)?;
|
||||||
.viewport_from_window
|
shared.viewports.get(id).map(|v| v.window.clone())
|
||||||
.get(&window_id)
|
|
||||||
.and_then(|id| shared.viewports.get(id).map(|v| v.window.clone()))
|
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
@@ -431,20 +447,20 @@ impl WinitApp for WgpuWinitApp<'_> {
|
|||||||
_: winit::event::DeviceId,
|
_: winit::event::DeviceId,
|
||||||
event: winit::event::DeviceEvent,
|
event: winit::event::DeviceEvent,
|
||||||
) -> crate::Result<EventResult> {
|
) -> crate::Result<EventResult> {
|
||||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
|
if let winit::event::DeviceEvent::MouseMotion { delta } = event
|
||||||
if let Some(running) = &mut self.running {
|
&& let Some(running) = &mut self.running
|
||||||
let mut shared = running.shared.borrow_mut();
|
{
|
||||||
if let Some(viewport) = shared
|
let mut shared = running.shared.borrow_mut();
|
||||||
.focused_viewport
|
if let Some(viewport) = shared
|
||||||
.and_then(|viewport| shared.viewports.get_mut(&viewport))
|
.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(egui_winit) = viewport.egui_winit.as_mut() {
|
||||||
}
|
egui_winit.on_mouse_motion(delta);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(window) = viewport.window.as_ref() {
|
if let Some(window) = viewport.window.as_ref() {
|
||||||
return Ok(EventResult::RepaintNext(window.id()));
|
return Ok(EventResult::RepaintNext(window.id()));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -463,7 +479,8 @@ impl WinitApp for WgpuWinitApp<'_> {
|
|||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
Ok(running.on_window_event(window_id, &event))
|
Ok(running.on_window_event(window_id, &event))
|
||||||
} else {
|
} 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
|
if let Some(viewport) = viewport_from_window
|
||||||
.get(&event.window_id)
|
.get(&event.window_id)
|
||||||
.and_then(|id| viewports.get_mut(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(
|
||||||
return Ok(winit_integration::on_accesskit_window_event(
|
egui_winit,
|
||||||
egui_winit,
|
event.window_id,
|
||||||
event.window_id,
|
&event.window_event,
|
||||||
&event.window_event,
|
));
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,10 +580,10 @@ impl WgpuWinitRunning<'_> {
|
|||||||
if viewport.viewport_ui_cb.is_none() {
|
if viewport.viewport_ui_cb.is_none() {
|
||||||
// This will only happen if this is an immediate viewport.
|
// 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.
|
// 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(viewport) = viewports.get(&viewport.ids.parent)
|
||||||
if let Some(window) = viewport.window.as_ref() {
|
&& let Some(window) = viewport.window.as_ref()
|
||||||
return Ok(EventResult::RepaintNext(window.id()));
|
{
|
||||||
}
|
return Ok(EventResult::RepaintNext(window.id()));
|
||||||
}
|
}
|
||||||
return Ok(EventResult::Wait);
|
return Ok(EventResult::Wait);
|
||||||
}
|
}
|
||||||
@@ -680,7 +696,7 @@ impl WgpuWinitRunning<'_> {
|
|||||||
screenshot_commands,
|
screenshot_commands,
|
||||||
);
|
);
|
||||||
|
|
||||||
for action in viewport.actions_requested.drain() {
|
for action in viewport.actions_requested.drain(..) {
|
||||||
match action {
|
match action {
|
||||||
ActionRequested::Screenshot { .. } => {
|
ActionRequested::Screenshot { .. } => {
|
||||||
// already handled above
|
// already handled above
|
||||||
@@ -731,17 +747,17 @@ impl WgpuWinitRunning<'_> {
|
|||||||
|
|
||||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||||
|
|
||||||
if let Some(window) = window {
|
if let Some(window) = window
|
||||||
if window.is_minimized() == Some(true) {
|
&& window.is_minimized() == Some(true)
|
||||||
// On Mac, a minimized Window uses up all CPU:
|
{
|
||||||
// https://github.com/emilk/egui/issues/325
|
// On Mac, a minimized Window uses up all CPU:
|
||||||
profiling::scope!("minimized_sleep");
|
// https://github.com/emilk/egui/issues/325
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
profiling::scope!("minimized_sleep");
|
||||||
}
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
if integration.should_close() {
|
if integration.should_close() {
|
||||||
Ok(EventResult::Exit)
|
Ok(EventResult::CloseRequested)
|
||||||
} else {
|
} else {
|
||||||
Ok(EventResult::Wait)
|
Ok(EventResult::Wait)
|
||||||
}
|
}
|
||||||
@@ -762,37 +778,68 @@ impl WgpuWinitRunning<'_> {
|
|||||||
let viewport_id = shared.viewport_from_window.get(&window_id).copied();
|
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
|
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the
|
||||||
// event handler.
|
// event handler. If this is not done, the compositor will assume that the window does not want
|
||||||
//
|
// to redraw and continue ahead.
|
||||||
// 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
|
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
|
||||||
// new frames to the compositor in time.
|
// new frames to the compositor in time. The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||||
//
|
|
||||||
// The flickering is technically glutin or glow's fault, but we should be responding properly
|
|
||||||
// to resizes anyway, as doing so avoids dropping frames.
|
// to resizes anyway, as doing so avoids dropping frames.
|
||||||
//
|
//
|
||||||
// See: https://github.com/emilk/egui/issues/903
|
// See: https://github.com/emilk/egui/issues/903
|
||||||
let mut repaint_asap = false;
|
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 {
|
match event {
|
||||||
winit::event::WindowEvent::Focused(new_focused) => {
|
winit::event::WindowEvent::Focused(focused) => {
|
||||||
shared.focused_viewport = new_focused.then(|| viewport_id).flatten();
|
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) => {
|
winit::event::WindowEvent::Resized(physical_size) => {
|
||||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
// 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
|
// See: https://github.com/rust-windowing/winit/issues/208
|
||||||
// This solves an issue where the app would panic when minimizing on Windows.
|
// This solves an issue where the app would panic when minimizing on Windows.
|
||||||
if let Some(viewport_id) = viewport_id {
|
if let Some(id) = viewport_id
|
||||||
if let (Some(width), Some(height)) = (
|
&& let (Some(width), Some(height)) = (
|
||||||
NonZeroU32::new(physical_size.width),
|
NonZeroU32::new(physical_size.width),
|
||||||
NonZeroU32::new(physical_size.height),
|
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!(
|
log::debug!(
|
||||||
"Received WindowEvent::CloseRequested for main viewport - shutting down."
|
"Received WindowEvent::CloseRequested for main viewport - shutting down."
|
||||||
);
|
);
|
||||||
return EventResult::Exit;
|
return EventResult::CloseRequested;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
|
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
|
||||||
|
|
||||||
if let Some(viewport_id) = viewport_id {
|
if let Some(viewport_id) = viewport_id
|
||||||
if let Some(viewport) = shared.viewports.get_mut(&viewport_id) {
|
&& let Some(viewport) = shared.viewports.get_mut(&viewport_id)
|
||||||
// Tell viewport it should close:
|
{
|
||||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
// 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,
|
// 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).
|
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||||
// `request_repaint_of` does a double-repaint though:
|
// `request_repaint_of` does a double-repaint though:
|
||||||
integration.egui_ctx.request_repaint_of(viewport_id);
|
integration.egui_ctx.request_repaint_of(viewport_id);
|
||||||
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
|
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
}
|
||||||
|
|
||||||
let event_response = viewport_id
|
let event_response = viewport_id
|
||||||
.and_then(|viewport_id| {
|
.and_then(|viewport_id| {
|
||||||
shared.viewports.get_mut(&viewport_id).and_then(|viewport| {
|
let viewport = shared.viewports.get_mut(&viewport_id)?;
|
||||||
Some(integration.on_window_event(
|
Some(integration.on_window_event(
|
||||||
viewport.window.as_deref()?,
|
viewport.window.as_deref()?,
|
||||||
viewport.egui_winit.as_mut()?,
|
viewport.egui_winit.as_mut()?,
|
||||||
event,
|
event,
|
||||||
))
|
))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if integration.should_close() {
|
if integration.should_close() {
|
||||||
EventResult::Exit
|
EventResult::CloseRequested
|
||||||
} else if event_response.repaint {
|
} else if event_response.repaint {
|
||||||
if repaint_asap {
|
if repaint_asap {
|
||||||
EventResult::RepaintNow(window_id)
|
EventResult::RepaintNow(window_id)
|
||||||
@@ -1035,10 +1081,10 @@ fn render_immediate_viewport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_viewports_not_in(
|
pub(crate) fn remove_viewports_not_in(
|
||||||
viewports: &mut ViewportIdMap<Viewport>,
|
viewports: &mut Viewports,
|
||||||
painter: &mut egui_wgpu::winit::Painter,
|
painter: &mut egui_wgpu::winit::Painter,
|
||||||
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
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();
|
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:
|
/// Add new viewports, and update existing ones:
|
||||||
fn handle_viewport_output(
|
fn handle_viewport_output(
|
||||||
egui_ctx: &egui::Context,
|
egui_ctx: &egui::Context,
|
||||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||||
viewports: &mut ViewportIdMap<Viewport>,
|
viewports: &mut Viewports,
|
||||||
painter: &mut egui_wgpu::winit::Painter,
|
painter: &mut egui_wgpu::winit::Painter,
|
||||||
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
||||||
) {
|
) {
|
||||||
@@ -1089,13 +1135,13 @@ fn handle_viewport_output(
|
|||||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(target_os = "linux") {
|
||||||
let new_inner_size = window.inner_size();
|
let new_inner_size = window.inner_size();
|
||||||
if new_inner_size != old_inner_size {
|
if new_inner_size != old_inner_size
|
||||||
if let (Some(width), Some(height)) = (
|
&& let (Some(width), Some(height)) = (
|
||||||
NonZeroU32::new(new_inner_size.width),
|
NonZeroU32::new(new_inner_size.width),
|
||||||
NonZeroU32::new(new_inner_size.height),
|
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>>,
|
viewport_ui_cb: Option<Arc<dyn Fn(&egui::Context) + Send + Sync>>,
|
||||||
painter: &mut egui_wgpu::winit::Painter,
|
painter: &mut egui_wgpu::winit::Painter,
|
||||||
) -> &'a mut Viewport {
|
) -> &'a mut Viewport {
|
||||||
|
use std::collections::btree_map::Entry;
|
||||||
|
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
if builder.icon.is_none() {
|
if builder.icon.is_none() {
|
||||||
@@ -1122,7 +1170,7 @@ fn initialize_or_update_viewport<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match viewports.entry(ids.this) {
|
match viewports.entry(ids.this) {
|
||||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
// New viewport:
|
// New viewport:
|
||||||
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
||||||
entry.insert(Viewport {
|
entry.insert(Viewport {
|
||||||
@@ -1131,14 +1179,14 @@ fn initialize_or_update_viewport<'a>(
|
|||||||
builder,
|
builder,
|
||||||
deferred_commands: vec![],
|
deferred_commands: vec![],
|
||||||
info: Default::default(),
|
info: Default::default(),
|
||||||
actions_requested: HashSet::new(),
|
actions_requested: Vec::new(),
|
||||||
viewport_ui_cb,
|
viewport_ui_cb,
|
||||||
window: None,
|
window: None,
|
||||||
egui_winit: None,
|
egui_winit: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
Entry::Occupied(mut entry) => {
|
||||||
// Patch an existing viewport:
|
// Patch an existing viewport:
|
||||||
let viewport = entry.get_mut();
|
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.
|
/// Causes a save of the client state when the persistence feature is enabled.
|
||||||
Save,
|
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,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl Stopwatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(&mut self) {
|
pub fn start(&mut self) {
|
||||||
assert!(self.start.is_none());
|
assert!(self.start.is_none(), "Stopwatch already running");
|
||||||
self.start = Some(Instant::now());
|
self.start = Some(Instant::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ impl Stopwatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resume(&mut self) {
|
pub fn resume(&mut self) {
|
||||||
assert!(self.start.is_none());
|
assert!(self.start.is_none(), "Stopwatch still running");
|
||||||
self.start = Some(Instant::now());
|
self.start = Some(Instant::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use egui::{TexturesDelta, UserData, ViewportCommand};
|
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 {
|
pub struct AppRunner {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code, clippy::allow_attributes)]
|
||||||
pub(crate) web_options: crate::WebOptions,
|
pub(crate) web_options: crate::WebOptions,
|
||||||
pub(crate) frame: epi::Frame,
|
pub(crate) frame: epi::Frame,
|
||||||
egui_ctx: egui::Context,
|
egui_ctx: egui::Context,
|
||||||
painter: super::ActiveWebPainter,
|
painter: Box<dyn WebPainter>,
|
||||||
pub(crate) input: super::WebInput,
|
pub(crate) input: super::WebInput,
|
||||||
app: Box<dyn epi::App>,
|
app: Box<dyn epi::App>,
|
||||||
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||||
@@ -34,6 +34,10 @@ impl Drop for AppRunner {
|
|||||||
impl AppRunner {
|
impl AppRunner {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Failure to initialize WebGL renderer, or failure to create app.
|
/// 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(
|
pub async fn new(
|
||||||
canvas: web_sys::HtmlCanvasElement,
|
canvas: web_sys::HtmlCanvasElement,
|
||||||
web_options: crate::WebOptions,
|
web_options: crate::WebOptions,
|
||||||
@@ -41,7 +45,41 @@ impl AppRunner {
|
|||||||
text_agent: TextAgent,
|
text_agent: TextAgent,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
let egui_ctx = egui::Context::default();
|
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 {
|
let info = epi::IntegrationInfo {
|
||||||
web_info: epi::WebInfo {
|
web_info: epi::WebInfo {
|
||||||
@@ -65,21 +103,27 @@ impl AppRunner {
|
|||||||
o.zoom_factor = 1.0;
|
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 {
|
let cc = epi::CreationContext {
|
||||||
egui_ctx: egui_ctx.clone(),
|
egui_ctx: egui_ctx.clone(),
|
||||||
integration_info: info.clone(),
|
integration_info: info.clone(),
|
||||||
storage: Some(&storage),
|
storage: Some(&storage),
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
gl: Some(painter.gl().clone()),
|
gl: gl.clone(),
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
get_proc_address: None,
|
get_proc_address: None,
|
||||||
|
|
||||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_render_state: painter.render_state(),
|
wgpu_render_state: wgpu_render_state.clone(),
|
||||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
|
||||||
wgpu_render_state: None,
|
|
||||||
};
|
};
|
||||||
let app = app_creator(&cc).map_err(|err| err.to_string())?;
|
let app = app_creator(&cc).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
@@ -88,15 +132,14 @@ impl AppRunner {
|
|||||||
storage: Some(Box::new(storage)),
|
storage: Some(Box::new(storage)),
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
gl: Some(painter.gl().clone()),
|
gl,
|
||||||
|
|
||||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
#[cfg(feature = "wgpu_no_default_features")]
|
||||||
wgpu_render_state: painter.render_state(),
|
wgpu_render_state,
|
||||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
|
||||||
wgpu_render_state: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
let needs_repaint = needs_repaint.clone();
|
||||||
egui_ctx.set_request_repaint_callback(move |info| {
|
egui_ctx.set_request_repaint_callback(move |info| {
|
||||||
@@ -304,8 +347,6 @@ impl AppRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_platform_output(&self, platform_output: egui::PlatformOutput) {
|
fn handle_platform_output(&self, platform_output: egui::PlatformOutput) {
|
||||||
#![allow(deprecated)]
|
|
||||||
|
|
||||||
#[cfg(feature = "web_screen_reader")]
|
#[cfg(feature = "web_screen_reader")]
|
||||||
if self.egui_ctx.options(|o| o.screen_reader) {
|
if self.egui_ctx.options(|o| o.screen_reader) {
|
||||||
super::screen_reader::speak(&platform_output.events_description());
|
super::screen_reader::speak(&platform_output.events_description());
|
||||||
@@ -314,13 +355,10 @@ impl AppRunner {
|
|||||||
let egui::PlatformOutput {
|
let egui::PlatformOutput {
|
||||||
commands,
|
commands,
|
||||||
cursor_icon,
|
cursor_icon,
|
||||||
open_url,
|
|
||||||
copied_text,
|
|
||||||
events: _, // already handled
|
events: _, // already handled
|
||||||
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
|
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
|
||||||
ime,
|
ime,
|
||||||
#[cfg(feature = "accesskit")]
|
accesskit_update: _, // not currently implemented
|
||||||
accesskit_update: _, // not currently implemented
|
|
||||||
num_completed_passes: _, // handled by `Context::run`
|
num_completed_passes: _, // handled by `Context::run`
|
||||||
request_discard_reasons: _, // handled by `Context::run`
|
request_discard_reasons: _, // handled by `Context::run`
|
||||||
} = platform_output;
|
} = platform_output;
|
||||||
@@ -341,14 +379,6 @@ impl AppRunner {
|
|||||||
|
|
||||||
super::set_cursor_icon(cursor_icon);
|
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() {
|
if self.has_focus() {
|
||||||
// The eframe app has focus.
|
// The eframe app has focus.
|
||||||
if ime.is_some() {
|
if ime.is_some() {
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ use super::percent_decode;
|
|||||||
/// Data gathered between frames.
|
/// Data gathered between frames.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct WebInput {
|
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>,
|
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`.
|
/// The raw input to `egui`.
|
||||||
pub raw: egui::RawInput,
|
pub raw: egui::RawInput,
|
||||||
}
|
}
|
||||||
@@ -50,11 +56,20 @@ impl WebInput {
|
|||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Stores when to do the next repaint.
|
/// 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 {
|
/// Rate limit for repaint. 0 means "unlimited". The rate may still be limited by vsync.
|
||||||
fn default() -> Self {
|
max_fps: u32,
|
||||||
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
|
}
|
||||||
|
|
||||||
|
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
|
/// Returns the time (in [`now_sec`] scale) when
|
||||||
/// we should next repaint.
|
/// we should next repaint.
|
||||||
pub fn when_to_repaint(&self) -> f64 {
|
pub fn when_to_repaint(&self) -> f64 {
|
||||||
*self.0.lock()
|
*self.next_repaint.lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unschedule repainting.
|
/// Unschedule repainting.
|
||||||
pub fn clear(&self) {
|
pub fn clear(&self) {
|
||||||
*self.0.lock() = f64::INFINITY;
|
*self.next_repaint.lock() = f64::INFINITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repaint_after(&self, num_seconds: f64) {
|
pub fn repaint_after(&self, num_seconds: f64) {
|
||||||
let mut repaint_time = self.0.lock();
|
let mut time = super::now_sec() + num_seconds;
|
||||||
*repaint_time = repaint_time.min(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 {
|
pub fn needs_repaint(&self) -> bool {
|
||||||
self.when_to_repaint() <= super::now_sec()
|
self.when_to_repaint() <= super::now_sec()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repaint_asap(&self) {
|
fn round_repaint_time_to_rate(&self, time: f64) -> f64 {
|
||||||
*self.0.lock() = f64::NEG_INFINITY;
|
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 crate::web::string_from_js_value;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
|
AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event,
|
||||||
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
|
location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event,
|
||||||
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
|
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos,
|
||||||
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast, JsValue, WebRunner,
|
push_touches, text_from_keyboard_event, translate_key,
|
||||||
DEBUG_RESIZE,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use js_sys::Reflect;
|
||||||
|
use web_sys::{Document, EventTarget, ShadowRoot};
|
||||||
|
|
||||||
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
|
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
|
||||||
// than what is probably needed.
|
// 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_touchcancel(runner_ref, &canvas)?;
|
||||||
|
|
||||||
install_wheel(runner_ref, &canvas)?;
|
install_wheel(runner_ref, &canvas)?;
|
||||||
|
install_gesture(runner_ref, &canvas)?;
|
||||||
install_drag_and_drop(runner_ref, &canvas)?;
|
install_drag_and_drop(runner_ref, &canvas)?;
|
||||||
install_window_events(runner_ref, &window)?;
|
install_window_events(runner_ref, &window)?;
|
||||||
install_color_scheme_change_event(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) {
|
if let Some(text) = text_from_keyboard_event(&event) {
|
||||||
let egui_event = egui::Event::Text(text);
|
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.input.raw.events.push(egui_event);
|
||||||
runner.needs_repaint.repaint_asap();
|
runner.needs_repaint.repaint_asap();
|
||||||
|
|
||||||
// If this is indeed text, then prevent any other action.
|
// 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.
|
// Use web options to tell if the event should be propagated to parent elements.
|
||||||
if !should_propagate {
|
if should_stop_propagation {
|
||||||
event.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) {
|
pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||||
let has_focus = runner.input.raw.focused;
|
let has_focus = runner.input.raw.focused;
|
||||||
if !has_focus {
|
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!
|
repeat: false, // egui will fill this in for us!
|
||||||
modifiers,
|
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.input.raw.events.push(egui_event);
|
||||||
runner.needs_repaint.repaint_asap();
|
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.
|
// 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.stop_propagation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,9 +227,10 @@ fn should_prevent_default_for_key(
|
|||||||
|
|
||||||
// Prevent cmd/ctrl plus these keys from triggering the default browser action:
|
// Prevent cmd/ctrl plus these keys from triggering the default browser action:
|
||||||
let keys = [
|
let keys = [
|
||||||
egui::Key::O, // open
|
egui::Key::Comma, // cmd-, opens options on macOS, which egui apps may wanna "steal"
|
||||||
egui::Key::P, // print (cmd-P is common for command palette)
|
egui::Key::O, // open
|
||||||
egui::Key::S, // save
|
egui::Key::P, // print (cmd-P is common for command palette)
|
||||||
|
egui::Key::S, // save
|
||||||
];
|
];
|
||||||
for key in keys {
|
for key in keys {
|
||||||
if egui_key == key && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) {
|
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)
|
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) {
|
pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||||
let modifiers = modifiers_from_kb_event(&event);
|
let modifiers = modifiers_from_kb_event(&event);
|
||||||
runner.input.raw.modifiers = modifiers;
|
runner.input.raw.modifiers = modifiers;
|
||||||
|
|
||||||
let mut propagate_event = false;
|
let mut should_stop_propagation = true;
|
||||||
|
|
||||||
if let Some(key) = translate_key(&event.key()) {
|
if let Some(key) = translate_key(&event.key()) {
|
||||||
let egui_event = egui::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,
|
repeat: false,
|
||||||
modifiers,
|
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);
|
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
|
// See https://github.com/emilk/egui/issues/4724
|
||||||
|
|
||||||
let keys_down = runner.egui_ctx().input(|i| i.keys_down.clone());
|
let keys_down = runner.egui_ctx().input(|i| i.keys_down.clone());
|
||||||
|
|
||||||
|
#[expect(clippy::iter_over_hash_type)]
|
||||||
for key in keys_down {
|
for key in keys_down {
|
||||||
let egui_event = egui::Event::Key {
|
let egui_event = egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
@@ -290,7 +299,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
|||||||
repeat: false,
|
repeat: false,
|
||||||
modifiers,
|
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);
|
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.
|
// 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;
|
let has_focus = runner.input.raw.focused;
|
||||||
if has_focus && !propagate_event {
|
if has_focus && should_stop_propagation {
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||||
runner_ref.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, runner| {
|
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 Some(data) = event.clipboard_data() {
|
||||||
if let Ok(text) = data.get_data("text") {
|
if let Ok(text) = data.get_data("text") {
|
||||||
let text = text.replace("\r\n", "\n");
|
let text = text.replace("\r\n", "\n");
|
||||||
|
|
||||||
let mut should_propagate = false;
|
let mut should_stop_propagation = true;
|
||||||
if !text.is_empty() && runner.input.raw.focused {
|
let mut should_prevent_default = true;
|
||||||
|
if !text.is_empty() {
|
||||||
let egui_event = egui::Event::Paste(text);
|
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.input.raw.events.push(egui_event);
|
||||||
runner.needs_repaint.repaint_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.
|
// 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.stop_propagation();
|
||||||
}
|
}
|
||||||
event.prevent_default();
|
|
||||||
|
if should_prevent_default {
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
|
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
|
||||||
if runner.input.raw.focused {
|
if !runner.input.raw.focused {
|
||||||
runner.input.raw.events.push(egui::Event::Cut);
|
return; // The eframe app is not interested
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
// 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.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| {
|
runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| {
|
||||||
if runner.input.raw.focused {
|
if !runner.input.raw.focused {
|
||||||
runner.input.raw.events.push(egui::Event::Copy);
|
return; // The eframe app is not interested
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
// 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.stop_propagation();
|
||||||
}
|
}
|
||||||
event.prevent_default();
|
|
||||||
|
if (runner.web_options.should_prevent_default)(&egui::Event::Copy) {
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
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
|
// 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.
|
// 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| {
|
runner_ref.add_event_listener(window, event_name, move |_: web_sys::Event, runner| {
|
||||||
if DEBUG_RESIZE {
|
if DEBUG_RESIZE {
|
||||||
log::debug!("{event_name:?}");
|
log::debug!("{event_name:?}");
|
||||||
@@ -444,16 +474,19 @@ fn install_color_scheme_change_event(
|
|||||||
runner_ref: &WebRunner,
|
runner_ref: &WebRunner,
|
||||||
window: &web_sys::Window,
|
window: &web_sys::Window,
|
||||||
) -> Result<(), JsValue> {
|
) -> Result<(), JsValue> {
|
||||||
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
|
for theme in [egui::Theme::Dark, egui::Theme::Light] {
|
||||||
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
|
if let Some(media_query_list) = prefers_color_scheme(window, theme)? {
|
||||||
&media_query_list,
|
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
|
||||||
"change",
|
&media_query_list,
|
||||||
|event, runner| {
|
"change",
|
||||||
let theme = theme_from_dark_mode(event.matches());
|
|_event, runner| {
|
||||||
runner.input.raw.system_theme = Some(theme);
|
if let Some(theme) = super::system_theme() {
|
||||||
runner.needs_repaint.repaint_asap();
|
runner.input.raw.system_theme = Some(theme);
|
||||||
},
|
runner.needs_repaint.repaint_asap();
|
||||||
)?;
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -484,7 +517,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|
|||||||
|event: web_sys::PointerEvent, runner: &mut AppRunner| {
|
|event: web_sys::PointerEvent, runner: &mut AppRunner| {
|
||||||
let modifiers = modifiers_from_mouse_event(&event);
|
let modifiers = modifiers_from_mouse_event(&event);
|
||||||
runner.input.raw.modifiers = modifiers;
|
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) {
|
if let Some(button) = button_from_mouse_event(&event) {
|
||||||
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
|
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
|
||||||
let modifiers = runner.input.raw.modifiers;
|
let modifiers = runner.input.raw.modifiers;
|
||||||
@@ -494,7 +527,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|
|||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
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);
|
runner.input.raw.events.push(egui_event);
|
||||||
|
|
||||||
// In Safari we are only allowed to write to the clipboard during the
|
// 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.
|
// 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.stop_propagation();
|
||||||
}
|
}
|
||||||
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
|
// 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,
|
pressed: false,
|
||||||
modifiers,
|
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);
|
runner.input.raw.events.push(egui_event);
|
||||||
|
|
||||||
// Previously on iOS, the canvas would not receive focus on
|
// 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:
|
// Make sure we paint the output of the above logic call asap:
|
||||||
runner.needs_repaint.repaint_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.
|
// 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.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.
|
/// 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).
|
/// Pass in the position in browser viewport coordinates (usually event.clientX/Y).
|
||||||
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
|
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
let root_node = runner.canvas().get_root_node();
|
||||||
let is_hovering_canvas = document
|
|
||||||
.element_from_point(pos.x, pos.y)
|
let element_at_point = if let Some(document) = root_node.dyn_ref::<Document>() {
|
||||||
.is_some_and(|element| element.eq(runner.canvas()));
|
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
|
let is_pointer_down = runner
|
||||||
.egui_ctx()
|
.egui_ctx()
|
||||||
.input(|i| i.pointer.any_down() || i.any_touches());
|
.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),
|
egui::pos2(event.client_x() as f32, event.client_y() as f32),
|
||||||
) {
|
) {
|
||||||
let egui_event = egui::Event::PointerMoved(pos);
|
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.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.
|
// 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.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();
|
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.
|
// 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.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,
|
target,
|
||||||
"touchstart",
|
"touchstart",
|
||||||
|event: web_sys::TouchEvent, runner| {
|
|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) {
|
if let Some((pos, _)) = primary_touch_pos(runner, &event) {
|
||||||
let egui_event = egui::Event::PointerButton {
|
let egui_event = egui::Event::PointerButton {
|
||||||
pos,
|
pos,
|
||||||
@@ -636,7 +689,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
|||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers: runner.input.raw.modifiers,
|
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);
|
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();
|
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.
|
// 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.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),
|
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
|
||||||
) {
|
) {
|
||||||
let egui_event = egui::Event::PointerMoved(pos);
|
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.input.raw.events.push(egui_event);
|
||||||
|
|
||||||
push_touches(runner, egui::TouchPhase::Move, &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.
|
// 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.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),
|
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
|
||||||
) {
|
) {
|
||||||
// First release mouse to click:
|
// 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 {
|
let egui_event = egui::Event::PointerButton {
|
||||||
pos,
|
pos,
|
||||||
button: egui::PointerButton::Primary,
|
button: egui::PointerButton::Primary,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
modifiers: runner.input.raw.modifiers,
|
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);
|
runner.input.raw.events.push(egui_event);
|
||||||
// Then remove hover effect:
|
// Then remove hover effect:
|
||||||
should_propagate |=
|
should_stop_propagation &=
|
||||||
(runner.web_options.should_propagate_event)(&egui::Event::PointerGone);
|
(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);
|
runner.input.raw.events.push(egui::Event::PointerGone);
|
||||||
|
|
||||||
push_touches(runner, egui::TouchPhase::End, &event);
|
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();
|
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.
|
// 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.stop_propagation();
|
||||||
}
|
}
|
||||||
event.prevent_default();
|
|
||||||
|
if should_prevent_default {
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
|
|
||||||
// Fix virtual keyboard IOS
|
// Fix virtual keyboard IOS
|
||||||
// Need call focus at the same time of event
|
// 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 {
|
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_.
|
// 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.
|
// 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
|
// 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.
|
// 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,
|
unit,
|
||||||
delta,
|
delta,
|
||||||
modifiers,
|
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.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();
|
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.
|
// 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.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> {
|
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) => {
|
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`:
|
// we rely on the resize observer to trigger the first `request_animation_frame`:
|
||||||
if let Err(err) = runner_ref.request_animation_frame() {
|
if let Err(err) = runner_ref.request_animation_frame() {
|
||||||
log::error!("{}", super::string_from_js_value(&err));
|
log::error!("{}", super::string_from_js_value(&err));
|
||||||
};
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("ResizeObserverContext callback: failed to lock runner");
|
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(
|
pub fn pos_from_mouse_event(
|
||||||
canvas: &web_sys::HtmlCanvasElement,
|
canvas: &web_sys::HtmlCanvasElement,
|
||||||
|
|||||||
@@ -23,25 +23,22 @@ pub use panic_handler::{PanicHandler, PanicSummary};
|
|||||||
pub use web_logger::WebLogger;
|
pub use web_logger::WebLogger;
|
||||||
pub use web_runner::WebRunner;
|
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");
|
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
|
||||||
|
|
||||||
mod web_painter;
|
mod web_painter;
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
mod web_painter_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;
|
mod web_painter_wgpu;
|
||||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
|
||||||
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
|
||||||
|
|
||||||
pub use backend::*;
|
pub use backend::*;
|
||||||
|
|
||||||
|
use egui::Theme;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::MediaQueryList;
|
use web_sys::{Document, MediaQueryList, Node};
|
||||||
|
|
||||||
use input::{
|
use input::{
|
||||||
button_from_mouse_event, modifiers_from_kb_event, modifiers_from_mouse_event,
|
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
|
/// - `<a>`/`<area>` with an `href` attribute
|
||||||
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
|
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
|
||||||
/// - any other element with a `tabindex` attribute
|
/// - any other element with a `tabindex` attribute
|
||||||
pub(crate) fn focused_element() -> Option<web_sys::Element> {
|
pub(crate) fn focused_element(root: &Node) -> Option<web_sys::Element> {
|
||||||
web_sys::window()?
|
if let Some(document) = root.dyn_ref::<Document>() {
|
||||||
.document()?
|
document.active_element()
|
||||||
.active_element()?
|
} else if let Some(shadow) = root.dyn_ref::<web_sys::ShadowRoot>() {
|
||||||
.dyn_into()
|
shadow.active_element()
|
||||||
.ok()
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
|
pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
|
||||||
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
|
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
|
||||||
let element = element.dyn_ref::<web_sys::Element>()?;
|
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)
|
Some(element == &focused_element)
|
||||||
}
|
}
|
||||||
try_has_focus(element).unwrap_or(false)
|
try_has_focus(element).unwrap_or(false)
|
||||||
@@ -109,24 +110,31 @@ pub fn native_pixels_per_point() -> f32 {
|
|||||||
///
|
///
|
||||||
/// `None` means unknown.
|
/// `None` means unknown.
|
||||||
pub fn system_theme() -> Option<egui::Theme> {
|
pub fn system_theme() -> Option<egui::Theme> {
|
||||||
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
|
let window = web_sys::window()?;
|
||||||
.ok()??
|
if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) {
|
||||||
.matches();
|
Some(Theme::Dark)
|
||||||
Some(theme_from_dark_mode(dark_mode))
|
} else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) {
|
||||||
}
|
Some(Theme::Light)
|
||||||
|
|
||||||
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
|
|
||||||
} else {
|
} 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.
|
/// Returns the canvas in client coordinates.
|
||||||
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
|
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
|
||||||
let bounding_rect = canvas.get_bounding_client_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();
|
let items = js_sys::Object::new();
|
||||||
|
|
||||||
// SAFETY: I hope so
|
// SAFETY: I hope so
|
||||||
#[allow(unsafe_code, unused_unsafe)] // Weird false positive
|
#[expect(unsafe_code, unused_unsafe)] // Weird false positive
|
||||||
unsafe {
|
unsafe {
|
||||||
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
|
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::{Document, Node};
|
||||||
|
|
||||||
use super::{AppRunner, WebRunner};
|
use super::{AppRunner, WebRunner};
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ pub struct TextAgent {
|
|||||||
|
|
||||||
impl TextAgent {
|
impl TextAgent {
|
||||||
/// Attach the agent to the document.
|
/// 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();
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
|
|
||||||
// create an `<input>` element
|
// create an `<input>` element
|
||||||
@@ -37,7 +38,17 @@ impl TextAgent {
|
|||||||
style.set_property("position", "absolute")?;
|
style.set_property("position", "absolute")?;
|
||||||
style.set_property("top", "0")?;
|
style.set_property("top", "0")?;
|
||||||
style.set_property("left", "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
|
// attach event listeners
|
||||||
|
|
||||||
@@ -168,7 +179,7 @@ impl TextAgent {
|
|||||||
|
|
||||||
if let Err(err) = self.input.focus() {
|
if let Err(err) = self.input.focus() {
|
||||||
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blur(&self) {
|
pub fn blur(&self) {
|
||||||
@@ -180,7 +191,7 @@ impl TextAgent {
|
|||||||
|
|
||||||
if let Err(err) = self.input.blur() {
|
if let Err(err) = self.input.blur() {
|
||||||
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
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`
|
/// * `tokio-1.24.1/src/runtime/runtime.rs`
|
||||||
/// * `rerun/src/main.rs`
|
/// * `rerun/src/main.rs`
|
||||||
/// * `core/src/ops/function.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 {
|
fn shorten_file_path(file_path: &str) -> &str {
|
||||||
if let Some(i) = file_path.rfind("/src/") {
|
if let Some(i) = file_path.rfind("/src/") {
|
||||||
if let Some(prev_slash) = file_path[..i].rfind('/') {
|
if let Some(prev_slash) = file_path[..i].rfind('/') {
|
||||||
@@ -126,12 +126,17 @@ fn shorten_file_path(file_path: &str) -> &str {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_shorten_file_path() {
|
fn test_shorten_file_path() {
|
||||||
for (before, after) in [
|
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"),
|
("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"),
|
("/weird/path/file.rs", "/weird/path/file.rs"),
|
||||||
]
|
] {
|
||||||
{
|
|
||||||
assert_eq!(shorten_file_path(before), after);
|
assert_eq!(shorten_file_path(before), after);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use egui::{Event, UserData, ViewportId};
|
use egui::{Event, UserData, ViewportId};
|
||||||
use egui_glow::glow;
|
use egui_glow::glow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast as _;
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use web_sys::HtmlCanvasElement;
|
use web_sys::HtmlCanvasElement;
|
||||||
|
|
||||||
@@ -20,14 +20,15 @@ impl WebPainterGlow {
|
|||||||
self.painter.gl()
|
self.painter.gl()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new(
|
pub fn new(
|
||||||
_ctx: egui::Context,
|
_ctx: egui::Context,
|
||||||
canvas: HtmlCanvasElement,
|
canvas: HtmlCanvasElement,
|
||||||
options: &WebOptions,
|
options: &WebOptions,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
let (gl, shader_prefix) =
|
let (gl, shader_prefix) =
|
||||||
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
|
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 gl = std::sync::Arc::new(gl);
|
||||||
|
|
||||||
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)
|
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::web_painter::WebPainter;
|
|
||||||
use crate::WebOptions;
|
|
||||||
use egui::{Event, UserData, ViewportId};
|
use egui::{Event, UserData, ViewportId};
|
||||||
use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
|
use egui_wgpu::{
|
||||||
use egui_wgpu::{RenderState, SurfaceErrorAction};
|
RenderState, SurfaceErrorAction,
|
||||||
|
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||||
|
};
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use web_sys::HtmlCanvasElement;
|
use web_sys::HtmlCanvasElement;
|
||||||
|
|
||||||
|
use super::web_painter::WebPainter;
|
||||||
|
|
||||||
pub(crate) struct WebPainterWgpu {
|
pub(crate) struct WebPainterWgpu {
|
||||||
canvas: HtmlCanvasElement,
|
canvas: HtmlCanvasElement,
|
||||||
surface: wgpu::Surface<'static>,
|
surface: wgpu::Surface<'static>,
|
||||||
surface_configuration: wgpu::SurfaceConfiguration,
|
surface_configuration: wgpu::SurfaceConfiguration,
|
||||||
render_state: Option<RenderState>,
|
render_state: Option<RenderState>,
|
||||||
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
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>,
|
depth_texture_view: Option<wgpu::TextureView>,
|
||||||
screen_capture_state: Option<CaptureState>,
|
screen_capture_state: Option<CaptureState>,
|
||||||
capture_tx: CaptureSender,
|
capture_tx: CaptureSender,
|
||||||
@@ -23,7 +25,6 @@ pub(crate) struct WebPainterWgpu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WebPainterWgpu {
|
impl WebPainterWgpu {
|
||||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
|
||||||
pub fn render_state(&self) -> Option<RenderState> {
|
pub fn render_state(&self) -> Option<RenderState> {
|
||||||
self.render_state.clone()
|
self.render_state.clone()
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ impl WebPainterWgpu {
|
|||||||
height_in_pixels: u32,
|
height_in_pixels: u32,
|
||||||
) -> Option<wgpu::TextureView> {
|
) -> Option<wgpu::TextureView> {
|
||||||
let device = &render_state.device;
|
let device = &render_state.device;
|
||||||
self.depth_format.map(|depth_format| {
|
self.depth_stencil_format.map(|depth_stencil_format| {
|
||||||
device
|
device
|
||||||
.create_texture(&wgpu::TextureDescriptor {
|
.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("egui_depth_texture"),
|
label: Some("egui_depth_texture"),
|
||||||
@@ -47,19 +48,18 @@ impl WebPainterWgpu {
|
|||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: depth_format,
|
format: depth_stencil_format,
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
view_formats: &[depth_format],
|
view_formats: &[depth_stencil_format],
|
||||||
})
|
})
|
||||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
ctx: egui::Context,
|
ctx: egui::Context,
|
||||||
canvas: web_sys::HtmlCanvasElement,
|
canvas: web_sys::HtmlCanvasElement,
|
||||||
options: &WebOptions,
|
options: &crate::WebOptions,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
log::debug!("Creating wgpu painter");
|
log::debug!("Creating wgpu painter");
|
||||||
|
|
||||||
@@ -68,15 +68,17 @@ impl WebPainterWgpu {
|
|||||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
.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(
|
let render_state = RenderState::create(
|
||||||
&options.wgpu_options,
|
&options.wgpu_options,
|
||||||
&instance,
|
&instance,
|
||||||
Some(&surface),
|
Some(&surface),
|
||||||
depth_format,
|
egui_wgpu::RendererOptions {
|
||||||
1,
|
dithering: options.dithering,
|
||||||
options.dithering,
|
depth_stencil_format,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
@@ -101,7 +103,7 @@ impl WebPainterWgpu {
|
|||||||
render_state: Some(render_state),
|
render_state: Some(render_state),
|
||||||
surface,
|
surface,
|
||||||
surface_configuration,
|
surface_configuration,
|
||||||
depth_format,
|
depth_stencil_format,
|
||||||
depth_texture_view: None,
|
depth_texture_view: None,
|
||||||
on_surface_error: options.wgpu_options.on_surface_error.clone(),
|
on_surface_error: options.wgpu_options.on_surface_error.clone(),
|
||||||
screen_capture_state: None,
|
screen_capture_state: None,
|
||||||
@@ -236,6 +238,7 @@ impl WebPainter for WebPainterWgpu {
|
|||||||
}),
|
}),
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
|
depth_slice: None,
|
||||||
})],
|
})],
|
||||||
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
|
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
|
||||||
wgpu::RenderPassDepthStencilAttachment {
|
wgpu::RenderPassDepthStencilAttachment {
|
||||||
@@ -274,18 +277,11 @@ impl WebPainter for WebPainterWgpu {
|
|||||||
&mut encoder,
|
&mut encoder,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Some((output_frame, capture_buffer))
|
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.
|
// Submit the commands: both the main buffer and user-defined ones.
|
||||||
render_state
|
render_state
|
||||||
.queue
|
.queue
|
||||||
@@ -307,6 +303,16 @@ impl WebPainter for WebPainterWgpu {
|
|||||||
frame.present();
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ use std::{cell::RefCell, rc::Rc};
|
|||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use crate::{epi, App};
|
use crate::{App, epi};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
AppRunner, PanicHandler,
|
||||||
events::{self, ResizeObserverContext},
|
events::{self, ResizeObserverContext},
|
||||||
text_agent::TextAgent,
|
text_agent::TextAgent,
|
||||||
AppRunner, PanicHandler,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This is how `eframe` runs your web application
|
/// This is how `eframe` runs your web application
|
||||||
@@ -37,7 +37,7 @@ pub struct WebRunner {
|
|||||||
|
|
||||||
impl WebRunner {
|
impl WebRunner {
|
||||||
/// Will install a panic handler that will catch and log any panics
|
/// 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 {
|
pub fn new() -> Self {
|
||||||
let panic_handler = PanicHandler::install();
|
let panic_handler = PanicHandler::install();
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ impl WebRunner {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// First set up the app runner:
|
// 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 =
|
let app_runner =
|
||||||
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
|
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
|
||||||
self.app_runner.replace(Some(app_runner));
|
self.app_runner.replace(Some(app_runner));
|
||||||
@@ -280,7 +280,7 @@ struct TargetEvent {
|
|||||||
closure: Closure<dyn FnMut(web_sys::Event)>,
|
closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[expect(unused)]
|
||||||
struct IntervalHandle {
|
struct IntervalHandle {
|
||||||
handle: i32,
|
handle: i32,
|
||||||
closure: Closure<dyn FnMut()>,
|
closure: Closure<dyn FnMut()>,
|
||||||
@@ -289,7 +289,7 @@ struct IntervalHandle {
|
|||||||
enum EventToUnsubscribe {
|
enum EventToUnsubscribe {
|
||||||
TargetEvent(TargetEvent),
|
TargetEvent(TargetEvent),
|
||||||
|
|
||||||
#[allow(unused)]
|
#[expect(unused)]
|
||||||
IntervalHandle(IntervalHandle),
|
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.
|
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
|
## 0.31.0 - 2025-02-04
|
||||||
* Upgrade to wgpu 24 [#5610](https://github.com/emilk/egui/pull/5610) by [@torokati44](https://github.com/torokati44)
|
* 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)
|
* 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
|
edition.workspace = true
|
||||||
rust-version.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
|
license.workspace = true
|
||||||
readme = "README.md"
|
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"]
|
categories = ["gui", "game-development"]
|
||||||
keywords = ["wgpu", "egui", "gui", "gamedev"]
|
keywords = ["wgpu", "egui", "gui", "gamedev"]
|
||||||
include = [
|
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
|
||||||
"../LICENSE-APACHE",
|
|
||||||
"../LICENSE-MIT",
|
|
||||||
"**/*.rs",
|
|
||||||
"**/*.wgsl",
|
|
||||||
"Cargo.toml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
@@ -31,10 +25,13 @@ all-features = true
|
|||||||
rustdoc-args = ["--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
|
||||||
[features]
|
[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`
|
## 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.
|
## Enables Wayland support for winit.
|
||||||
wayland = ["winit?/wayland"]
|
wayland = ["winit?/wayland"]
|
||||||
@@ -43,14 +40,16 @@ wayland = ["winit?/wayland"]
|
|||||||
x11 = ["winit?/x11"]
|
x11 = ["winit?/x11"]
|
||||||
|
|
||||||
## Make the renderer `Sync` on wasm, exploiting that by default wasm isn't multithreaded.
|
## 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).
|
## 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).
|
## 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.
|
## Thus that usage is guarded against with compiler errors in wgpu.
|
||||||
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"]
|
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]
|
[dependencies]
|
||||||
egui = { workspace = true, default-features = false }
|
|
||||||
epaint = { workspace = true, default-features = false, features = ["bytemuck"] }
|
epaint = { workspace = true, default-features = false, features = ["bytemuck"] }
|
||||||
|
|
||||||
ahash.workspace = true
|
ahash.workspace = true
|
||||||
@@ -65,4 +64,5 @@ wgpu = { workspace = true, features = ["wgsl"] }
|
|||||||
|
|
||||||
# Optional dependencies:
|
# Optional dependencies:
|
||||||
|
|
||||||
|
egui = { workspace = true, optional = true, default-features = false }
|
||||||
winit = { workspace = true, optional = true, default-features = false }
|
winit = { workspace = true, optional = true, default-features = false }
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use egui::{UserData, ViewportId};
|
use egui::{UserData, ViewportId};
|
||||||
use epaint::ColorImage;
|
use epaint::ColorImage;
|
||||||
use std::sync::{mpsc, Arc};
|
use std::sync::{Arc, mpsc};
|
||||||
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
|
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
|
||||||
|
|
||||||
/// A texture and a buffer for reading the rendered frame back to the cpu.
|
/// 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
|
/// 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
|
/// 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
|
/// 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
|
// 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)
|
// 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.
|
// 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 {
|
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("egui_screen_capture_buffer"),
|
label: Some("egui_screen_capture_buffer"),
|
||||||
size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
|
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),
|
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||||
store: StoreOp::Store,
|
store: StoreOp::Store,
|
||||||
},
|
},
|
||||||
|
depth_slice: None,
|
||||||
})],
|
})],
|
||||||
depth_stencil_attachment: None,
|
depth_stencil_attachment: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
@@ -184,7 +186,7 @@ impl CaptureState {
|
|||||||
tx: CaptureSender,
|
tx: CaptureSender,
|
||||||
viewport_id: ViewportId,
|
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 = Arc::new(buffer);
|
||||||
let buffer_clone = buffer.clone();
|
let buffer_clone = buffer.clone();
|
||||||
let buffer_slice = buffer_clone.slice(..);
|
let buffer_slice = buffer_clone.slice(..);
|
||||||
@@ -195,13 +197,15 @@ impl CaptureState {
|
|||||||
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
|
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
|
||||||
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 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;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
||||||
if let Err(err) = 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;
|
return;
|
||||||
}
|
}
|
||||||
let buffer_slice = buffer.slice(..);
|
let buffer_slice = buffer.slice(..);
|
||||||
@@ -226,10 +230,10 @@ impl CaptureState {
|
|||||||
tx.send((
|
tx.send((
|
||||||
viewport_id,
|
viewport_id,
|
||||||
data,
|
data,
|
||||||
ColorImage {
|
ColorImage::new(
|
||||||
size: [tex_extent.width as usize, tex_extent.height as usize],
|
[tex_extent.width as usize, tex_extent.height as usize],
|
||||||
pixels,
|
pixels,
|
||||||
},
|
),
|
||||||
))
|
))
|
||||||
.ok();
|
.ok();
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ struct VertexOutput {
|
|||||||
|
|
||||||
struct Locals {
|
struct Locals {
|
||||||
screen_size: vec2<f32>,
|
screen_size: vec2<f32>,
|
||||||
dithering: u32, // 1 if dithering is enabled, 0 otherwise
|
|
||||||
// Uniform buffers need to be at least 16 bytes in WebGL.
|
/// 1 if dithering is enabled, 0 otherwise
|
||||||
// See https://github.com/gfx-rs/wgpu/issues/2072
|
dithering: u32,
|
||||||
_padding: 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;
|
@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(0) var r_tex_color: texture_2d<f32>;
|
||||||
@group(1) @binding(1) var r_tex_sampler: sampler;
|
@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
|
@fragment
|
||||||
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
// We always have an sRGB aware texture at the moment.
|
// We expect "normal" textures that are NOT sRGB-aware.
|
||||||
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
let tex_gamma = sample_texture(in);
|
||||||
let tex_gamma = gamma_from_linear_rgba(tex_linear);
|
|
||||||
var out_color_gamma = in.color * tex_gamma;
|
var out_color_gamma = in.color * tex_gamma;
|
||||||
// Dither the float color down to eight bits to reduce banding.
|
// Dither the float color down to eight bits to reduce banding.
|
||||||
// This step is optional for egui backends.
|
// This step is optional for egui backends.
|
||||||
@@ -115,9 +149,8 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
// We always have an sRGB aware texture at the moment.
|
// We expect "normal" textures that are NOT sRGB-aware.
|
||||||
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
let tex_gamma = sample_texture(in);
|
||||||
let tex_gamma = gamma_from_linear_rgba(tex_linear);
|
|
||||||
var out_color_gamma = in.color * tex_gamma;
|
var out_color_gamma = in.color * tex_gamma;
|
||||||
// Dither the float color down to eight bits to reduce banding.
|
// Dither the float color down to eight bits to reduce banding.
|
||||||
// This step is optional for egui backends.
|
// This step is optional for egui backends.
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pub use renderer::*;
|
|||||||
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
|
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
|
||||||
|
|
||||||
/// Helpers for capturing screenshots of the UI.
|
/// Helpers for capturing screenshots of the UI.
|
||||||
|
#[cfg(feature = "capture")]
|
||||||
pub mod capture;
|
pub mod capture;
|
||||||
|
|
||||||
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
|
/// 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.
|
/// An error produced by egui-wgpu.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum WgpuError {
|
pub enum WgpuError {
|
||||||
#[error("Failed to create wgpu adapter, no suitable adapter found: {0}")]
|
#[error(transparent)]
|
||||||
NoSuitableAdapterFound(String),
|
RequestAdapterError(#[from] wgpu::RequestAdapterError),
|
||||||
|
|
||||||
|
#[error("Adapter selection failed: {0}")]
|
||||||
|
CustomNativeAdapterSelectionError(String),
|
||||||
|
|
||||||
#[error("There was no valid format for the surface at all.")]
|
#[error("There was no valid format for the surface at all.")]
|
||||||
NoSurfaceFormatsAvailable,
|
NoSurfaceFormatsAvailable,
|
||||||
@@ -89,7 +93,7 @@ async fn request_adapter(
|
|||||||
instance: &wgpu::Instance,
|
instance: &wgpu::Instance,
|
||||||
power_preference: wgpu::PowerPreference,
|
power_preference: wgpu::PowerPreference,
|
||||||
compatible_surface: Option<&wgpu::Surface<'_>>,
|
compatible_surface: Option<&wgpu::Surface<'_>>,
|
||||||
_available_adapters: &[wgpu::Adapter],
|
available_adapters: &[wgpu::Adapter],
|
||||||
) -> Result<wgpu::Adapter, WgpuError> {
|
) -> Result<wgpu::Adapter, WgpuError> {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
@@ -104,48 +108,58 @@ async fn request_adapter(
|
|||||||
force_fallback_adapter: false,
|
force_fallback_adapter: false,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| {
|
.inspect_err(|_err| {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
if cfg!(target_arch = "wasm32") {
|
||||||
if _available_adapters.is_empty() {
|
// Nothing to add here
|
||||||
log::info!("No wgpu adapters found");
|
} else if available_adapters.is_empty() {
|
||||||
} else if _available_adapters.len() == 1 {
|
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!(
|
log::info!(
|
||||||
"The only available wgpu adapter was not suitable: {}",
|
"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 {
|
} else {
|
||||||
log::info!(
|
log::info!(
|
||||||
"No suitable wgpu adapter found out of the {} available ones: {}",
|
"No suitable wgpu adapter found out of the {} available ones: {}",
|
||||||
_available_adapters.len(),
|
available_adapters.len(),
|
||||||
describe_adapters(_available_adapters)
|
describe_adapters(available_adapters)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
if cfg!(target_arch = "wasm32") {
|
||||||
log::debug!(
|
|
||||||
"Picked wgpu adapter: {}",
|
|
||||||
adapter_info_summary(&adapter.get_info())
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
if _available_adapters.len() == 1 {
|
|
||||||
log::debug!(
|
|
||||||
"Picked the only available wgpu adapter: {}",
|
|
||||||
adapter_info_summary(&adapter.get_info())
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
log::info!(
|
|
||||||
"There were {} available wgpu adapters: {}",
|
|
||||||
_available_adapters.len(),
|
|
||||||
describe_adapters(_available_adapters)
|
|
||||||
);
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Picked wgpu adapter: {}",
|
"Picked wgpu adapter: {}",
|
||||||
adapter_info_summary(&adapter.get_info())
|
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)
|
Ok(adapter)
|
||||||
@@ -160,9 +174,7 @@ impl RenderState {
|
|||||||
config: &WgpuConfiguration,
|
config: &WgpuConfiguration,
|
||||||
instance: &wgpu::Instance,
|
instance: &wgpu::Instance,
|
||||||
compatible_surface: Option<&wgpu::Surface<'static>>,
|
compatible_surface: Option<&wgpu::Surface<'static>>,
|
||||||
depth_format: Option<wgpu::TextureFormat>,
|
options: RendererOptions,
|
||||||
msaa_samples: u32,
|
|
||||||
dithering: bool,
|
|
||||||
) -> Result<Self, WgpuError> {
|
) -> Result<Self, WgpuError> {
|
||||||
profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
|
profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
|
||||||
|
|
||||||
@@ -184,7 +196,6 @@ impl RenderState {
|
|||||||
power_preference,
|
power_preference,
|
||||||
native_adapter_selector: _native_adapter_selector,
|
native_adapter_selector: _native_adapter_selector,
|
||||||
device_descriptor,
|
device_descriptor,
|
||||||
trace_path,
|
|
||||||
}) => {
|
}) => {
|
||||||
let adapter = {
|
let adapter = {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@@ -194,7 +205,7 @@ impl RenderState {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if let Some(native_adapter_selector) = _native_adapter_selector {
|
if let Some(native_adapter_selector) = _native_adapter_selector {
|
||||||
native_adapter_selector(&available_adapters, compatible_surface)
|
native_adapter_selector(&available_adapters, compatible_surface)
|
||||||
.map_err(WgpuError::NoSuitableAdapterFound)
|
.map_err(WgpuError::CustomNativeAdapterSelectionError)
|
||||||
} else {
|
} else {
|
||||||
request_adapter(
|
request_adapter(
|
||||||
instance,
|
instance,
|
||||||
@@ -209,7 +220,7 @@ impl RenderState {
|
|||||||
let (device, queue) = {
|
let (device, queue) = {
|
||||||
profiling::scope!("request_device");
|
profiling::scope!("request_device");
|
||||||
adapter
|
adapter
|
||||||
.request_device(&(*device_descriptor)(&adapter), trace_path.as_deref())
|
.request_device(&(*device_descriptor)(&adapter))
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,17 +243,11 @@ impl RenderState {
|
|||||||
};
|
};
|
||||||
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
|
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
|
||||||
|
|
||||||
let renderer = Renderer::new(
|
let renderer = Renderer::new(&device, target_format, options);
|
||||||
&device,
|
|
||||||
target_format,
|
|
||||||
depth_format,
|
|
||||||
msaa_samples,
|
|
||||||
dithering,
|
|
||||||
);
|
|
||||||
|
|
||||||
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
|
// 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.
|
// 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 {
|
Ok(Self {
|
||||||
adapter,
|
adapter,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -255,7 +260,6 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
|
fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
|
||||||
if adapters.is_empty() {
|
if adapters.is_empty() {
|
||||||
"(none)".to_owned()
|
"(none)".to_owned()
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
|
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
|
||||||
|
|
||||||
use ahash::HashMap;
|
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 _;
|
use wgpu::util::DeviceExt as _;
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ impl Callback {
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # 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 {
|
pub trait CallbackTrait: Send + Sync {
|
||||||
fn prepare(
|
fn prepare(
|
||||||
&self,
|
&self,
|
||||||
@@ -140,21 +141,16 @@ impl ScreenDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Uniform buffer used when rendering.
|
/// Uniform buffer used when rendering.
|
||||||
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct UniformBuffer {
|
struct UniformBuffer {
|
||||||
screen_size_in_points: [f32; 2],
|
screen_size_in_points: [f32; 2],
|
||||||
dithering: u32,
|
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 {
|
/// 1 to do manual filtering for more predictable kittest snapshot images.
|
||||||
fn eq(&self, other: &Self) -> bool {
|
///
|
||||||
self.screen_size_in_points == other.screen_size_in_points
|
/// See also <https://github.com/emilk/egui/issues/5295>.
|
||||||
&& self.dithering == other.dithering
|
predictable_texture_filtering: u32,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SlicedBuffer {
|
struct SlicedBuffer {
|
||||||
@@ -175,6 +171,69 @@ pub struct Texture {
|
|||||||
pub options: Option<epaint::textures::TextureOptions>,
|
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.
|
/// Renderer for a egui based GUI.
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
pipeline: wgpu::RenderPipeline,
|
pipeline: wgpu::RenderPipeline,
|
||||||
@@ -194,7 +253,7 @@ pub struct Renderer {
|
|||||||
next_user_texture_id: u64,
|
next_user_texture_id: u64,
|
||||||
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
|
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
|
||||||
|
|
||||||
dithering: bool,
|
options: RendererOptions,
|
||||||
|
|
||||||
/// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
|
/// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
|
||||||
///
|
///
|
||||||
@@ -210,9 +269,7 @@ impl Renderer {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
output_color_format: wgpu::TextureFormat,
|
output_color_format: wgpu::TextureFormat,
|
||||||
output_depth_format: Option<wgpu::TextureFormat>,
|
options: RendererOptions,
|
||||||
msaa_samples: u32,
|
|
||||||
dithering: bool,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
@@ -229,8 +286,8 @@ impl Renderer {
|
|||||||
label: Some("egui_uniform_buffer"),
|
label: Some("egui_uniform_buffer"),
|
||||||
contents: bytemuck::cast_slice(&[UniformBuffer {
|
contents: bytemuck::cast_slice(&[UniformBuffer {
|
||||||
screen_size_in_points: [0.0, 0.0],
|
screen_size_in_points: [0.0, 0.0],
|
||||||
dithering: u32::from(dithering),
|
dithering: u32::from(options.dithering),
|
||||||
_padding: Default::default(),
|
predictable_texture_filtering: u32::from(options.predictable_texture_filtering),
|
||||||
}]),
|
}]),
|
||||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
});
|
});
|
||||||
@@ -299,13 +356,15 @@ impl Renderer {
|
|||||||
push_constant_ranges: &[],
|
push_constant_ranges: &[],
|
||||||
});
|
});
|
||||||
|
|
||||||
let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
|
let depth_stencil = options
|
||||||
format,
|
.depth_stencil_format
|
||||||
depth_write_enabled: false,
|
.map(|format| wgpu::DepthStencilState {
|
||||||
depth_compare: wgpu::CompareFunction::Always,
|
format,
|
||||||
stencil: wgpu::StencilState::default(),
|
depth_write_enabled: false,
|
||||||
bias: wgpu::DepthBiasState::default(),
|
depth_compare: wgpu::CompareFunction::Always,
|
||||||
});
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
});
|
||||||
|
|
||||||
let pipeline = {
|
let pipeline = {
|
||||||
profiling::scope!("create_render_pipeline");
|
profiling::scope!("create_render_pipeline");
|
||||||
@@ -337,14 +396,14 @@ impl Renderer {
|
|||||||
depth_stencil,
|
depth_stencil,
|
||||||
multisample: wgpu::MultisampleState {
|
multisample: wgpu::MultisampleState {
|
||||||
alpha_to_coverage_enabled: false,
|
alpha_to_coverage_enabled: false,
|
||||||
count: msaa_samples,
|
count: options.msaa_samples.max(1),
|
||||||
mask: !0,
|
mask: !0,
|
||||||
},
|
},
|
||||||
|
|
||||||
fragment: Some(wgpu::FragmentState {
|
fragment: Some(wgpu::FragmentState {
|
||||||
module: &module,
|
module: &module,
|
||||||
entry_point: Some(if output_color_format.is_srgb() {
|
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"
|
"fs_main_linear_framebuffer"
|
||||||
} else {
|
} else {
|
||||||
"fs_main_gamma_framebuffer" // this is what we prefer
|
"fs_main_gamma_framebuffer" // this is what we prefer
|
||||||
@@ -392,17 +451,13 @@ impl Renderer {
|
|||||||
},
|
},
|
||||||
uniform_buffer,
|
uniform_buffer,
|
||||||
// Buffers on wgpu are zero initialized, so this is indeed its current state!
|
// Buffers on wgpu are zero initialized, so this is indeed its current state!
|
||||||
previous_uniform_buffer_content: UniformBuffer {
|
previous_uniform_buffer_content: UniformBuffer::zeroed(),
|
||||||
screen_size_in_points: [0.0, 0.0],
|
|
||||||
dithering: 0,
|
|
||||||
_padding: 0,
|
|
||||||
},
|
|
||||||
uniform_bind_group,
|
uniform_bind_group,
|
||||||
texture_bind_group_layout,
|
texture_bind_group_layout,
|
||||||
textures: HashMap::default(),
|
textures: HashMap::default(),
|
||||||
next_user_texture_id: 0,
|
next_user_texture_id: 0,
|
||||||
samplers: HashMap::default(),
|
samplers: HashMap::default(),
|
||||||
dithering,
|
options,
|
||||||
callback_resources: CallbackResources::default(),
|
callback_resources: CallbackResources::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,15 +619,6 @@ impl Renderer {
|
|||||||
);
|
);
|
||||||
Cow::Borrowed(&image.pixels)
|
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());
|
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
|
||||||
|
|
||||||
@@ -638,9 +684,9 @@ impl Renderer {
|
|||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
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,
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let origin = wgpu::Origin3d::ZERO;
|
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 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
|
/// 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(
|
pub fn register_native_texture(
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
@@ -747,9 +793,9 @@ impl Renderer {
|
|||||||
/// This allows applications to specify individual minification/magnification filters as well as
|
/// This allows applications to specify individual minification/magnification filters as well as
|
||||||
/// custom mipmap and tiling options.
|
/// 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.
|
/// 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(
|
pub fn register_native_texture_with_sampler_options(
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
@@ -796,7 +842,7 @@ impl Renderer {
|
|||||||
/// [`wgpu::SamplerDescriptor`] options.
|
/// [`wgpu::SamplerDescriptor`] options.
|
||||||
///
|
///
|
||||||
/// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler 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(
|
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
@@ -855,8 +901,8 @@ impl Renderer {
|
|||||||
|
|
||||||
let uniform_buffer_content = UniformBuffer {
|
let uniform_buffer_content = UniformBuffer {
|
||||||
screen_size_in_points,
|
screen_size_in_points,
|
||||||
dithering: u32::from(self.dithering),
|
dithering: u32::from(self.options.dithering),
|
||||||
_padding: Default::default(),
|
predictable_texture_filtering: u32::from(self.options.predictable_texture_filtering),
|
||||||
};
|
};
|
||||||
if uniform_buffer_content != self.previous_uniform_buffer_content {
|
if uniform_buffer_content != self.previous_uniform_buffer_content {
|
||||||
profiling::scope!("update uniforms");
|
profiling::scope!("update uniforms");
|
||||||
@@ -882,7 +928,7 @@ impl Renderer {
|
|||||||
callbacks.push(c.0.as_ref());
|
callbacks.push(c.0.as_ref());
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
|
log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
|
||||||
};
|
}
|
||||||
acc
|
acc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -909,7 +955,11 @@ impl Renderer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let Some(mut index_buffer_staging) = index_buffer_staging else {
|
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;
|
let mut index_offset = 0;
|
||||||
@@ -948,7 +998,11 @@ impl Renderer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let Some(mut vertex_buffer_staging) = vertex_buffer_staging else {
|
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;
|
let mut vertex_offset = 0;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ impl WgpuSetup {
|
|||||||
pub async fn new_instance(&self) -> wgpu::Instance {
|
pub async fn new_instance(&self) -> wgpu::Instance {
|
||||||
match self {
|
match self {
|
||||||
Self::CreateNew(create_new) => {
|
Self::CreateNew(create_new) => {
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut, clippy::allow_attributes)]
|
||||||
let mut backends = create_new.instance_descriptor.backends;
|
let mut backends = create_new.instance_descriptor.backends;
|
||||||
|
|
||||||
// Don't try WebGPU if we're not in a secure context.
|
// 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)
|
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -126,13 +126,6 @@ pub struct WgpuSetupCreateNew {
|
|||||||
/// Configuration passed on device request, given an adapter
|
/// Configuration passed on device request, given an adapter
|
||||||
pub device_descriptor:
|
pub device_descriptor:
|
||||||
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
|
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 {
|
impl Clone for WgpuSetupCreateNew {
|
||||||
@@ -142,7 +135,6 @@ impl Clone for WgpuSetupCreateNew {
|
|||||||
power_preference: self.power_preference,
|
power_preference: self.power_preference,
|
||||||
native_adapter_selector: self.native_adapter_selector.clone(),
|
native_adapter_selector: self.native_adapter_selector.clone(),
|
||||||
device_descriptor: self.device_descriptor.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",
|
"native_adapter_selector",
|
||||||
&self.native_adapter_selector.is_some(),
|
&self.native_adapter_selector.is_some(),
|
||||||
)
|
)
|
||||||
.field("trace_path", &self.trace_path)
|
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,6 +162,7 @@ impl Default for WgpuSetupCreateNew {
|
|||||||
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
|
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
|
||||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||||
backend_options: wgpu::BackendOptions::from_env_or_default(),
|
backend_options: wgpu::BackendOptions::from_env_or_default(),
|
||||||
|
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||||
},
|
},
|
||||||
|
|
||||||
power_preference: wgpu::PowerPreference::from_env()
|
power_preference: wgpu::PowerPreference::from_env()
|
||||||
@@ -187,20 +179,15 @@ impl Default for WgpuSetupCreateNew {
|
|||||||
|
|
||||||
wgpu::DeviceDescriptor {
|
wgpu::DeviceDescriptor {
|
||||||
label: Some("egui wgpu device"),
|
label: Some("egui wgpu device"),
|
||||||
required_features: wgpu::Features::default(),
|
|
||||||
required_limits: wgpu::Limits {
|
required_limits: wgpu::Limits {
|
||||||
// When using a depth buffer, we have to be able to create a texture
|
// 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.
|
// large enough for the entire surface, and we want to support 4k+ displays.
|
||||||
max_texture_dimension_2d: 8192,
|
max_texture_dimension_2d: 8192,
|
||||||
..base_limits
|
..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::missing_errors_doc)]
|
||||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
|
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
|
||||||
use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration};
|
use crate::{
|
||||||
|
RendererOptions,
|
||||||
|
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||||
|
};
|
||||||
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
||||||
use std::{num::NonZeroU32, sync::Arc};
|
use std::{num::NonZeroU32, sync::Arc};
|
||||||
|
|
||||||
@@ -11,6 +14,7 @@ struct SurfaceState {
|
|||||||
alpha_mode: wgpu::CompositeAlphaMode,
|
alpha_mode: wgpu::CompositeAlphaMode,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
resizing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||||
@@ -21,10 +25,8 @@ struct SurfaceState {
|
|||||||
pub struct Painter {
|
pub struct Painter {
|
||||||
context: Context,
|
context: Context,
|
||||||
configuration: WgpuConfiguration,
|
configuration: WgpuConfiguration,
|
||||||
msaa_samples: u32,
|
options: RendererOptions,
|
||||||
support_transparent_backbuffer: bool,
|
support_transparent_backbuffer: bool,
|
||||||
dithering: bool,
|
|
||||||
depth_format: Option<wgpu::TextureFormat>,
|
|
||||||
screen_capture_state: Option<CaptureState>,
|
screen_capture_state: Option<CaptureState>,
|
||||||
|
|
||||||
instance: wgpu::Instance,
|
instance: wgpu::Instance,
|
||||||
@@ -54,10 +56,8 @@ impl Painter {
|
|||||||
pub async fn new(
|
pub async fn new(
|
||||||
context: Context,
|
context: Context,
|
||||||
configuration: WgpuConfiguration,
|
configuration: WgpuConfiguration,
|
||||||
msaa_samples: u32,
|
|
||||||
depth_format: Option<wgpu::TextureFormat>,
|
|
||||||
support_transparent_backbuffer: bool,
|
support_transparent_backbuffer: bool,
|
||||||
dithering: bool,
|
options: RendererOptions,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (capture_tx, capture_rx) = capture_channel();
|
let (capture_tx, capture_rx) = capture_channel();
|
||||||
let instance = configuration.wgpu_setup.new_instance().await;
|
let instance = configuration.wgpu_setup.new_instance().await;
|
||||||
@@ -65,10 +65,8 @@ impl Painter {
|
|||||||
Self {
|
Self {
|
||||||
context,
|
context,
|
||||||
configuration,
|
configuration,
|
||||||
msaa_samples,
|
options,
|
||||||
support_transparent_backbuffer,
|
support_transparent_backbuffer,
|
||||||
dithering,
|
|
||||||
depth_format,
|
|
||||||
screen_capture_state: None,
|
screen_capture_state: None,
|
||||||
|
|
||||||
instance,
|
instance,
|
||||||
@@ -204,9 +202,7 @@ impl Painter {
|
|||||||
&self.configuration,
|
&self.configuration,
|
||||||
&self.instance,
|
&self.instance,
|
||||||
Some(&surface),
|
Some(&surface),
|
||||||
self.depth_format,
|
self.options,
|
||||||
self.msaa_samples,
|
|
||||||
self.dithering,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
self.render_state.get_or_insert(render_state)
|
self.render_state.get_or_insert(render_state)
|
||||||
@@ -220,7 +216,9 @@ impl Painter {
|
|||||||
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
|
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
|
||||||
wgpu::CompositeAlphaMode::PostMultiplied
|
wgpu::CompositeAlphaMode::PostMultiplied
|
||||||
} else {
|
} 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
|
wgpu::CompositeAlphaMode::Auto
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -233,6 +231,7 @@ impl Painter {
|
|||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
alpha_mode,
|
alpha_mode,
|
||||||
|
resizing: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let Some(width) = NonZeroU32::new(size.width) else {
|
let Some(width) = NonZeroU32::new(size.width) else {
|
||||||
@@ -277,7 +276,7 @@ impl Painter {
|
|||||||
|
|
||||||
Self::configure_surface(surface_state, render_state, &self.configuration);
|
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(
|
self.depth_texture_view.insert(
|
||||||
viewport_id,
|
viewport_id,
|
||||||
render_state
|
render_state
|
||||||
@@ -290,7 +289,7 @@ impl Painter {
|
|||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: self.msaa_samples,
|
sample_count: self.options.msaa_samples.max(1),
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: depth_format,
|
format: depth_format,
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
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())
|
.then_some(self.render_state.as_ref())
|
||||||
.flatten()
|
.flatten()
|
||||||
{
|
{
|
||||||
@@ -318,7 +317,7 @@ impl Painter {
|
|||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: self.msaa_samples,
|
sample_count: self.options.msaa_samples.max(1),
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: texture_format,
|
format: texture_format,
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
@@ -326,7 +325,60 @@ impl Painter {
|
|||||||
})
|
})
|
||||||
.create_view(&wgpu::TextureViewDescriptor::default()),
|
.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(
|
pub fn on_window_resized(
|
||||||
@@ -344,7 +396,9 @@ impl Painter {
|
|||||||
height_in_pixels,
|
height_in_pixels,
|
||||||
);
|
);
|
||||||
} else {
|
} 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 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))
|
.then_some(self.msaa_texture_view.get(&viewport_id))
|
||||||
.flatten()
|
.flatten()
|
||||||
.map_or((&target_view, None), |texture_view| {
|
.map_or((&target_view, None), |texture_view| {
|
||||||
@@ -467,6 +521,7 @@ impl Painter {
|
|||||||
}),
|
}),
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
|
depth_slice: None,
|
||||||
})],
|
})],
|
||||||
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
|
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
|
||||||
wgpu::RenderPassDepthStencilAttachment {
|
wgpu::RenderPassDepthStencilAttachment {
|
||||||
@@ -493,14 +548,12 @@ impl Painter {
|
|||||||
&screen_descriptor,
|
&screen_descriptor,
|
||||||
);
|
);
|
||||||
|
|
||||||
if capture {
|
if capture && let Some(capture_state) = &mut self.screen_capture_state {
|
||||||
if let Some(capture_state) = &mut self.screen_capture_state {
|
capture_buffer = Some(capture_state.copy_textures(
|
||||||
capture_buffer = Some(capture_state.copy_textures(
|
&render_state.device,
|
||||||
&render_state.device,
|
&output_frame,
|
||||||
&output_frame,
|
&mut encoder,
|
||||||
&mut encoder,
|
));
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,16 +583,16 @@ impl Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(capture_buffer) = capture_buffer {
|
if let Some(capture_buffer) = capture_buffer
|
||||||
if let Some(screen_capture_state) = &mut self.screen_capture_state {
|
&& let Some(screen_capture_state) = &mut self.screen_capture_state
|
||||||
screen_capture_state.read_screen_rgba(
|
{
|
||||||
self.context.clone(),
|
screen_capture_state.read_screen_rgba(
|
||||||
capture_buffer,
|
self.context.clone(),
|
||||||
capture_data,
|
capture_buffer,
|
||||||
self.capture_tx.clone(),
|
capture_data,
|
||||||
viewport_id,
|
self.capture_tx.clone(),
|
||||||
);
|
viewport_id,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -575,7 +628,7 @@ impl Painter {
|
|||||||
.retain(|id, _| active_viewports.contains(id));
|
.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) {
|
pub fn destroy(&mut self) {
|
||||||
// TODO(emilk): something here?
|
// 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.
|
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
|
## 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)
|
* 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)
|
* 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"
|
description = "Bindings for using egui with winit"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.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
|
license.workspace = true
|
||||||
readme = "README.md"
|
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"]
|
categories = ["gui", "game-development"]
|
||||||
keywords = ["winit", "egui", "gui", "gamedev"]
|
keywords = ["winit", "egui", "gui", "gamedev"]
|
||||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
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"]
|
default = ["clipboard", "links", "wayland", "winit/default", "x11"]
|
||||||
|
|
||||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
## 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
|
# 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
|
# - 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"]
|
x11 = ["winit/x11", "bytemuck"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { workspace = true, default-features = false, features = ["log"] }
|
egui = { workspace = true, default-features = false }
|
||||||
|
|
||||||
ahash.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
profiling.workspace = true
|
profiling.workspace = true
|
||||||
raw-window-handle.workspace = true
|
raw-window-handle.workspace = true
|
||||||
@@ -67,7 +66,7 @@ winit = { workspace = true, default-features = false }
|
|||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
|
|
||||||
# feature accesskit
|
# feature accesskit
|
||||||
accesskit_winit = { version = "0.23", optional = true }
|
accesskit_winit = { workspace = true, optional = true }
|
||||||
|
|
||||||
bytemuck = { workspace = true, optional = true }
|
bytemuck = { workspace = true, optional = true }
|
||||||
|
|
||||||
@@ -75,17 +74,30 @@ bytemuck = { workspace = true, optional = true }
|
|||||||
document-features = { workspace = true, optional = true }
|
document-features = { workspace = true, optional = true }
|
||||||
|
|
||||||
serde = { 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]
|
[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
|
# 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
|
# 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.
|
# 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]
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
arboard = { version = "3.3", optional = true, default-features = false, features = [
|
arboard = { workspace = true, optional = true, features = ["image-data"] }
|
||||||
"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,
|
/// 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.
|
/// then a fallback clipboard that just works within the same app is used instead.
|
||||||
pub struct Clipboard {
|
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>,
|
arboard: Option<arboard::Clipboard>,
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
@@ -28,7 +31,10 @@ impl Clipboard {
|
|||||||
/// Construct a new instance
|
/// Construct a new instance
|
||||||
pub fn new(_raw_display_handle: Option<RawDisplayHandle>) -> Self {
|
pub fn new(_raw_display_handle: Option<RawDisplayHandle>) -> Self {
|
||||||
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(),
|
arboard: init_arboard(),
|
||||||
|
|
||||||
#[cfg(all(
|
#[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 {
|
if let Some(clipboard) = &mut self.arboard {
|
||||||
return match clipboard.get_text() {
|
return match clipboard.get_text() {
|
||||||
Ok(text) => Some(text),
|
Ok(text) => Some(text),
|
||||||
@@ -98,7 +107,10 @@ impl Clipboard {
|
|||||||
return;
|
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 Some(clipboard) = &mut self.arboard {
|
||||||
if let Err(err) = clipboard.set_text(text) {
|
if let Err(err) = clipboard.set_text(text) {
|
||||||
log::error!("arboard copy/cut error: {err}");
|
log::error!("arboard copy/cut error: {err}");
|
||||||
@@ -110,7 +122,10 @@ impl Clipboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_image(&mut self, image: &egui::ColorImage) {
|
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 Some(clipboard) = &mut self.arboard {
|
||||||
if let Err(err) = clipboard.set_image(arboard::ImageData {
|
if let Err(err) = clipboard.set_image(arboard::ImageData {
|
||||||
width: image.width(),
|
width: image.width(),
|
||||||
@@ -123,12 +138,17 @@ impl Clipboard {
|
|||||||
return;
|
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;
|
_ = 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> {
|
fn init_arboard() -> Option<arboard::Clipboard> {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
@@ -161,7 +181,7 @@ fn init_smithay_clipboard(
|
|||||||
|
|
||||||
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
|
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
|
||||||
log::trace!("Initializing smithay clipboard…");
|
log::trace!("Initializing smithay clipboard…");
|
||||||
#[allow(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
|
Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
|
||||||
} else {
|
} else {
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
|
|||||||
pub use winit;
|
pub use winit;
|
||||||
|
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
|
mod safe_area;
|
||||||
mod window_settings;
|
mod window_settings;
|
||||||
|
|
||||||
pub use window_settings::WindowSettings;
|
pub use window_settings::WindowSettings;
|
||||||
|
|
||||||
use ahash::HashSet;
|
|
||||||
use raw_window_handle::HasDisplayHandle;
|
use raw_window_handle::HasDisplayHandle;
|
||||||
|
|
||||||
use winit::{
|
use winit::{
|
||||||
@@ -166,12 +166,14 @@ impl State {
|
|||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
pub fn init_accesskit<T: From<accesskit_winit::Event> + Send>(
|
pub fn init_accesskit<T: From<accesskit_winit::Event> + Send>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
window: &Window,
|
window: &Window,
|
||||||
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
||||||
) {
|
) {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy(
|
self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy(
|
||||||
|
event_loop,
|
||||||
window,
|
window,
|
||||||
event_loop_proxy,
|
event_loop_proxy,
|
||||||
));
|
));
|
||||||
@@ -273,6 +275,21 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use winit::event::WindowEvent;
|
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 {
|
match event {
|
||||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||||
let native_pixels_per_point = *scale_factor as f32;
|
let native_pixels_per_point = *scale_factor as f32;
|
||||||
@@ -295,8 +312,8 @@ impl State {
|
|||||||
consumed: self.egui_ctx.wants_pointer_input(),
|
consumed: self.egui_ctx.wants_pointer_input(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::MouseWheel { delta, .. } => {
|
WindowEvent::MouseWheel { delta, phase, .. } => {
|
||||||
self.on_mouse_wheel(window, *delta);
|
self.on_mouse_wheel(window, *delta, *phase);
|
||||||
EventResponse {
|
EventResponse {
|
||||||
repaint: true,
|
repaint: true,
|
||||||
consumed: self.egui_ctx.wants_pointer_input(),
|
consumed: self.egui_ctx.wants_pointer_input(),
|
||||||
@@ -371,7 +388,7 @@ impl State {
|
|||||||
winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => {
|
winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => {
|
||||||
self.ime_event_disable();
|
self.ime_event_disable();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
EventResponse {
|
EventResponse {
|
||||||
repaint: true,
|
repaint: true,
|
||||||
@@ -406,10 +423,19 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::Focused(focused) => {
|
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
|
self.egui_input
|
||||||
.events
|
.events
|
||||||
.push(egui::Event::WindowFocused(*focused));
|
.push(egui::Event::WindowFocused(focused));
|
||||||
EventResponse {
|
EventResponse {
|
||||||
repaint: true,
|
repaint: true,
|
||||||
consumed: false,
|
consumed: false,
|
||||||
@@ -490,9 +516,7 @@ impl State {
|
|||||||
// Things we completely ignore:
|
// Things we completely ignore:
|
||||||
WindowEvent::ActivationTokenDone { .. }
|
WindowEvent::ActivationTokenDone { .. }
|
||||||
| WindowEvent::AxisMotion { .. }
|
| WindowEvent::AxisMotion { .. }
|
||||||
| WindowEvent::DoubleTapGesture { .. }
|
| WindowEvent::DoubleTapGesture { .. } => EventResponse {
|
||||||
| WindowEvent::RotationGesture { .. }
|
|
||||||
| WindowEvent::PanGesture { .. } => EventResponse {
|
|
||||||
repaint: false,
|
repaint: false,
|
||||||
consumed: false,
|
consumed: false,
|
||||||
},
|
},
|
||||||
@@ -507,6 +531,34 @@ impl State {
|
|||||||
consumed: self.egui_ctx.wants_pointer_input(),
|
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,
|
state: winit::event::ElementState,
|
||||||
button: winit::event::MouseButton,
|
button: winit::event::MouseButton,
|
||||||
) {
|
) {
|
||||||
if let Some(pos) = self.pointer_pos_in_points {
|
if let Some(pos) = self.pointer_pos_in_points
|
||||||
if let Some(button) = translate_mouse_button(button) {
|
&& let Some(button) = translate_mouse_button(button)
|
||||||
let pressed = state == winit::event::ElementState::Pressed;
|
{
|
||||||
|
let pressed = state == winit::event::ElementState::Pressed;
|
||||||
|
|
||||||
self.egui_input.events.push(egui::Event::PointerButton {
|
self.egui_input.events.push(egui::Event::PointerButton {
|
||||||
pos,
|
pos,
|
||||||
button,
|
button,
|
||||||
pressed,
|
pressed,
|
||||||
modifiers: self.egui_input.modifiers,
|
modifiers: self.egui_input.modifiers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if self.simulate_touch_screen {
|
if self.simulate_touch_screen {
|
||||||
if pressed {
|
if pressed {
|
||||||
self.any_pointer_button_down = true;
|
self.any_pointer_button_down = true;
|
||||||
|
|
||||||
self.egui_input.events.push(egui::Event::Touch {
|
self.egui_input.events.push(egui::Event::Touch {
|
||||||
device_id: egui::TouchDeviceId(0),
|
device_id: egui::TouchDeviceId(0),
|
||||||
id: egui::TouchId(0),
|
id: egui::TouchId(0),
|
||||||
phase: egui::TouchPhase::Start,
|
phase: egui::TouchPhase::Start,
|
||||||
pos,
|
pos,
|
||||||
force: None,
|
force: None,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.any_pointer_button_down = false;
|
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 {
|
self.egui_input.events.push(egui::Event::Touch {
|
||||||
device_id: egui::TouchDeviceId(0),
|
device_id: egui::TouchDeviceId(0),
|
||||||
id: egui::TouchId(0),
|
id: egui::TouchId(0),
|
||||||
phase: egui::TouchPhase::End,
|
phase: egui::TouchPhase::End,
|
||||||
pos,
|
pos,
|
||||||
force: None,
|
force: None,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,12 +681,7 @@ impl State {
|
|||||||
self.egui_input.events.push(egui::Event::Touch {
|
self.egui_input.events.push(egui::Event::Touch {
|
||||||
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
|
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
|
||||||
id: egui::TouchId::from(touch.id),
|
id: egui::TouchId::from(touch.id),
|
||||||
phase: match touch.phase {
|
phase: to_egui_touch_phase(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,
|
|
||||||
},
|
|
||||||
pos: egui::pos2(
|
pos: egui::pos2(
|
||||||
touch.location.x as f32 / pixels_per_point,
|
touch.location.x as f32 / pixels_per_point,
|
||||||
touch.location.y 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);
|
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,
|
egui::vec2(x as f32, y as f32) / pixels_per_point,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
let phase = to_egui_touch_phase(phase);
|
||||||
let modifiers = self.egui_input.modifiers;
|
let modifiers = self.egui_input.modifiers;
|
||||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||||
unit,
|
unit,
|
||||||
delta,
|
delta,
|
||||||
|
phase,
|
||||||
modifiers,
|
modifiers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -727,7 +781,7 @@ impl State {
|
|||||||
// When telling users "Press Ctrl-F to find", this is where we should
|
// 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
|
// 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.
|
// 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,
|
text,
|
||||||
|
|
||||||
@@ -746,7 +800,7 @@ impl State {
|
|||||||
None
|
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
|
// Helpful logging to enable when adding new key support
|
||||||
log::trace!(
|
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
|
// Make sure there is text, and that it is not control characters
|
||||||
// (e.g. delete is sent as "\u{f728}" on macOS).
|
// (e.g. delete is sent as "\u{f728}" on macOS).
|
||||||
if !text.is_empty() && text.chars().all(is_printable_char) {
|
if !text.is_empty() && text.chars().all(is_printable_char) {
|
||||||
@@ -803,7 +861,7 @@ impl State {
|
|||||||
if pressed && !is_cmd {
|
if pressed && !is_cmd {
|
||||||
self.egui_input
|
self.egui_input
|
||||||
.events
|
.events
|
||||||
.push(egui::Event::Text(text.to_string()));
|
.push(egui::Event::Text(text.to_owned()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -822,18 +880,14 @@ impl State {
|
|||||||
window: &Window,
|
window: &Window,
|
||||||
platform_output: egui::PlatformOutput,
|
platform_output: egui::PlatformOutput,
|
||||||
) {
|
) {
|
||||||
#![allow(deprecated)]
|
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
let egui::PlatformOutput {
|
let egui::PlatformOutput {
|
||||||
commands,
|
commands,
|
||||||
cursor_icon,
|
cursor_icon,
|
||||||
open_url,
|
|
||||||
copied_text,
|
|
||||||
events: _, // handled elsewhere
|
events: _, // handled elsewhere
|
||||||
mutable_text_under_cursor: _, // only used in eframe web
|
mutable_text_under_cursor: _, // only used in eframe web
|
||||||
ime,
|
ime,
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
accesskit_update,
|
accesskit_update,
|
||||||
num_completed_passes: _, // `egui::Context::run` handles this
|
num_completed_passes: _, // `egui::Context::run` handles this
|
||||||
request_discard_reasons: _, // `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);
|
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();
|
let allow_ime = ime.is_some();
|
||||||
if self.allow_ime != allow_ime {
|
if self.allow_ime != allow_ime {
|
||||||
self.allow_ime = allow_ime;
|
self.allow_ime = allow_ime;
|
||||||
@@ -894,12 +940,15 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
if let Some(accesskit) = self.accesskit.as_mut() {
|
if let Some(accesskit) = self.accesskit.as_mut()
|
||||||
if let Some(update) = accesskit_update {
|
&& let Some(update) = accesskit_update
|
||||||
profiling::scope!("accesskit");
|
{
|
||||||
accesskit.update_if_active(|| 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) {
|
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 {
|
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
|
||||||
match theme {
|
match theme {
|
||||||
winit::window::Theme::Dark => Theme::Dark,
|
winit::window::Theme::Dark => Theme::Dark,
|
||||||
@@ -1020,7 +1078,7 @@ pub fn update_viewport_info(
|
|||||||
fn open_url_in_browser(_url: &str) {
|
fn open_url_in_browser(_url: &str) {
|
||||||
#[cfg(feature = "webbrowser")]
|
#[cfg(feature = "webbrowser")]
|
||||||
if let Err(err) = webbrowser::open(_url) {
|
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"))]
|
#[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::F33 => Key::F33,
|
||||||
NamedKey::F34 => Key::F34,
|
NamedKey::F34 => Key::F34,
|
||||||
NamedKey::F35 => Key::F35,
|
NamedKey::F35 => Key::F35,
|
||||||
|
|
||||||
|
NamedKey::BrowserBack => Key::BrowserBack,
|
||||||
_ => {
|
_ => {
|
||||||
log::trace!("Unknown key: {named_key:?}");
|
log::trace!("Unknown key: {named_key:?}");
|
||||||
return None;
|
return None;
|
||||||
@@ -1326,7 +1386,7 @@ pub fn process_viewport_commands(
|
|||||||
info: &mut ViewportInfo,
|
info: &mut ViewportInfo,
|
||||||
commands: impl IntoIterator<Item = ViewportCommand>,
|
commands: impl IntoIterator<Item = ViewportCommand>,
|
||||||
window: &Window,
|
window: &Window,
|
||||||
actions_requested: &mut HashSet<ActionRequested>,
|
actions_requested: &mut Vec<ActionRequested>,
|
||||||
) {
|
) {
|
||||||
for command in commands {
|
for command in commands {
|
||||||
process_viewport_command(egui_ctx, window, command, info, actions_requested);
|
process_viewport_command(egui_ctx, window, command, info, actions_requested);
|
||||||
@@ -1338,9 +1398,9 @@ fn process_viewport_command(
|
|||||||
window: &Window,
|
window: &Window,
|
||||||
command: ViewportCommand,
|
command: ViewportCommand,
|
||||||
info: &mut ViewportInfo,
|
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;
|
use winit::window::ResizeDirection;
|
||||||
|
|
||||||
@@ -1357,10 +1417,10 @@ fn process_viewport_command(
|
|||||||
}
|
}
|
||||||
ViewportCommand::StartDrag => {
|
ViewportCommand::StartDrag => {
|
||||||
// If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed!
|
// If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed!
|
||||||
if window.has_focus() {
|
if window.has_focus()
|
||||||
if let Err(err) = window.drag_window() {
|
&& let Err(err) = window.drag_window()
|
||||||
log::warn!("{command:?}: {err}");
|
{
|
||||||
}
|
log::warn!("{command:?}: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ViewportCommand::InnerSize(size) => {
|
ViewportCommand::InnerSize(size) => {
|
||||||
@@ -1531,16 +1591,16 @@ fn process_viewport_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ViewportCommand::Screenshot(user_data) => {
|
ViewportCommand::Screenshot(user_data) => {
|
||||||
actions_requested.insert(ActionRequested::Screenshot(user_data));
|
actions_requested.push(ActionRequested::Screenshot(user_data));
|
||||||
}
|
}
|
||||||
ViewportCommand::RequestCut => {
|
ViewportCommand::RequestCut => {
|
||||||
actions_requested.insert(ActionRequested::Cut);
|
actions_requested.push(ActionRequested::Cut);
|
||||||
}
|
}
|
||||||
ViewportCommand::RequestCopy => {
|
ViewportCommand::RequestCopy => {
|
||||||
actions_requested.insert(ActionRequested::Copy);
|
actions_requested.push(ActionRequested::Copy);
|
||||||
}
|
}
|
||||||
ViewportCommand::RequestPaste => {
|
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> {
|
) -> Result<Window, winit::error::OsError> {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
let window_attributes =
|
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
|
||||||
create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone());
|
|
||||||
let window = event_loop.create_window(window_attributes)?;
|
let window = event_loop.create_window(window_attributes)?;
|
||||||
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
|
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
|
||||||
Ok(window)
|
Ok(window)
|
||||||
@@ -1567,28 +1626,10 @@ pub fn create_window(
|
|||||||
|
|
||||||
pub fn create_winit_window_attributes(
|
pub fn create_winit_window_attributes(
|
||||||
egui_ctx: &egui::Context,
|
egui_ctx: &egui::Context,
|
||||||
event_loop: &ActiveEventLoop,
|
|
||||||
viewport_builder: ViewportBuilder,
|
viewport_builder: ViewportBuilder,
|
||||||
) -> winit::window::WindowAttributes {
|
) -> winit::window::WindowAttributes {
|
||||||
profiling::function_scope!();
|
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 {
|
let ViewportBuilder {
|
||||||
title,
|
title,
|
||||||
position,
|
position,
|
||||||
@@ -1610,9 +1651,11 @@ pub fn create_winit_window_attributes(
|
|||||||
|
|
||||||
// macOS:
|
// macOS:
|
||||||
fullsize_content_view: _fullsize_content_view,
|
fullsize_content_view: _fullsize_content_view,
|
||||||
|
movable_by_window_background: _movable_by_window_background,
|
||||||
title_shown: _title_shown,
|
title_shown: _title_shown,
|
||||||
titlebar_buttons_shown: _titlebar_buttons_shown,
|
titlebar_buttons_shown: _titlebar_buttons_shown,
|
||||||
titlebar_shown: _titlebar_shown,
|
titlebar_shown: _titlebar_shown,
|
||||||
|
has_shadow: _has_shadow,
|
||||||
|
|
||||||
// Windows:
|
// Windows:
|
||||||
drag_and_drop: _drag_and_drop,
|
drag_and_drop: _drag_and_drop,
|
||||||
@@ -1662,40 +1705,46 @@ pub fn create_winit_window_attributes(
|
|||||||
})
|
})
|
||||||
.with_active(active.unwrap_or(true));
|
.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"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
if let Some(size) = inner_size {
|
{
|
||||||
window_attributes = window_attributes.with_inner_size(PhysicalSize::new(
|
use winit::dpi::{LogicalPosition, LogicalSize};
|
||||||
pixels_per_point * size.x,
|
let zoom_factor = egui_ctx.zoom_factor();
|
||||||
pixels_per_point * size.y,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
if let Some(size) = inner_size {
|
||||||
if let Some(size) = min_inner_size {
|
window_attributes = window_attributes
|
||||||
window_attributes = window_attributes.with_min_inner_size(PhysicalSize::new(
|
.with_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
||||||
pixels_per_point * size.x,
|
}
|
||||||
pixels_per_point * size.y,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
if let Some(size) = min_inner_size {
|
||||||
if let Some(size) = max_inner_size {
|
window_attributes = window_attributes
|
||||||
window_attributes = window_attributes.with_max_inner_size(PhysicalSize::new(
|
.with_min_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
||||||
pixels_per_point * size.x,
|
}
|
||||||
pixels_per_point * size.y,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
if let Some(size) = max_inner_size {
|
||||||
if let Some(pos) = position {
|
window_attributes = window_attributes
|
||||||
window_attributes = window_attributes.with_position(PhysicalPosition::new(
|
.with_max_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
|
||||||
pixels_per_point * pos.x,
|
}
|
||||||
pixels_per_point * pos.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")]
|
#[cfg(target_os = "ios")]
|
||||||
{
|
{
|
||||||
// Unused:
|
// Unused:
|
||||||
|
_ = egui_ctx;
|
||||||
_ = pixels_per_point;
|
_ = pixels_per_point;
|
||||||
_ = position;
|
_ = position;
|
||||||
_ = inner_size;
|
_ = inner_size;
|
||||||
@@ -1756,7 +1805,9 @@ pub fn create_winit_window_attributes(
|
|||||||
.with_title_hidden(!_title_shown.unwrap_or(true))
|
.with_title_hidden(!_title_shown.unwrap_or(true))
|
||||||
.with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
|
.with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
|
||||||
.with_titlebar_transparent(!_titlebar_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
|
window_attributes
|
||||||
@@ -1783,10 +1834,10 @@ pub fn apply_viewport_builder_to_window(
|
|||||||
window: &Window,
|
window: &Window,
|
||||||
builder: &ViewportBuilder,
|
builder: &ViewportBuilder,
|
||||||
) {
|
) {
|
||||||
if let Some(mouse_passthrough) = builder.mouse_passthrough {
|
if let Some(mouse_passthrough) = builder.mouse_passthrough
|
||||||
if let Err(err) = window.set_cursor_hittest(!mouse_passthrough) {
|
&& let Err(err) = window.set_cursor_hittest(!mouse_passthrough)
|
||||||
log::warn!("set_cursor_hittest failed: {err}");
|
{
|
||||||
}
|
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);
|
let pixels_per_point = pixels_per_point(egui_ctx, window);
|
||||||
|
|
||||||
if let Some(size) = builder.inner_size {
|
if let Some(size) = builder.inner_size
|
||||||
if window
|
&& window
|
||||||
.request_inner_size(PhysicalSize::new(
|
.request_inner_size(PhysicalSize::new(
|
||||||
pixels_per_point * size.x,
|
pixels_per_point * size.x,
|
||||||
pixels_per_point * size.y,
|
pixels_per_point * size.y,
|
||||||
))
|
))
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
log::debug!("Failed to set window size");
|
log::debug!("Failed to set window size");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(size) = builder.min_inner_size {
|
if let Some(size) = builder.min_inner_size {
|
||||||
window.set_min_inner_size(Some(PhysicalSize::new(
|
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;
|
use winit::event::DeviceEvent;
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
DeviceEvent::Added { .. } => "DeviceEvent::Added",
|
DeviceEvent::Added => "DeviceEvent::Added",
|
||||||
DeviceEvent::Removed { .. } => "DeviceEvent::Removed",
|
DeviceEvent::Removed => "DeviceEvent::Removed",
|
||||||
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
|
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
|
||||||
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
|
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
|
||||||
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
|
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
|
||||||
@@ -1857,11 +1907,11 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
|
|||||||
WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone",
|
WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone",
|
||||||
WindowEvent::Resized { .. } => "WindowEvent::Resized",
|
WindowEvent::Resized { .. } => "WindowEvent::Resized",
|
||||||
WindowEvent::Moved { .. } => "WindowEvent::Moved",
|
WindowEvent::Moved { .. } => "WindowEvent::Moved",
|
||||||
WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested",
|
WindowEvent::CloseRequested => "WindowEvent::CloseRequested",
|
||||||
WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed",
|
WindowEvent::Destroyed => "WindowEvent::Destroyed",
|
||||||
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
|
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
|
||||||
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
|
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
|
||||||
WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled",
|
WindowEvent::HoveredFileCancelled => "WindowEvent::HoveredFileCancelled",
|
||||||
WindowEvent::Focused { .. } => "WindowEvent::Focused",
|
WindowEvent::Focused { .. } => "WindowEvent::Focused",
|
||||||
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
|
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
|
||||||
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
|
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
|
||||||
@@ -1872,7 +1922,7 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
|
|||||||
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
|
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
|
||||||
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
|
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
|
||||||
WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture",
|
WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture",
|
||||||
WindowEvent::RedrawRequested { .. } => "WindowEvent::RedrawRequested",
|
WindowEvent::RedrawRequested => "WindowEvent::RedrawRequested",
|
||||||
WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture",
|
WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture",
|
||||||
WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture",
|
WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture",
|
||||||
WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",
|
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]
|
[features]
|
||||||
default = ["default_fonts"]
|
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`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`.
|
||||||
bytemuck = ["epaint/bytemuck"]
|
bytemuck = ["epaint/bytemuck"]
|
||||||
|
|
||||||
@@ -44,18 +40,10 @@ cint = ["epaint/cint"]
|
|||||||
## Enable the [`hex_color`] macro.
|
## Enable the [`hex_color`] macro.
|
||||||
color-hex = ["epaint/color-hex"]
|
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 set, egui will use `include_bytes!` to bundle some fonts.
|
||||||
## If you plan on specifying your own fonts you may disable this feature.
|
## If you plan on specifying your own fonts you may disable this feature.
|
||||||
default_fonts = ["epaint/default_fonts"]
|
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`](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"]
|
mint = ["epaint/mint"]
|
||||||
|
|
||||||
@@ -69,7 +57,7 @@ persistence = ["serde", "epaint/serde", "ron"]
|
|||||||
rayon = ["epaint/rayon"]
|
rayon = ["epaint/rayon"]
|
||||||
|
|
||||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
## 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
|
## Change Vertex layout to be compatible with unity
|
||||||
unity = ["epaint/unity"]
|
unity = ["epaint/unity"]
|
||||||
@@ -83,19 +71,21 @@ _override_unity = ["epaint/_override_unity"]
|
|||||||
emath = { workspace = true, default-features = false }
|
emath = { workspace = true, default-features = false }
|
||||||
epaint = { workspace = true, default-features = false }
|
epaint = { workspace = true, default-features = false }
|
||||||
|
|
||||||
|
accesskit.workspace = true
|
||||||
ahash.workspace = true
|
ahash.workspace = true
|
||||||
bitflags.workspace = true
|
bitflags.workspace = true
|
||||||
|
log.workspace = true
|
||||||
nohash-hasher.workspace = true
|
nohash-hasher.workspace = true
|
||||||
profiling.workspace = true
|
profiling.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
unicode-segmentation.workspace = true
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
accesskit = { version = "0.17.0", optional = true }
|
|
||||||
|
|
||||||
backtrace = { workspace = true, optional = true }
|
backtrace = { workspace = true, optional = true }
|
||||||
|
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { workspace = true, optional = true }
|
document-features = { workspace = true, optional = true }
|
||||||
|
|
||||||
log = { workspace = true, optional = true }
|
|
||||||
ron = { workspace = true, optional = true }
|
ron = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
|
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!
|
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!
|
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::{
|
use crate::{
|
||||||
emath::{remap_clamp, NumExt as _},
|
|
||||||
Id, IdMap, InputState,
|
Id, IdMap, InputState,
|
||||||
|
emath::{NumExt as _, remap_clamp},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[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.
|
/// 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 {
|
pub trait CacheTrait: 'static + Send + Sync {
|
||||||
/// Call once per frame to evict cache.
|
/// Call once per frame to evict cache.
|
||||||
fn update(&mut self);
|
fn update(&mut self);
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ pub fn capture() -> String {
|
|||||||
backtrace::resolve_frame(frame, |symbol| {
|
backtrace::resolve_frame(frame, |symbol| {
|
||||||
let mut file_and_line = symbol.filename().map(shorten_source_file_path);
|
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(file_and_line) = &mut file_and_line
|
||||||
if let Some(line_nr) = symbol.lineno() {
|
&& let Some(line_nr) = symbol.lineno()
|
||||||
file_and_line.push_str(&format!(":{line_nr}"));
|
{
|
||||||
}
|
file_and_line.push_str(&format!(":{line_nr}"));
|
||||||
}
|
}
|
||||||
let file_and_line = file_and_line.unwrap_or_default();
|
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]
|
#[test]
|
||||||
fn test_shorten_path() {
|
fn test_shorten_path() {
|
||||||
for (before, after) in [
|
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"),
|
("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"),
|
("/weird/path/file.rs", "/weird/path/file.rs"),
|
||||||
]
|
] {
|
||||||
{
|
|
||||||
use std::str::FromStr as _;
|
use std::str::FromStr as _;
|
||||||
let before = std::path::PathBuf::from_str(before).unwrap();
|
let before = std::path::PathBuf::from_str(before).unwrap();
|
||||||
assert_eq!(shorten_source_file_path(&before), after);
|
assert_eq!(shorten_source_file_path(&before), after);
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
use emath::GuiRounding as _;
|
use emath::GuiRounding as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
|
Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
|
||||||
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
|
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// State of an [`Area`] that is persisted between frames.
|
/// 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()`].
|
/// The previous rectangle used by this area can be obtained through [`crate::Memory::area_rect()`].
|
||||||
#[must_use = "You should call .show()"]
|
#[must_use = "You should call .show()"]
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Area {
|
pub struct Area {
|
||||||
pub(crate) id: Id,
|
pub(crate) id: Id,
|
||||||
kind: UiKind,
|
info: UiStackInfo,
|
||||||
sense: Option<Sense>,
|
sense: Option<Sense>,
|
||||||
movable: bool,
|
movable: bool,
|
||||||
interactable: bool,
|
interactable: bool,
|
||||||
@@ -120,6 +120,8 @@ pub struct Area {
|
|||||||
anchor: Option<(Align2, Vec2)>,
|
anchor: Option<(Align2, Vec2)>,
|
||||||
new_pos: Option<Pos2>,
|
new_pos: Option<Pos2>,
|
||||||
fade_in: bool,
|
fade_in: bool,
|
||||||
|
layout: Layout,
|
||||||
|
sizing_pass: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetWithState for Area {
|
impl WidgetWithState for Area {
|
||||||
@@ -131,7 +133,7 @@ impl Area {
|
|||||||
pub fn new(id: Id) -> Self {
|
pub fn new(id: Id) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
kind: UiKind::GenericArea,
|
info: UiStackInfo::new(UiKind::GenericArea),
|
||||||
sense: None,
|
sense: None,
|
||||||
movable: true,
|
movable: true,
|
||||||
interactable: true,
|
interactable: true,
|
||||||
@@ -145,6 +147,8 @@ impl Area {
|
|||||||
pivot: Align2::LEFT_TOP,
|
pivot: Align2::LEFT_TOP,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
fade_in: true,
|
fade_in: true,
|
||||||
|
layout: Layout::default(),
|
||||||
|
sizing_pass: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +166,16 @@ impl Area {
|
|||||||
/// Default to [`UiKind::GenericArea`].
|
/// Default to [`UiKind::GenericArea`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn kind(mut self, kind: UiKind) -> Self {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,10 +352,38 @@ impl Area {
|
|||||||
self.fade_in = fade_in;
|
self.fade_in = fade_in;
|
||||||
self
|
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 {
|
pub(crate) struct Prepared {
|
||||||
kind: UiKind,
|
info: Option<UiStackInfo>,
|
||||||
layer_id: LayerId,
|
layer_id: LayerId,
|
||||||
state: AreaState,
|
state: AreaState,
|
||||||
move_response: Response,
|
move_response: Response,
|
||||||
@@ -358,6 +399,7 @@ pub(crate) struct Prepared {
|
|||||||
sizing_pass: bool,
|
sizing_pass: bool,
|
||||||
|
|
||||||
fade_in: bool,
|
fade_in: bool,
|
||||||
|
layout: Layout,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Area {
|
impl Area {
|
||||||
@@ -366,7 +408,7 @@ impl Area {
|
|||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<R> {
|
) -> InnerResponse<R> {
|
||||||
let prepared = self.begin(ctx);
|
let mut prepared = self.begin(ctx);
|
||||||
let mut content_ui = prepared.content_ui(ctx);
|
let mut content_ui = prepared.content_ui(ctx);
|
||||||
let inner = add_contents(&mut content_ui);
|
let inner = add_contents(&mut content_ui);
|
||||||
let response = prepared.end(ctx, content_ui);
|
let response = prepared.end(ctx, content_ui);
|
||||||
@@ -376,7 +418,7 @@ impl Area {
|
|||||||
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
|
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
|
||||||
let Self {
|
let Self {
|
||||||
id,
|
id,
|
||||||
kind,
|
info,
|
||||||
sense,
|
sense,
|
||||||
movable,
|
movable,
|
||||||
order,
|
order,
|
||||||
@@ -390,9 +432,11 @@ impl Area {
|
|||||||
constrain,
|
constrain,
|
||||||
constrain_rect,
|
constrain_rect,
|
||||||
fade_in,
|
fade_in,
|
||||||
|
layout,
|
||||||
|
sizing_pass: force_sizing_pass,
|
||||||
} = self;
|
} = 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);
|
let layer_id = LayerId::new(order, id);
|
||||||
|
|
||||||
@@ -405,6 +449,10 @@ impl Area {
|
|||||||
interactable,
|
interactable,
|
||||||
last_became_visible_at: None,
|
last_became_visible_at: None,
|
||||||
});
|
});
|
||||||
|
if force_sizing_pass {
|
||||||
|
sizing_pass = true;
|
||||||
|
state.size = None;
|
||||||
|
}
|
||||||
state.pivot = pivot;
|
state.pivot = pivot;
|
||||||
state.interactable = interactable;
|
state.interactable = interactable;
|
||||||
if let Some(new_pos) = new_pos {
|
if let Some(new_pos) = new_pos {
|
||||||
@@ -477,10 +525,21 @@ impl Area {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if movable && move_response.dragged() {
|
// Used to prevent drift
|
||||||
if let Some(pivot_pos) = &mut state.pivot_pos {
|
let pivot_at_start_of_drag_id = id.with("pivot_at_drag_start");
|
||||||
*pivot_pos += move_response.drag_delta();
|
|
||||||
}
|
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())
|
if (move_response.dragged() || move_response.clicked())
|
||||||
@@ -494,20 +553,21 @@ impl Area {
|
|||||||
move_response
|
move_response
|
||||||
};
|
};
|
||||||
|
|
||||||
if constrain {
|
state.set_left_top_pos(round_area_position(
|
||||||
state.set_left_top_pos(
|
ctx,
|
||||||
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
|
if constrain {
|
||||||
);
|
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
|
||||||
}
|
} else {
|
||||||
|
state.left_top_pos()
|
||||||
state.set_left_top_pos(state.left_top_pos());
|
},
|
||||||
|
));
|
||||||
|
|
||||||
// Update response with possibly moved/constrained rect:
|
// Update response with possibly moved/constrained rect:
|
||||||
move_response.rect = state.rect();
|
move_response.rect = state.rect();
|
||||||
move_response.interact_rect = state.rect();
|
move_response.interact_rect = state.rect();
|
||||||
|
|
||||||
Prepared {
|
Prepared {
|
||||||
kind,
|
info: Some(info),
|
||||||
layer_id,
|
layer_id,
|
||||||
state,
|
state,
|
||||||
move_response,
|
move_response,
|
||||||
@@ -516,10 +576,21 @@ impl Area {
|
|||||||
constrain_rect,
|
constrain_rect,
|
||||||
sizing_pass,
|
sizing_pass,
|
||||||
fade_in,
|
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 {
|
impl Prepared {
|
||||||
pub(crate) fn state(&self) -> &AreaState {
|
pub(crate) fn state(&self) -> &AreaState {
|
||||||
&self.state
|
&self.state
|
||||||
@@ -537,13 +608,16 @@ impl Prepared {
|
|||||||
self.constrain_rect
|
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 max_rect = self.state.rect();
|
||||||
|
|
||||||
let mut ui_builder = UiBuilder::new()
|
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)
|
.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 {
|
if !self.enabled {
|
||||||
ui_builder = ui_builder.disabled();
|
ui_builder = ui_builder.disabled();
|
||||||
@@ -555,16 +629,16 @@ impl Prepared {
|
|||||||
let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
|
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
|
ui.set_clip_rect(self.constrain_rect); // Don't paint outside our bounds
|
||||||
|
|
||||||
if self.fade_in {
|
if self.fade_in
|
||||||
if let Some(last_became_visible_at) = self.state.last_became_visible_at {
|
&& 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 age =
|
||||||
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
|
ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
|
||||||
let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
|
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
|
||||||
ui.multiply_opacity(opacity);
|
let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
|
||||||
if opacity < 1.0 {
|
ui.multiply_opacity(opacity);
|
||||||
ctx.request_repaint();
|
if opacity < 1.0 {
|
||||||
}
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,10 +653,10 @@ impl Prepared {
|
|||||||
self.move_response.id
|
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 {
|
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
|
||||||
let Self {
|
let Self {
|
||||||
kind: _,
|
info: _,
|
||||||
layer_id,
|
layer_id,
|
||||||
mut state,
|
mut state,
|
||||||
move_response: mut response,
|
move_response: mut response,
|
||||||
@@ -598,6 +672,12 @@ impl Prepared {
|
|||||||
response.rect = final_rect;
|
response.rect = final_rect;
|
||||||
response.interact_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));
|
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
|
||||||
|
|
||||||
if sizing_pass {
|
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();
|
let current_column_bb = column_bbs.last_mut().unwrap();
|
||||||
if rect.left() < current_column_bb.right() {
|
if rect.left() < current_column_bb.right() {
|
||||||
// same column
|
// same column
|
||||||
*current_column_bb = current_column_bb.union(rect);
|
*current_column_bb |= rect;
|
||||||
} else {
|
} else {
|
||||||
// new column
|
// new column
|
||||||
column_bbs.push(rect);
|
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 std::hash::Hash;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
|
Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle,
|
||||||
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, WidgetInfo, WidgetText, WidgetType,
|
TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||||
|
emath, epaint, pos2, remap, remap_clamp, vec2,
|
||||||
};
|
};
|
||||||
use emath::GuiRounding as _;
|
use emath::GuiRounding as _;
|
||||||
use epaint::{Shape, StrokeKind};
|
use epaint::{Shape, StrokeKind};
|
||||||
@@ -203,11 +204,16 @@ impl CollapsingState {
|
|||||||
add_body: impl FnOnce(&mut Ui) -> R,
|
add_body: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<InnerResponse<R>> {
|
) -> Option<InnerResponse<R>> {
|
||||||
let openness = self.openness(ui.ctx());
|
let openness = self.openness(ui.ctx());
|
||||||
|
|
||||||
|
let builder = UiBuilder::new()
|
||||||
|
.ui_stack_info(UiStackInfo::new(UiKind::Collapsible))
|
||||||
|
.closable();
|
||||||
|
|
||||||
if openness <= 0.0 {
|
if openness <= 0.0 {
|
||||||
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
|
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
|
||||||
None
|
None
|
||||||
} else if openness < 1.0 {
|
} 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() {
|
let max_height = if self.state.open && self.state.open_height.is_none() {
|
||||||
// First frame of expansion.
|
// First frame of expansion.
|
||||||
// We don't know full height yet, but we will next frame.
|
// 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();
|
let mut min_rect = child_ui.min_rect();
|
||||||
self.state.open_height = Some(min_rect.height());
|
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
|
self.store(child_ui.ctx()); // remember the height
|
||||||
|
|
||||||
// Pretend children took up at most `max_height` space:
|
// Pretend children took up at most `max_height` space:
|
||||||
@@ -234,7 +243,10 @@ impl CollapsingState {
|
|||||||
ret
|
ret
|
||||||
}))
|
}))
|
||||||
} else {
|
} 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();
|
let full_size = ret_response.response.rect.size();
|
||||||
self.state.open_height = Some(full_size.y);
|
self.state.open_height = Some(full_size.y);
|
||||||
self.store(ui.ctx()); // remember the height
|
self.store(ui.ctx()); // remember the height
|
||||||
|
|||||||
@@ -1,31 +1,25 @@
|
|||||||
use epaint::Shape;
|
use epaint::Shape;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter,
|
Align2, Context, Id, InnerResponse, NumExt as _, Painter, Popup, PopupCloseBehavior, Rect,
|
||||||
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
|
Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo,
|
||||||
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
WidgetText, WidgetType, epaint, style::StyleModifier, style::WidgetVisuals, vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(unused_imports)] // Documentation
|
#[expect(unused_imports)] // Documentation
|
||||||
use crate::style::Spacing;
|
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
|
/// 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.
|
/// A drop-down selection menu with a descriptive label.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # egui::__run_test_ui(|ui| {
|
/// # egui::__run_test_ui(|ui| {
|
||||||
/// # #[derive(Debug, PartialEq)]
|
/// # #[derive(Debug, PartialEq, Copy, Clone)]
|
||||||
/// # enum Enum { First, Second, Third }
|
/// # enum Enum { First, Second, Third }
|
||||||
/// # let mut selected = Enum::First;
|
/// # let mut selected = Enum::First;
|
||||||
|
/// let before = selected;
|
||||||
/// egui::ComboBox::from_label("Select one!")
|
/// egui::ComboBox::from_label("Select one!")
|
||||||
/// .selected_text(format!("{:?}", selected))
|
/// .selected_text(format!("{:?}", selected))
|
||||||
/// .show_ui(ui, |ui| {
|
/// .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");
|
/// ui.selectable_value(&mut selected, Enum::Third, "Third");
|
||||||
/// }
|
/// }
|
||||||
/// );
|
/// );
|
||||||
|
///
|
||||||
|
/// if selected != before {
|
||||||
|
/// // Handle selection change
|
||||||
|
/// }
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use = "You should call .show*"]
|
#[must_use = "You should call .show*"]
|
||||||
@@ -46,6 +44,7 @@ pub struct ComboBox {
|
|||||||
icon: Option<IconPainter>,
|
icon: Option<IconPainter>,
|
||||||
wrap_mode: Option<TextWrapMode>,
|
wrap_mode: Option<TextWrapMode>,
|
||||||
close_behavior: Option<PopupCloseBehavior>,
|
close_behavior: Option<PopupCloseBehavior>,
|
||||||
|
popup_style: StyleModifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComboBox {
|
impl ComboBox {
|
||||||
@@ -60,6 +59,7 @@ impl ComboBox {
|
|||||||
icon: None,
|
icon: None,
|
||||||
wrap_mode: None,
|
wrap_mode: None,
|
||||||
close_behavior: None,
|
close_behavior: None,
|
||||||
|
popup_style: StyleModifier::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ impl ComboBox {
|
|||||||
icon: None,
|
icon: None,
|
||||||
wrap_mode: None,
|
wrap_mode: None,
|
||||||
close_behavior: None,
|
close_behavior: None,
|
||||||
|
popup_style: StyleModifier::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +90,12 @@ impl ComboBox {
|
|||||||
icon: None,
|
icon: None,
|
||||||
wrap_mode: None,
|
wrap_mode: None,
|
||||||
close_behavior: None,
|
close_behavior: None,
|
||||||
|
popup_style: StyleModifier::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Without label.
|
/// Without label.
|
||||||
#[deprecated = "Renamed id_salt"]
|
#[deprecated = "Renamed from_id_salt"]
|
||||||
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
|
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
|
||||||
Self::from_id_salt(id_salt)
|
Self::from_id_salt(id_salt)
|
||||||
}
|
}
|
||||||
@@ -135,7 +137,6 @@ impl ComboBox {
|
|||||||
/// rect: egui::Rect,
|
/// rect: egui::Rect,
|
||||||
/// visuals: &egui::style::WidgetVisuals,
|
/// visuals: &egui::style::WidgetVisuals,
|
||||||
/// _is_open: bool,
|
/// _is_open: bool,
|
||||||
/// _above_or_below: egui::AboveOrBelow,
|
|
||||||
/// ) {
|
/// ) {
|
||||||
/// let rect = egui::Rect::from_center_size(
|
/// let rect = egui::Rect::from_center_size(
|
||||||
/// rect.center(),
|
/// rect.center(),
|
||||||
@@ -154,10 +155,8 @@ impl ComboBox {
|
|||||||
/// .show_ui(ui, |_ui| {});
|
/// .show_ui(ui, |_ui| {});
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn icon(
|
#[inline]
|
||||||
mut self,
|
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self {
|
||||||
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.icon = Some(Box::new(icon_fn));
|
self.icon = Some(Box::new(icon_fn));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -196,6 +195,16 @@ impl ComboBox {
|
|||||||
self
|
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.
|
/// Show the combo box, with the given ui code for the menu contents.
|
||||||
///
|
///
|
||||||
/// Returns `InnerResponse { inner: None }` if the combo box is closed.
|
/// Returns `InnerResponse { inner: None }` if the combo box is closed.
|
||||||
@@ -221,6 +230,7 @@ impl ComboBox {
|
|||||||
icon,
|
icon,
|
||||||
wrap_mode,
|
wrap_mode,
|
||||||
close_behavior,
|
close_behavior,
|
||||||
|
popup_style,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let button_id = ui.make_persistent_id(id_salt);
|
let button_id = ui.make_persistent_id(id_salt);
|
||||||
@@ -229,21 +239,24 @@ impl ComboBox {
|
|||||||
let mut ir = combo_box_dyn(
|
let mut ir = combo_box_dyn(
|
||||||
ui,
|
ui,
|
||||||
button_id,
|
button_id,
|
||||||
selected_text,
|
selected_text.clone(),
|
||||||
menu_contents,
|
menu_contents,
|
||||||
icon,
|
icon,
|
||||||
wrap_mode,
|
wrap_mode,
|
||||||
close_behavior,
|
close_behavior,
|
||||||
|
popup_style,
|
||||||
(width, height),
|
(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 {
|
if let Some(label) = label {
|
||||||
ir.response.widget_info(|| {
|
let label_response = ui.label(label);
|
||||||
WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text())
|
ir.response = ir.response.labelled_by(label_response.id);
|
||||||
});
|
ir.response |= label_response;
|
||||||
ir.response |= ui.label(label);
|
|
||||||
} else {
|
|
||||||
ir.response
|
|
||||||
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), ""));
|
|
||||||
}
|
}
|
||||||
ir
|
ir
|
||||||
})
|
})
|
||||||
@@ -298,7 +311,7 @@ impl ComboBox {
|
|||||||
|
|
||||||
/// Check if the [`ComboBox`] with the given id has its popup menu currently opened.
|
/// Check if the [`ComboBox`] with the given id has its popup menu currently opened.
|
||||||
pub fn is_open(ctx: &Context, id: Id) -> bool {
|
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.
|
/// 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>(
|
fn combo_box_dyn<'c, R>(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
button_id: Id,
|
button_id: Id,
|
||||||
@@ -316,27 +329,12 @@ fn combo_box_dyn<'c, R>(
|
|||||||
icon: Option<IconPainter>,
|
icon: Option<IconPainter>,
|
||||||
wrap_mode: Option<TextWrapMode>,
|
wrap_mode: Option<TextWrapMode>,
|
||||||
close_behavior: Option<PopupCloseBehavior>,
|
close_behavior: Option<PopupCloseBehavior>,
|
||||||
|
popup_style: StyleModifier,
|
||||||
(width, height): (Option<f32>, Option<f32>),
|
(width, height): (Option<f32>, Option<f32>),
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
let popup_id = ComboBox::widget_to_popup_id(button_id);
|
let popup_id = ComboBox::widget_to_popup_id(button_id);
|
||||||
|
|
||||||
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
|
let is_popup_open = Popup::is_id_open(ui.ctx(), popup_id);
|
||||||
|
|
||||||
let popup_height = ui.memory(|m| {
|
|
||||||
m.areas()
|
|
||||||
.get(popup_id)
|
|
||||||
.and_then(|state| state.size)
|
|
||||||
.map_or(100.0, |size| size.y)
|
|
||||||
});
|
|
||||||
|
|
||||||
let above_or_below =
|
|
||||||
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
|
|
||||||
< ui.ctx().screen_rect().bottom()
|
|
||||||
{
|
|
||||||
AboveOrBelow::Below
|
|
||||||
} else {
|
|
||||||
AboveOrBelow::Above
|
|
||||||
};
|
|
||||||
|
|
||||||
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
|
let 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),
|
icon_rect.expand(visuals.expansion),
|
||||||
visuals,
|
visuals,
|
||||||
is_popup_open,
|
is_popup_open,
|
||||||
above_or_below,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
paint_default_icon(
|
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
||||||
ui.painter(),
|
|
||||||
icon_rect.expand(visuals.expansion),
|
|
||||||
visuals,
|
|
||||||
above_or_below,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
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 height = height.unwrap_or_else(|| ui.spacing().combo_height);
|
||||||
|
|
||||||
let inner = crate::popup::popup_above_or_below_widget(
|
let inner = Popup::menu(&button_response)
|
||||||
ui,
|
.id(popup_id)
|
||||||
popup_id,
|
.width(button_response.rect.width())
|
||||||
&button_response,
|
.close_behavior(close_behavior)
|
||||||
above_or_below,
|
.style(popup_style)
|
||||||
close_behavior,
|
.show(|ui| {
|
||||||
|ui| {
|
ui.set_min_width(ui.available_width());
|
||||||
|
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.max_height(height)
|
.max_height(height)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
@@ -427,8 +416,8 @@ fn combo_box_dyn<'c, R>(
|
|||||||
menu_contents(ui)
|
menu_contents(ui)
|
||||||
})
|
})
|
||||||
.inner
|
.inner
|
||||||
},
|
})
|
||||||
);
|
.map(|r| r.inner);
|
||||||
|
|
||||||
InnerResponse {
|
InnerResponse {
|
||||||
inner,
|
inner,
|
||||||
@@ -484,33 +473,19 @@ fn button_frame(
|
|||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_default_icon(
|
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
|
||||||
painter: &Painter,
|
|
||||||
rect: Rect,
|
|
||||||
visuals: &WidgetVisuals,
|
|
||||||
above_or_below: AboveOrBelow,
|
|
||||||
) {
|
|
||||||
let rect = Rect::from_center_size(
|
let rect = Rect::from_center_size(
|
||||||
rect.center(),
|
rect.center(),
|
||||||
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
||||||
);
|
);
|
||||||
|
|
||||||
match above_or_below {
|
// Downward pointing triangle
|
||||||
AboveOrBelow::Above => {
|
// Previously, we would show an up arrow when we expected the popup to open upwards
|
||||||
// Upward pointing triangle
|
// (due to lack of space below the button), but this could look weird in edge cases, so this
|
||||||
painter.add(Shape::convex_polygon(
|
// feature was removed. (See https://github.com/emilk/egui/pull/5713#issuecomment-2654420245)
|
||||||
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
painter.add(Shape::convex_polygon(
|
||||||
visuals.fg_stroke.color,
|
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||||
Stroke::NONE,
|
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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
//! Frame container
|
//! Frame container
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
|
InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, epaint,
|
||||||
UiStackInfo,
|
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.
|
/// 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::__run_test_ui(|ui| {
|
||||||
/// egui::Frame::none()
|
/// egui::Frame::NONE
|
||||||
/// .fill(egui::Color32::RED)
|
/// .fill(egui::Color32::RED)
|
||||||
/// .show(ui, |ui| {
|
/// .show(ui, |ui| {
|
||||||
/// ui.label("Label with red background");
|
/// ui.label("Label with red background");
|
||||||
@@ -143,7 +143,8 @@ pub struct Frame {
|
|||||||
#[test]
|
#[test]
|
||||||
fn frame_size() {
|
fn frame_size() {
|
||||||
assert_eq!(
|
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."
|
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -337,10 +338,10 @@ impl Frame {
|
|||||||
///
|
///
|
||||||
/// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`].
|
/// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn total_margin(&self) -> Marginf {
|
pub fn total_margin(&self) -> MarginF32 {
|
||||||
Marginf::from(self.inner_margin)
|
MarginF32::from(self.inner_margin)
|
||||||
+ Marginf::from(self.stroke.width)
|
+ MarginF32::from(self.stroke.width)
|
||||||
+ Marginf::from(self.outer_margin)
|
+ MarginF32::from(self.outer_margin)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the `fill_rect` from the `content_rect`.
|
/// Calculate the `fill_rect` from the `content_rect`.
|
||||||
@@ -354,14 +355,14 @@ impl Frame {
|
|||||||
///
|
///
|
||||||
/// This is the visible and interactive rectangle.
|
/// This is the visible and interactive rectangle.
|
||||||
pub fn widget_rect(&self, content_rect: Rect) -> Rect {
|
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`.
|
/// 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`].
|
/// 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 {
|
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();
|
let content_rect = self.content_ui.min_rect();
|
||||||
content_rect
|
content_rect
|
||||||
+ self.frame.inner_margin
|
+ self.frame.inner_margin
|
||||||
+ Marginf::from(self.frame.stroke.width)
|
+ MarginF32::from(self.frame.stroke.width)
|
||||||
+ self.frame.outer_margin
|
+ 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.
|
//! For instance, a [`Frame`] adds a frame and background to some contained UI.
|
||||||
|
|
||||||
pub(crate) mod area;
|
pub(crate) mod area;
|
||||||
|
mod close_tag;
|
||||||
pub mod collapsing_header;
|
pub mod collapsing_header;
|
||||||
mod combo_box;
|
mod combo_box;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
|
pub mod menu;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
|
pub mod old_popup;
|
||||||
pub mod panel;
|
pub mod panel;
|
||||||
pub mod popup;
|
mod popup;
|
||||||
pub(crate) mod resize;
|
pub(crate) mod resize;
|
||||||
mod scene;
|
mod scene;
|
||||||
pub mod scroll_area;
|
pub mod scroll_area;
|
||||||
mod sides;
|
mod sides;
|
||||||
|
mod tooltip;
|
||||||
pub(crate) mod window;
|
pub(crate) mod window;
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
area::{Area, AreaState},
|
area::{Area, AreaState},
|
||||||
|
close_tag::ClosableTag,
|
||||||
collapsing_header::{CollapsingHeader, CollapsingResponse},
|
collapsing_header::{CollapsingHeader, CollapsingResponse},
|
||||||
combo_box::*,
|
combo_box::*,
|
||||||
frame::Frame,
|
frame::Frame,
|
||||||
modal::{Modal, ModalResponse},
|
modal::{Modal, ModalResponse},
|
||||||
panel::{CentralPanel, Panel, PanelSide, VerticalSide, HorizontalSide},
|
old_popup::*,
|
||||||
|
panel::{CentralPanel, HorizontalSide, Panel, PanelSide, VerticalSide},
|
||||||
popup::*,
|
popup::*,
|
||||||
resize::Resize,
|
resize::Resize,
|
||||||
scene::Scene,
|
scene::{DragPanButtons, Scene},
|
||||||
scroll_area::ScrollArea,
|
scroll_area::ScrollArea,
|
||||||
sides::Sides,
|
sides::Sides,
|
||||||
|
tooltip::*,
|
||||||
window::Window,
|
window::Window,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
use emath::{Align2, Vec2};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
|
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
|
||||||
};
|
};
|
||||||
use emath::{Align2, Vec2};
|
|
||||||
|
|
||||||
/// A modal dialog.
|
/// A modal dialog.
|
||||||
|
///
|
||||||
/// Similar to a [`crate::Window`] but centered and with a backdrop that
|
/// Similar to a [`crate::Window`] but centered and with a backdrop that
|
||||||
/// blocks input to the rest of the UI.
|
/// blocks input to the rest of the UI.
|
||||||
///
|
///
|
||||||
/// You can show multiple modals on top of each other. The topmost modal will always be
|
/// You can show multiple modals on top of each other. The topmost modal will always be
|
||||||
/// the most recently shown one.
|
/// 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 struct Modal {
|
||||||
pub area: Area,
|
pub area: Area,
|
||||||
pub backdrop_color: Color32,
|
pub backdrop_color: Color32,
|
||||||
@@ -16,7 +20,9 @@ pub struct Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn new(id: Id) -> Self {
|
||||||
Self {
|
Self {
|
||||||
area: Self::default_area(id),
|
area: Self::default_area(id),
|
||||||
@@ -26,6 +32,7 @@ impl Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an area customized for a modal.
|
/// Returns an area customized for a modal.
|
||||||
|
///
|
||||||
/// Makes these changes to the default area:
|
/// Makes these changes to the default area:
|
||||||
/// - sense: hover
|
/// - sense: hover
|
||||||
/// - anchor: center
|
/// - anchor: center
|
||||||
@@ -74,18 +81,16 @@ impl Modal {
|
|||||||
frame,
|
frame,
|
||||||
} = self;
|
} = 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.set_modal_layer(area.layer());
|
||||||
(
|
mem.top_modal_layer() == Some(area.layer())
|
||||||
mem.top_modal_layer() == Some(area.layer()),
|
|
||||||
mem.any_popup_open(),
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
let any_popup_open = crate::Popup::is_any_open(ctx);
|
||||||
let InnerResponse {
|
let InnerResponse {
|
||||||
inner: (inner, backdrop_response),
|
inner: (inner, backdrop_response),
|
||||||
response,
|
response,
|
||||||
} = area.show(ctx, |ui| {
|
} = 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 bg_sense = Sense::CLICK | Sense::DRAG;
|
||||||
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
|
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
|
||||||
backdrop.set_min_size(bg_rect.size());
|
backdrop.set_min_size(bg_rect.size());
|
||||||
@@ -150,7 +155,10 @@ impl<T> ModalResponse<T> {
|
|||||||
let escape_clicked =
|
let escape_clicked =
|
||||||
|| ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
|
|| 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()
|
self.backdrop_response.clicked()
|
||||||
|
|| ui_close_called
|
||||||
|| (self.is_top_modal && !self.any_popup_open && escape_clicked())
|
|| (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::{
|
use crate::{
|
||||||
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt, Rect, Response, Sense, Shape, Ui,
|
Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense, Shape, Ui,
|
||||||
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
|
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -220,7 +220,7 @@ impl Resize {
|
|||||||
.at_least(self.min_size)
|
.at_least(self.min_size)
|
||||||
.at_most(self.max_size)
|
.at_most(self.max_size)
|
||||||
.at_most(
|
.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();
|
.round_ui();
|
||||||
|
|
||||||
@@ -241,14 +241,12 @@ impl Resize {
|
|||||||
|
|
||||||
let corner_id = self.resizable.any().then(|| id.with("__resize_corner"));
|
let corner_id = self.resizable.any().then(|| id.with("__resize_corner"));
|
||||||
|
|
||||||
if let Some(corner_id) = corner_id {
|
if let Some(corner_id) = corner_id
|
||||||
if let Some(corner_response) = ui.ctx().read_response(corner_id) {
|
&& let Some(corner_response) = ui.ctx().read_response(corner_id)
|
||||||
if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
|
&& let Some(pointer_pos) = corner_response.interact_pointer_pos()
|
||||||
// Respond to the interaction early to avoid frame delay.
|
{
|
||||||
user_requested_size =
|
// Respond to the interaction early to avoid frame delay.
|
||||||
Some(pointer_pos - position + 0.5 * corner_response.rect.size());
|
user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(user_requested_size) = user_requested_size {
|
if let Some(user_requested_size) = user_requested_size {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use core::f32;
|
use core::f32;
|
||||||
|
|
||||||
use emath::{GuiRounding, Pos2};
|
use emath::{GuiRounding as _, Pos2};
|
||||||
|
|
||||||
use crate::{
|
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.
|
/// 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()"]
|
#[must_use = "You should call .show()"]
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
zoom_range: Rangef,
|
zoom_range: Rangef,
|
||||||
|
sense: Sense,
|
||||||
max_inner_size: Vec2,
|
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 {
|
impl Default for Scene {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
zoom_range: Rangef::new(f32::EPSILON, 1.0),
|
zoom_range: Rangef::new(f32::EPSILON, 1.0),
|
||||||
|
sense: Sense::click_and_drag(),
|
||||||
max_inner_size: Vec2::splat(1000.0),
|
max_inner_size: Vec2::splat(1000.0),
|
||||||
|
drag_pan_buttons: DragPanButtons::all(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,6 +90,17 @@ impl Scene {
|
|||||||
Default::default()
|
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.
|
/// Set the allowed zoom range.
|
||||||
///
|
///
|
||||||
/// The default zoom range is `0.0..=1.0`,
|
/// The default zoom range is `0.0..=1.0`,
|
||||||
@@ -82,6 +121,15 @@ impl Scene {
|
|||||||
self
|
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` contains the view bounds of the inner [`Ui`].
|
||||||
///
|
///
|
||||||
/// `scene_rect` will be mutated by any panning/zooming done by the user.
|
/// `scene_rect` will be mutated by any panning/zooming done by the user.
|
||||||
@@ -118,8 +166,10 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !scene_rect_was_good {
|
if !scene_rect_was_good {
|
||||||
// Auto-reset if the trsnsformation goes bad somehow (or started bad).
|
// Auto-reset if the transformation goes bad somehow (or started bad).
|
||||||
*scene_rect = inner_rect;
|
// 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
|
ret
|
||||||
@@ -147,7 +197,7 @@ impl Scene {
|
|||||||
UiBuilder::new()
|
UiBuilder::new()
|
||||||
.layer_id(scene_layer_id)
|
.layer_id(scene_layer_id)
|
||||||
.max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size))
|
.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();
|
let mut pan_response = local_ui.response();
|
||||||
@@ -158,17 +208,17 @@ impl Scene {
|
|||||||
// Set a correct global clip rect:
|
// Set a correct global clip rect:
|
||||||
local_ui.set_clip_rect(to_global.inverse() * outer_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:
|
// Add the actual contents to the area:
|
||||||
let ret = add_contents(&mut local_ui);
|
let ret = add_contents(&mut local_ui);
|
||||||
|
|
||||||
// This ensures we catch clicks/drags/pans anywhere on the background.
|
// This ensures we catch clicks/drags/pans anywhere on the background.
|
||||||
local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui());
|
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 {
|
InnerResponse {
|
||||||
response: pan_response,
|
response: pan_response,
|
||||||
inner: ret,
|
inner: ret,
|
||||||
@@ -177,43 +227,51 @@ impl Scene {
|
|||||||
|
|
||||||
/// Helper function to handle pan and zoom interactions on a response.
|
/// 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) {
|
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();
|
to_global.translation += to_global.scaling * resp.drag_delta();
|
||||||
resp.mark_changed();
|
resp.mark_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos()) {
|
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos())
|
||||||
if resp.contains_pointer() {
|
&& resp.contains_pointer()
|
||||||
let pointer_in_scene = to_global.inverse() * mouse_pos;
|
{
|
||||||
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
|
let pointer_in_scene = to_global.inverse() * mouse_pos;
|
||||||
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
|
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
|
// 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.
|
// avoid `ui_from_scene` to change slightly due to floating point errors.
|
||||||
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
|
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
|
||||||
return;
|
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#![allow(clippy::needless_range_loop)]
|
||||||
|
|
||||||
|
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
||||||
|
|
||||||
|
use emath::GuiRounding as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2,
|
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
|
||||||
Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
|
UiKind, UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[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`].
|
/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
|
||||||
///
|
///
|
||||||
/// By default, scroll bars only show up when needed, i.e. when the contents
|
/// 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()"]
|
#[must_use = "You should call .show()"]
|
||||||
pub struct ScrollArea {
|
pub struct ScrollArea {
|
||||||
/// Do we have horizontal/vertical scrolling enabled?
|
/// Do we have horizontal/vertical scrolling enabled?
|
||||||
scroll_enabled: Vec2b,
|
direction_enabled: Vec2b,
|
||||||
|
|
||||||
auto_shrink: Vec2b,
|
auto_shrink: Vec2b,
|
||||||
max_size: Vec2,
|
max_size: Vec2,
|
||||||
@@ -178,10 +289,10 @@ pub struct ScrollArea {
|
|||||||
id_salt: Option<Id>,
|
id_salt: Option<Id>,
|
||||||
offset_x: Option<f32>,
|
offset_x: Option<f32>,
|
||||||
offset_y: Option<f32>,
|
offset_y: Option<f32>,
|
||||||
|
on_hover_cursor: Option<CursorIcon>,
|
||||||
/// If false, we ignore scroll events.
|
on_drag_cursor: Option<CursorIcon>,
|
||||||
scrolling_enabled: bool,
|
scroll_source: ScrollSource,
|
||||||
drag_to_scroll: bool,
|
wheel_scroll_multiplier: Vec2,
|
||||||
|
|
||||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||||
/// end position until user manually changes position. It will become true
|
/// 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.
|
/// Create a scroll area where you decide which axis has scrolling enabled.
|
||||||
/// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling.
|
/// 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 {
|
Self {
|
||||||
scroll_enabled: scroll_enabled.into(),
|
direction_enabled: direction_enabled.into(),
|
||||||
auto_shrink: Vec2b::TRUE,
|
auto_shrink: Vec2b::TRUE,
|
||||||
max_size: Vec2::INFINITY,
|
max_size: Vec2::INFINITY,
|
||||||
min_scrolled_size: Vec2::splat(64.0),
|
min_scrolled_size: Vec2::splat(64.0),
|
||||||
@@ -231,8 +342,10 @@ impl ScrollArea {
|
|||||||
id_salt: None,
|
id_salt: None,
|
||||||
offset_x: None,
|
offset_x: None,
|
||||||
offset_y: None,
|
offset_y: None,
|
||||||
scrolling_enabled: true,
|
on_hover_cursor: None,
|
||||||
drag_to_scroll: true,
|
on_drag_cursor: None,
|
||||||
|
scroll_source: ScrollSource::default(),
|
||||||
|
wheel_scroll_multiplier: Vec2::splat(1.0),
|
||||||
stick_to_end: Vec2b::FALSE,
|
stick_to_end: Vec2b::FALSE,
|
||||||
animated: true,
|
animated: true,
|
||||||
}
|
}
|
||||||
@@ -355,17 +468,41 @@ impl ScrollArea {
|
|||||||
self
|
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.
|
/// Turn on/off scrolling on the horizontal axis.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hscroll(mut self, hscroll: bool) -> Self {
|
pub fn hscroll(mut self, hscroll: bool) -> Self {
|
||||||
self.scroll_enabled[0] = hscroll;
|
self.direction_enabled[0] = hscroll;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turn on/off scrolling on the vertical axis.
|
/// Turn on/off scrolling on the vertical axis.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn vscroll(mut self, vscroll: bool) -> Self {
|
pub fn vscroll(mut self, vscroll: bool) -> Self {
|
||||||
self.scroll_enabled[1] = vscroll;
|
self.direction_enabled[1] = vscroll;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,16 +510,8 @@ impl ScrollArea {
|
|||||||
///
|
///
|
||||||
/// You can pass in `false`, `true`, `[false, true]` etc.
|
/// You can pass in `false`, `true`, `[false, true]` etc.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn scroll(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
|
pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
|
||||||
self.scroll_enabled = scroll_enabled.into();
|
self.direction_enabled = direction_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();
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,9 +524,14 @@ impl ScrollArea {
|
|||||||
/// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
|
/// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
|
||||||
///
|
///
|
||||||
/// This controls both scrolling directions.
|
/// This controls both scrolling directions.
|
||||||
|
#[deprecated = "Use `ScrollArea::scroll_source()"]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn enable_scrolling(mut self, enable: bool) -> Self {
|
pub fn enable_scrolling(mut self, enable: bool) -> Self {
|
||||||
self.scrolling_enabled = enable;
|
self.scroll_source = if enable {
|
||||||
|
ScrollSource::ALL
|
||||||
|
} else {
|
||||||
|
ScrollSource::NONE
|
||||||
|
};
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,9 +542,28 @@ impl ScrollArea {
|
|||||||
/// If `true`, the [`ScrollArea`] will sense drags.
|
/// If `true`, the [`ScrollArea`] will sense drags.
|
||||||
///
|
///
|
||||||
/// Default: `true`.
|
/// Default: `true`.
|
||||||
|
#[deprecated = "Use `ScrollArea::scroll_source()"]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +590,7 @@ impl ScrollArea {
|
|||||||
|
|
||||||
/// Is any scrolling enabled?
|
/// Is any scrolling enabled?
|
||||||
pub(crate) fn is_any_scroll_enabled(&self) -> bool {
|
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
|
/// The scroll handle will stick to the rightmost position even while the content size
|
||||||
@@ -472,7 +625,7 @@ struct Prepared {
|
|||||||
auto_shrink: Vec2b,
|
auto_shrink: Vec2b,
|
||||||
|
|
||||||
/// Does this `ScrollArea` have horizontal/vertical scrolling enabled?
|
/// 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.
|
/// Smoothly interpolated boolean of whether or not to show the scroll bars.
|
||||||
show_bars_factor: Vec2,
|
show_bars_factor: Vec2,
|
||||||
@@ -500,20 +653,24 @@ struct Prepared {
|
|||||||
/// `viewport.min == ZERO` means we scrolled to the top.
|
/// `viewport.min == ZERO` means we scrolled to the top.
|
||||||
viewport: Rect,
|
viewport: Rect,
|
||||||
|
|
||||||
scrolling_enabled: bool,
|
scroll_source: ScrollSource,
|
||||||
|
wheel_scroll_multiplier: Vec2,
|
||||||
stick_to_end: Vec2b,
|
stick_to_end: Vec2b,
|
||||||
|
|
||||||
/// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
|
/// 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.
|
/// 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],
|
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
|
||||||
|
|
||||||
|
/// The response from dragging the background (if enabled)
|
||||||
|
background_drag_response: Option<Response>,
|
||||||
|
|
||||||
animated: bool,
|
animated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollArea {
|
impl ScrollArea {
|
||||||
fn begin(self, ui: &mut Ui) -> Prepared {
|
fn begin(self, ui: &mut Ui) -> Prepared {
|
||||||
let Self {
|
let Self {
|
||||||
scroll_enabled,
|
direction_enabled,
|
||||||
auto_shrink,
|
auto_shrink,
|
||||||
max_size,
|
max_size,
|
||||||
min_scrolled_size,
|
min_scrolled_size,
|
||||||
@@ -522,14 +679,15 @@ impl ScrollArea {
|
|||||||
id_salt,
|
id_salt,
|
||||||
offset_x,
|
offset_x,
|
||||||
offset_y,
|
offset_y,
|
||||||
scrolling_enabled,
|
on_hover_cursor,
|
||||||
drag_to_scroll,
|
on_drag_cursor,
|
||||||
|
scroll_source,
|
||||||
|
wheel_scroll_multiplier,
|
||||||
stick_to_end,
|
stick_to_end,
|
||||||
animated,
|
animated,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let ctx = ui.ctx().clone();
|
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_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
|
||||||
let id = ui.make_persistent_id(id_salt);
|
let id = ui.make_persistent_id(id_salt);
|
||||||
@@ -546,7 +704,7 @@ impl ScrollArea {
|
|||||||
let show_bars: Vec2b = match scroll_bar_visibility {
|
let show_bars: Vec2b = match scroll_bar_visibility {
|
||||||
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
|
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
|
||||||
ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
|
ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
|
||||||
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
|
ScrollBarVisibility::AlwaysVisible => direction_enabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_bars_factor = Vec2::new(
|
let show_bars_factor = Vec2::new(
|
||||||
@@ -568,7 +726,7 @@ impl ScrollArea {
|
|||||||
// one shouldn't collapse into nothingness.
|
// one shouldn't collapse into nothingness.
|
||||||
// See https://github.com/emilk/egui/issues/1097
|
// See https://github.com/emilk/egui/issues/1097
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if scroll_enabled[d] {
|
if direction_enabled[d] {
|
||||||
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
|
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -585,13 +743,19 @@ impl ScrollArea {
|
|||||||
} else {
|
} else {
|
||||||
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
|
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if scroll_enabled[d] {
|
if direction_enabled[d] {
|
||||||
content_max_size[d] = f32::INFINITY;
|
content_max_size[d] = f32::INFINITY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
|
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(
|
let mut content_ui = ui.new_child(
|
||||||
UiBuilder::new()
|
UiBuilder::new()
|
||||||
.ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
|
.ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
|
||||||
@@ -603,7 +767,7 @@ impl ScrollArea {
|
|||||||
let clip_rect_margin = ui.visuals().clip_rect_margin;
|
let clip_rect_margin = ui.visuals().clip_rect_margin;
|
||||||
let mut content_clip_rect = ui.clip_rect();
|
let mut content_clip_rect = ui.clip_rect();
|
||||||
for d in 0..2 {
|
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.min[d] = inner_rect.min[d] - clip_rect_margin;
|
||||||
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
||||||
} else {
|
} else {
|
||||||
@@ -619,56 +783,72 @@ impl ScrollArea {
|
|||||||
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||||
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
|
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
|
||||||
|
|
||||||
if (scrolling_enabled && drag_to_scroll)
|
let background_drag_response =
|
||||||
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
|
if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
|
||||||
{
|
// Drag contents to scroll (for touch screens mostly).
|
||||||
// Drag contents to scroll (for touch screens mostly).
|
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
// or we will steal input from the widgets we contain.
|
||||||
// or we will steal input from the widgets we contain.
|
let content_response_option = state
|
||||||
let content_response_option = state
|
.interact_rect
|
||||||
.interact_rect
|
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
|
||||||
.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
|
if content_response_option
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|response| response.drag_stopped())
|
.is_some_and(|response| response.dragged())
|
||||||
{
|
{
|
||||||
state.vel =
|
for d in 0..2 {
|
||||||
scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
|
if direction_enabled[d] {
|
||||||
}
|
ui.input(|input| {
|
||||||
for d in 0..2 {
|
state.offset[d] -= input.pointer.delta()[d];
|
||||||
// Kinetic scrolling
|
});
|
||||||
let stop_speed = 20.0; // Pixels per second.
|
state.scroll_stuck_to_end[d] = false;
|
||||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
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;
|
let friction = friction_coeff * dt;
|
||||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
||||||
state.vel[d] = 0.0;
|
state.vel[d] = 0.0;
|
||||||
} else {
|
} else {
|
||||||
state.vel[d] -= friction * state.vel[d].signum();
|
state.vel[d] -= friction * state.vel[d].signum();
|
||||||
// Offset has an inverted coordinate system compared to
|
// Offset has an inverted coordinate system compared to
|
||||||
// the velocity, so we subtract it instead of adding it
|
// the velocity, so we subtract it instead of adding it
|
||||||
state.offset[d] -= state.vel[d] * dt;
|
state.offset[d] -= state.vel[d] * dt;
|
||||||
ctx.request_repaint();
|
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
|
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
|
||||||
// above).
|
// above).
|
||||||
@@ -709,7 +889,7 @@ impl ScrollArea {
|
|||||||
id,
|
id,
|
||||||
state,
|
state,
|
||||||
auto_shrink,
|
auto_shrink,
|
||||||
scroll_enabled,
|
direction_enabled,
|
||||||
show_bars_factor,
|
show_bars_factor,
|
||||||
current_bar_use,
|
current_bar_use,
|
||||||
scroll_bar_visibility,
|
scroll_bar_visibility,
|
||||||
@@ -717,9 +897,11 @@ impl ScrollArea {
|
|||||||
inner_rect,
|
inner_rect,
|
||||||
content_ui,
|
content_ui,
|
||||||
viewport,
|
viewport,
|
||||||
scrolling_enabled,
|
scroll_source,
|
||||||
|
wheel_scroll_multiplier,
|
||||||
stick_to_end,
|
stick_to_end,
|
||||||
saved_scroll_target,
|
saved_scroll_target,
|
||||||
|
background_drag_response,
|
||||||
animated,
|
animated,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -776,7 +958,7 @@ impl ScrollArea {
|
|||||||
|
|
||||||
let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
|
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.
|
viewport_ui.skip_ahead_auto_ids(min_row); // Make sure we get consistent IDs.
|
||||||
add_contents(viewport_ui, min_row..max_row)
|
add_contents(viewport_ui, min_row..max_row)
|
||||||
})
|
})
|
||||||
@@ -824,16 +1006,18 @@ impl Prepared {
|
|||||||
mut state,
|
mut state,
|
||||||
inner_rect,
|
inner_rect,
|
||||||
auto_shrink,
|
auto_shrink,
|
||||||
scroll_enabled,
|
direction_enabled,
|
||||||
mut show_bars_factor,
|
mut show_bars_factor,
|
||||||
current_bar_use,
|
current_bar_use,
|
||||||
scroll_bar_visibility,
|
scroll_bar_visibility,
|
||||||
scroll_bar_rect,
|
scroll_bar_rect,
|
||||||
content_ui,
|
content_ui,
|
||||||
viewport: _,
|
viewport: _,
|
||||||
scrolling_enabled,
|
scroll_source,
|
||||||
|
wheel_scroll_multiplier,
|
||||||
stick_to_end,
|
stick_to_end,
|
||||||
saved_scroll_target,
|
saved_scroll_target,
|
||||||
|
background_drag_response,
|
||||||
animated,
|
animated,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
@@ -854,7 +1038,7 @@ impl Prepared {
|
|||||||
.ctx()
|
.ctx()
|
||||||
.pass_state_mut(|state| state.scroll_target[d].take());
|
.pass_state_mut(|state| state.scroll_target[d].take());
|
||||||
|
|
||||||
if scroll_enabled[d] {
|
if direction_enabled[d] {
|
||||||
if let Some(target) = scroll_target {
|
if let Some(target) = scroll_target {
|
||||||
let pass_state::ScrollTarget {
|
let pass_state::ScrollTarget {
|
||||||
range,
|
range,
|
||||||
@@ -890,7 +1074,7 @@ impl Prepared {
|
|||||||
|
|
||||||
delta += delta_update;
|
delta += delta_update;
|
||||||
animation = animation_update;
|
animation = animation_update;
|
||||||
};
|
}
|
||||||
|
|
||||||
if delta != 0.0 {
|
if delta != 0.0 {
|
||||||
let target_offset = state.offset[d] + delta;
|
let target_offset = state.offset[d] + delta;
|
||||||
@@ -921,7 +1105,7 @@ impl Prepared {
|
|||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if saved_scroll_target[d].is_some() {
|
if saved_scroll_target[d].is_some() {
|
||||||
state.scroll_target[d] = saved_scroll_target[d].clone();
|
state.scroll_target[d] = saved_scroll_target[d].clone();
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -930,7 +1114,7 @@ impl Prepared {
|
|||||||
let mut inner_size = inner_rect.size();
|
let mut inner_size = inner_rect.size();
|
||||||
|
|
||||||
for d in 0..2 {
|
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, 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
|
(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).
|
(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 outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
|
||||||
|
|
||||||
let content_is_too_large = Vec2b::new(
|
let content_is_too_large = Vec2b::new(
|
||||||
scroll_enabled[0] && inner_rect.width() < content_size.x,
|
direction_enabled[0] && inner_rect.width() < content_size.x,
|
||||||
scroll_enabled[1] && inner_rect.height() < content_size.y,
|
direction_enabled[1] && inner_rect.height() < content_size.y,
|
||||||
);
|
);
|
||||||
|
|
||||||
let max_offset = content_size - inner_rect.size();
|
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
|
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 {
|
for d in 0..2 {
|
||||||
if scroll_enabled[d] {
|
if direction_enabled[d] {
|
||||||
let scroll_delta = ui.ctx().input_mut(|input| {
|
let scroll_delta = ui.ctx().input(|input| {
|
||||||
if always_scroll_enabled_direction {
|
if always_scroll_enabled_direction {
|
||||||
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
|
// 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 {
|
} 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_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
|
||||||
let scrolling_down = state.offset[d] < max_offset[d] && 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:
|
// Clear scroll delta so no parent scroll will use it:
|
||||||
ui.ctx().input_mut(|input| {
|
ui.ctx().input_mut(|input| {
|
||||||
if always_scroll_enabled_direction {
|
if always_scroll_enabled_direction {
|
||||||
input.smooth_scroll_delta[0] = 0.0;
|
input.smooth_scroll_delta()[0] = 0.0;
|
||||||
input.smooth_scroll_delta[1] = 0.0;
|
input.smooth_scroll_delta()[1] = 0.0;
|
||||||
} else {
|
} 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 {
|
let show_scroll_this_frame = match scroll_bar_visibility {
|
||||||
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
|
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
|
||||||
ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
|
ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
|
||||||
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
|
ScrollBarVisibility::AlwaysVisible => direction_enabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Avoid frame delay; start showing scroll bar right away:
|
// Avoid frame delay; start showing scroll bar right away:
|
||||||
@@ -1034,6 +1228,7 @@ impl Prepared {
|
|||||||
|
|
||||||
let is_hovering_bar_area = is_hovering_outer_rect
|
let is_hovering_bar_area = is_hovering_outer_rect
|
||||||
&& ui.rect_contains_pointer(max_bar_rect)
|
&& ui.rect_contains_pointer(max_bar_rect)
|
||||||
|
&& !is_dragging_background
|
||||||
|| state.scroll_bar_interaction[d];
|
|| state.scroll_bar_interaction[d];
|
||||||
|
|
||||||
let is_hovering_bar_area_t = ui
|
let is_hovering_bar_area_t = ui
|
||||||
@@ -1090,23 +1285,37 @@ impl Prepared {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle_rect = if d == 0 {
|
let calculate_handle_rect = |d, offset: &Vec2| {
|
||||||
Rect::from_min_max(
|
let handle_size = if d == 0 {
|
||||||
pos2(from_content(state.offset.x), cross.min),
|
from_content(offset.x + inner_rect.width()) - from_content(offset.x)
|
||||||
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
|
} else {
|
||||||
)
|
from_content(offset.y + inner_rect.height()) - from_content(offset.y)
|
||||||
} else {
|
}
|
||||||
Rect::from_min_max(
|
.max(scroll_style.handle_min_length);
|
||||||
pos2(cross.min, from_content(state.offset.y)),
|
|
||||||
pos2(
|
let handle_start_point = remap_clamp(
|
||||||
cross.max,
|
offset[d],
|
||||||
from_content(state.offset.y + inner_rect.height()),
|
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 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()
|
Sense::click_and_drag()
|
||||||
} else {
|
} else {
|
||||||
Sense::hover()
|
Sense::hover()
|
||||||
@@ -1131,11 +1340,13 @@ impl Prepared {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
|
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
|
||||||
state.offset[d] = remap(
|
let handle_travel =
|
||||||
new_handle_top,
|
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
|
||||||
scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
|
state.offset[d] = if handle_travel.start() == handle_travel.end() {
|
||||||
0.0..=content_size[d],
|
0.0
|
||||||
);
|
} else {
|
||||||
|
remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
|
||||||
|
};
|
||||||
|
|
||||||
// some manual action taken, scroll not stuck
|
// some manual action taken, scroll not stuck
|
||||||
state.scroll_stuck_to_end[d] = false;
|
state.scroll_stuck_to_end[d] = false;
|
||||||
@@ -1154,33 +1365,9 @@ impl Prepared {
|
|||||||
|
|
||||||
if ui.is_rect_visible(outer_scroll_bar_rect) {
|
if ui.is_rect_visible(outer_scroll_bar_rect) {
|
||||||
// Avoid frame-delay by calculating a new handle rect:
|
// Avoid frame-delay by calculating a new handle rect:
|
||||||
let mut handle_rect = if d == 0 {
|
let handle_rect = calculate_handle_rect(d, &state.offset);
|
||||||
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 visuals = if scrolling_enabled {
|
let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
|
||||||
// Pick visuals based on interaction with the handle.
|
// Pick visuals based on interaction with the handle.
|
||||||
// Remember that the response is for the whole scroll bar!
|
// Remember that the response is for the whole scroll bar!
|
||||||
let is_hovering_handle = response.hovered()
|
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