1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Merge branch 'main' into common-panels

This commit is contained in:
Emil Ernerfeldt
2025-11-16 11:31:23 +01:00
575 changed files with 22977 additions and 10608 deletions

1
.gitattributes vendored
View File

@@ -1,6 +1,7 @@
* text=auto eol=lf
Cargo.lock linguist-generated=false
*.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
# Exclude some small files from LFS:
crates/eframe/data/* !filter !diff !merge text=auto eol=lf

View File

@@ -10,13 +10,13 @@ assignees: ''
<!--
First look if there is already a similar bug report. If there is, upvote the issue with 👍
Please also check if the bug is still present in latest master! Do so by adding the following lines to your Cargo.toml:
Please also check if the bug is still present in latest main! Do so by adding the following lines to your Cargo.toml:
[patch.crates-io]
egui = { git = "https://github.com/emilk/egui", branch = "master" }
egui = { git = "https://github.com/emilk/egui", branch = "main" }
# if you're using eframe:
eframe = { git = "https://github.com/emilk/egui", branch = "master" }
eframe = { git = "https://github.com/emilk/egui", branch = "main" }
-->
**Describe the bug**

View File

@@ -1,5 +1,5 @@
<!--
Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) before opening a Pull Request!
Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request!
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!

View File

@@ -5,8 +5,15 @@ on: [push, pull_request]
jobs:
cargo-machete:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.88
- name: Machete install
## The official cargo-machete action
uses: bnjbvr/cargo-machete@v0.9.1
- name: Checkout
uses: actions/checkout@v3
- name: Machete
run: cargo install cargo-machete --locked && cargo machete
uses: actions/checkout@v4
- name: Machete Check
run: cargo machete

View File

@@ -1,9 +1,9 @@
name: Deploy web demo
on:
# We only run this on merges to master
# We only run this on merges to main
push:
branches: ["master"]
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
@@ -11,7 +11,6 @@ on:
# release:
# types: ["published"]
permissions:
contents: write # for committing to gh-pages branch
@@ -29,8 +28,8 @@ jobs:
# Single deploy job since we're just deploying
deploy:
name: Deploy web demo
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -39,16 +38,19 @@ jobs:
with:
profile: minimal
target: wasm32-unknown-unknown
toolchain: 1.81.0
toolchain: 1.88.0
override: true
- uses: Swatinem/rust-cache@v2
with:
prefix-key: "web-demo-"
- name: "Install wasmopt / binaryen"
run: |
sudo apt-get update && sudo apt-get install binaryen
- name: Install wasm-opt
uses: sigoden/install-binary@v1
with:
repo: WebAssembly/binaryen
tag: version_123
name: wasm-opt
- run: |
scripts/build_demo_web.sh --release

View 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.'
})

View File

@@ -16,6 +16,7 @@ on:
jobs:
label:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check for a "do-not-merge" label
uses: mheap/github-action-required-labels@v3
@@ -29,4 +30,4 @@ jobs:
with:
mode: minimum
count: 1
labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_kittest, egui-wgpu, egui-winit, egui, epaint, epaint_default_fonts, exclude from changelog, typo"
labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_kittest, egui-wgpu, egui-winit, egui, emath, epaint, epaint_default_fonts, exclude from changelog, typo"

View File

@@ -5,6 +5,7 @@ on: [push, pull_request]
jobs:
check-binary-files:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
@@ -25,7 +26,7 @@ jobs:
exclude_pattern=$(printf "|^%s" "${exclude_paths[@]}" | sed 's/^|//')
if comm -23 <(git ls-files | grep -Ev "$exclude_pattern" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then
echo "Error: Found binary file with extension .$ext not tracked by git LFS. See CONTRIBUTING.md"
echo "Error: Found binary file with extension .$ext not tracked by git LFS. See https://github.com/emilk/egui/blob/main/CONTRIBUTING.md#working-with-git-lfs"
exit 1
fi
done

View File

@@ -15,16 +15,23 @@ on:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v2
- run: rustup toolchain install stable --profile minimal --target wasm32-unknown-unknown
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.88.0
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
prefix-key: "pr-preview-"
- name: "Install wasmopt / binaryen"
run: |
sudo apt-get update && sudo apt-get install binaryen
- name: Install wasm-opt
uses: sigoden/install-binary@v1
with:
repo: WebAssembly/binaryen
tag: version_123
name: wasm-opt
- run: |
scripts/build_demo_web.sh --release

View File

@@ -11,6 +11,7 @@ on:
jobs:
cleanup:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4

23
.github/workflows/preview_comment.yml vendored Normal file
View 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'

View File

@@ -20,6 +20,7 @@ concurrency:
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -60,5 +61,7 @@ jobs:
message: |
Preview available at https://egui-pr-preview.github.io/pr/${{ env.URL_SLUG }}
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.
View snapshot changes at [kitdiff](https://rerun-io.github.io/kitdiff/?url=https://github.com/emilk/egui/pull/${{ env.PR_NUMBER }})
pr_number: ${{ env.PR_NUMBER }}
comment_tag: 'egui-preview'

View File

@@ -5,20 +5,21 @@ name: Rust
env:
RUSTFLAGS: -D warnings
RUSTDOCFLAGS: -D warnings
NIGHTLY_VERSION: nightly-2024-09-11
NIGHTLY_VERSION: nightly-2025-09-16
jobs:
fmt-crank-check-test:
name: Format + check
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
lfs: true
- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.81.0
toolchain: 1.88.0
- name: Install packages (Linux)
if: runner.os == 'Linux'
@@ -37,39 +38,28 @@ jobs:
- name: Lint vertical spacing
run: ./scripts/lint.py
- name: check --all-features
run: cargo check --locked --all-features --all-targets
- run: cargo clippy --locked --all-features --all-targets
- name: check egui_extras --all-features
run: cargo check --locked --all-features -p egui_extras
- run: cargo clippy --locked --all-features -p egui_extras
- name: check default features
run: cargo check --locked --all-targets
- run: cargo clippy --locked --all-targets
- name: check --no-default-features
run: cargo check --locked --no-default-features --lib --all-targets
- run: cargo clippy --locked --no-default-features --lib --all-targets
- name: check eframe --no-default-features
run: cargo check --locked --no-default-features --features x11 --lib -p eframe
- run: cargo clippy --locked --no-default-features --lib -p eframe --features x11
- name: check egui_extras --no-default-features
run: cargo check --locked --no-default-features --lib -p egui_extras
- run: cargo clippy --locked --no-default-features --lib -p eframe --features x11,wgpu_no_default_features
- name: check epaint --no-default-features
run: cargo check --locked --no-default-features --lib -p epaint
- run: cargo clippy --locked --no-default-features --lib -p egui_extras
- run: cargo clippy --locked --no-default-features --lib -p epaint
# Regression test for https://github.com/emilk/egui/issues/4771
- name: cargo check -p test_egui_extras_compilation
run: cargo check -p test_egui_extras_compilation
- run: cargo clippy -p test_egui_extras_compilation
- name: cargo doc --lib
run: cargo doc --lib --no-deps --all-features
- run: cargo doc --lib --no-deps --all-features
- name: cargo doc --document-private-items
run: cargo doc --document-private-items --no-deps --all-features
- name: clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo doc --document-private-items --no-deps --all-features
- name: clippy release
run: cargo clippy --all-targets --all-features --release -- -D warnings
@@ -79,11 +69,12 @@ jobs:
check_wasm:
name: Check wasm32 + wasm-bindgen
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.81.0
toolchain: 1.88.0
targets: wasm32-unknown-unknown
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
@@ -91,19 +82,19 @@ jobs:
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- name: Check wasm32 egui_demo_app
run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown
- name: clippy wasm32 egui_demo_app
run: cargo clippy -p egui_demo_app --lib --target wasm32-unknown-unknown
- name: Check wasm32 egui_demo_app --all-features
run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
- name: clippy wasm32 egui_demo_app --all-features
run: cargo clippy -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
- name: Check wasm32 eframe
run: cargo check -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
- name: clippy wasm32 eframe
run: cargo clippy -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
- name: wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.1.0
with:
version: "0.2.97"
version: "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
- run: ./scripts/wasm_bindgen_check.sh --skip-setup
@@ -114,20 +105,21 @@ jobs:
check_wasm_atomics:
name: Check wasm32+atomics
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{env.NIGHTLY_VERSION}}
targets: wasm32-unknown-unknown
components: rust-src
- name: Check wasm32+atomics eframe with wgpu
run: RUSTFLAGS='-C target-feature=+atomics' cargo +${{env.NIGHTLY_VERSION}} check -p eframe --lib --no-default-features --features wgpu --target wasm32-unknown-unknown -Z build-std=std,panic_abort
run: RUSTFLAGS='-C target-feature=+atomics' cargo +${{env.NIGHTLY_VERSION}} check -p eframe --lib --no-default-features --features wgpu,wgpu/webgpu --target wasm32-unknown-unknown -Z build-std=std,panic_abort
# ---------------------------------------------------------------------------
@@ -151,11 +143,12 @@ jobs:
name: cargo-deny ${{ matrix.target }}
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
rust-version: "1.81.0"
rust-version: "1.88.0"
log-level: error
command: check
arguments: --target ${{ matrix.target }}
@@ -165,18 +158,21 @@ jobs:
android:
name: android
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.81.0
toolchain: 1.88.0
targets: aarch64-linux-android
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- run: cargo check --features wgpu,android-native-activity --target aarch64-linux-android
# Default features disabled to turn off accesskit, which does not work
# with NativeActivity.
- run: cargo check --features wgpu,android-native-activity --target aarch64-linux-android --no-default-features
working-directory: crates/eframe
# ---------------------------------------------------------------------------
@@ -184,12 +180,13 @@ jobs:
ios:
name: ios
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.81.0
toolchain: 1.88.0
targets: aarch64-apple-ios
- name: Set up cargo cache
@@ -204,20 +201,19 @@ jobs:
windows:
name: Check Windows
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.81.0
toolchain: 1.88.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- name: Check all
run: cargo check --all-targets --all-features
- run: cargo clippy --all-targets --all-features
- name: Check hello_world
run: cargo check -p hello_world
- run: cargo clippy -p hello_world
# ---------------------------------------------------------------------------
@@ -225,14 +221,14 @@ jobs:
name: Run tests
# We run the tests on macOS because it will run with an actual GPU
runs-on: macos-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
lfs: true
- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.81.0
toolchain: 1.88.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2

View File

@@ -8,23 +8,29 @@ jobs:
# install and run locally: cargo install typos-cli && typos
name: typos
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4
- name: Check spelling of entire workspace
uses: crate-ci/typos@master
uses: crate-ci/typos@v1.38.0
linkinator:
name: linkinator
lychee:
name: lychee
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: jprochazk/linkinator-action@main
with:
linksToSkip: "https://crates.io/crates/.*, http://localhost:.*" # Avoid crates.io rate-limiting
retry: true
retryErrors: true
retryErrorsCount: 5
retryErrorsJitter: 2000
- name: Don't check CHANGELOG.md files
# This is really stupid but lychee doesn't have a way of excluding files via GLOB:
# https://github.com/lycheeverse/lychee/issues/1608
# We need to exclude CHANGELOG.md since we don't want to have a CI failure everytime some contributor decides
# to change their username.
run: rm -r */**/CHANGELOG.md CHANGELOG.md
- name: Link Checker
uses: lycheeverse/lychee-action@v2
with:
args: "'**/*.md' '**/*.toml' --exclude localhost --exclude reddit.com" # I guess reddit doesn't like github action IPs

View 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'

View File

@@ -3,9 +3,12 @@
# run: typos
[default.extend-words]
ime = "ime" # Input Method Editor
ime = "ime" # Input Method Editor
nknown = "nknown" # part of @55nknown username
ro = "ro" # read-only, also part of the username @Phen-Ro
isse = "isse" # part of @IsseW username
tye = "tye" # part of @tye-exe username
ro = "ro" # read-only, also part of the username @Phen-Ro
typ = "typ" # Often used because `type` is a keyword in Rust
# I mistype these so often
tesalator = "tessellator"
@@ -17,5 +20,133 @@ teselation = "tessellation"
tessalation = "tessellation"
tesselation = "tessellation"
# Use the more common spelling
adaptor = "adapter"
adaptors = "adapters"
# For consistency we prefer American English:
aeroplane = "airplane"
analogue = "analog"
analyse = "analyze"
appetiser = "appetizer"
arbour = "arbor"
ardour = "arbor"
armour = "armor"
artefact = "artifact"
authorise = "authorize"
behaviour = "behavior"
behavioural = "behavioral"
British = "American"
calibre = "caliber"
# cancelled = "canceled" # winit uses this :(
candour = "candor"
capitalise = "capitalize"
catalogue = "catalog"
centre = "center"
characterise = "characterize"
chequerboard = "checkerboard"
chequered = "checkered"
civilise = "civilize"
clamour = "clamor"
colonise = "colonize"
colour = "color"
coloured = "colored"
cosy = "cozy"
criticise = "criticize"
defence = "defense"
demeanour = "demeanor"
dialogue = "dialog"
distil = "distill"
doughnut = "donut"
dramatise = "dramatize"
draught = "draft"
emphasise = "emphasize"
endeavour = "endeavor"
enrol = "enroll"
epilogue = "epilog"
equalise = "equalize"
favour = "favor"
favourite = "favorite"
fibre = "fiber"
flavour = "flavor"
fulfil = "fufill"
gaol = "jail"
grey = "gray"
greys = "grays"
greyscale = "grayscale"
harbour = "habor"
honour = "honor"
humour = "humor"
instalment = "installment"
instil = "instill"
jewellery = "jewelry"
kerb = "curb"
labour = "labor"
litre = "liter"
lustre = "luster"
meagre = "meager"
metre = "meter"
mobilise = "mobilize"
monologue = "monolog"
naturalise = "naturalize"
neighbour = "neighbor"
neighbourhood = "neighborhood"
normalise = "normalize"
normalised = "normalized"
odour = "odor"
offence = "offense"
organise = "organize"
parlour = "parlor"
plough = "plow"
popularise = "popularize"
pretence = "pretense"
programme = "program"
prologue = "prolog"
rancour = "rancor"
realise = "realize"
recognise = "recognize"
recognised = "recognized"
rigour = "rigor"
rumour = "rumor"
sabre = "saber"
satirise = "satirize"
saviour = "savior"
savour = "savor"
sceptical = "skeptical"
sceptre = "scepter"
sepulchre = "sepulcher"
serialisation = "serialization"
serialise = "serialize"
serialised = "serialized"
skilful = "skillful"
sombre = "somber"
specialisation = "specialization"
specialise = "specialize"
specialised = "specialized"
splendour = "splendor"
standardise = "standardize"
sulphur = "sulfur"
symbolise = "symbolize"
theatre = "theater"
tonne = "ton"
travelogue = "travelog"
tumour = "tumor"
valour = "valor"
vaporise = "vaporize"
vigour = "vigor"
# null-terminated is the name of the wikipedia article!
# https://en.wikipedia.org/wiki/Null-terminated_string
nullterminated = "null-terminated"
zeroterminated = "null-terminated"
zero-terminated = "null-terminated"
[files]
extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated
[default]
extend-ignore-re = [
"#\\[doc\\(alias = .*", # We suggest "grey" in some doc
]

28
.vscode/settings.json vendored
View File

@@ -12,28 +12,14 @@
"target_wasm/**": true,
"target/**": true,
},
// Tell Rust Analyzer to use its own target directory, so we don't need to wait for it to finish wen we want to `cargo run`
"rust-analyzer.check.overrideCommand": [
"cargo",
"clippy",
"--target-dir=target_ra",
"--workspace",
"--message-format=json",
"--all-targets",
"--all-features",
],
"rust-analyzer.cargo.buildScripts.overrideCommand": [
"cargo",
"clippy",
"--quiet",
"--target-dir=target_ra",
"--workspace",
"--message-format=json",
"--all-targets",
"--all-features",
],
"rust-analyzer.check.command": "clippy",
// Whether `--workspace` should be passed to `cargo clippy`. If false, `-p <package>` will be passed instead.
"rust-analyzer.check.workspace": false,
"rust-analyzer.cargo.allTargets": true,
"rust-analyzer.cargo.features": "all",
// Use a separate target directory for Rust Analyzer so it doesn't prevent cargo/clippy from doing things.
"rust-analyzer.cargo.targetDir": "target_ra",
"rust-analyzer.showUnlinkedFileNotification": false,
// Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`.
// Don't forget to put it in a comment again before committing.
// "rust-analyzer.cargo.target": "wasm32-unknown-unknown",

View File

@@ -51,7 +51,7 @@ Thin wrapper around `egui_demo_lib` so we can compile it to a web site or a nati
Depends on `egui_demo_lib` + `eframe`.
### `egui_kittest`
A test harness for egui based on [kittest](https://github.com/rerun/kittest) and [AccessKit](https://github.com/AccessKit/accesskit/).
A test harness for egui based on [kittest](https://github.com/rerun-io/kittest) and [AccessKit](https://github.com/AccessKit/accesskit/).
### Other integrations

View File

@@ -14,6 +14,394 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.2 - 2025-11-13
### ⭐ Added
* Add `Plugin::on_widget_under_pointer` to support widget inspector [#7652](https://github.com/emilk/egui/pull/7652) by [@juancampa](https://github.com/juancampa)
* Add `Response::total_drag_delta` and `PointerState::total_drag_delta` [#7708](https://github.com/emilk/egui/pull/7708) by [@emilk](https://github.com/emilk)
### 🔧 Changed
* Improve accessibility and testability of `ComboBox` [#7658](https://github.com/emilk/egui/pull/7658) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🐛 Fixed
* Fix `profiling::scope` compile error when profiling using `tracing` backend [#7646](https://github.com/emilk/egui/pull/7646) by [@PPakalns](https://github.com/PPakalns)
* Fix edge cases in "smart aiming" in sliders [#7680](https://github.com/emilk/egui/pull/7680) by [@emilk](https://github.com/emilk)
* Hide scroll bars when dragging other things [#7689](https://github.com/emilk/egui/pull/7689) by [@emilk](https://github.com/emilk)
* Prevent widgets sometimes appearing to move relative to each other [#7710](https://github.com/emilk/egui/pull/7710) by [@emilk](https://github.com/emilk)
* Fix `ui.response().interact(Sense::click())` being flakey [#7713](https://github.com/emilk/egui/pull/7713) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.33.0 - 2025-10-09 - `egui::Plugin`, better kerning, kitdiff viewer
Highlights from this release:
- `egui::Plugin` a improved way to create and access egui plugins
- [kitdiff](https://github.com/rerun-io/kitdiff), a viewer for egui_kittest image snapshots (and a general image diff tool)
- better kerning
### Improved kerning
As a step towards using [parley](https://github.com/linebender/parley) for font rendering, @valadaptive has refactored the font loading and rendering code. A result of this (next to the font rendering code being much nicer now) is improved kerning.
Notice how the c moved away from the k:
![Oct-09-2025 16-21-58](https://github.com/user-attachments/assets/d4a17e87-5e98-40db-a85a-fa77fa77aceb)
### `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.
![svg-scaling](https://github.com/user-attachments/assets/faf63f0c-0ff7-47a0-a4cb-7210efeccb72)
##### 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.
![image](https://github.com/user-attachments/assets/7f370aaf-886a-423c-8391-c378849b63ca)
##### Details
* Improve text sharpness [#5838](https://github.com/emilk/egui/pull/5838) by [@emilk](https://github.com/emilk)
* Improve text rendering in light mode [#7290](https://github.com/emilk/egui/pull/7290) by [@emilk](https://github.com/emilk)
* Improve texture filtering by doing it in gamma space [#7311](https://github.com/emilk/egui/pull/7311) by [@emilk](https://github.com/emilk)
* Make text underline and strikethrough pixel perfect crisp [#5857](https://github.com/emilk/egui/pull/5857) by [@emilk](https://github.com/emilk)
### Migration guide
We have some silently breaking changes (code compiles fine but behavior changed) that require special care:
#### Menus close on click by default
- previously menus would only close on click outside
- either
- remove the `ui.close_menu()` calls from button click handlers since they are obsolete
- if the menu should stay open on clicks, change the `PopupCloseBehavior`:
```rs
// Change this
ui.menu_button("Text", |ui| { /* Menu Content */ });
// To this:
MenuButton::new("Text").config(
MenuConfig::default().close_behavior(PopupCloseBehavior::CloseOnClickOutside),
).ui(ui, |ui| { /* Menu Content */ });
```
You can also change the behavior only for a single SubMenu by using `SubMenuButton`, but by default it should be passed to any submenus when using `MenuButton`.
#### `Memory::is_popup_open` api now requires calls to `Memory::keep_popup_open`
- The popup will immediately close if `keep_popup_open` is not called.
- It's recommended to use the new `Popup` api which handles this for you.
- If you can't switch to the new api for some reason, update the code to call `keep_popup_open`:
```rs
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
ui.memory_mut(|mem| mem.keep_popup_open(popup_id)); // <- add this line
let area_response = Area::new(popup_id).show(...)
}
```
### ⭐ Other improvements
* Add `Label::show_tooltip_when_elided` [#5710](https://github.com/emilk/egui/pull/5710) by [@bryceberger](https://github.com/bryceberger)
* Deprecate `Ui::allocate_new_ui` in favor of `Ui::scope_builder` [#5764](https://github.com/emilk/egui/pull/5764) by [@lucasmerlin](https://github.com/lucasmerlin)
* Add `expand_bg` to customize size of text background [#5365](https://github.com/emilk/egui/pull/5365) by [@MeGaGiGaGon](https://github.com/MeGaGiGaGon)
* Add assert messages and print bad argument values in asserts [#5216](https://github.com/emilk/egui/pull/5216) by [@bircni](https://github.com/bircni)
* Use `TextBuffer` for `layouter` in `TextEdit` instead of `&str` [#5712](https://github.com/emilk/egui/pull/5712) by [@kernelkind](https://github.com/kernelkind)
* Add a `Slider::update_while_editing(bool)` API [#5978](https://github.com/emilk/egui/pull/5978) by [@mbernat](https://github.com/mbernat)
* Add `Scene::drag_pan_buttons` option. Allows specifying which pointer buttons pan the scene by dragging [#5892](https://github.com/emilk/egui/pull/5892) by [@mitchmindtree](https://github.com/mitchmindtree)
* Add `Scene::sense` to customize how `Scene` responds to user input [#5893](https://github.com/emilk/egui/pull/5893) by [@mitchmindtree](https://github.com/mitchmindtree)
* Rework `TextEdit` arrow navigation to handle Unicode graphemes [#5812](https://github.com/emilk/egui/pull/5812) by [@MStarha](https://github.com/MStarha)
* `ScrollArea` improvements for user configurability [#5443](https://github.com/emilk/egui/pull/5443) by [@MStarha](https://github.com/MStarha)
* Add `Response::clicked_with_open_in_background` [#7093](https://github.com/emilk/egui/pull/7093) by [@emilk](https://github.com/emilk)
* Add `Modifiers::matches_any` [#7123](https://github.com/emilk/egui/pull/7123) by [@emilk](https://github.com/emilk)
* Add `Context::format_modifiers` [#7125](https://github.com/emilk/egui/pull/7125) by [@emilk](https://github.com/emilk)
* Add `OperatingSystem::is_mac` [#7122](https://github.com/emilk/egui/pull/7122) by [@emilk](https://github.com/emilk)
* Support vertical-only scrolling by holding down Alt [#7124](https://github.com/emilk/egui/pull/7124) by [@emilk](https://github.com/emilk)
* Support for back-button on Android [#7073](https://github.com/emilk/egui/pull/7073) by [@ardocrat](https://github.com/ardocrat)
* Select all text in DragValue when gaining focus via keyboard [#7107](https://github.com/emilk/egui/pull/7107) by [@Azkellas](https://github.com/Azkellas)
* Add `Context::current_pass_index` [#7276](https://github.com/emilk/egui/pull/7276) by [@emilk](https://github.com/emilk)
* Add `Context::cumulative_frame_nr` [#7278](https://github.com/emilk/egui/pull/7278) by [@emilk](https://github.com/emilk)
* Add `Visuals::text_edit_bg_color` [#7283](https://github.com/emilk/egui/pull/7283) by [@emilk](https://github.com/emilk)
* Add `Visuals::weak_text_alpha` and `weak_text_color` [#7285](https://github.com/emilk/egui/pull/7285) by [@emilk](https://github.com/emilk)
* Add support for scrolling via accesskit / kittest [#7286](https://github.com/emilk/egui/pull/7286) by [@lucasmerlin](https://github.com/lucasmerlin)
* Update area struct to allow force resizing [#7114](https://github.com/emilk/egui/pull/7114) by [@blackberryfloat](https://github.com/blackberryfloat)
* Add `egui::Sides` `shrink_left` / `shrink_right` [#7295](https://github.com/emilk/egui/pull/7295) by [@lucasmerlin](https://github.com/lucasmerlin)
* Set intrinsic size for Label [#7328](https://github.com/emilk/egui/pull/7328) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🔧 Changed
* Raise MSRV to 1.85 [#6848](https://github.com/emilk/egui/pull/6848) by [@torokati44](https://github.com/torokati44), [#7279](https://github.com/emilk/egui/pull/7279) by [@emilk](https://github.com/emilk)
* Set `hint_text` in `WidgetInfo` [#5724](https://github.com/emilk/egui/pull/5724) by [@bircni](https://github.com/bircni)
* Implement `Default` for `ThemePreference` [#5702](https://github.com/emilk/egui/pull/5702) by [@MichaelGrupp](https://github.com/MichaelGrupp)
* Align `available_rect` docs with the new reality after #4590 [#5701](https://github.com/emilk/egui/pull/5701) by [@podusowski](https://github.com/podusowski)
* Clarify platform-specific details for `Viewport` positioning [#5715](https://github.com/emilk/egui/pull/5715) by [@aspiringLich](https://github.com/aspiringLich)
* Simplify the text cursor API [#5785](https://github.com/emilk/egui/pull/5785) by [@valadaptive](https://github.com/valadaptive)
* Bump accesskit to 0.19 [#7040](https://github.com/emilk/egui/pull/7040) by [@valadaptive](https://github.com/valadaptive)
* Better define the meaning of `SizeHint` [#7079](https://github.com/emilk/egui/pull/7079) by [@emilk](https://github.com/emilk)
* Move all input-related options into `InputOptions` [#7121](https://github.com/emilk/egui/pull/7121) by [@emilk](https://github.com/emilk)
* `Button` inherits the `alt_text` of the `Image` in it, if any [#7136](https://github.com/emilk/egui/pull/7136) by [@emilk](https://github.com/emilk)
* Change API of `Tooltip` slightly [#7151](https://github.com/emilk/egui/pull/7151) by [@emilk](https://github.com/emilk)
* Use Rust edition 2024 [#7280](https://github.com/emilk/egui/pull/7280) by [@emilk](https://github.com/emilk)
* Change `ui.disable()` to modify opacity [#7282](https://github.com/emilk/egui/pull/7282) by [@emilk](https://github.com/emilk)
* Make the font atlas use a color image [#7298](https://github.com/emilk/egui/pull/7298) by [@valadaptive](https://github.com/valadaptive)
* Implement `BitOr` and `BitOrAssign` for `Rect` [#7319](https://github.com/emilk/egui/pull/7319) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🔥 Removed
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
* Remove `SelectableLabel` [#7277](https://github.com/emilk/egui/pull/7277) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🐛 Fixed
* `Scene`: make `scene_rect` full size on reset [#5801](https://github.com/emilk/egui/pull/5801) by [@graydenshand](https://github.com/graydenshand)
* `Scene`: `TextEdit` selection when placed in a `Scene` [#5791](https://github.com/emilk/egui/pull/5791) by [@karhu](https://github.com/karhu)
* `Scene`: Set transform layer before calling user content [#5884](https://github.com/emilk/egui/pull/5884) by [@mitchmindtree](https://github.com/mitchmindtree)
* Fix: transform `TextShape` underline width [#5865](https://github.com/emilk/egui/pull/5865) by [@emilk](https://github.com/emilk)
* Fix missing repaint after `consume_key` [#7134](https://github.com/emilk/egui/pull/7134) by [@lucasmerlin](https://github.com/lucasmerlin)
* Update `emoji-icon-font` with fix for fullwidth latin characters [#7067](https://github.com/emilk/egui/pull/7067) by [@emilk](https://github.com/emilk)
* Mark all keys as released if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix scroll handle extending outside of `ScrollArea` [#5286](https://github.com/emilk/egui/pull/5286) by [@gilbertoalexsantos](https://github.com/gilbertoalexsantos)
* Fix `Response::clicked_elsewhere` not returning `true` sometimes [#5798](https://github.com/emilk/egui/pull/5798) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix kinetic scrolling on touch devices [#5778](https://github.com/emilk/egui/pull/5778) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix `DragValue` expansion when editing [#5809](https://github.com/emilk/egui/pull/5809) by [@MStarha](https://github.com/MStarha)
* Fix disabled `DragValue` eating focus, causing focus to reset [#5826](https://github.com/emilk/egui/pull/5826) by [@KonaeAkira](https://github.com/KonaeAkira)
* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk)
* Improve drag-to-select text (add margins) [#5797](https://github.com/emilk/egui/pull/5797) by [@hankjordan](https://github.com/hankjordan)
* Fix bug in pointer movement detection [#5329](https://github.com/emilk/egui/pull/5329) by [@rustbasic](https://github.com/rustbasic)
* Protect against NaN in hit-test code [#6851](https://github.com/emilk/egui/pull/6851) by [@Skgland](https://github.com/Skgland)
* Fix image button panicking with tiny `available_space` [#6900](https://github.com/emilk/egui/pull/6900) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix links and text selection in horizontal_wrapped layout [#6905](https://github.com/emilk/egui/pull/6905) by [@lucasmerlin](https://github.com/lucasmerlin)
* Fix `leading_space` sometimes being ignored during paragraph splitting [#7031](https://github.com/emilk/egui/pull/7031) by [@afishhh](https://github.com/afishhh)
* Fix typo in deprecation message for `ComboBox::from_id_source` [#7055](https://github.com/emilk/egui/pull/7055) by [@aelmizeb](https://github.com/aelmizeb)
* Bug fix: make sure `end_pass` is called for all loaders [#7072](https://github.com/emilk/egui/pull/7072) by [@emilk](https://github.com/emilk)
* Report image alt text as text if widget contains no other text [#7142](https://github.com/emilk/egui/pull/7142) by [@lucasmerlin](https://github.com/lucasmerlin)
* Slider: move by at least the next increment when using fixed_decimals [#7066](https://github.com/emilk/egui/pull/7066) by [@0x53A](https://github.com/0x53A)
* Fix crash when using infinite widgets [#7296](https://github.com/emilk/egui/pull/7296) by [@emilk](https://github.com/emilk)
* Fix `debug_assert` triggered by `menu`/`intersect_ray` [#7299](https://github.com/emilk/egui/pull/7299) by [@emilk](https://github.com/emilk)
* Change `Rect::area` to return zero for negative rectangles [#7305](https://github.com/emilk/egui/pull/7305) by [@emilk](https://github.com/emilk)
### 🚀 Performance
* Optimize editing long text by caching each paragraph [#5411](https://github.com/emilk/egui/pull/5411) by [@afishhh](https://github.com/afishhh)
* Make `WidgetText` smaller and faster [#6903](https://github.com/emilk/egui/pull/6903) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.31.1 - 2025-03-05
* Fix sizing bug in `TextEdit::singleline` [#5640](https://github.com/emilk/egui/pull/5640) by [@IaVashik](https://github.com/IaVashik)
* Fix panic when rendering thin textured rectangles [#5692](https://github.com/emilk/egui/pull/5692) by [@PPakalns](https://github.com/PPakalns)
## 0.31.0 - 2025-02-04 - Scene container, improved rendering quality
### Highlights ✨
@@ -103,7 +491,7 @@ In order to pave the path for more complex and customizable styling solutions, w
* Improved support for transform layers ([#5465](https://github.com/emilk/egui/pull/5465), [#5468](https://github.com/emilk/egui/pull/5468), [#5429](https://github.com/emilk/egui/pull/5429))
#### `egui_kittest`
This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/master/crates/egui_kittest).
This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/main/crates/egui_kittest).
`egui_kittest` is a testing framework for egui, allowing you to test both automation (simulated clicks and other events),
and also do screenshot testing (useful for regression tests).
`egui_kittest` is built using [`kittest`](https://github.com/rerun-io/kittest), which is a general GUI testing framework that aims to work with any Rust GUI (not just egui!).
@@ -321,7 +709,7 @@ There also has been several small improvements to the look of egui:
* The `extra_asserts` and `extra_debug_asserts` feature flags have been removed ([#4478](https://github.com/emilk/egui/pull/4478))
* Remove `Event::Scroll` and handle it in egui. Use `Event::MouseWheel` instead ([#4524](https://github.com/emilk/egui/pull/4524))
* `Event::Zoom` is no longer emitted on ctrl+scroll. Use `InputState::smooth_scroll_delta` instead ([#4524](https://github.com/emilk/egui/pull/4524))
* `ui.set_enabled` and `set_visbile` have been deprecated ([#4614](https://github.com/emilk/egui/pull/4614))
* `ui.set_enabled` and `set_visible` have been deprecated ([#4614](https://github.com/emilk/egui/pull/4614))
* `DragValue::clamp_range` renamed to `range` (([#4728](https://github.com/emilk/egui/pull/4728))
### ⭐ Added
@@ -937,7 +1325,7 @@ egui_extras::install_image_loaders(egui_ctx);
## 0.18.0 - 2022-04-30
### ⭐ Added
* Added `Shape::Callback` for backend-specific painting, [with an example](https://github.com/emilk/egui/tree/master/examples/custom_3d_glow) ([#1351](https://github.com/emilk/egui/pull/1351)).
* Added `Shape::Callback` for backend-specific painting, [with an example](https://github.com/emilk/egui/tree/main/examples/custom_3d_glow) ([#1351](https://github.com/emilk/egui/pull/1351)).
* Added `Frame::canvas` ([#1362](https://github.com/emilk/egui/pull/1362)).
* `Context::request_repaint` will now wake up UI thread, if integrations has called `Context::set_request_repaint_callback` ([#1366](https://github.com/emilk/egui/pull/1366)).
* Added `Plot::allow_scroll`, `Plot::allow_zoom` no longer affects scrolling ([#1382](https://github.com/emilk/egui/pull/1382)).

View File

@@ -34,14 +34,11 @@ Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pi
You can test your code locally by running `./scripts/check.sh`.
There are snapshots test that might need to be updated.
Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them.
If CI keeps complaining about snapshots (which could happen if you don't use macOS, snapshots in CI are currently
rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from
the last CI run of your PR (which will download the `test_results` artifact).
For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md).
We use [git-lfs](https://git-lfs.com/) to store big files in the repository.
Make sure you have it installed (running `git lfs ls-files` from the repository root should list some files).
Don't forget to run `git lfs install` after installing the git-lfs binary.
You need to add any .png images to `git lfs`.
If the CI complains about this, make sure you run `git add --renormalize .`.
Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info.
If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs.
When you have something that works, open a draft PR. You may get some helpful feedback early!
@@ -51,6 +48,34 @@ Don't worry about having many small commits in the PR - they will be squashed to
Please keep pull requests small and focused. The smaller it is, the more likely it is to get merged.
## Working with git lfs
We use [git-lfs](https://git-lfs.com/) to store big files in the repository.
Make sure you have it installed (running `git lfs ls-files` from the repository root should list some files).
Don't forget to run `git lfs install` in this repo after installing the git-lfs binary.
You need to add any .png images to `git lfs` (see the .gitattributes file for rules and exclusions).
If the CI complains about lfs, try running `git add --renormalize .`.
Common git-lfs commands:
```bash
# Install git-lfs in the repo (installs git hooks)
git lfs install
# Move a file to git lfs
git lfs track "path/to/file/or/pattern" # OR manually edit .gitattributes
git add --renormalize . # Moves already added files to lfs (according to .gitattributes)
# Move a file from lfs to regular git
git lfs untrack "path/to/file/or/pattern" # OR manually edit .gitattributes
git add --renormalize . # Moves already added files to regular git (according to .gitattributes)
# Push to a contributor remote (see https://github.com/cli/cli/discussions/8794#discussioncomment-8695076)
git push --no-verify
# Push git lfs files to contributor remote:
git push origin $(git branch --show-current) && git push --no-verify && git push origin --delete $(git branch --show-current)
```
## PR review
Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs!
@@ -103,6 +128,7 @@ While using an immediate mode gui is simple, implementing one is a lot more tric
* Flip `if !condition {} else {}`
* Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`)
* Put each type in their own file, unless they are trivial (e.g. a `struct` with no `impl`)
* Put most generic arguments first (e.g. `Context`), and most specific last
* Break the above rules when it makes sense

2575
Cargo.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,30 @@
[workspace]
resolver = "2"
members = [
"crates/ecolor",
"crates/egui_demo_app",
"crates/egui_demo_lib",
"crates/egui_extras",
"crates/egui_glow",
"crates/egui_kittest",
"crates/egui-wgpu",
"crates/egui-winit",
"crates/egui",
"crates/emath",
"crates/epaint",
"crates/epaint_default_fonts",
"crates/ecolor",
"crates/egui_demo_app",
"crates/egui_demo_lib",
"crates/egui_extras",
"crates/egui_glow",
"crates/egui_kittest",
"crates/egui-wgpu",
"crates/egui-winit",
"crates/egui",
"crates/emath",
"crates/epaint",
"crates/epaint_default_fonts",
"examples/*",
"tests/*",
"examples/*",
"tests/*",
"xtask",
"xtask",
]
[workspace.package]
edition = "2021"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.81"
version = "0.31.0"
rust-version = "1.88"
version = "0.33.2"
[profile.release]
@@ -55,54 +55,95 @@ opt-level = 2
[workspace.dependencies]
emath = { version = "0.31.0", path = "crates/emath", default-features = false }
ecolor = { version = "0.31.0", path = "crates/ecolor", default-features = false }
epaint = { version = "0.31.0", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.31.0", path = "crates/epaint_default_fonts" }
egui = { version = "0.31.0", path = "crates/egui", default-features = false }
egui-winit = { version = "0.31.0", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.31.0", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.31.0", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.31.0", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.31.0", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.31.0", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.31.0", path = "crates/eframe", default-features = false }
emath = { version = "0.33.2", path = "crates/emath", default-features = false }
ecolor = { version = "0.33.2", path = "crates/ecolor", default-features = false }
epaint = { version = "0.33.2", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.33.2", path = "crates/epaint_default_fonts" }
egui = { version = "0.33.2", path = "crates/egui", default-features = false }
egui-winit = { version = "0.33.2", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.33.2", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.33.2", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.33.2", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.33.2", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.33.2", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.33.2", path = "crates/eframe", default-features = false }
ahash = { version = "0.8.11", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
"std",
accesskit = "0.21.1"
accesskit_consumer = "0.30.1"
accesskit_winit = "0.29.1"
ab_glyph = "0.2.32"
ahash = { version = "0.8.12", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
"std",
] }
backtrace = "0.3"
bitflags = "2.6"
bytemuck = "1.7.2"
criterion = { version = "0.5.1", default-features = false }
dify = { version = "0.7", default-features = false }
document-features = "0.2.10"
glow = "0.16"
glutin = { version = "0.32.0", default-features = false }
android_logger = "0.15.1"
arboard = { version = "3.6.1", default-features = false }
backtrace = "0.3.76"
bitflags = "2.9.4"
bytemuck = "1.24.0"
chrono = { version = "0.4.42", default-features = false }
cint = "0.3.1"
color-hex = "0.2.0"
criterion = { version = "0.7.0", default-features = false }
dify = { version = "0.7.4", default-features = false }
directories = "6.0.0"
document-features = "0.2.11"
ehttp = { version = "0.5.0", default-features = false }
enum-map = "2.7.3"
env_logger = { version = "0.11.8", default-features = false }
glow = "0.16.0"
glutin = { version = "0.32.3", default-features = false }
glutin-winit = { version = "0.5.0", default-features = false }
home = "0.5.9"
image = { version = "0.25", default-features = false }
kittest = { version = "0.1" }
log = { version = "0.4", features = ["std"] }
nohash-hasher = "0.2"
parking_lot = "0.12"
pollster = "0.4"
profiling = { version = "1.0.16", default-features = false }
puffin = "0.19"
puffin_http = "0.16"
raw-window-handle = "0.6.0"
ron = "0.8"
serde = { version = "1", features = ["derive"] }
thiserror = "1.0.37"
type-map = "0.5.0"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = "0.3.70"
image = { version = "0.25.6", default-features = false }
js-sys = "0.3.77"
kittest = { version = "0.3.0" }
log = { version = "0.4.28", features = ["std"] }
memoffset = "0.9.1"
mimalloc = "0.1.48"
mime_guess2 = { version = "2.3.1", default-features = false }
mint = "0.5.9"
nohash-hasher = "0.2.0"
objc2 = "0.5.2"
objc2-app-kit = { version = "0.2.2", default-features = false }
objc2-foundation = { version = "0.2.2", default-features = false }
objc2-ui-kit = { version = "0.2.2", default-features = false }
open = "5.3.2"
parking_lot = "0.12.5"
percent-encoding = "2.3.2"
poll-promise = { version = "0.3.0", default-features = false }
pollster = "0.4.0"
profiling = { version = "1.0.17", default-features = false }
puffin = "0.19.1"
puffin_http = "0.16.1"
rand = "0.9.2"
raw-window-handle = "0.6.2"
rayon = "1.11.0"
resvg = { version = "0.45.1", default-features = false }
rfd = "0.15.4"
ron = "0.11.0"
serde = { version = "1.0.228", features = ["derive"] }
similar-asserts = "1.7.0"
smallvec = "1.15.1"
smithay-clipboard = "0.7.2"
static_assertions = "1.1.0"
syntect = { version = "5.3.0", default-features = false }
tempfile = "3.23.0"
thiserror = "2.0.17"
tokio = "1.47.1"
type-map = "0.5.1"
unicode_names2 = { version = "2.0.0", default-features = false }
unicode-segmentation = "1.12.0"
wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
wasm-bindgen-futures = "0.4.0"
wayland-cursor = { version = "0.31.11", default-features = false }
web-sys = "0.3.77"
web-time = "1.1.0" # Timekeeping for native and web
wgpu = { version = "24.0.0", default-features = false }
windows-sys = "0.59"
winit = { version = "0.30.7", default-features = false }
webbrowser = "1.0.5"
wgpu = { version = "27.0.1", default-features = false, features = ["std"] }
windows-sys = "0.61.2"
winit = { version = "0.30.12", default-features = false }
[workspace.lints.rust]
unsafe_code = "deny"
@@ -115,7 +156,7 @@ rust_2021_prelude_collisions = "warn"
semicolon_in_expressions_from_macros = "warn"
trivial_numeric_casts = "warn"
unexpected_cfgs = "warn"
unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_lifetimes = "warn"
@@ -130,25 +171,33 @@ broken_intra_doc_links = "warn"
# See also clippy.toml
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
allow_attributes = "warn"
as_ptr_cast_mut = "warn"
await_holding_lock = "warn"
bool_to_int_with_if = "warn"
branches_sharing_code = "warn"
char_lit_as_u8 = "warn"
checked_conversions = "warn"
clear_with_drain = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
debug_assert_with_mut_call = "warn"
default_union_representation = "warn"
derive_partial_eq_without_eq = "warn"
disallowed_macros = "warn" # See clippy.toml
disallowed_methods = "warn" # See clippy.toml
disallowed_names = "warn" # See clippy.toml
disallowed_script_idents = "warn" # See clippy.toml
disallowed_types = "warn" # See clippy.toml
disallowed_macros = "warn" # See clippy.toml
disallowed_methods = "warn" # See clippy.toml
disallowed_names = "warn" # See clippy.toml
disallowed_script_idents = "warn" # See clippy.toml
disallowed_types = "warn" # See clippy.toml
doc_comment_double_space_linebreaks = "warn"
doc_link_with_quotes = "warn"
doc_markdown = "warn"
elidable_lifetime_names = "warn"
empty_enum = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_outer_attr = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
exit = "warn"
@@ -165,9 +214,11 @@ fn_to_numeric_cast_any = "warn"
from_iter_instead_of_collect = "warn"
get_unwrap = "warn"
if_let_mutex = "warn"
ignore_without_reason = "warn"
implicit_clone = "warn"
implied_bounds_in_impls = "warn"
imprecise_flops = "warn"
inconsistent_struct_constructor = "warn"
index_refutable_slice = "warn"
inefficient_to_string = "warn"
infinite_loop = "warn"
@@ -178,33 +229,37 @@ iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
iter_on_empty_collections = "warn"
iter_on_single_items = "warn"
iter_over_hash_type = "warn"
iter_without_into_iter = "warn"
large_digit_groups = "warn"
large_futures = "warn"
large_include_file = "warn"
large_stack_arrays = "warn"
large_stack_frames = "warn"
large_types_passed_by_value = "warn"
let_unit_value = "warn"
linkedlist = "warn"
literal_string_with_formatting_args = "warn"
lossy_float_literal = "warn"
macro_use_imports = "warn"
manual_assert = "warn"
manual_clamp = "warn"
manual_instant_elapsed = "warn"
manual_is_power_of_two = "warn"
manual_is_variant_and = "warn"
manual_let_else = "warn"
manual_midpoint = "warn" # NOTE `midpoint` is often a lot slower for floats, so we have our own `emath::fast_midpoint` function.
manual_ok_or = "warn"
manual_string_new = "warn"
map_err_ignore = "warn"
map_flatten = "warn"
map_unwrap_or = "warn"
match_bool = "warn"
match_on_vec_items = "warn"
match_same_arms = "warn"
match_wild_err_arm = "warn"
match_wildcard_for_single_variants = "warn"
mem_forget = "warn"
mismatching_type_param_order = "warn"
missing_assert_message = "warn"
missing_enforced_import_renames = "warn"
missing_errors_doc = "warn"
missing_safety_doc = "warn"
@@ -217,11 +272,16 @@ needless_for_each = "warn"
needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
negative_feature_names = "warn"
non_std_lazy_statics = "warn"
non_zero_suggestions = "warn"
nonstandard_macro_braces = "warn"
option_as_ref_cloned = "warn"
option_option = "warn"
path_buf_push_overwrite = "warn"
pathbuf_init_then_push = "warn"
precedence_bits = "warn"
print_stderr = "warn"
print_stdout = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
pub_underscore_fields = "warn"
@@ -233,10 +293,13 @@ ref_as_ptr = "warn"
ref_option_ref = "warn"
ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_and_then = "warn"
same_functions_in_if_condition = "warn"
semicolon_if_nothing_returned = "warn"
set_contains_or_insert = "warn"
single_char_pattern = "warn"
single_match_else = "warn"
single_option_map = "warn"
str_split_at_newline = "warn"
str_to_string = "warn"
string_add = "warn"
@@ -247,8 +310,10 @@ string_to_string = "warn"
suspicious_command_arg_space = "warn"
suspicious_xor_used_as_pow = "warn"
todo = "warn"
too_long_first_doc_paragraph = "warn"
trailing_empty_array = "warn"
trait_duplication_in_bounds = "warn"
transmute_ptr_to_ptr = "warn"
tuple_array_conversions = "warn"
unchecked_duration_subtraction = "warn"
undocumented_unsafe_blocks = "warn"
@@ -256,14 +321,22 @@ unimplemented = "warn"
uninhabited_references = "warn"
uninlined_format_args = "warn"
unnecessary_box_returns = "warn"
unnecessary_debug_formatting = "warn"
unnecessary_literal_bound = "warn"
unnecessary_safety_comment = "warn"
unnecessary_safety_doc = "warn"
unnecessary_self_imports = "warn"
unnecessary_semicolon = "warn"
unnecessary_struct_initialization = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
unused_async = "warn"
unused_peekable = "warn"
unused_rounding = "warn"
unused_self = "warn"
unused_trait_names = "warn"
use_self = "warn"
useless_let_if_seq = "warn"
useless_transmute = "warn"
verbose_file_reads = "warn"
wildcard_dependencies = "warn"
@@ -271,17 +344,17 @@ zero_sized_map_values = "warn"
# TODO(emilk): maybe enable more of these lints?
iter_over_hash_type = "allow"
missing_assert_message = "allow"
comparison_chain = "allow"
should_panic_without_expect = "allow"
too_many_lines = "allow"
unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one
unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one
# These are meh:
assigning_clones = "allow" # No please
assigning_clones = "allow" # No please
let_underscore_must_use = "allow"
let_underscore_untyped = "allow"
manual_range_contains = "allow" # this one is just worse imho
self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
manual_range_contains = "allow" # this one is just worse imho
map_unwrap_or = "allow" # so is this one
self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
significant_drop_tightening = "allow" # Too many false positives
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports

View File

@@ -4,14 +4,14 @@
[![Latest version](https://img.shields.io/crates/v/egui.svg)](https://crates.io/crates/egui)
[![Documentation](https://docs.rs/egui/badge.svg)](https://docs.rs/egui)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
[![Build Status](https://github.com/emilk/egui/workflows/CI/badge.svg)](https://github.com/emilk/egui/actions?workflow=CI)
[![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-MIT)
[![Apache](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-APACHE)
[![Build Status](https://github.com/emilk/egui/workflows/Rust/badge.svg)](https://github.com/emilk/egui/actions/workflows/rust.yml)
[![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/emilk/egui/blob/main/LICENSE-MIT)
[![Apache](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/emilk/egui/blob/main/LICENSE-APACHE)
[![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq)
<div align="center">
<a href="https://www.rerun.io/"><img src="media/rerun_io_logo.png" width="250"></a>
<a href="https://www.rerun.io/"><img src="https://github.com/user-attachments/assets/78e79463-4357-461b-bbd1-31aa5ef5e1a2" width="250"></a>
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
an SDK for visualizing streams of multimodal data.
@@ -27,7 +27,7 @@ egui aims to be the easiest-to-use Rust GUI library, and the simplest way to mak
egui can be used anywhere you can draw textured triangles, which means you can easily integrate it into your game engine of choice.
[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) is the official egui framework, which supports writing apps for Web, Linux, Mac, Windows, and Android.
[`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) is the official egui framework, which supports writing apps for Web, Linux, Mac, Windows, and Android.
## Example
@@ -46,7 +46,7 @@ ui.label(format!("Hello '{name}', age {age}"));
ui.image(egui::include_image!("ferris.png"));
```
<img alt="Dark mode" src="media/demo.gif"> &nbsp; &nbsp; <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"> &nbsp; &nbsp; <img alt="Light mode" src="https://github.com/user-attachments/assets/a5e7da93-89a8-4ba0-86b8-0fa2228a4f62" height="278">
## Sections:
@@ -68,19 +68,19 @@ ui.image(egui::include_image!("ferris.png"));
## Quick start
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/master/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md).
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md).
## Demo
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with Wasm and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with Wasm and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
@@ -99,7 +99,7 @@ On Fedora Rawhide you need to run:
* Easy to integrate into any environment
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
* Pure immediate mode: no callbacks
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/toggle_switch.rs)
* Modular: You should be able to use small parts of egui and combine them in new ways
* Safe: there is no `unsafe` code in egui
* Minimal dependencies
@@ -136,28 +136,20 @@ Still, egui can be used to create professional looking applications, like [the R
Check out the [3rd party egui crates wiki](https://github.com/emilk/egui/wiki/3rd-party-egui-crates) for even more
widgets and features, maintained by the community.
<img src="media/widget_gallery_0.23.gif" width="50%">
<img src="https://github.com/user-attachments/assets/13e73b76-e456-42bd-8ec9-220802834268" width="50%">
Light Theme:
<img src="media/widget_gallery_0.23_light.png" width="50%">
<img src="https://github.com/user-attachments/assets/2e38972c-a444-4894-b32f-47a2719cf369" width="50%">
## Dependencies
`egui` has a minimal set of default dependencies:
* [`ab_glyph`](https://crates.io/crates/ab_glyph)
* [`ahash`](https://crates.io/crates/ahash)
* [`bitflags`](https://crates.io/crates/bitflags)
* [`nohash-hasher`](https://crates.io/crates/nohash-hasher)
* [`parking_lot`](https://crates.io/crates/parking_lot)
`egui` has a minimal set of default dependencies.
Heavier dependencies are kept out of `egui`, even as opt-in.
All code in `egui` is Wasm-friendly (even outside a browser).
To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/master/crates/egui_extras) crate.
To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/main/crates/egui_extras) crate.
[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc,
[`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc,
## Who is egui for?
@@ -178,16 +170,16 @@ An integration needs to do the following each frame:
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
* Call into the application GUI code
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs))
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/main/crates/egui_glow/src/painter.rs))
### Official integrations
These are the official egui integrations:
* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`
* [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps
* [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API)
* [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit)
* [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`
* [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps
* [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API)
* [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit)
### 3rd party integrations
@@ -268,7 +260,7 @@ This is not yet as powerful as say CSS, [but this is going to improve](https://g
Here is an example (from https://github.com/a-liashenko/TinyPomodoro):
<img src="media/pompodoro-skin.png" width="50%">
<img src="https://github.com/user-attachments/assets/e6107237-2547-41d6-996b-9a20ae0345ab" width="50%">
### How do I use egui with `async`?
If you call `.await` in your GUI code, the UI will freeze, which is very bad UX. Instead, keep the GUI thread non-blocking and communicate with any concurrent tasks (`async` tasks or other threads) with something like:
@@ -287,7 +279,7 @@ egui includes optional support for [AccessKit](https://accesskit.dev/), which cu
The original discussion of accessibility in egui is at <https://github.com/emilk/egui/issues/167>. Now that AccessKit support is merged, providing a strong foundation for future accessibility work, please open new issues on specific accessibility problems.
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/main/crates/eframe)?
`egui` is a 2D user interface library for laying out and interacting with buttons, sliders, etc.
`egui` has no idea if it is running on the web or natively, and does not know how to collect input or show things on screen.
@@ -304,15 +296,16 @@ If you want to embed 3D into an egui view there are two options:
#### `Shape::Callback`
Example:
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_glow/src/main.rs>
* <https://github.com/emilk/egui/blob/main/examples/custom_3d_glow/src/main.rs>
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
#### Render-to-texture
You can also render your 3D scene to a texture and display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use.
Examples:
* Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs
* Using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) + [`VTK (C++)`](https://vtk.org/): https://github.com/Gerharddc/vtk-egui-demo
## Other
@@ -354,11 +347,11 @@ Notable contributions by:
* [@AsmPrgmC3](https://github.com/AsmPrgmC3): [Proper sRGBA blending for web](https://github.com/emilk/egui/pull/650)
* [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685)
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543)
* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868)
* [@KentaTheBugMaker](https://github.com/KentaTheBugMaker): [Port glow painter to web](https://github.com/emilk/egui/pull/868)
* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050)
* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625)
* [@mwcampbell](https://github.com/mwcampbell): [AccessKit](https://github.com/AccessKit/accesskit) [integration](https://github.com/emilk/egui/pull/2294)
* [@hasenbanck](https://github.com/hasenbanck), [@s-nie](https://github.com/s-nie), [@Wumpf](https://github.com/Wumpf): [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)
* [@hasenbanck](https://github.com/hasenbanck), [@s-nie](https://github.com/s-nie), [@Wumpf](https://github.com/Wumpf): [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)
* [@jprochazk](https://github.com/jprochazk): [egui image API](https://github.com/emilk/egui/issues/3291)
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
@@ -376,7 +369,7 @@ Default fonts:
---
<div align="center">
<a href="https://www.rerun.io/"><img src="media/rerun_io_logo.png" width="440"></a>
<a href="https://www.rerun.io/"><img src="https://github.com/user-attachments/assets/78e79463-4357-461b-bbd1-31aa5ef5e1a2" width="440"></a>
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
an SDK for visualizing streams of multimodal data.

View File

@@ -9,7 +9,7 @@ All crates under the [`crates/`](crates/) folder are published in lock-step, wit
The only exception to this are patch releases, where we sometimes only patch a single crate.
The egui version in egui `master` is always the version of the last published crates. This is so that users can easily patch their egui crates to egui `master` if they want to.
The egui version in egui `main` is always the version of the last published crates. This is so that users can easily patch their egui crates to egui `main` if they want to.
## Governance
Releases are generally done by [emilk](https://github.com/emilk/), but the [rerun-io](https://github.com/rerun-io/) organization (where emilk is CTO) also has publish rights to all the crates.
@@ -22,8 +22,11 @@ We don't update the MSRV in a patch release, unless we really, really need to.
# Release process
## Patch release
* [ ] Make a branch off of the latest release
* [ ] copy this checklist to a new egui issue, called "Release 0.xx.y"
* [ ] close all issues in the milestone for this release
## Special steps for patch release
* [ ] make a branch off of the _latest_ release
* [ ] cherry-pick what you want to release
* [ ] run `cargo semver-checks`
@@ -45,41 +48,32 @@ We don't update the MSRV in a patch release, unless we really, really need to.
* [ ] check that CI is green
## Preparation
* [ ] make sure there are no important unmerged PRs
* [ ] Ensure we don't have any patch/git dependencies (e.g. kittest)
* [ ] Create a branch called `release-0.xx.0` and open a PR for it
* [ ] run `scripts/generate_example_screenshots.sh` if needed
* [ ] write a short release note that fits in a bluesky post
* [ ] record gif for `CHANGELOG.md` release note (and later bluesky post)
* [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write`
* [ ] bump version numbers in workspace `Cargo.toml`
* [ ] update changelogs
* [ ] run `scripts/generate_changelog.py --version 0.x.0 --write`
* [ ] read changelogs and clean them up if needed
* [ ] write a good intro with highlight for the main changelog
* [ ] run `typos`
## Actual release
I usually do this all on the `master` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `master` later.
* [ ] Run `typos`
* [ ] `git commit -m 'Release 0.x.0 - <release title>'`
* [ ] `cargo publish` (see below)
* [ ] bump version numbers in workspace `Cargo.toml`
* [ ] check that CI for the PR is green
* [ ] publish the crates by running `scripts/publish_crates.sh`
* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - <release title>'`
* [ ] `git pull --tags ; git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force ; git push --tags`
* [ ] merge release PR or push to `master`
* [ ] check that CI is green
* [ ] merge release PR as `Release 0.x.0 - <release title>`
* [ ] check that CI for `main` is green
* [ ] do a GitHub release: https://github.com/emilk/egui/releases/new
* Follow the format of the last release
* [ ] wait for documentation to build: https://docs.rs/releases/queue
* follow the format of the last release
* [ ] wait for the documentation build to finish: https://docs.rs/releases/queue
* [ ] https://docs.rs/egui/ works
* [ ] https://docs.rs/eframe/ works
### `cargo publish`:
```
(cd crates/emath && cargo publish --quiet) && echo "✅ emath"
(cd crates/ecolor && cargo publish --quiet) && echo "✅ ecolor"
(cd crates/epaint_default_fonts && cargo publish --quiet) && echo "✅ epaint_default_fonts"
(cd crates/epaint && cargo publish --quiet) && echo "✅ epaint"
(cd crates/egui && cargo publish --quiet) && echo "✅ egui"
(cd crates/egui-winit && cargo publish --quiet) && echo "✅ egui-winit"
(cd crates/egui-wgpu && cargo publish --quiet) && echo "✅ egui-wgpu"
(cd crates/egui_kittest && cargo publish --quiet) && echo "✅ egui_kittest"
(cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras"
(cd crates/egui_demo_lib && cargo publish --quiet) && echo "✅ egui_demo_lib"
(cd crates/egui_glow && cargo publish --quiet) && echo "✅ egui_glow"
(cd crates/eframe && cargo publish --quiet) && echo "✅ eframe"
```
## Announcements
* [ ] [Bluesky](https://bsky.app/profile/ernerfeldt.bsky.social)
@@ -88,10 +82,17 @@ I usually do this all on the `master` branch, but doing it in a release branch i
* [ ] [r/programming](https://www.reddit.com/r/programming/comments/1bocsf6/announcing_egui_027_an_easytouse_crossplatform/)
* [ ] [This Week in Rust](https://github.com/rust-lang/this-week-in-rust/pull/5167)
## After release
* [ ] publish new `eframe_template`
* [ ] update `eframe_template`
* [ ] publish new `egui_plot`
* [ ] publish new `egui_table`
* [ ] publish new `egui_tiles`
* [ ] make a PR to `egui_commonmark`
* [ ] make a PR to `rerun`
## Finally
* [ ] close the milestone
* [ ] close this issue
* [ ] improve `RELEASES.md` with what you learned this time around

View File

@@ -3,7 +3,7 @@
# -----------------------------------------------------------------------------
# Section identical to scripts/clippy_wasm/clippy.toml:
msrv = "1.81"
msrv = "1.88"
allow-unwrap-in-tests = true
@@ -23,29 +23,25 @@ type-complexity-threshold = 350
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros
disallowed-macros = [
'dbg',
'std::unimplemented',
'std::dbg',
'std::unimplemented',
# TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses
# 'std::eprint',
# 'std::eprintln',
# 'std::print',
# 'std::println',
# TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses
# 'std::eprint',
# 'std::eprintln',
# 'std::print',
# 'std::println',
]
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods
disallowed-methods = [
"std::env::temp_dir", # Use the tempdir crate instead
# NOTE: There are many things that aren't allowed on wasm,
# but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406)
# so we do that in `clipppy_wasm.toml` instead.
# There are many things that aren't allowed on wasm,
# but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406)
# so we do that in `clipppy_wasm.toml` instead.
"std::thread::spawn", # Use `std::thread::Builder` and name the thread
"sha1::Digest::new", # SHA1 is cryptographically broken
"std::panic::catch_unwind", # We compile with `panic = "abort"`
{ path = "std::env::temp_dir", readon = "Use the tempfile crate instead" },
{ path = "std::panic::catch_unwind", reason = "We compile with `panic = abort" },
{ path = "std::thread::spawn", readon = "Use `std::thread::Builder` and name the thread" },
]
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names
@@ -53,28 +49,24 @@ disallowed-names = []
# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
disallowed-types = [
# Use the faster & simpler non-poisonable primitives in `parking_lot` instead
"std::sync::Mutex",
"std::sync::RwLock",
"std::sync::Condvar",
# "std::sync::Once", # enabled for now as the `log_once` macro uses it internally
"ring::digest::SHA1_FOR_LEGACY_USE_ONLY", # SHA1 is cryptographically broken
"winit::dpi::LogicalSize", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account
"winit::dpi::LogicalPosition", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account
{ path = "std::sync::Condvar", reason = "Use parking_lot instead" },
{ path = "std::sync::Mutex", reason = "Use epaint::mutex instead" },
{ path = "std::sync::RwLock", reason = "Use epaint::mutex instead" },
{ path = "winit::dpi::LogicalPosition", reason = "We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account" },
{ path = "winit::dpi::LogicalSize", reason = "We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account" },
# "std::sync::Once", # enabled for now as the `log_once` macro uses it internally
]
# -----------------------------------------------------------------------------
# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
doc-valid-idents = [
# You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
"AccessKit",
"WebGL",
"WebGL1",
"WebGL2",
"WebGPU",
"VirtualBox",
"..",
# You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
"AccessKit",
"WebGL",
"WebGL1",
"WebGL2",
"WebGPU",
"VirtualBox",
"..",
]

View File

@@ -6,6 +6,38 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.2 - 2025-11-13
Nothing new
## 0.33.0 - 2025-10-09
* Align `Color32` to 4 bytes [#7318](https://github.com/emilk/egui/pull/7318) by [@anti-social](https://github.com/anti-social)
* Make the `hex_color` macro `const` [#7444](https://github.com/emilk/egui/pull/7444) by [@YgorSouza](https://github.com/YgorSouza)
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
## 0.32.3 - 2025-09-12
Nothing new
## 0.32.2 - 2025-09-04
Nothing new
## 0.32.1 - 2025-08-15
Nothing new
## 0.32.0 - 2025-07-10
* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk)
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
* Make `Hsva` derive serde [#7132](https://github.com/emilk/egui/pull/7132) by [@bircni](https://github.com/bircni)
## 0.31.1 - 2025-03-05
Nothing new
## 0.31.0 - 2025-02-04
* Add `Color32::CYAN` and `Color32::MAGENTA` [#5663](https://github.com/emilk/egui/pull/5663) by [@juancampa](https://github.com/juancampa)

View File

@@ -1,10 +1,7 @@
[package]
name = "ecolor"
version.workspace = true
authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
"Andreas Reich <reichandreas@gmx.de>",
]
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>", "Andreas Reich <reichandreas@gmx.de>"]
description = "Color structs and color conversion utilities"
edition.workspace = true
rust-version.workspace = true
@@ -39,10 +36,10 @@ emath.workspace = true
bytemuck = { workspace = true, optional = true, features = ["derive"] }
## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries.
cint = { version = "0.3.1", optional = true }
cint = { workspace = true, optional = true }
## Enable the [`hex_color`] macro.
color-hex = { version = "0.2.0", optional = true }
color-hex = { workspace = true, optional = true }
## Enable this when generating docs.
document-features = { workspace = true, optional = true }

View File

@@ -8,4 +8,6 @@
A simple color storage and conversion library.
Made for [`egui`](https://github.com/emilk/egui/).
This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/).
If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead.

View File

@@ -1,4 +1,4 @@
use super::{linear_f32_from_linear_u8, linear_u8_from_linear_f32, Color32, Hsva, HsvaGamma, Rgba};
use super::{Color32, Hsva, HsvaGamma, Rgba, linear_f32_from_linear_u8, linear_u8_from_linear_f32};
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
// ---- Color32 ----

View File

@@ -1,21 +1,37 @@
use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
/// This format is used for space-efficient color representation (32 bits).
///
/// Instead of manipulating this directly it is often better
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
///
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
/// Alpha channel is in linear space.
/// Internally this uses 0-255 gamma space `sRGBA` color with _premultiplied alpha_.
///
/// The special value of alpha=0 means the color is to be treated as an additive color.
/// It's the non-linear ("gamma") values that are multiplied with the alpha.
///
/// Premultiplied alpha means that the color values have been pre-multiplied with the alpha (opacity).
/// This is in contrast with "normal" RGBA, where the alpha is _separate_ (or "unmultiplied").
/// Using premultiplied alpha has some advantages:
/// * It allows encoding additive colors
/// * It is the better way to blend colors, e.g. when filtering texture colors
/// * Because the above, it is the better way to encode colors in a GPU texture
///
/// The color space is assumed to be [sRGB](https://en.wikipedia.org/wiki/SRGB).
///
/// All operations on `Color32` are done in "gamma space" (see <https://en.wikipedia.org/wiki/SRGB>).
/// This is not physically correct, but it is fast and sometimes more perceptually even than linear space.
/// If you instead want to perform these operations in linear-space color, use [`Rgba`].
///
/// An `alpha=0` means the color is to be treated as an additive color.
#[repr(C)]
#[repr(align(4))]
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
pub struct Color32(pub(crate) [u8; 4]);
impl std::fmt::Debug for Color32 {
/// Prints the contents with premultiplied alpha!
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let [r, g, b, a] = self.0;
write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
@@ -87,44 +103,49 @@ impl Color32 {
/// i.e. often taken to mean "no color".
pub const PLACEHOLDER: Self = Self::from_rgba_premultiplied(64, 254, 0, 128);
#[deprecated = "Renamed to PLACEHOLDER"]
pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER;
/// From RGB with alpha of 255 (opaque).
#[inline]
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self([r, g, b, 255])
}
/// From RGB into an additive color (will make everything it blend with brighter).
#[inline]
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
Self([r, g, b, 0])
}
/// From `sRGBA` with premultiplied alpha.
///
/// You likely want to use [`Self::from_rgba_unmultiplied`] instead.
#[inline]
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
Self([r, g, b, a])
}
/// From `sRGBA` WITHOUT premultiplied alpha.
/// From `sRGBA` with separate alpha.
///
/// This is a "normal" RGBA value that you would find in a color picker or a table somewhere.
///
/// You can use [`Self::to_srgba_unmultiplied`] to get back these values,
/// but for transparent colors what you get back might be slightly different (rounding errors).
#[inline]
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
use std::sync::OnceLock;
match a {
// common-case optimization
// common-case optimization:
0 => Self::TRANSPARENT,
// common-case optimization
// common-case optimization:
255 => Self::from_rgb(r, g, b),
a => {
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
let lut = LOOKUP_TABLE.get_or_init(|| {
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8};
(0..=u16::MAX)
.map(|i| {
let [value, alpha] = i.to_ne_bytes();
let value_lin = linear_f32_from_gamma_u8(value);
let alpha_lin = linear_f32_from_linear_u8(alpha);
gamma_u8_from_linear_f32(value_lin * alpha_lin)
fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
})
.collect()
});
@@ -136,22 +157,47 @@ impl Color32 {
}
}
/// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
///
/// It is slightly slower when operating on non-const data.
#[inline]
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
match a {
// common-case optimization:
0 => Self::TRANSPARENT,
// common-case optimization:
255 => Self::from_rgb(r, g, b),
a => {
let r = fast_round(r as f32 * linear_f32_from_linear_u8(a));
let g = fast_round(g as f32 * linear_f32_from_linear_u8(a));
let b = fast_round(b as f32 * linear_f32_from_linear_u8(a));
Self::from_rgba_premultiplied(r, g, b, a)
}
}
}
/// Opaque gray.
#[doc(alias = "from_grey")]
#[inline]
pub const fn from_gray(l: u8) -> Self {
Self([l, l, l, 255])
}
/// Black with the given opacity.
#[inline]
pub const fn from_black_alpha(a: u8) -> Self {
Self([0, 0, 0, a])
}
/// White with the given opacity.
#[inline]
pub fn from_white_alpha(a: u8) -> Self {
Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into()
Self([a, a, a, a])
}
/// Additive white.
#[inline]
pub const fn from_additive_luminance(l: u8) -> Self {
Self([l, l, l, 0])
@@ -162,21 +208,25 @@ impl Color32 {
self.a() == 255
}
/// Red component multiplied by alpha.
#[inline]
pub const fn r(&self) -> u8 {
self.0[0]
}
/// Green component multiplied by alpha.
#[inline]
pub const fn g(&self) -> u8 {
self.0[1]
}
/// Blue component multiplied by alpha.
#[inline]
pub const fn b(&self) -> u8 {
self.0[2]
}
/// Alpha (opacity).
#[inline]
pub const fn a(&self) -> u8 {
self.0[3]
@@ -213,9 +263,26 @@ impl Color32 {
(self.r(), self.g(), self.b(), self.a())
}
/// Convert to a normal "unmultiplied" RGBA color (i.e. with separate alpha).
///
/// This will unmultiply the alpha.
///
/// This is the inverse of [`Self::from_rgba_unmultiplied`],
/// but due to precision problems it may return slightly different values for transparent colors.
#[inline]
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
Rgba::from(*self).to_srgba_unmultiplied()
let [r, g, b, a] = self.to_array();
match a {
// Common-case optimization.
0 | 255 => self.to_array(),
a => {
let factor = 255.0 / a as f32;
let r = fast_round(factor * r as f32);
let g = fast_round(factor * g as f32);
let b = fast_round(factor * b as f32);
[r, g, b, a]
}
}
}
/// Multiply with 0.5 to make color half as opaque, perceptually.
@@ -225,7 +292,10 @@ impl Color32 {
/// This is perceptually even, and faster that [`Self::linear_multiply`].
#[inline]
pub fn gamma_multiply(self, factor: f32) -> Self {
debug_assert!(0.0 <= factor && factor.is_finite());
debug_assert!(
0.0 <= factor && factor.is_finite(),
"factor should be finite, but was {factor}"
);
let Self([r, g, b, a]) = self;
Self([
(r as f32 * factor + 0.5) as u8,
@@ -258,7 +328,10 @@ impl Color32 {
/// You likely want to use [`Self::gamma_multiply`] instead.
#[inline]
pub fn linear_multiply(self, factor: f32) -> Self {
debug_assert!(0.0 <= factor && factor.is_finite());
debug_assert!(
0.0 <= factor && factor.is_finite(),
"factor should be finite, but was {factor}"
);
// As an unfortunate side-effect of using premultiplied alpha
// we need a somewhat expensive conversion to linear space and back.
Rgba::from(self).multiply(factor).into()
@@ -291,10 +364,18 @@ impl Color32 {
)
}
/// Blend two colors, so that `self` is behind the argument.
/// Blend two colors in gamma space, so that `self` is behind the argument.
pub fn blend(self, on_top: Self) -> Self {
self.gamma_multiply_u8(255 - on_top.a()) + on_top
}
/// Intensity of the color.
///
/// Returns a value in the range 0-1.
/// The brighter the color, the closer to 1.
pub fn intensity(&self) -> f32 {
(self.r() as f32 * 0.299 + self.g() as f32 * 0.587 + self.b() as f32 * 0.114) / 255.0
}
}
impl std::ops::Mul for Color32 {
@@ -325,3 +406,133 @@ impl std::ops::Add for Color32 {
])
}
}
#[cfg(test)]
mod test {
use super::*;
fn test_rgba() -> impl Iterator<Item = [u8; 4]> {
[
[0, 0, 0, 0],
[0, 0, 0, 255],
[10, 0, 30, 0],
[10, 0, 30, 40],
[10, 100, 200, 0],
[10, 100, 200, 100],
[10, 100, 200, 200],
[10, 100, 200, 255],
[10, 100, 200, 40],
[10, 20, 0, 0],
[10, 20, 0, 255],
[10, 20, 30, 255],
[10, 20, 30, 40],
[255, 255, 255, 0],
[255, 255, 255, 255],
]
.into_iter()
}
#[test]
fn test_color32_additive() {
let opaque = Color32::from_rgb(40, 50, 60);
let additive = Color32::from_rgb(255, 127, 10).additive();
assert_eq!(additive.blend(opaque), opaque, "opaque on top of additive");
assert_eq!(
opaque.blend(additive),
Color32::from_rgb(255, 177, 70),
"additive on top of opaque"
);
}
#[test]
fn test_color32_blend_vs_gamma_blend() {
let opaque = Color32::from_rgb(0x60, 0x60, 0x60);
let transparent = Color32::from_rgba_unmultiplied(168, 65, 65, 79);
assert_eq!(
transparent.blend(opaque),
opaque,
"Opaque on top of transparent"
);
// Blending in gamma-space is the de-facto standard almost everywhere.
// Browsers and most image editors do it, and so it is what users expect.
assert_eq!(
opaque.blend(transparent),
Color32::from_rgb(
blend(0x60, 168, 79),
blend(0x60, 65, 79),
blend(0x60, 65, 79)
),
"Transparent on top of opaque"
);
fn blend(dest: u8, src: u8, alpha: u8) -> u8 {
let src = src as f32 / 255.0;
let dest = dest as f32 / 255.0;
let alpha = alpha as f32 / 255.0;
fast_round((src * alpha + dest * (1.0 - alpha)) * 255.0)
}
}
#[test]
fn color32_unmultiplied_round_trip() {
for in_rgba in test_rgba() {
let [r, g, b, a] = in_rgba;
if a == 0 {
continue;
}
let c = Color32::from_rgba_unmultiplied(r, g, b, a);
let out_rgba = c.to_srgba_unmultiplied();
if a == 255 {
assert_eq!(in_rgba, out_rgba);
} else {
// There will be small rounding errors whenever the alpha is not 0 or 255,
// because we multiply and then unmultiply the alpha.
for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) {
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
}
}
}
}
#[test]
fn from_black_white_alpha() {
for a in 0..=255 {
assert_eq!(
Color32::from_white_alpha(a),
Color32::from_rgba_unmultiplied(255, 255, 255, a)
);
assert_eq!(
Color32::from_white_alpha(a),
Color32::WHITE.gamma_multiply_u8(a)
);
assert_eq!(
Color32::from_black_alpha(a),
Color32::from_rgba_unmultiplied(0, 0, 0, a)
);
assert_eq!(
Color32::from_black_alpha(a),
Color32::BLACK.gamma_multiply_u8(a)
);
}
}
#[test]
fn to_from_rgba() {
for [r, g, b, a] in test_rgba() {
let original = Color32::from_rgba_unmultiplied(r, g, b, a);
let constfn = Color32::from_rgba_unmultiplied_const(r, g, b, a);
let rgba = Rgba::from(original);
let back = Color32::from(rgba);
assert_eq!(back, original);
assert_eq!(constfn, original);
}
assert_eq!(
Color32::from(Rgba::from_rgba_unmultiplied(1.0, 0.0, 0.0, 0.5)),
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
);
}
}

View File

@@ -31,10 +31,11 @@
/// let _ = ecolor::hex_color!("#20212x");
/// ```
///
/// The macro cannot be used in a `const` context.
/// The macro can be used in a `const` context.
///
/// ```compile_fail
/// ```
/// const COLOR: ecolor::Color32 = ecolor::hex_color!("#202122");
/// assert_eq!(COLOR, ecolor::Color32::from_rgb(0x20, 0x21, 0x22));
/// ```
#[macro_export]
macro_rules! hex_color {
@@ -42,7 +43,7 @@ macro_rules! hex_color {
let array = $crate::color_hex::color_from_hex!($s);
match array.as_slice() {
[r, g, b] => $crate::Color32::from_rgb(*r, *g, *b),
[r, g, b, a] => $crate::Color32::from_rgba_unmultiplied(*r, *g, *b, *a),
[r, g, b, a] => $crate::Color32::from_rgba_unmultiplied_const(*r, *g, *b, *a),
_ => panic!("Invalid hex color length: expected 3 (RGB) or 4 (RGBA) bytes"),
}
}};

View File

@@ -90,14 +90,15 @@ impl HexColor {
let [r, gb] = u16::from_str_radix(s, 16)
.map_err(ParseHexColorError::InvalidInt)?
.to_be_bytes();
let [r, g, b] = [r, gb >> 4, gb & 0x0f].map(|u| u << 4 | u);
let [r, g, b] = [r, gb >> 4, gb & 0x0f].map(|u| (u << 4) | u);
Ok(Self::Hex3(Color32::from_rgb(r, g, b)))
}
4 => {
let [r_g, b_a] = u16::from_str_radix(s, 16)
.map_err(ParseHexColorError::InvalidInt)?
.to_be_bytes();
let [r, g, b, a] = [r_g >> 4, r_g & 0x0f, b_a >> 4, b_a & 0x0f].map(|u| u << 4 | u);
let [r, g, b, a] =
[r_g >> 4, r_g & 0x0f, b_a >> 4, b_a & 0x0f].map(|u| (u << 4) | u);
Ok(Self::Hex4(Color32::from_rgba_unmultiplied(r, g, b, a)))
}
6 => {
@@ -207,17 +208,22 @@ mod tests {
#[test]
fn hex_string_round_trip() {
use Color32 as C;
let cases = [
C::from_rgba_unmultiplied(10, 20, 30, 0),
C::from_rgba_unmultiplied(10, 20, 30, 40),
C::from_rgba_unmultiplied(10, 20, 30, 255),
C::from_rgba_unmultiplied(0, 20, 30, 0),
C::from_rgba_unmultiplied(10, 0, 30, 40),
C::from_rgba_unmultiplied(10, 20, 0, 255),
[0, 20, 30, 0],
[10, 0, 30, 40],
[10, 100, 200, 0],
[10, 100, 200, 100],
[10, 100, 200, 200],
[10, 100, 200, 255],
[10, 100, 200, 40],
[10, 20, 0, 255],
[10, 20, 30, 0],
[10, 20, 30, 255],
[10, 20, 30, 40],
];
for color in cases {
assert_eq!(C::from_hex(color.to_hex().as_str()), Ok(color));
for [r, g, b, a] in cases {
let color = Color32::from_rgba_unmultiplied(r, g, b, a);
assert_eq!(Color32::from_hex(color.to_hex().as_str()), Ok(color));
}
}
}

View File

@@ -1,10 +1,10 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
linear_u8_from_linear_f32, Color32, Rgba,
Color32, Rgba, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32,
};
/// Hue, saturation, value, alpha. All in the range [0, 1].
/// No premultiplied alpha.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Hsva {
/// hue 0-1
@@ -29,30 +29,20 @@ impl Hsva {
/// From `sRGBA` with premultiplied alpha
#[inline]
pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self {
Self::from_rgba_premultiplied(
linear_f32_from_gamma_u8(r),
linear_f32_from_gamma_u8(g),
linear_f32_from_gamma_u8(b),
linear_f32_from_linear_u8(a),
)
Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
}
/// From `sRGBA` without premultiplied alpha
#[inline]
pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self {
Self::from_rgba_unmultiplied(
linear_f32_from_gamma_u8(r),
linear_f32_from_gamma_u8(g),
linear_f32_from_gamma_u8(b),
linear_f32_from_linear_u8(a),
)
Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
}
/// From linear RGBA with premultiplied alpha
#[inline]
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![allow(clippy::many_single_char_names)]
if a == 0.0 {
if a <= 0.0 {
if r == 0.0 && b == 0.0 && a == 0.0 {
Self::default()
} else {
@@ -152,13 +142,7 @@ impl Hsva {
#[inline]
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
let [r, g, b, a] = self.to_rgba_premultiplied();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
linear_u8_from_linear_f32(a),
]
Color32::from(*self).to_array()
}
/// To gamma-space 0-255.
@@ -250,7 +234,7 @@ pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
}
#[test]
#[ignore] // a bit expensive
#[ignore = "too expensive"]
fn test_hsv_roundtrip() {
for r in 0..=255 {
for g in 0..=255 {

View File

@@ -1,4 +1,4 @@
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
use crate::{Color32, Hsva, Rgba, gamma_from_linear, linear_from_gamma};
/// Like Hsva but with the `v` value (brightness) being gamma corrected
/// so that it is somewhat perceptually even.

View File

@@ -1,9 +1,20 @@
//! Color conversions and types.
//!
//! This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/).
//!
//! If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead.
//!
//! If you want a compact color representation, use [`Color32`].
//! If you want to manipulate RGBA colors use [`Rgba`].
//! If you want to manipulate RGBA colors in linear space use [`Rgba`].
//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
//!
//! ## Conventions
//! The word "gamma" or "srgb" is used to refer to values in the non-linear space defined by
//! [the sRGB transfer function](https://en.wikipedia.org/wiki/SRGB).
//! We use `u8` for anything in the "gamma" space.
//!
//! We use `f32` in 0-1 range for anything in the linear space.
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
@@ -39,23 +50,46 @@ pub use hex_color_runtime::*;
impl From<Color32> for Rgba {
fn from(srgba: Color32) -> Self {
Self([
linear_f32_from_gamma_u8(srgba.0[0]),
linear_f32_from_gamma_u8(srgba.0[1]),
linear_f32_from_gamma_u8(srgba.0[2]),
linear_f32_from_linear_u8(srgba.0[3]),
])
let [r, g, b, a] = srgba.to_array();
if a == 0 {
// Additive, or completely transparent
Self([
linear_f32_from_gamma_u8(r),
linear_f32_from_gamma_u8(g),
linear_f32_from_gamma_u8(b),
0.0,
])
} else {
let a = linear_f32_from_linear_u8(a);
Self([
linear_from_gamma(r as f32 / (255.0 * a)) * a,
linear_from_gamma(g as f32 / (255.0 * a)) * a,
linear_from_gamma(b as f32 / (255.0 * a)) * a,
a,
])
}
}
}
impl From<Rgba> for Color32 {
fn from(rgba: Rgba) -> Self {
Self([
gamma_u8_from_linear_f32(rgba.0[0]),
gamma_u8_from_linear_f32(rgba.0[1]),
gamma_u8_from_linear_f32(rgba.0[2]),
linear_u8_from_linear_f32(rgba.0[3]),
])
let [r, g, b, a] = rgba.to_array();
if a == 0.0 {
// Additive, or completely transparent
Self([
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
0,
])
} else {
Self([
fast_round(gamma_u8_from_linear_f32(r / a) as f32 * a),
fast_round(gamma_u8_from_linear_f32(g / a) as f32 * a),
fast_round(gamma_u8_from_linear_f32(b / a) as f32 * a),
linear_u8_from_linear_f32(a),
])
}
}
}
@@ -71,7 +105,7 @@ pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
/// linear [0, 255] -> linear [0, 1].
/// Useful for alpha-channel.
#[inline(always)]
pub fn linear_f32_from_linear_u8(a: u8) -> f32 {
pub const fn linear_f32_from_linear_u8(a: u8) -> f32 {
a as f32 / 255.0
}
@@ -96,7 +130,7 @@ pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
fast_round(a * 255.0)
}
fn fast_round(r: f32) -> u8 {
const fn fast_round(r: f32) -> u8 {
(r + 0.5) as _ // rust does a saturating cast since 1.45
}

View File

@@ -1,9 +1,8 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
linear_u8_from_linear_f32,
};
use crate::Color32;
/// 0-1 linear space `RGBA` color with premultiplied alpha.
///
/// See [`crate::Color32`] for explanation of what "premultiplied alpha" means.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -34,12 +33,11 @@ pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
} else if f.is_nan() {
state.write_u8(1);
} else {
use std::hash::Hash;
use std::hash::Hash as _;
f.to_bits().hash(state);
}
}
#[allow(clippy::derived_hash_with_manual_eq)]
impl std::hash::Hash for Rgba {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
@@ -70,20 +68,12 @@ impl Rgba {
#[inline]
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
let r = linear_f32_from_gamma_u8(r);
let g = linear_f32_from_gamma_u8(g);
let b = linear_f32_from_gamma_u8(b);
let a = linear_f32_from_linear_u8(a);
Self::from_rgba_premultiplied(r, g, b, a)
Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
}
#[inline]
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
let r = linear_f32_from_gamma_u8(r);
let g = linear_f32_from_gamma_u8(g);
let b = linear_f32_from_gamma_u8(b);
let a = linear_f32_from_linear_u8(a);
Self::from_rgba_premultiplied(r * a, g * a, b * a, a)
Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
}
#[inline]
@@ -99,15 +89,24 @@ impl Rgba {
#[inline]
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
debug_assert!(0.0 <= l && l <= 1.0);
debug_assert!(0.0 <= a && a <= 1.0);
debug_assert!(
0.0 <= l && l <= 1.0,
"l should be in the range [0, 1], but was {l}"
);
debug_assert!(
0.0 <= a && a <= 1.0,
"a should be in the range [0, 1], but was {a}"
);
Self([l * a, l * a, l * a, a])
}
/// Transparent black
#[inline]
pub fn from_black_alpha(a: f32) -> Self {
debug_assert!(0.0 <= a && a <= 1.0);
debug_assert!(
0.0 <= a && a <= 1.0,
"a should be in the range [0, 1], but was {a}"
);
Self([0.0, 0.0, 0.0, a])
}
@@ -211,13 +210,12 @@ impl Rgba {
/// unmultiply the alpha
#[inline]
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
let [r, g, b, a] = self.to_rgba_unmultiplied();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
linear_u8_from_linear_f32(a.abs()),
]
crate::Color32::from(*self).to_srgba_unmultiplied()
}
/// Blend two colors in linear space, so that `self` is behind the argument.
pub fn blend(self, on_top: Self) -> Self {
self.multiply(1.0 - on_top.a()) + on_top
}
}
@@ -276,3 +274,72 @@ impl std::ops::Mul<Rgba> for f32 {
])
}
}
#[cfg(test)]
mod test {
use super::*;
fn test_rgba() -> impl Iterator<Item = [u8; 4]> {
[
[0, 0, 0, 0],
[0, 0, 0, 255],
[10, 0, 30, 0],
[10, 0, 30, 40],
[10, 100, 200, 0],
[10, 100, 200, 100],
[10, 100, 200, 200],
[10, 100, 200, 255],
[10, 100, 200, 40],
[10, 20, 0, 0],
[10, 20, 0, 255],
[10, 20, 30, 255],
[10, 20, 30, 40],
[255, 255, 255, 0],
[255, 255, 255, 255],
]
.into_iter()
}
#[test]
fn test_rgba_blend() {
let opaque = Rgba::from_rgb(0.4, 0.5, 0.6);
let transparent = Rgba::from_rgb(1.0, 0.5, 0.0).multiply(0.3);
assert_eq!(
transparent.blend(opaque),
opaque,
"Opaque on top of transparent"
);
assert_eq!(
opaque.blend(transparent),
Rgba::from_rgb(
0.7 * 0.4 + 0.3 * 1.0,
0.7 * 0.5 + 0.3 * 0.5,
0.7 * 0.6 + 0.3 * 0.0
),
"Transparent on top of opaque"
);
}
#[test]
fn test_rgba_roundtrip() {
for in_rgba in test_rgba() {
let [r, g, b, a] = in_rgba;
if a == 0 {
continue;
}
let rgba = Rgba::from_srgba_unmultiplied(r, g, b, a);
let out_rgba = rgba.to_srgba_unmultiplied();
if a == 255 {
assert_eq!(in_rgba, out_rgba);
} else {
// There will be small rounding errors whenever the alpha is not 0 or 255,
// because we multiply and then unmultiply the alpha.
for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) {
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
}
}
}
}
}

View File

@@ -7,6 +7,73 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.2 - 2025-11-13
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
* Make sure `native_pixels_per_point` is set during app creation [#7683](https://github.com/emilk/egui/pull/7683) by [@emilk](https://github.com/emilk)
## 0.33.0 - 2025-10-09
### ⭐ Added
* Add an option to limit the repaint rate in the web runner [#7482](https://github.com/emilk/egui/pull/7482) by [@s-nie](https://github.com/s-nie)
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)
* Add support for the safe area on iOS [#7578](https://github.com/emilk/egui/pull/7578) by [@irh](https://github.com/irh)
### 🔧 Changed
* Replace `winapi` with `windows-sys` crate [#7416](https://github.com/emilk/egui/pull/7416) by [@unlimitedsola](https://github.com/unlimitedsola)
* Prevent default action on command-comma in eframe web [#7547](https://github.com/emilk/egui/pull/7547) by [@emilk](https://github.com/emilk)
* Warn if `DYLD_LIBRARY_PATH` is set and we find no wgpu adapter [#7572](https://github.com/emilk/egui/pull/7572) by [@emilk](https://github.com/emilk)
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
### 🐛 Fixed
* Properly end winit event loop [#7565](https://github.com/emilk/egui/pull/7565) by [@tye-exe](https://github.com/tye-exe)
* Fix eframe window not being focused on mac on startup [#7593](https://github.com/emilk/egui/pull/7593) by [@emilk](https://github.com/emilk)
* Fix black flash on start in glow eframe backend [#7616](https://github.com/emilk/egui/pull/7616) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.32.3 - 2025-09-12
Nothing new
## 0.32.2 - 2025-09-04
Nothing new
## 0.32.1 - 2025-08-15
* Enable wgpu default features in eframe / egui_wgpu default features [#7344](https://github.com/emilk/egui/pull/7344) by [@lucasmerlin](https://github.com/lucasmerlin)
* Request a redraw when the url change through the `popstate` event listener [#7403](https://github.com/emilk/egui/pull/7403) by [@irevoire](https://github.com/irevoire)
## 0.32.0 - 2025-07-10
### ⭐ Added
* Add pointer events and focus handling for apps run in a Shadow DOM [#5627](https://github.com/emilk/egui/pull/5627) by [@xxvvii](https://github.com/xxvvii)
* MacOS: Add `movable_by_window_background` option to viewport [#5412](https://github.com/emilk/egui/pull/5412) by [@jim-ec](https://github.com/jim-ec)
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
* Add external eventloop support [#6750](https://github.com/emilk/egui/pull/6750) by [@wpbrown](https://github.com/wpbrown)
### 🔧 Changed
* Update MSRV to 1.85 [#7279](https://github.com/emilk/egui/pull/7279) by [@emilk](https://github.com/emilk)
* Use Rust edition 2024 [#7280](https://github.com/emilk/egui/pull/7280) by [@emilk](https://github.com/emilk)
* Rename `should_propagate_event` and add `should_prevent_default` [#5779](https://github.com/emilk/egui/pull/5779) by [@th0rex](https://github.com/th0rex)
* Clarify platform-specific details for `Viewport` positioning [#5715](https://github.com/emilk/egui/pull/5715) by [@aspiringLich](https://github.com/aspiringLich)
* Enhance stability on Windows [#5723](https://github.com/emilk/egui/pull/5723) by [@rustbasic](https://github.com/rustbasic)
* Set `web-sys` min version to `0.3.73` [#5862](https://github.com/emilk/egui/pull/5862) by [@wareya](https://github.com/wareya)
* Bump `ron` to `0.10.1` [#6861](https://github.com/emilk/egui/pull/6861) by [@torokati44](https://github.com/torokati44)
* Disallow `accesskit` on Android NativeActivity, making `hello_android` working again [#6855](https://github.com/emilk/egui/pull/6855) by [@podusowski](https://github.com/podusowski)
* Respect and detect `prefers-color-scheme: no-preference` [#7293](https://github.com/emilk/egui/pull/7293) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Mark all keys as up if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
* Fix text distortion on mobile devices/browsers with `glow` backend [#6893](https://github.com/emilk/egui/pull/6893) by [@wareya](https://github.com/wareya)
* Workaround libpng crash on macOS by not creating `NSImage` from png data [#7252](https://github.com/emilk/egui/pull/7252) by [@Wumpf](https://github.com/Wumpf)
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
* Web: only consume copy/cut events if the canvas has focus [#7270](https://github.com/emilk/egui/pull/7270) by [@emilk](https://github.com/emilk)
## 0.31.1 - 2025-03-05
Nothing new
## 0.31.0 - 2025-02-04
* Web: Fix incorrect scale when moving to screen with new DPI [#5631](https://github.com/emilk/egui/pull/5631) by [@emilk](https://github.com/emilk)
* Re-enable IME support on Linux [#5198](https://github.com/emilk/egui/pull/5198) by [@YgorSouza](https://github.com/YgorSouza)

View File

@@ -5,19 +5,13 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively"
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
homepage = "https://github.com/emilk/egui/tree/main/crates/eframe"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"]
include = [
"../LICENSE-APACHE",
"../LICENSE-MIT",
"**/*.rs",
"Cargo.toml",
"data/icon.png",
]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
[package.metadata.docs.rs]
all-features = true
@@ -34,16 +28,15 @@ workspace = true
default = [
"accesskit",
"default_fonts",
"glow",
"wayland", # Required for Linux support (including CI!)
"wayland", # Required for Linux support (including CI!)
"web_screen_reader",
"wgpu",
"winit/default",
"x11",
"egui-wgpu?/fragile-send-sync-non-atomic-wasm",
]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
accesskit = ["egui-winit/accesskit"]
# Allow crates to choose an android-activity backend via Winit
# - It's important that most applications should not have to depend on android-activity directly, and can
@@ -59,17 +52,15 @@ android-native-activity = ["egui-winit/android-native-activity"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
##
## There is generally no need to enable both the `wgpu` and `glow` features,
## but if you do you can pick the renderer to use with [`NativeOptions::renderer`]
## and `WebOptions::renderer`.
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
## Enable saving app state to disk.
persistence = [
"dep:home",
"egui-winit/serde",
"egui/persistence",
"ron",
"serde",
]
persistence = ["dep:home", "egui-winit/serde", "egui/persistence", "ron", "serde"]
## Enables wayland support and fixes clipboard issue.
##
@@ -85,25 +76,30 @@ wayland = [
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
##
## For other platforms, use the `accesskit` feature instead.
web_screen_reader = [
"web-sys/SpeechSynthesis",
"web-sys/SpeechSynthesisUtterance",
]
web_screen_reader = ["web-sys/SpeechSynthesis", "web-sys/SpeechSynthesisUtterance"]
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
## Enable [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)).
##
## This overrides the `glow` feature.
## There is generally no need to enable both the `wgpu` and `glow` features,
## but if you do you can pick the renderer to use with [`NativeOptions::renderer`]
## and `WebOptions::renderer`.
##
## By default, only WebGPU is enabled on web.
## If you want to enable WebGL, you need to turn on the `webgl` feature of crate `wgpu`:
##
## ```toml
## wgpu = { version = "*", features = ["webgpu", "webgl"] }
## ```
## Switching from `wgpu (the default)` to `glow` can significantly reduce your binary size
## (including the .wasm of a web app).
## See <https://github.com/emilk/egui/issues/5889> for more details.
##
## By default, eframe will prefer WebGPU over WebGL, but
## you can configure this at run-time with [`NativeOptions::wgpu_options`].
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
wgpu = ["wgpu_no_default_features", "egui-wgpu/default"]
## This is exactly like the `wgpu` feature, but does NOT enable the default features of `wgpu` and `egui-wgpu`.
##
## This means that no `wgpu` backends are enabled. You will need to enable them yourself, e.g. like this:
##
## ```toml
## wgpu = { version = "*", features = ["dx12", "metal", "webgl"] }
## ```
wgpu_no_default_features = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
## Enables compiling for x11.
x11 = [
@@ -121,10 +117,7 @@ x11 = [
__screenshot = []
[dependencies]
egui = { workspace = true, default-features = false, features = [
"bytemuck",
"log",
] }
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
ahash.workspace = true
document-features.workspace = true
@@ -132,7 +125,7 @@ log.workspace = true
parking_lot.workspace = true
profiling.workspace = true
raw-window-handle.workspace = true
static_assertions = "1.1.0"
static_assertions.workspace = true
web-time.workspace = true
# Optional dependencies
@@ -145,45 +138,35 @@ serde = { workspace = true, optional = true }
# -------------------------------------------
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { workspace = true, default-features = false, features = [
"clipboard",
"links",
] }
egui-winit = { workspace = true, default-features = false, features = ["clipboard", "links"] }
image = { workspace = true, features = ["png"] } # Needed for app icon
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
# optional native:
egui-wgpu = { workspace = true, optional = true, features = [
"winit",
"capture",
] } # if wgpu is used, use it with winit
pollster = { workspace = true, optional = true } # needed for wgpu
glutin = { workspace = true, optional = true, default-features = false, features = [
"egl",
"wgl",
] }
glutin = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] }
glutin-winit = { workspace = true, optional = true, default-features = false, features = [
"egl",
"wgl",
] }
home = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true, features = [
# Let's enable some backends so that users can use `eframe` out-of-the-box
# without having to explicitly opt-in to backends
"metal",
"webgpu",
] }
wgpu = { workspace = true, optional = true }
# mac:
[target.'cfg(any(target_os = "macos"))'.dependencies]
objc2 = "0.5.1"
objc2-foundation = { version = "0.2.0", default-features = false, features = [
objc2.workspace = true
objc2-foundation = { workspace = true, default-features = false, features = [
"std",
"block2",
"NSData",
"NSString",
] }
objc2-app-kit = { version = "0.2.0", default-features = false, features = [
objc2-app-kit = { workspace = true, default-features = false, features = [
"std",
"NSApplication",
"NSImage",
@@ -194,11 +177,12 @@ objc2-app-kit = { version = "0.2.0", default-features = false, features = [
# windows:
[target.'cfg(any(target_os = "windows"))'.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_UI_Shell",
"Win32_System_Com",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }
# -------------------------------------------
@@ -206,8 +190,8 @@ windows-sys = { workspace = true, features = [
[target.'cfg(target_arch = "wasm32")'.dependencies]
bytemuck.workspace = true
image = { workspace = true, features = ["png"] } # For copying images
js-sys = "0.3"
percent-encoding = "2.1"
js-sys.workspace = true
percent-encoding.workspace = true
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
web-sys = { workspace = true, features = [
@@ -253,6 +237,7 @@ web-sys = { workspace = true, features = [
"ResizeObserverEntry",
"ResizeObserverOptions",
"ResizeObserverSize",
"ShadowRoot",
"Storage",
"Touch",
"TouchEvent",
@@ -266,13 +251,9 @@ web-sys = { workspace = true, features = [
] }
# optional web:
egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit
wgpu = { workspace = true, optional = true, features = [
# Let's enable some backends so that users can use `eframe` out-of-the-box
# without having to explicitly opt-in to backends
"webgpu",
] }
egui-wgpu = { workspace = true, optional = true, features = ["capture"] } # if wgpu is used, use it without (!) winit
wgpu = { workspace = true, optional = true }
# Native dev dependencies for testing
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
directories = "5"
directories.workspace = true

View File

@@ -7,7 +7,7 @@
`eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (for Linux, Mac, Windows, and Android) or as a web app (using [Wasm](https://en.wikipedia.org/wiki/WebAssembly)).
To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
There is also a tutorial video at <https://www.youtube.com/watch?v=NtUkr_z7l84>.
@@ -16,7 +16,7 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
---
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit).
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit).
To use on Linux, first run:
@@ -24,14 +24,14 @@ To use on Linux, first run:
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
```
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You need to either use `edition = "2024"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
You can opt-in to the using [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
## Alternatives
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/crates/egui_glow/examples/pure_glow.rs>.
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/main/crates/egui_glow/examples/pure_glow.rs>.
## Limitations when running egui on the web

View File

@@ -10,7 +10,7 @@
use std::any::Any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use crate::native::winit_integration::UserEvent;
#[cfg(not(target_arch = "wasm32"))]
@@ -22,7 +22,7 @@ use raw_window_handle::{
use static_assertions::assert_not_impl_any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
/// Hook into the building of an event loop before it is run
@@ -30,7 +30,7 @@ pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
/// You can configure any platform specific details required on top of the default configuration
/// done by `EFrame`.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
/// Hook into the building of a the native window.
@@ -38,7 +38,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
/// You can configure any platform specific details required on top of the default configuration
/// done by `eframe`.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
type DynError = Box<dyn std::error::Error + Send + Sync>;
@@ -79,7 +79,7 @@ pub struct CreationContext<'s> {
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
///
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
/// Raw platform window handle
@@ -91,7 +91,7 @@ pub struct CreationContext<'s> {
pub(crate) raw_display_handle: Result<RawDisplayHandle, HandleError>,
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasWindowHandle for CreationContext<'_> {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
@@ -100,7 +100,7 @@ impl HasWindowHandle for CreationContext<'_> {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasDisplayHandle for CreationContext<'_> {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
@@ -121,7 +121,7 @@ impl CreationContext<'_> {
gl: None,
#[cfg(feature = "glow")]
get_proc_address: None,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
#[cfg(not(target_arch = "wasm32"))]
raw_window_handle: Err(HandleError::NotSupported),
@@ -133,7 +133,7 @@ impl CreationContext<'_> {
// ----------------------------------------------------------------------------
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
pub trait App {
/// Called each time the UI needs repainting, which may be many times per second.
///
@@ -317,7 +317,7 @@ pub struct NativeOptions {
pub hardware_acceleration: HardwareAcceleration,
/// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub renderer: Renderer,
/// This controls what happens when you close the main eframe window.
@@ -340,7 +340,7 @@ pub struct NativeOptions {
/// event loop before it is run.
///
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub event_loop_builder: Option<EventLoopBuilderHook>,
/// Hook into the building of a window.
@@ -349,7 +349,7 @@ pub struct NativeOptions {
/// window appearance.
///
/// Note: A [`NativeOptions`] clone will not include any `window_builder` hook.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub window_builder: Option<WindowBuilderHook>,
#[cfg(feature = "glow")]
@@ -367,7 +367,7 @@ pub struct NativeOptions {
pub centered: bool,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
/// Controls whether or not the native window position and size will be
@@ -404,13 +404,13 @@ impl Clone for NativeOptions {
Self {
viewport: self.viewport.clone(),
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
event_loop_builder: None, // Skip any builder callbacks if cloning
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
window_builder: None, // Skip any builder callbacks if cloning
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: self.wgpu_options.clone(),
persistence_path: self.persistence_path.clone(),
@@ -435,15 +435,15 @@ impl Default for NativeOptions {
stencil_buffer: 0,
hardware_acceleration: HardwareAcceleration::Preferred,
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
renderer: Renderer::default(),
run_and_return: true,
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
event_loop_builder: None,
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
window_builder: None,
#[cfg(feature = "glow")]
@@ -451,7 +451,7 @@ impl Default for NativeOptions {
centered: false,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
persist_window: true,
@@ -471,6 +471,10 @@ impl Default for NativeOptions {
/// Options when using `eframe` in a web page.
#[cfg(target_arch = "wasm32")]
pub struct WebOptions {
/// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub renderer: Renderer,
/// Sets the number of bits in the depth buffer.
///
/// `egui` doesn't need the depth buffer, so the default value is 0.
@@ -484,7 +488,7 @@ pub struct WebOptions {
pub webgl_context_option: WebGlContextOption,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
/// Controls whether to apply dithering to minimize banding artifacts.
@@ -499,27 +503,43 @@ pub struct WebOptions {
/// If the web event corresponding to an egui event should be propagated
/// to the rest of the web page.
///
/// The default is `false`, meaning
/// The default is `true`, meaning
/// [`stopPropagation`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)
/// is called on every event.
pub should_propagate_event: Box<dyn Fn(&egui::Event) -> bool>,
/// is called on every event, and the event is not propagated to the rest of the web page.
pub should_stop_propagation: Box<dyn Fn(&egui::Event) -> bool>,
/// Whether the web event corresponding to an egui event should have `prevent_default` called
/// on it or not.
///
/// Defaults to true.
pub should_prevent_default: Box<dyn Fn(&egui::Event) -> bool>,
/// Maximum rate at which to repaint. This can be used to artificially reduce the repaint rate below
/// vsync in order to save resources.
pub max_fps: Option<u32>,
}
#[cfg(target_arch = "wasm32")]
impl Default for WebOptions {
fn default() -> Self {
Self {
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
renderer: Renderer::default(),
depth_buffer: 0,
#[cfg(feature = "glow")]
webgl_context_option: WebGlContextOption::BestFirst,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
dithering: true,
should_propagate_event: Box::new(|_| false),
should_stop_propagation: Box::new(|_| true),
should_prevent_default: Box::new(|_| true),
max_fps: None,
}
}
}
@@ -548,7 +568,7 @@ pub enum WebGlContextOption {
/// What rendering backend to use.
///
/// You need to enable the "glow" and "wgpu" features to have a choice.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
@@ -558,47 +578,49 @@ pub enum Renderer {
Glow,
/// Use [`egui_wgpu`] renderer for [`wgpu`](https://github.com/gfx-rs/wgpu).
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
Wgpu,
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl Default for Renderer {
fn default() -> Self {
#[cfg(not(feature = "glow"))]
#[cfg(not(feature = "wgpu"))]
compile_error!("eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'");
#[cfg(not(feature = "wgpu_no_default_features"))]
compile_error!(
"eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"
);
#[cfg(feature = "glow")]
#[cfg(not(feature = "wgpu"))]
#[cfg(not(feature = "wgpu_no_default_features"))]
return Self::Glow;
#[cfg(not(feature = "glow"))]
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
return Self::Wgpu;
// By default, only the `glow` feature is enabled, so if the user added `wgpu` to the feature list
// they probably wanted to use wgpu:
// It's weird that the user has enabled both glow and wgpu,
// but let's pick the better of the two (wgpu):
#[cfg(feature = "glow")]
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
return Self::Wgpu;
}
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl std::fmt::Display for Renderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "glow")]
Self::Glow => "glow".fmt(f),
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
Self::Wgpu => "wgpu".fmt(f),
}
}
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl std::str::FromStr for Renderer {
type Err = String;
@@ -607,10 +629,12 @@ impl std::str::FromStr for Renderer {
#[cfg(feature = "glow")]
"glow" => Ok(Self::Glow),
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
"wgpu" => Ok(Self::Wgpu),
_ => Err(format!("eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."))
_ => Err(format!(
"eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."
)),
}
}
}
@@ -638,7 +662,7 @@ pub struct Frame {
Option<Box<dyn FnMut(glow::Texture) -> egui::TextureId>>,
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
#[doc(hidden)]
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
@@ -655,7 +679,7 @@ pub struct Frame {
#[cfg(not(target_arch = "wasm32"))]
assert_not_impl_any!(Frame: Clone);
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasWindowHandle for Frame {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
@@ -664,7 +688,7 @@ impl HasWindowHandle for Frame {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasDisplayHandle for Frame {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
@@ -688,7 +712,7 @@ impl Frame {
#[cfg(not(target_arch = "wasm32"))]
raw_window_handle: Err(HandleError::NotSupported),
storage: None,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
}
}
@@ -696,7 +720,7 @@ impl Frame {
/// True if you are in a web environment.
///
/// Equivalent to `cfg!(target_arch = "wasm32")`
#[allow(clippy::unused_self)]
#[expect(clippy::unused_self)]
pub fn is_web(&self) -> bool {
cfg!(target_arch = "wasm32")
}
@@ -747,7 +771,7 @@ impl Frame {
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
///
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
self.wgpu_render_state.as_ref()
}
@@ -882,16 +906,15 @@ pub trait Storage {
#[cfg(feature = "ron")]
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
profiling::function_scope!(key);
storage
.get_string(key)
.and_then(|value| match ron::from_str(&value) {
Ok(value) => Some(value),
Err(err) => {
// This happens on when we break the format, e.g. when updating egui.
log::debug!("Failed to decode RON: {err}");
None
}
})
let value = storage.get_string(key)?;
match ron::from_str(&value) {
Ok(value) => Some(value),
Err(err) => {
// This happens on when we break the format, e.g. when updating egui.
log::debug!("Failed to decode RON: {err}");
None
}
}
}
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
@@ -900,7 +923,7 @@ pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, valu
profiling::function_scope!(key);
match ron::ser::to_string(value) {
Ok(string) => storage.set_string(key, string),
Err(err) => log::error!("eframe failed to encode data using ron: {}", err),
Err(err) => log::error!("eframe failed to encode data using ron: {err}"),
}
}

View File

@@ -3,7 +3,7 @@
//! If you are planning to write an app for web or native,
//! and want to use [`egui`] for everything, then `eframe` is for you!
//!
//! To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
//! To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
//!
//! In short, you implement [`App`] (especially [`App::update`]) and then
@@ -69,7 +69,7 @@
//! #[wasm_bindgen]
//! impl WebHandle {
//! /// Installs a panic hook, then returns.
//! #[allow(clippy::new_without_default)]
//! #[expect(clippy::new_without_default)]
//! #[wasm_bindgen(constructor)]
//! pub fn new() -> Self {
//! // Redirect [`log`] message to `console.log` and friends:
@@ -144,13 +144,22 @@
#![warn(missing_docs)] // let's keep eframe well-documented
#![allow(clippy::needless_doctest_main)]
// Limitation imposed by `accesskit_winit`:
// https://github.com/AccessKit/accesskit/tree/accesskit-v0.18.0/platforms/winit#android-activity-compatibility`
#[cfg(all(
target_os = "android",
feature = "accesskit",
feature = "android-native-activity"
))]
compile_error!("`accesskit` feature is only available with `android-game-activity`");
// Re-export all useful libraries:
pub use {egui, egui::emath, egui::epaint};
#[cfg(feature = "glow")]
pub use {egui_glow, glow};
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub use {egui_wgpu, wgpu};
mod epi;
@@ -179,11 +188,19 @@ pub use web::{WebLogger, WebRunner};
// When compiling natively
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
mod native;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use native::run::EframeWinitApplication;
#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use native::run::EframePumpStatus;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
#[cfg(feature = "persistence")]
pub use native::file_storage::storage_dir;
@@ -192,7 +209,7 @@ pub mod icon_data;
/// This is how you start a native (desktop) app.
///
/// The first argument is name of your app, which is a an identifier
/// The first argument is name of your app, which is an identifier
/// used for the save location of persistence (see [`App::save`]).
/// It is also used as the application id on wayland.
/// If you set no title on the viewport, the app id will be used
@@ -235,13 +252,113 @@ pub mod icon_data;
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[allow(clippy::needless_pass_by_value)]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
pub fn run_native(
app_name: &str,
mut native_options: NativeOptions,
app_creator: AppCreator<'_>,
) -> Result {
let renderer = init_native(app_name, &mut native_options);
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator)
}
#[cfg(feature = "wgpu_no_default_features")]
Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, native_options, app_creator)
}
}
}
/// Provides a proxy for your native eframe application to run on your own event loop.
///
/// See `run_native` for details about `app_name`.
///
/// Call from `fn main` like this:
/// ``` no_run
/// use eframe::{egui, UserEvent};
/// use winit::event_loop::{ControlFlow, EventLoop};
///
/// fn main() -> eframe::Result {
/// let native_options = eframe::NativeOptions::default();
/// let eventloop = EventLoop::<UserEvent>::with_user_event().build()?;
/// eventloop.set_control_flow(ControlFlow::Poll);
///
/// let mut winit_app = eframe::create_native(
/// "MyExtApp",
/// native_options,
/// Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
/// &eventloop,
/// );
///
/// eventloop.run_app(&mut winit_app)?;
///
/// Ok(())
/// }
///
/// #[derive(Default)]
/// struct MyEguiApp {}
///
/// impl MyEguiApp {
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
/// Self::default()
/// }
/// }
///
/// impl eframe::App for MyEguiApp {
/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
/// egui::CentralPanel::default().show(ctx, |ui| {
/// ui.heading("Hello World!");
/// });
/// }
/// }
/// ```
///
/// See the `external_eventloop` example for a more complete example.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub fn create_native<'a>(
app_name: &str,
mut native_options: NativeOptions,
app_creator: AppCreator<'a>,
event_loop: &winit::event_loop::EventLoop<UserEvent>,
) -> EframeWinitApplication<'a> {
let renderer = init_native(app_name, &mut native_options);
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
EframeWinitApplication::new(native::run::create_glow(
app_name,
native_options,
app_creator,
event_loop,
))
}
#[cfg(feature = "wgpu_no_default_features")]
Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
EframeWinitApplication::new(native::run::create_wgpu(
app_name,
native_options,
app_creator,
event_loop,
))
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
#[cfg(not(feature = "__screenshot"))]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
@@ -254,28 +371,16 @@ pub fn run_native(
let renderer = native_options.renderer;
#[cfg(all(feature = "glow", feature = "wgpu"))]
#[cfg(all(feature = "glow", feature = "wgpu_no_default_features"))]
{
match renderer {
match native_options.renderer {
Renderer::Glow => "glow",
Renderer::Wgpu => "wgpu",
};
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
}
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator)
}
#[cfg(feature = "wgpu")]
Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, native_options, app_creator)
}
}
renderer
}
// ----------------------------------------------------------------------------
@@ -315,7 +420,7 @@ pub fn run_native(
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub fn run_simple_native(
app_name: &str,
native_options: NativeOptions,
@@ -367,7 +472,7 @@ pub enum Error {
OpenGL(egui_glow::PainterError),
/// An error from [`wgpu`].
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
Wgpu(egui_wgpu::WgpuError),
}
@@ -405,7 +510,7 @@ impl From<egui_glow::PainterError> for Error {
}
}
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
impl From<egui_wgpu::WgpuError> for Error {
#[inline]
fn from(err: egui_wgpu::WgpuError) -> Self {
@@ -446,7 +551,7 @@ impl std::fmt::Display for Error {
write!(f, "egui_glow: {err}")
}
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
Self::Wgpu(err) => {
write!(f, "WGPU error: {err}")
}

View File

@@ -14,10 +14,10 @@ pub struct AppTitleIconSetter {
impl AppTitleIconSetter {
pub fn new(title: String, mut icon_data: Option<Arc<IconData>>) -> Self {
if let Some(icon) = &icon_data {
if **icon == IconData::default() {
icon_data = None;
}
if let Some(icon) = &icon_data
&& **icon == IconData::default()
{
icon_data = None;
}
Self {
@@ -47,7 +47,7 @@ enum AppIconStatus {
NotSetTryAgain,
/// We successfully set the icon and it should be visible now.
#[allow(dead_code)] // Not used on Linux
#[allow(dead_code, clippy::allow_attributes)] // Not used on Linux
Set,
}
@@ -71,16 +71,20 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta
#[cfg(target_os = "macos")]
return set_title_and_icon_mac(_title, _icon_data);
#[allow(unreachable_code)]
#[allow(unreachable_code, clippy::allow_attributes)]
AppIconStatus::NotSetIgnored
}
/// Set icon for Windows applications.
#[cfg(target_os = "windows")]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
use winapi::um::winuser;
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
use windows_sys::Win32::UI::WindowsAndMessaging::{
CreateIconFromResourceEx, GetSystemMetrics, HICON, ICON_BIG, ICON_SMALL, LR_DEFAULTCOLOR,
SM_CXICON, SM_CXSMICON, SendMessageW, WM_SETICON,
};
// We would get fairly far already with winit's `set_window_icon` (which is exposed to eframe) actually!
// However, it only sets ICON_SMALL, i.e. doesn't allow us to set a higher resolution icon for the task bar.
@@ -92,16 +96,13 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
// * using undocumented SetConsoleIcon method (successfully queried via GetProcAddress)
// SAFETY: WinApi function without side-effects.
let window_handle = unsafe { winuser::GetActiveWindow() };
let window_handle = unsafe { GetActiveWindow() };
if window_handle.is_null() {
// The Window isn't available yet. Try again later!
return AppIconStatus::NotSetTryAgain;
}
fn create_hicon_with_scale(
unscaled_image: &image::RgbaImage,
target_size: i32,
) -> winapi::shared::windef::HICON {
fn create_hicon_with_scale(unscaled_image: &image::RgbaImage, target_size: i32) -> HICON {
let image_scaled = image::imageops::resize(
unscaled_image,
target_size as _,
@@ -127,14 +128,14 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
// SAFETY: Creating an HICON which should be readonly on our data.
unsafe {
winuser::CreateIconFromResourceEx(
CreateIconFromResourceEx(
image_scaled_bytes.as_mut_ptr(),
image_scaled_bytes.len() as u32,
1, // Means this is an icon, not a cursor.
0x00030000, // Version number of the HICON
target_size, // Note that this method can scale, but it does so *very* poorly. So let's avoid that!
target_size,
winuser::LR_DEFAULTCOLOR,
LR_DEFAULTCOLOR,
)
}
}
@@ -155,7 +156,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
// Note that ICON_SMALL may be used even if we don't render a title bar as it may be used in alt+tab!
{
// SAFETY: WinAPI getter function with no known side effects.
let icon_size_big = unsafe { winuser::GetSystemMetrics(winuser::SM_CXICON) };
let icon_size_big = unsafe { GetSystemMetrics(SM_CXICON) };
let icon_big = create_hicon_with_scale(&unscaled_image, icon_size_big);
if icon_big.is_null() {
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
@@ -163,10 +164,10 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
} else {
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
winuser::SendMessageW(
SendMessageW(
window_handle,
winuser::WM_SETICON,
winuser::ICON_BIG as usize,
WM_SETICON,
ICON_BIG as usize,
icon_big as isize,
);
}
@@ -174,7 +175,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
}
{
// SAFETY: WinAPI getter function with no known side effects.
let icon_size_small = unsafe { winuser::GetSystemMetrics(winuser::SM_CXSMICON) };
let icon_size_small = unsafe { GetSystemMetrics(SM_CXSMICON) };
let icon_small = create_hicon_with_scale(&unscaled_image, icon_size_small);
if icon_small.is_null() {
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
@@ -182,10 +183,10 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
} else {
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
winuser::SendMessageW(
SendMessageW(
window_handle,
winuser::WM_SETICON,
winuser::ICON_SMALL as usize,
WM_SETICON,
ICON_SMALL as usize,
icon_small as isize,
);
}
@@ -198,20 +199,25 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
/// Set icon & app title for `MacOS` applications.
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
profiling::function_scope!();
use objc2::ClassType;
use objc2::ClassType as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::{NSData, NSString};
use objc2_foundation::NSString;
let png_bytes = if let Some(icon_data) = icon_data {
match icon_data.to_png_bytes() {
Ok(png_bytes) => Some(png_bytes),
// Do NOT use png even though creating `NSImage` from it is much easier than from raw images data!
//
// Some MacOS versions have a bug where creating an `NSImage` from a png will cause it to load an arbitrary `libpng.dylib`.
// If this dylib isn't the right version, the application will crash with SIGBUS.
// For details see https://github.com/emilk/egui/issues/7155
let image = if let Some(icon_data) = icon_data {
match icon_data.to_image() {
Ok(image) => Some(image),
Err(err) => {
log::warn!("Failed to convert IconData to png: {err}");
log::warn!("Failed to read icon data: {err}");
return AppIconStatus::NotSetIgnored;
}
}
@@ -220,7 +226,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
};
// TODO(madsmtm): Move this into `objc2-app-kit`
extern "C" {
unsafe extern "C" {
static NSApp: Option<&'static NSApplication>;
}
@@ -231,25 +237,50 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
return AppIconStatus::NotSetIgnored;
};
if let Some(png_bytes) = png_bytes {
let data = NSData::from_vec(png_bytes);
if let Some(image) = image {
use objc2_app_kit::{NSBitmapImageRep, NSDeviceRGBColorSpace};
use objc2_foundation::NSSize;
log::trace!("NSImage::initWithData…");
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
log::trace!(
"NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel"
);
let Some(image_rep) = NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
[image.as_raw().as_ptr().cast_mut()].as_mut_ptr(),
image.width() as isize,
image.height() as isize,
8, // bits per sample
4, // samples per pixel
true, // has alpha
false, // is not planar
NSDeviceRGBColorSpace,
(image.width() * 4) as isize, // bytes per row
32 // bits per pixel
) else {
log::warn!("Failed to create NSBitmapImageRep from app icon data.");
return AppIconStatus::NotSetIgnored;
};
log::trace!("NSImage::initWithSize");
let app_icon = NSImage::initWithSize(
NSImage::alloc(),
NSSize::new(image.width() as f64, image.height() as f64),
);
log::trace!("NSImage::addRepresentation");
app_icon.addRepresentation(&image_rep);
profiling::scope!("setApplicationIconImage_");
log::trace!("setApplicationIconImage…");
app.setApplicationIconImage(app_icon.as_deref());
app.setApplicationIconImage(Some(&app_icon));
}
// Change the title in the top bar - for python processes this would be again "python" otherwise.
if let Some(main_menu) = app.mainMenu() {
if let Some(item) = main_menu.itemAtIndex(0) {
if let Some(app_menu) = item.submenu() {
profiling::scope!("setTitle_");
app_menu.setTitle(&NSString::from_str(title));
}
}
if let Some(main_menu) = app.mainMenu()
&& let Some(item) = main_menu.itemAtIndex(0)
&& let Some(app_menu) = item.submenu()
{
profiling::scope!("setTitle_");
app_menu.setTitle(&NSString::from_str(title));
}
// The title in the Dock apparently can't be changed.

View File

@@ -52,14 +52,13 @@ pub fn viewport_builder(
viewport_builder = viewport_builder.with_position(pos);
}
if clamp_size_to_monitor_size {
if let Some(initial_window_size) = viewport_builder.inner_size {
let initial_window_size = egui::NumExt::at_most(
initial_window_size,
largest_monitor_point_size(egui_zoom_factor, event_loop),
);
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
}
if clamp_size_to_monitor_size && let Some(initial_window_size) = viewport_builder.inner_size
{
let initial_window_size = egui::NumExt::at_most(
initial_window_size,
largest_monitor_point_size(egui_zoom_factor, event_loop),
);
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
}
viewport_builder.inner_size
@@ -136,7 +135,7 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
None
}
#[allow(clippy::unnecessary_wraps)]
#[allow(clippy::allow_attributes, clippy::unnecessary_wraps)]
pub fn create_storage_with_file(_file: impl Into<PathBuf>) -> Option<Box<dyn epi::Storage>> {
#[cfg(feature = "persistence")]
return Some(Box::new(
@@ -169,7 +168,7 @@ pub struct EpiIntegration {
}
impl EpiIntegration {
#[allow(clippy::too_many_arguments)]
#[allow(clippy::allow_attributes, clippy::too_many_arguments)]
pub fn new(
egui_ctx: egui::Context,
window: &winit::window::Window,
@@ -180,7 +179,9 @@ impl EpiIntegration {
#[cfg(feature = "glow")] glow_register_native_texture: Option<
Box<dyn FnMut(glow::Texture) -> egui::TextureId>,
>,
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
#[cfg(feature = "wgpu_no_default_features")] wgpu_render_state: Option<
egui_wgpu::RenderState,
>,
) -> Self {
let frame = epi::Frame {
info: epi::IntegrationInfo { cpu_usage: None },
@@ -189,7 +190,7 @@ impl EpiIntegration {
gl,
#[cfg(feature = "glow")]
glow_register_native_texture,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state,
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
@@ -326,30 +327,32 @@ impl EpiIntegration {
}
}
#[allow(clippy::unused_self)]
pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
pub fn save(&mut self, app: &mut dyn epi::App, window: Option<&winit::window::Window>) {
#[cfg(not(feature = "persistence"))]
let _ = (self, app, window);
#[cfg(feature = "persistence")]
if let Some(storage) = self.frame.storage_mut() {
profiling::function_scope!();
if let Some(window) = _window {
if self.persist_window {
profiling::scope!("native_window");
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
);
}
if let Some(window) = window
&& self.persist_window
{
profiling::scope!("native_window");
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
);
}
if _app.persist_egui_memory() {
if app.persist_egui_memory() {
profiling::scope!("egui_memory");
self.egui_ctx
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
}
{
profiling::scope!("App::save");
_app.save(storage);
app.save(storage);
}
profiling::scope!("Storage::flush");

View File

@@ -27,7 +27,7 @@ impl Drop for EventLoopGuard {
}
// Helper function to safely use the current event loop
#[allow(unsafe_code)]
#[expect(unsafe_code)]
pub fn with_current_event_loop<F, R>(f: F) -> Option<R>
where
F: FnOnce(&ActiveEventLoop) -> R,

View File

@@ -1,6 +1,6 @@
use std::{
collections::HashMap,
io::Write,
io::Write as _,
path::{Path, PathBuf},
};
@@ -42,20 +42,20 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
// Adapted from
// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37
#[cfg(all(windows, not(target_vendor = "uwp")))]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn roaming_appdata() -> Option<PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::ffi::OsStringExt as _;
use std::ptr;
use std::slice;
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::CoTaskMemFree;
use windows_sys::Win32::UI::Shell::{
FOLDERID_RoamingAppData, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY,
FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
};
extern "C" {
unsafe extern "C" {
fn wcslen(buf: *const u16) -> usize;
}
let mut path_raw = ptr::null_mut();
@@ -72,7 +72,7 @@ fn roaming_appdata() -> Option<PathBuf> {
};
let path = if result == S_OK {
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us.
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a null-terminated string for us.
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
Some(PathBuf::from(OsString::from_wide(path_slice)))
} else {
@@ -118,7 +118,7 @@ impl FileStorage {
pub(crate) fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
profiling::function_scope!();
let ron_filepath: PathBuf = ron_filepath.into();
log::debug!("Loading app state from {:?}…", ron_filepath);
log::debug!("Loading app state from {}…", ron_filepath.display());
Self {
kv: read_ron(&ron_filepath).unwrap_or_default(),
ron_filepath,
@@ -133,9 +133,8 @@ impl FileStorage {
if let Some(data_dir) = storage_dir(app_id) {
if let Err(err) = std::fs::create_dir_all(&data_dir) {
log::warn!(
"Saving disabled: Failed to create app path at {:?}: {}",
data_dir,
err
"Saving disabled: Failed to create app path at {}: {err}",
data_dir.display()
);
None
} else {
@@ -193,12 +192,11 @@ impl crate::Storage for FileStorage {
fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
profiling::function_scope!();
if let Some(parent_dir) = file_path.parent() {
if !parent_dir.exists() {
if let Err(err) = std::fs::create_dir_all(parent_dir) {
log::warn!("Failed to create directory {parent_dir:?}: {err}");
}
}
if let Some(parent_dir) = file_path.parent()
&& !parent_dir.exists()
&& let Err(err) = std::fs::create_dir_all(parent_dir)
{
log::warn!("Failed to create directory {}: {err}", parent_dir.display());
}
match std::fs::File::create(file_path) {
@@ -207,16 +205,17 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
let config = Default::default();
profiling::scope!("ron::serialize");
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
if let Err(err) = ron::Options::default()
.to_io_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
{
log::warn!("Failed to serialize app state: {}", err);
log::warn!("Failed to serialize app state: {err}");
} else {
log::trace!("Persisted to {:?}", file_path);
log::trace!("Persisted to {}", file_path.display());
}
}
Err(err) => {
log::warn!("Failed to create file {file_path:?}: {err}");
log::warn!("Failed to create file {}: {err}", file_path.display());
}
}
}
@@ -234,7 +233,7 @@ where
match ron::de::from_reader(reader) {
Ok(value) => Some(value),
Err(err) => {
log::warn!("Failed to parse RON: {}", err);
log::warn!("Failed to parse RON: {err}");
None
}
}

View File

@@ -11,34 +11,34 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
use egui_winit::ActionRequested;
use glutin::{
config::GlConfig,
context::NotCurrentGlContext,
display::GetGlDisplay,
prelude::{GlDisplay, PossiblyCurrentGlContext},
surface::GlSurface,
config::GlConfig as _,
context::NotCurrentGlContext as _,
display::GetGlDisplay as _,
prelude::{GlDisplay as _, PossiblyCurrentGlContext as _},
surface::GlSurface as _,
};
use raw_window_handle::HasWindowHandle;
use raw_window_handle::HasWindowHandle as _;
use winit::{
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
window::{Window, WindowId},
};
use ahash::{HashMap, HashSet};
use ahash::HashMap;
use egui::{
DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, ViewportClass, ViewportId,
ViewportIdMap, ViewportIdPair, ViewportInfo, ViewportOutput,
DeferredViewportUiCallback, ImmediateViewport, OrderedViewportIdMap, ViewportBuilder,
ViewportClass, ViewportId, ViewportIdPair, ViewportInfo, ViewportOutput,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use crate::{
native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions,
Result, Storage,
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::epi_integration::EpiIntegration,
};
use super::{
epi_integration, event_loop_context,
winit_integration::{create_egui_context, EventResult, UserEvent, WinitApp},
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
};
// ----------------------------------------------------------------------------
@@ -94,9 +94,9 @@ struct GlutinWindowContext {
current_gl_context: Option<glutin::context::PossiblyCurrentContext>,
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
viewports: ViewportIdMap<Viewport>,
viewports: OrderedViewportIdMap<Viewport>,
viewport_from_window: HashMap<WindowId, ViewportId>,
window_from_viewport: ViewportIdMap<WindowId>,
window_from_viewport: OrderedViewportIdMap<WindowId>,
focused_viewport: Option<ViewportId>,
}
@@ -107,7 +107,7 @@ struct Viewport {
builder: ViewportBuilder,
deferred_commands: Vec<egui::viewport::ViewportCommand>,
info: ViewportInfo,
actions_requested: HashSet<egui_winit::ActionRequested>,
actions_requested: Vec<egui_winit::ActionRequested>,
/// The user-callback that shows the ui.
/// None for immediate viewports.
@@ -139,7 +139,7 @@ impl<'app> GlowWinitApp<'app> {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn create_glutin_windowed_context(
egui_ctx: &egui::Context,
event_loop: &ActiveEventLoop,
@@ -239,7 +239,7 @@ impl<'app> GlowWinitApp<'app> {
let painter = painter.clone();
move |native| painter.borrow_mut().register_native_texture(native)
})),
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
None,
);
@@ -272,7 +272,7 @@ impl<'app> GlowWinitApp<'app> {
..
} = viewport
{
egui_winit.init_accesskit(window, event_loop_proxy);
egui_winit.init_accesskit(event_loop, window, event_loop_proxy);
}
}
@@ -281,10 +281,9 @@ impl<'app> GlowWinitApp<'app> {
.viewport
.mouse_passthrough
.unwrap_or(false)
&& let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false)
{
if let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false) {
log::warn!("set_cursor_hittest(false) failed: {err}");
}
log::warn!("set_cursor_hittest(false) failed: {err}");
}
let app_creator = std::mem::take(&mut self.app_creator)
@@ -302,7 +301,7 @@ impl<'app> GlowWinitApp<'app> {
storage: integration.frame.storage(),
gl: Some(gl),
get_proc_address: Some(&get_proc_address),
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
@@ -336,10 +335,10 @@ impl<'app> GlowWinitApp<'app> {
}
Ok(self.running.insert(GlowWinitRunning {
glutin,
painter,
integration,
app,
glutin,
painter,
}))
}
}
@@ -362,8 +361,12 @@ impl WinitApp for GlowWinitApp<'_> {
fn window_id_from_viewport_id(&self, id: ViewportId) -> Option<WindowId> {
self.running
.as_ref()
.and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied())
.as_ref()?
.glutin
.borrow()
.window_from_viewport
.get(&id)
.copied()
}
fn save(&mut self) {
@@ -436,20 +439,20 @@ impl WinitApp for GlowWinitApp<'_> {
_: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) -> crate::Result<EventResult> {
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
if let Some(running) = &mut self.running {
let mut glutin = running.glutin.borrow_mut();
if let Some(viewport) = glutin
.focused_viewport
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(delta);
}
if let winit::event::DeviceEvent::MouseMotion { delta } = event
&& let Some(running) = &mut self.running
{
let mut glutin = running.glutin.borrow_mut();
if let Some(viewport) = glutin
.focused_viewport
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(delta);
}
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
}
}
@@ -466,7 +469,7 @@ impl WinitApp for GlowWinitApp<'_> {
if let Some(running) = &mut self.running {
Ok(running.on_window_event(window_id, &event))
} else {
Ok(EventResult::Wait)
Ok(EventResult::Exit)
}
}
@@ -476,16 +479,15 @@ impl WinitApp for GlowWinitApp<'_> {
if let Some(running) = &self.running {
let mut glutin = running.glutin.borrow_mut();
if let Some(viewport_id) = glutin.viewport_from_window.get(&event.window_id).copied() {
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
if let Some(egui_winit) = &mut viewport.egui_winit {
return Ok(winit_integration::on_accesskit_window_event(
egui_winit,
event.window_id,
&event.window_event,
));
}
}
if let Some(viewport_id) = glutin.viewport_from_window.get(&event.window_id).copied()
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
&& let Some(egui_winit) = &mut viewport.egui_winit
{
return Ok(winit_integration::on_accesskit_window_event(
egui_winit,
event.window_id,
&event.window_event,
));
}
}
@@ -523,10 +525,10 @@ impl GlowWinitRunning<'_> {
if is_immediate && viewport_id != ViewportId::ROOT {
// This will only happen if this is an immediate viewport.
// That means that the viewport cannot be rendered by itself and needs his parent to be rendered.
if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent) {
if let Some(window) = parent_viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent)
&& let Some(window) = parent_viewport.window.as_ref()
{
return Ok(EventResult::RepaintNext(window.id()));
}
return Ok(EventResult::Wait);
}
@@ -561,6 +563,12 @@ impl GlowWinitRunning<'_> {
(raw_input, viewport_ui_cb)
};
// HACK: In order to get the right clear_color, the system theme needs to be set, which
// usually only happens in the `update` call. So we call Options::begin_pass early
// to set the right theme. Without this there would be a black flash on the first frame.
self.integration
.egui_ctx
.options_mut(|opt| opt.begin_pass(&raw_input));
let clear_color = self
.app
.clear_color(&self.integration.egui_ctx.style().visuals);
@@ -671,7 +679,7 @@ impl GlowWinitRunning<'_> {
);
{
for action in viewport.actions_requested.drain() {
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
@@ -723,10 +731,10 @@ impl GlowWinitRunning<'_> {
// give it time to settle:
#[cfg(feature = "__screenshot")]
if integration.egui_ctx.cumulative_pass_nr() == 2 {
if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
}
if integration.egui_ctx.cumulative_pass_nr() == 2
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
{
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
}
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
@@ -743,7 +751,7 @@ impl GlowWinitRunning<'_> {
}
if integration.should_close() {
Ok(EventResult::Exit)
Ok(EventResult::CloseRequested)
} else {
Ok(EventResult::Wait)
}
@@ -773,19 +781,33 @@ impl GlowWinitRunning<'_> {
let mut repaint_asap = false;
match event {
winit::event::WindowEvent::Focused(new_focused) => {
glutin.focused_viewport = new_focused.then(|| viewport_id).flatten();
winit::event::WindowEvent::Focused(focused) => {
let focused = if cfg!(target_os = "macos")
&& let Some(viewport_id) = viewport_id
&& let Some(viewport) = glutin.viewports.get(&viewport_id)
&& let Some(window) = &viewport.window
{
// TODO(emilk): remove this work-around once we update winit
// https://github.com/rust-windowing/winit/issues/4371
// https://github.com/emilk/egui/issues/7588
window.has_focus()
} else {
*focused
};
glutin.focused_viewport = focused.then_some(viewport_id).flatten();
}
winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if 0 < physical_size.width && 0 < physical_size.height {
if let Some(viewport_id) = viewport_id {
repaint_asap = true;
glutin.resize(viewport_id, *physical_size);
}
if 0 < physical_size.width
&& 0 < physical_size.height
&& let Some(viewport_id) = viewport_id
{
repaint_asap = true;
glutin.resize(viewport_id, *physical_size);
}
}
@@ -794,44 +816,31 @@ impl GlowWinitRunning<'_> {
log::debug!(
"Received WindowEvent::CloseRequested for main viewport - shutting down."
);
return EventResult::Exit;
return EventResult::CloseRequested;
}
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
if let Some(viewport_id) = viewport_id {
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
// Tell viewport it should close:
viewport.info.events.push(egui::ViewportEvent::Close);
if let Some(viewport_id) = viewport_id
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
{
// Tell viewport it should close:
viewport.info.events.push(egui::ViewportEvent::Close);
// We may need to repaint both us and our parent to close the window,
// and perhaps twice (once to notice the close-event, once again to enforce it).
// `request_repaint_of` does a double-repaint though:
self.integration.egui_ctx.request_repaint_of(viewport_id);
self.integration
.egui_ctx
.request_repaint_of(viewport.ids.parent);
}
// We may need to repaint both us and our parent to close the window,
// and perhaps twice (once to notice the close-event, once again to enforce it).
// `request_repaint_of` does a double-repaint though:
self.integration.egui_ctx.request_repaint_of(viewport_id);
self.integration
.egui_ctx
.request_repaint_of(viewport.ids.parent);
}
}
winit::event::WindowEvent::Destroyed => {
log::debug!(
"Received WindowEvent::Destroyed for viewport {:?}",
viewport_id
);
if viewport_id == Some(ViewportId::ROOT) {
return EventResult::Exit;
} else {
return EventResult::Wait;
}
}
_ => {}
}
if self.integration.should_close() {
return EventResult::Exit;
return EventResult::CloseRequested;
}
let mut event_response = egui_winit::EventResponse {
@@ -901,7 +910,7 @@ fn change_gl_context(
}
impl GlutinWindowContext {
#[allow(unsafe_code)]
#[expect(unsafe_code)]
unsafe fn new(
egui_ctx: &egui::Context,
viewport_builder: ViewportBuilder,
@@ -959,7 +968,6 @@ impl GlutinWindowContext {
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
egui_ctx,
event_loop,
viewport_builder.clone(),
)));
@@ -1016,7 +1024,9 @@ impl GlutinWindowContext {
let gl_context = match gl_context_result {
Ok(it) => it,
Err(err) => {
log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}");
log::warn!(
"Failed to create context using default context attributes {context_attributes:?} due to error: {err}"
);
log::debug!(
"Retrying with fallback context attributes: {fallback_context_attributes:?}"
);
@@ -1030,15 +1040,27 @@ impl GlutinWindowContext {
let not_current_gl_context = Some(gl_context);
let mut viewport_from_window = HashMap::default();
let mut window_from_viewport = ViewportIdMap::default();
let mut info = ViewportInfo::default();
let mut window_from_viewport = OrderedViewportIdMap::default();
let mut viewport_info = ViewportInfo::default();
if let Some(window) = &window {
viewport_from_window.insert(window.id(), ViewportId::ROOT);
window_from_viewport.insert(ViewportId::ROOT, window.id());
egui_winit::update_viewport_info(&mut info, egui_ctx, window, true);
egui_winit::update_viewport_info(&mut viewport_info, egui_ctx, window, true);
// Tell egui right away about native_pixels_per_point etc,
// so that the app knows about it during app creation:
let pixels_per_point = egui_winit::pixels_per_point(egui_ctx, window);
egui_ctx.input_mut(|i| {
i.raw
.viewports
.insert(ViewportId::ROOT, viewport_info.clone());
i.pixels_per_point = pixels_per_point;
});
}
let mut viewports = ViewportIdMap::default();
let mut viewports = OrderedViewportIdMap::default();
viewports.insert(
ViewportId::ROOT,
Viewport {
@@ -1046,7 +1068,7 @@ impl GlutinWindowContext {
class: ViewportClass::Root,
builder: viewport_builder,
deferred_commands: vec![],
info,
info: viewport_info,
actions_requested: Default::default(),
viewport_ui_cb: None,
gl_surface: None,
@@ -1094,7 +1116,7 @@ impl GlutinWindowContext {
}
/// Create a surface, window, and winit integration for the viewport, if missing.
#[allow(unsafe_code)]
#[expect(unsafe_code)]
pub(crate) fn initialize_window(
&mut self,
viewport_id: ViewportId,
@@ -1113,7 +1135,6 @@ impl GlutinWindowContext {
log::debug!("Creating a window for viewport {viewport_id:?}");
let window_attributes = egui_winit::create_winit_window_attributes(
&self.egui_ctx,
event_loop,
viewport.builder.clone(),
);
if window_attributes.transparent()
@@ -1241,21 +1262,21 @@ impl GlutinWindowContext {
let width_px = NonZeroU32::new(physical_size.width).unwrap_or(NonZeroU32::MIN);
let height_px = NonZeroU32::new(physical_size.height).unwrap_or(NonZeroU32::MIN);
if let Some(viewport) = self.viewports.get(&viewport_id) {
if let Some(gl_surface) = &viewport.gl_surface {
change_gl_context(
&mut self.current_gl_context,
&mut self.not_current_gl_context,
gl_surface,
);
gl_surface.resize(
self.current_gl_context
.as_ref()
.expect("failed to get current context to resize surface"),
width_px,
height_px,
);
}
if let Some(viewport) = self.viewports.get(&viewport_id)
&& let Some(gl_surface) = &viewport.gl_surface
{
change_gl_context(
&mut self.current_gl_context,
&mut self.not_current_gl_context,
gl_surface,
);
gl_surface.resize(
self.current_gl_context
.as_ref()
.expect("failed to get current context to resize surface"),
width_px,
height_px,
);
}
}
@@ -1265,7 +1286,7 @@ impl GlutinWindowContext {
pub(crate) fn remove_viewports_not_in(
&mut self,
viewport_output: &ViewportIdMap<ViewportOutput>,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
) {
// GC old viewports
self.viewports
@@ -1280,7 +1301,7 @@ impl GlutinWindowContext {
&mut self,
event_loop: &ActiveEventLoop,
egui_ctx: &egui::Context,
viewport_output: &ViewportIdMap<ViewportOutput>,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
) {
profiling::function_scope!();
@@ -1337,7 +1358,7 @@ impl GlutinWindowContext {
}
fn initialize_or_update_viewport(
viewports: &mut ViewportIdMap<Viewport>,
viewports: &mut OrderedViewportIdMap<Viewport>,
ids: ViewportIdPair,
class: ViewportClass,
mut builder: ViewportBuilder,
@@ -1345,6 +1366,8 @@ fn initialize_or_update_viewport(
) -> &mut Viewport {
profiling::function_scope!();
use std::collections::btree_map::Entry;
if builder.icon.is_none() {
// Inherit icon from parent
builder.icon = viewports
@@ -1353,7 +1376,7 @@ fn initialize_or_update_viewport(
}
match viewports.entry(ids.this) {
std::collections::hash_map::Entry::Vacant(entry) => {
Entry::Vacant(entry) => {
// New viewport:
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
entry.insert(Viewport {
@@ -1370,7 +1393,7 @@ fn initialize_or_update_viewport(
})
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
Entry::Occupied(mut entry) => {
// Patch an existing viewport:
let viewport = entry.get_mut();
@@ -1566,6 +1589,6 @@ fn save_screenshot_and_exit(
});
log::info!("Screenshot saved to {path:?}.");
#[allow(clippy::exit)]
#[expect(clippy::exit)]
std::process::exit(0);
}

View File

@@ -12,5 +12,5 @@ pub(crate) mod winit_integration;
#[cfg(feature = "glow")]
mod glow_integration;
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
mod wgpu_integration;

View File

@@ -10,9 +10,8 @@ use ahash::HashMap;
use super::winit_integration::{UserEvent, WinitApp};
use crate::{
epi,
Result, epi,
native::{event_loop_context, winit_integration::EventResult},
Result,
};
// ----------------------------------------------------------------------------
@@ -93,48 +92,57 @@ impl<T: WinitApp> WinitAppWrapper<T> {
log::trace!("event_result: {event_result:?}");
let combined_result = event_result.and_then(|event_result| {
match event_result {
EventResult::Wait => {
event_loop.set_control_flow(ControlFlow::Wait);
Ok(event_result)
}
EventResult::RepaintNow(window_id) => {
log::trace!("RepaintNow of {window_id:?}",);
let mut event_result = event_result;
if cfg!(target_os = "windows") {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
self.winit_app.run_ui_and_paint(event_loop, window_id)
} else {
// Fix for https://github.com/emilk/egui/issues/2425
self.windows_next_repaint_times
.insert(window_id, Instant::now());
Ok(event_result)
}
}
EventResult::RepaintNext(window_id) => {
log::trace!("RepaintNext of {window_id:?}",);
if cfg!(target_os = "windows")
&& let Ok(EventResult::RepaintNow(window_id)) = event_result
{
log::trace!("RepaintNow of {window_id:?}");
self.windows_next_repaint_times
.insert(window_id, Instant::now());
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
event_result = self.winit_app.run_ui_and_paint(event_loop, window_id);
}
let combined_result = event_result.map(|event_result| match event_result {
EventResult::Wait => {
event_loop.set_control_flow(ControlFlow::Wait);
event_result
}
EventResult::RepaintNow(window_id) => {
log::trace!("RepaintNow of {window_id:?}",);
self.windows_next_repaint_times
.insert(window_id, Instant::now());
event_result
}
EventResult::RepaintNext(window_id) => {
log::trace!("RepaintNext of {window_id:?}",);
self.windows_next_repaint_times
.insert(window_id, Instant::now());
event_result
}
EventResult::RepaintAt(window_id, repaint_time) => {
self.windows_next_repaint_times.insert(
window_id,
self.windows_next_repaint_times
.insert(window_id, Instant::now());
Ok(event_result)
}
EventResult::RepaintAt(window_id, repaint_time) => {
self.windows_next_repaint_times.insert(
window_id,
self.windows_next_repaint_times
.get(&window_id)
.map_or(repaint_time, |last| (*last).min(repaint_time)),
);
Ok(event_result)
}
EventResult::Save => {
save = true;
Ok(event_result)
}
EventResult::Exit => {
exit = true;
Ok(event_result)
}
.get(&window_id)
.map_or(repaint_time, |last| (*last).min(repaint_time)),
);
event_result
}
EventResult::Save => {
save = true;
event_result
}
EventResult::Exit => {
exit = true;
event_result
}
EventResult::CloseRequested => {
// The windows need to be dropped whilst the event loop is running to allow for proper cleanup.
self.winit_app.save_and_destroy();
event_result
}
});
@@ -142,7 +150,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
log::error!("Exiting because of error: {err}");
exit = true;
self.return_result = Err(err);
};
}
if save {
log::debug!("Received an EventResult::Save - saving app state");
@@ -159,7 +167,6 @@ impl<T: WinitApp> WinitAppWrapper<T> {
log::debug!("Exiting with return code 0");
#[allow(clippy::exit)]
std::process::exit(0);
}
}
@@ -174,7 +181,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
.retain(|window_id, repaint_time| {
if now < *repaint_time {
return true; // not yet ready
};
}
event_loop.set_control_flow(ControlFlow::Poll);
@@ -190,7 +197,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
if let Some(next_repaint_time) = next_repaint_time {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
};
}
}
}
@@ -313,7 +320,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
#[cfg(not(target_os = "ios"))]
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, winit_app: impl WinitApp) -> Result {
use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
use winit::platform::run_on_demand::EventLoopExtRunOnDemand as _;
log::trace!("Entering the winit event loop (run_app_on_demand)…");
@@ -359,9 +366,22 @@ pub fn run_glow(
run_and_exit(event_loop, glow_eframe)
}
#[cfg(feature = "glow")]
pub fn create_glow<'a>(
app_name: &str,
native_options: epi::NativeOptions,
app_creator: epi::AppCreator<'a>,
event_loop: &EventLoop<UserEvent>,
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::glow_integration::GlowWinitApp;
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
WinitAppWrapper::new(glow_eframe, true)
}
// ----------------------------------------------------------------------------
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub fn run_wgpu(
app_name: &str,
mut native_options: epi::NativeOptions,
@@ -383,3 +403,120 @@ pub fn run_wgpu(
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe)
}
#[cfg(feature = "wgpu_no_default_features")]
pub fn create_wgpu<'a>(
app_name: &str,
native_options: epi::NativeOptions,
app_creator: epi::AppCreator<'a>,
event_loop: &EventLoop<UserEvent>,
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::wgpu_integration::WgpuWinitApp;
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
WinitAppWrapper::new(wgpu_eframe, true)
}
// ----------------------------------------------------------------------------
/// A proxy to the eframe application that implements [`ApplicationHandler`].
///
/// This can be run directly on your own [`EventLoop`] by itself or with other
/// windows you manage outside of eframe.
pub struct EframeWinitApplication<'a> {
wrapper: Box<dyn ApplicationHandler<UserEvent> + 'a>,
control_flow: ControlFlow,
}
impl ApplicationHandler<UserEvent> for EframeWinitApplication<'_> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.resumed(event_loop);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
self.wrapper.window_event(event_loop, window_id, event);
}
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
self.wrapper.new_events(event_loop, cause);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
self.wrapper.user_event(event_loop, event);
}
fn device_event(
&mut self,
event_loop: &ActiveEventLoop,
device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
self.wrapper.device_event(event_loop, device_id, event);
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.about_to_wait(event_loop);
self.control_flow = event_loop.control_flow();
}
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.suspended(event_loop);
}
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.exiting(event_loop);
}
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.memory_warning(event_loop);
}
}
impl<'a> EframeWinitApplication<'a> {
pub(crate) fn new<T: ApplicationHandler<UserEvent> + 'a>(app: T) -> Self {
Self {
wrapper: Box::new(app),
control_flow: ControlFlow::default(),
}
}
/// Pump the `EventLoop` to check for and dispatch pending events to this application.
///
/// Returns either the exit code for the application or the final state of the [`ControlFlow`]
/// after all events have been dispatched in this iteration.
///
/// This is useful when your [`EventLoop`] is not the main event loop for your application.
/// See the `external_eventloop_async` example.
#[cfg(not(target_os = "ios"))]
pub fn pump_eframe_app(
&mut self,
event_loop: &mut EventLoop<UserEvent>,
timeout: Option<std::time::Duration>,
) -> EframePumpStatus {
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
match event_loop.pump_app_events(timeout, self) {
PumpStatus::Continue => EframePumpStatus::Continue(self.control_flow),
PumpStatus::Exit(code) => EframePumpStatus::Exit(code),
}
}
}
/// Either an exit code or a [`ControlFlow`] from the [`ActiveEventLoop`].
///
/// The result of [`EframeWinitApplication::pump_eframe_app`].
#[cfg(not(target_os = "ios"))]
pub enum EframePumpStatus {
/// The final state of the [`ControlFlow`] after all events have been dispatched
///
/// Callers should perform the action that is appropriate for the [`ControlFlow`] value.
Continue(ControlFlow),
/// The exit code for the application
Exit(i32),
}

View File

@@ -15,18 +15,19 @@ use winit::{
window::{Window, WindowId},
};
use ahash::{HashMap, HashSet, HashSetExt};
use ahash::HashMap;
use egui::{
DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass,
ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput,
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
ViewportOutput,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use winit_integration::UserEvent;
use crate::{
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
};
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
@@ -70,9 +71,10 @@ pub struct SharedState {
painter: egui_wgpu::winit::Painter,
viewport_from_window: HashMap<WindowId, ViewportId>,
focused_viewport: Option<ViewportId>,
resized_viewport: Option<ViewportId>,
}
pub type Viewports = ViewportIdMap<Viewport>;
pub type Viewports = egui::OrderedViewportIdMap<Viewport>;
pub struct Viewport {
ids: ViewportIdPair,
@@ -80,7 +82,7 @@ pub struct Viewport {
builder: ViewportBuilder,
deferred_commands: Vec<egui::viewport::ViewportCommand>,
info: ViewportInfo,
actions_requested: HashSet<ActionRequested>,
actions_requested: Vec<ActionRequested>,
/// `None` for sync viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -182,19 +184,37 @@ impl<'app> WgpuWinitApp<'app> {
builder: ViewportBuilder,
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
profiling::function_scope!();
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
self.native_options.wgpu_options.clone(),
self.native_options.multisampling.max(1) as _,
egui_wgpu::depth_format_from_bits(
self.native_options.depth_buffer,
self.native_options.stencil_buffer,
),
self.native_options.viewport.transparent.unwrap_or(false),
self.native_options.dithering,
egui_wgpu::RendererOptions {
msaa_samples: self.native_options.multisampling as _,
depth_stencil_format: egui_wgpu::depth_format_from_bits(
self.native_options.depth_buffer,
self.native_options.stencil_buffer,
),
dithering: self.native_options.dithering,
..Default::default()
},
));
let mut viewport_info = ViewportInfo::default();
egui_winit::update_viewport_info(&mut viewport_info, &egui_ctx, &window, true);
{
// Tell egui right away about native_pixels_per_point etc,
// so that the app knows about it during app creation:
let pixels_per_point = egui_winit::pixels_per_point(&egui_ctx, &window);
egui_ctx.input_mut(|i| {
i.raw
.viewports
.insert(ViewportId::ROOT, viewport_info.clone());
i.pixels_per_point = pixels_per_point;
});
}
let window = Arc::new(window);
{
@@ -236,7 +256,7 @@ impl<'app> WgpuWinitApp<'app> {
});
}
#[allow(unused_mut)] // used for accesskit
#[allow(unused_mut, clippy::allow_attributes)] // used for accesskit
let mut egui_winit = egui_winit::State::new(
egui_ctx.clone(),
ViewportId::ROOT,
@@ -249,7 +269,7 @@ impl<'app> WgpuWinitApp<'app> {
#[cfg(feature = "accesskit")]
{
let event_loop_proxy = self.repaint_proxy.lock().clone();
egui_winit.init_accesskit(&window, event_loop_proxy);
egui_winit.init_accesskit(event_loop, &window, event_loop_proxy);
}
let app_creator = std::mem::take(&mut self.app_creator)
@@ -274,9 +294,6 @@ impl<'app> WgpuWinitApp<'app> {
let mut viewport_from_window = HashMap::default();
viewport_from_window.insert(window.id(), ViewportId::ROOT);
let mut info = ViewportInfo::default();
egui_winit::update_viewport_info(&mut info, &egui_ctx, &window, true);
let mut viewports = Viewports::default();
viewports.insert(
ViewportId::ROOT,
@@ -285,7 +302,7 @@ impl<'app> WgpuWinitApp<'app> {
class: ViewportClass::Root,
builder,
deferred_commands: vec![],
info,
info: viewport_info,
actions_requested: Default::default(),
viewport_ui_cb: None,
window: Some(window),
@@ -299,6 +316,7 @@ impl<'app> WgpuWinitApp<'app> {
viewports,
painter,
focused_viewport: Some(ViewportId::ROOT),
resized_viewport: None,
}));
{
@@ -333,10 +351,8 @@ impl WinitApp for WgpuWinitApp<'_> {
.as_ref()
.and_then(|r| {
let shared = r.shared.borrow();
shared
.viewport_from_window
.get(&window_id)
.and_then(|id| shared.viewports.get(id).map(|v| v.window.clone()))
let id = shared.viewport_from_window.get(&window_id)?;
shared.viewports.get(id).map(|v| v.window.clone())
})
.flatten()
}
@@ -431,20 +447,20 @@ impl WinitApp for WgpuWinitApp<'_> {
_: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) -> crate::Result<EventResult> {
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
if let Some(running) = &mut self.running {
let mut shared = running.shared.borrow_mut();
if let Some(viewport) = shared
.focused_viewport
.and_then(|viewport| shared.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(delta);
}
if let winit::event::DeviceEvent::MouseMotion { delta } = event
&& let Some(running) = &mut self.running
{
let mut shared = running.shared.borrow_mut();
if let Some(viewport) = shared
.focused_viewport
.and_then(|viewport| shared.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(delta);
}
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
}
}
@@ -463,7 +479,8 @@ impl WinitApp for WgpuWinitApp<'_> {
if let Some(running) = &mut self.running {
Ok(running.on_window_event(window_id, &event))
} else {
Ok(EventResult::Wait)
// running is removed to get ready for exiting
Ok(EventResult::Exit)
}
}
@@ -479,14 +496,13 @@ impl WinitApp for WgpuWinitApp<'_> {
if let Some(viewport) = viewport_from_window
.get(&event.window_id)
.and_then(|id| viewports.get_mut(id))
&& let Some(egui_winit) = &mut viewport.egui_winit
{
if let Some(egui_winit) = &mut viewport.egui_winit {
return Ok(winit_integration::on_accesskit_window_event(
egui_winit,
event.window_id,
&event.window_event,
));
}
return Ok(winit_integration::on_accesskit_window_event(
egui_winit,
event.window_id,
&event.window_event,
));
}
}
@@ -564,10 +580,10 @@ impl WgpuWinitRunning<'_> {
if viewport.viewport_ui_cb.is_none() {
// This will only happen if this is an immediate viewport.
// That means that the viewport cannot be rendered by itself and needs his parent to be rendered.
if let Some(viewport) = viewports.get(&viewport.ids.parent) {
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
if let Some(viewport) = viewports.get(&viewport.ids.parent)
&& let Some(window) = viewport.window.as_ref()
{
return Ok(EventResult::RepaintNext(window.id()));
}
return Ok(EventResult::Wait);
}
@@ -680,7 +696,7 @@ impl WgpuWinitRunning<'_> {
screenshot_commands,
);
for action in viewport.actions_requested.drain() {
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot { .. } => {
// already handled above
@@ -731,17 +747,17 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window {
if window.is_minimized() == Some(true) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
if let Some(window) = window
&& window.is_minimized() == Some(true)
{
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
if integration.should_close() {
Ok(EventResult::Exit)
Ok(EventResult::CloseRequested)
} else {
Ok(EventResult::Wait)
}
@@ -762,37 +778,68 @@ impl WgpuWinitRunning<'_> {
let viewport_id = shared.viewport_from_window.get(&window_id).copied();
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the
// event handler.
//
// If this is not done, the compositor will assume that the window does not want to redraw,
// and continue ahead.
// event handler. If this is not done, the compositor will assume that the window does not want
// to redraw and continue ahead.
//
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
// new frames to the compositor in time.
//
// The flickering is technically glutin or glow's fault, but we should be responding properly
// new frames to the compositor in time. The flickering is technically glutin or glow's fault, but we should be responding properly
// to resizes anyway, as doing so avoids dropping frames.
//
// See: https://github.com/emilk/egui/issues/903
let mut repaint_asap = false;
// On MacOS the asap repaint is not enough. The drawn frames must be synchronized with
// the CoreAnimation transactions driving the window resize process.
//
// Thus, Painter, responsible for wgpu surfaces and their resize, has to be notified of the
// resize lifecycle, yet winit does not provide any events for that. To work around,
// the last resized viewport is tracked until any next non-resize event is received.
//
// Accidental state change during the resize process due to an unexpected event fire
// is ok, state will switch back upon next resize event.
//
// See: https://github.com/emilk/egui/issues/903
if let Some(id) = viewport_id
&& shared.resized_viewport == viewport_id
{
shared.painter.on_window_resize_state_change(id, false);
shared.resized_viewport = None;
}
match event {
winit::event::WindowEvent::Focused(new_focused) => {
shared.focused_viewport = new_focused.then(|| viewport_id).flatten();
winit::event::WindowEvent::Focused(focused) => {
let focused = if cfg!(target_os = "macos")
&& let Some(viewport_id) = viewport_id
&& let Some(viewport) = shared.viewports.get(&viewport_id)
&& let Some(window) = &viewport.window
{
// TODO(emilk): remove this work-around once we update winit
// https://github.com/rust-windowing/winit/issues/4371
// https://github.com/emilk/egui/issues/7588
window.has_focus()
} else {
*focused
};
shared.focused_viewport = focused.then_some(viewport_id).flatten();
}
winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if let Some(viewport_id) = viewport_id {
if let (Some(width), Some(height)) = (
if let Some(id) = viewport_id
&& let (Some(width), Some(height)) = (
NonZeroU32::new(physical_size.width),
NonZeroU32::new(physical_size.height),
) {
repaint_asap = true;
shared.painter.on_window_resized(viewport_id, width, height);
)
{
if shared.resized_viewport != viewport_id {
shared.resized_viewport = viewport_id;
shared.painter.on_window_resize_state_change(id, true);
}
shared.painter.on_window_resized(id, width, height);
repaint_asap = true;
}
}
@@ -801,42 +848,41 @@ impl WgpuWinitRunning<'_> {
log::debug!(
"Received WindowEvent::CloseRequested for main viewport - shutting down."
);
return EventResult::Exit;
return EventResult::CloseRequested;
}
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
if let Some(viewport_id) = viewport_id {
if let Some(viewport) = shared.viewports.get_mut(&viewport_id) {
// Tell viewport it should close:
viewport.info.events.push(egui::ViewportEvent::Close);
if let Some(viewport_id) = viewport_id
&& let Some(viewport) = shared.viewports.get_mut(&viewport_id)
{
// Tell viewport it should close:
viewport.info.events.push(egui::ViewportEvent::Close);
// We may need to repaint both us and our parent to close the window,
// and perhaps twice (once to notice the close-event, once again to enforce it).
// `request_repaint_of` does a double-repaint though:
integration.egui_ctx.request_repaint_of(viewport_id);
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
}
// We may need to repaint both us and our parent to close the window,
// and perhaps twice (once to notice the close-event, once again to enforce it).
// `request_repaint_of` does a double-repaint though:
integration.egui_ctx.request_repaint_of(viewport_id);
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
}
}
_ => {}
};
}
let event_response = viewport_id
.and_then(|viewport_id| {
shared.viewports.get_mut(&viewport_id).and_then(|viewport| {
Some(integration.on_window_event(
viewport.window.as_deref()?,
viewport.egui_winit.as_mut()?,
event,
))
})
let viewport = shared.viewports.get_mut(&viewport_id)?;
Some(integration.on_window_event(
viewport.window.as_deref()?,
viewport.egui_winit.as_mut()?,
event,
))
})
.unwrap_or_default();
if integration.should_close() {
EventResult::Exit
EventResult::CloseRequested
} else if event_response.repaint {
if repaint_asap {
EventResult::RepaintNow(window_id)
@@ -1035,10 +1081,10 @@ fn render_immediate_viewport(
}
pub(crate) fn remove_viewports_not_in(
viewports: &mut ViewportIdMap<Viewport>,
viewports: &mut Viewports,
painter: &mut egui_wgpu::winit::Painter,
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
viewport_output: &ViewportIdMap<ViewportOutput>,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
) {
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
@@ -1051,8 +1097,8 @@ pub(crate) fn remove_viewports_not_in(
/// Add new viewports, and update existing ones:
fn handle_viewport_output(
egui_ctx: &egui::Context,
viewport_output: &ViewportIdMap<ViewportOutput>,
viewports: &mut ViewportIdMap<Viewport>,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
viewports: &mut Viewports,
painter: &mut egui_wgpu::winit::Painter,
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
) {
@@ -1089,13 +1135,13 @@ fn handle_viewport_output(
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux") {
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size {
if let (Some(width), Some(height)) = (
if new_inner_size != old_inner_size
&& let (Some(width), Some(height)) = (
NonZeroU32::new(new_inner_size.width),
NonZeroU32::new(new_inner_size.height),
) {
painter.on_window_resized(viewport_id, width, height);
}
)
{
painter.on_window_resized(viewport_id, width, height);
}
}
}
@@ -1112,6 +1158,8 @@ fn initialize_or_update_viewport<'a>(
viewport_ui_cb: Option<Arc<dyn Fn(&egui::Context) + Send + Sync>>,
painter: &mut egui_wgpu::winit::Painter,
) -> &'a mut Viewport {
use std::collections::btree_map::Entry;
profiling::function_scope!();
if builder.icon.is_none() {
@@ -1122,7 +1170,7 @@ fn initialize_or_update_viewport<'a>(
}
match viewports.entry(ids.this) {
std::collections::hash_map::Entry::Vacant(entry) => {
Entry::Vacant(entry) => {
// New viewport:
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
entry.insert(Viewport {
@@ -1131,14 +1179,14 @@ fn initialize_or_update_viewport<'a>(
builder,
deferred_commands: vec![],
info: Default::default(),
actions_requested: HashSet::new(),
actions_requested: Vec::new(),
viewport_ui_cb,
window: None,
egui_winit: None,
})
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
Entry::Occupied(mut entry) => {
// Patch an existing viewport:
let viewport = entry.get_mut();

View File

@@ -124,6 +124,25 @@ pub enum EventResult {
/// Causes a save of the client state when the persistence feature is enabled.
Save,
/// Starts the process of ending eframe execution whilst allowing for proper
/// clean up of resources.
///
/// # Warning
/// This event **must** occur before [`Exit`] to correctly exit eframe code.
/// If in doubt, return this event.
///
/// [`Exit`]: [EventResult::Exit]
CloseRequested,
/// The event loop will exit, now.
/// The correct circumstance to return this event is in response to a winit "Destroyed" event.
///
/// # Warning
/// The [`CloseRequested`] **must** occur before this event to ensure that winit
/// is able to remove any open windows. Otherwise the window(s) will remain open
/// until the program terminates.
///
/// [`CloseRequested`]: EventResult::CloseRequested
Exit,
}

View File

@@ -18,7 +18,7 @@ impl Stopwatch {
}
pub fn start(&mut self) {
assert!(self.start.is_none());
assert!(self.start.is_none(), "Stopwatch already running");
self.start = Some(Instant::now());
}
@@ -29,7 +29,7 @@ impl Stopwatch {
}
pub fn resume(&mut self) {
assert!(self.start.is_none());
assert!(self.start.is_none(), "Stopwatch still running");
self.start = Some(Instant::now());
}

View File

@@ -1,15 +1,15 @@
use egui::{TexturesDelta, UserData, ViewportCommand};
use crate::{epi, App};
use crate::{App, epi, web::web_painter::WebPainter};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint};
use super::{NeedRepaint, now_sec, text_agent::TextAgent};
pub struct AppRunner {
#[allow(dead_code)]
#[allow(dead_code, clippy::allow_attributes)]
pub(crate) web_options: crate::WebOptions,
pub(crate) frame: epi::Frame,
egui_ctx: egui::Context,
painter: super::ActiveWebPainter,
painter: Box<dyn WebPainter>,
pub(crate) input: super::WebInput,
app: Box<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
@@ -34,6 +34,10 @@ impl Drop for AppRunner {
impl AppRunner {
/// # Errors
/// Failure to initialize WebGL renderer, or failure to create app.
#[cfg_attr(
not(feature = "wgpu_no_default_features"),
expect(clippy::unused_async)
)]
pub async fn new(
canvas: web_sys::HtmlCanvasElement,
web_options: crate::WebOptions,
@@ -41,7 +45,41 @@ impl AppRunner {
text_agent: TextAgent,
) -> Result<Self, String> {
let egui_ctx = egui::Context::default();
let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?;
#[allow(clippy::allow_attributes, unused_assignments)]
#[cfg(feature = "glow")]
let mut gl = None;
#[allow(clippy::allow_attributes, unused_assignments)]
#[cfg(feature = "wgpu_no_default_features")]
let mut wgpu_render_state = None;
let painter = match web_options.renderer {
#[cfg(feature = "glow")]
epi::Renderer::Glow => {
log::debug!("Using the glow renderer");
let painter = super::web_painter_glow::WebPainterGlow::new(
egui_ctx.clone(),
canvas,
&web_options,
)?;
gl = Some(painter.gl().clone());
Box::new(painter) as Box<dyn WebPainter>
}
#[cfg(feature = "wgpu_no_default_features")]
epi::Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
let painter = super::web_painter_wgpu::WebPainterWgpu::new(
egui_ctx.clone(),
canvas,
&web_options,
)
.await?;
wgpu_render_state = painter.render_state();
Box::new(painter) as Box<dyn WebPainter>
}
};
let info = epi::IntegrationInfo {
web_info: epi::WebInfo {
@@ -65,21 +103,27 @@ impl AppRunner {
o.zoom_factor = 1.0;
});
// Tell egui right away about native_pixels_per_point
// so that the app knows about it during app creation:
egui_ctx.input_mut(|i| {
let viewport_info = i.raw.viewports.entry(egui::ViewportId::ROOT).or_default();
viewport_info.native_pixels_per_point = Some(super::native_pixels_per_point());
i.pixels_per_point = super::native_pixels_per_point();
});
let cc = epi::CreationContext {
egui_ctx: egui_ctx.clone(),
integration_info: info.clone(),
storage: Some(&storage),
#[cfg(feature = "glow")]
gl: Some(painter.gl().clone()),
gl: gl.clone(),
#[cfg(feature = "glow")]
get_proc_address: None,
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
wgpu_render_state: painter.render_state(),
#[cfg(all(feature = "wgpu", feature = "glow"))]
wgpu_render_state: None,
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: wgpu_render_state.clone(),
};
let app = app_creator(&cc).map_err(|err| err.to_string())?;
@@ -88,15 +132,14 @@ impl AppRunner {
storage: Some(Box::new(storage)),
#[cfg(feature = "glow")]
gl: Some(painter.gl().clone()),
gl,
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
wgpu_render_state: painter.render_state(),
#[cfg(all(feature = "wgpu", feature = "glow"))]
wgpu_render_state: None,
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state,
};
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
let needs_repaint: std::sync::Arc<NeedRepaint> =
std::sync::Arc::new(NeedRepaint::new(web_options.max_fps));
{
let needs_repaint = needs_repaint.clone();
egui_ctx.set_request_repaint_callback(move |info| {
@@ -304,8 +347,6 @@ impl AppRunner {
}
fn handle_platform_output(&self, platform_output: egui::PlatformOutput) {
#![allow(deprecated)]
#[cfg(feature = "web_screen_reader")]
if self.egui_ctx.options(|o| o.screen_reader) {
super::screen_reader::speak(&platform_output.events_description());
@@ -314,13 +355,10 @@ impl AppRunner {
let egui::PlatformOutput {
commands,
cursor_icon,
open_url,
copied_text,
events: _, // already handled
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
ime,
#[cfg(feature = "accesskit")]
accesskit_update: _, // not currently implemented
accesskit_update: _, // not currently implemented
num_completed_passes: _, // handled by `Context::run`
request_discard_reasons: _, // handled by `Context::run`
} = platform_output;
@@ -341,14 +379,6 @@ impl AppRunner {
super::set_cursor_icon(cursor_icon);
if let Some(open) = open_url {
super::open_url(&open.url, open.new_tab);
}
if !copied_text.is_empty() {
super::set_clipboard_text(&copied_text);
}
if self.has_focus() {
// The eframe app has focus.
if ime.is_some() {

View File

@@ -11,9 +11,15 @@ use super::percent_decode;
/// Data gathered between frames.
#[derive(Default)]
pub(crate) struct WebInput {
/// Required because we don't get a position on touched
/// Required because we don't get a position on touchend
pub primary_touch: Option<egui::TouchId>,
/// Helps to track the delta scale from gesture events
pub accumulated_scale: f32,
/// Helps to track the delta rotation from gesture events
pub accumulated_rotation: f32,
/// The raw input to `egui`.
pub raw: egui::RawInput,
}
@@ -50,11 +56,20 @@ impl WebInput {
// ----------------------------------------------------------------------------
/// Stores when to do the next repaint.
pub(crate) struct NeedRepaint(Mutex<f64>);
pub(crate) struct NeedRepaint {
/// Time in seconds when the next repaint should happen.
next_repaint: Mutex<f64>,
impl Default for NeedRepaint {
fn default() -> Self {
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
/// Rate limit for repaint. 0 means "unlimited". The rate may still be limited by vsync.
max_fps: u32,
}
impl NeedRepaint {
pub fn new(max_fps: Option<u32>) -> Self {
Self {
next_repaint: Mutex::new(f64::NEG_INFINITY), // start with a repaint
max_fps: max_fps.unwrap_or(0),
}
}
}
@@ -62,25 +77,43 @@ impl NeedRepaint {
/// Returns the time (in [`now_sec`] scale) when
/// we should next repaint.
pub fn when_to_repaint(&self) -> f64 {
*self.0.lock()
*self.next_repaint.lock()
}
/// Unschedule repainting.
pub fn clear(&self) {
*self.0.lock() = f64::INFINITY;
*self.next_repaint.lock() = f64::INFINITY;
}
pub fn repaint_after(&self, num_seconds: f64) {
let mut repaint_time = self.0.lock();
*repaint_time = repaint_time.min(super::now_sec() + num_seconds);
let mut time = super::now_sec() + num_seconds;
time = self.round_repaint_time_to_rate(time);
let mut repaint_time = self.next_repaint.lock();
*repaint_time = repaint_time.min(time);
}
/// Request a repaint. Depending on the presence of rate limiting, this may not be instant.
pub fn repaint(&self) {
let time = self.round_repaint_time_to_rate(super::now_sec());
let mut repaint_time = self.next_repaint.lock();
*repaint_time = repaint_time.min(time);
}
pub fn repaint_asap(&self) {
*self.next_repaint.lock() = f64::NEG_INFINITY;
}
pub fn needs_repaint(&self) -> bool {
self.when_to_repaint() <= super::now_sec()
}
pub fn repaint_asap(&self) {
*self.0.lock() = f64::NEG_INFINITY;
fn round_repaint_time_to_rate(&self, time: f64) -> f64 {
if self.max_fps == 0 {
time
} else {
let interval = 1.0 / self.max_fps as f64;
(time / interval).ceil() * interval
}
}
}

View File

@@ -1,15 +1,15 @@
use web_sys::EventTarget;
use crate::web::string_from_js_value;
use super::{
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast, JsValue, WebRunner,
DEBUG_RESIZE,
AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event,
location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event,
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos,
push_touches, text_from_keyboard_event, translate_key,
};
use js_sys::Reflect;
use web_sys::{Document, EventTarget, ShadowRoot};
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
// than what is probably needed.
@@ -102,6 +102,7 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
install_touchcancel(runner_ref, &canvas)?;
install_wheel(runner_ref, &canvas)?;
install_gesture(runner_ref, &canvas)?;
install_drag_and_drop(runner_ref, &canvas)?;
install_window_events(runner_ref, &window)?;
install_color_scheme_change_event(runner_ref, &window)?;
@@ -139,15 +140,20 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
{
if let Some(text) = text_from_keyboard_event(&event) {
let egui_event = egui::Event::Text(text);
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation =
(runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default =
(runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
// If this is indeed text, then prevent any other action.
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
// Use web options to tell if the event should be propagated to parent elements.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
}
@@ -158,7 +164,7 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
)
}
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
let has_focus = runner.input.raw.focused;
if !has_focus {
@@ -184,7 +190,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
repeat: false, // egui will fill this in for us!
modifiers,
};
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
@@ -201,7 +207,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
}
@@ -221,9 +227,10 @@ fn should_prevent_default_for_key(
// Prevent cmd/ctrl plus these keys from triggering the default browser action:
let keys = [
egui::Key::O, // open
egui::Key::P, // print (cmd-P is common for command palette)
egui::Key::S, // save
egui::Key::Comma, // cmd-, opens options on macOS, which egui apps may wanna "steal"
egui::Key::O, // open
egui::Key::P, // print (cmd-P is common for command palette)
egui::Key::S, // save
];
for key in keys {
if egui_key == key && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) {
@@ -256,12 +263,12 @@ fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
runner_ref.add_event_listener(target, "keyup", on_keyup)
}
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
let modifiers = modifiers_from_kb_event(&event);
runner.input.raw.modifiers = modifiers;
let mut propagate_event = false;
let mut should_stop_propagation = true;
if let Some(key) = translate_key(&event.key()) {
let egui_event = egui::Event::Key {
@@ -271,7 +278,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
repeat: false,
modifiers,
};
propagate_event |= (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
runner.input.raw.events.push(egui_event);
}
@@ -282,6 +289,8 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
// See https://github.com/emilk/egui/issues/4724
let keys_down = runner.egui_ctx().input(|i| i.keys_down.clone());
#[expect(clippy::iter_over_hash_type)]
for key in keys_down {
let egui_event = egui::Event::Key {
key,
@@ -290,7 +299,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
repeat: false,
modifiers,
};
propagate_event |= (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
runner.input.raw.events.push(egui_event);
}
}
@@ -299,70 +308,91 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
let has_focus = runner.input.raw.focused;
if has_focus && !propagate_event {
if has_focus && should_stop_propagation {
event.stop_propagation();
}
}
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, runner| {
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
if let Some(data) = event.clipboard_data() {
if let Ok(text) = data.get_data("text") {
let text = text.replace("\r\n", "\n");
let mut should_propagate = false;
if !text.is_empty() && runner.input.raw.focused {
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if !text.is_empty() {
let egui_event = egui::Event::Paste(text);
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation =
(runner.web_options.should_stop_propagation)(&egui_event);
should_prevent_default =
(runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
}
}
})?;
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
if runner.input.raw.focused {
runner.input.raw.events.push(egui::Event::Cut);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
runner.input.raw.events.push(egui::Event::Cut);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !(runner.web_options.should_propagate_event)(&egui::Event::Cut) {
if (runner.web_options.should_stop_propagation)(&egui::Event::Cut) {
event.stop_propagation();
}
event.prevent_default();
if (runner.web_options.should_prevent_default)(&egui::Event::Cut) {
event.prevent_default();
}
})?;
runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| {
if runner.input.raw.focused {
runner.input.raw.events.push(egui::Event::Copy);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
runner.input.raw.events.push(egui::Event::Copy);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !(runner.web_options.should_propagate_event)(&egui::Event::Copy) {
if (runner.web_options.should_stop_propagation)(&egui::Event::Copy) {
event.stop_propagation();
}
event.prevent_default();
if (runner.web_options.should_prevent_default)(&egui::Event::Copy) {
event.prevent_default();
}
})?;
Ok(())
@@ -380,7 +410,7 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result
// No need to subscribe to "resize": we already subscribe to the canvas
// size using a ResizeObserver, and we also subscribe to DPR changes of the monitor.
for event_name in &["load", "pagehide", "pageshow"] {
for event_name in &["load", "pagehide", "pageshow", "popstate"] {
runner_ref.add_event_listener(window, event_name, move |_: web_sys::Event, runner| {
if DEBUG_RESIZE {
log::debug!("{event_name:?}");
@@ -444,16 +474,19 @@ fn install_color_scheme_change_event(
runner_ref: &WebRunner,
window: &web_sys::Window,
) -> Result<(), JsValue> {
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|event, runner| {
let theme = theme_from_dark_mode(event.matches());
runner.input.raw.system_theme = Some(theme);
runner.needs_repaint.repaint_asap();
},
)?;
for theme in [egui::Theme::Dark, egui::Theme::Light] {
if let Some(media_query_list) = prefers_color_scheme(window, theme)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|_event, runner| {
if let Some(theme) = super::system_theme() {
runner.input.raw.system_theme = Some(theme);
runner.needs_repaint.repaint_asap();
}
},
)?;
}
}
Ok(())
@@ -484,7 +517,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|event: web_sys::PointerEvent, runner: &mut AppRunner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
let mut should_propagate = false;
let mut should_stop_propagation = true;
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
let modifiers = runner.input.raw.modifiers;
@@ -494,7 +527,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
pressed: true,
modifiers,
};
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
runner.input.raw.events.push(egui_event);
// In Safari we are only allowed to write to the clipboard during the
@@ -506,7 +539,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
@@ -536,7 +569,10 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
pressed: false,
modifiers,
};
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation =
(runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default =
(runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
// Previously on iOS, the canvas would not receive focus on
@@ -555,10 +591,12 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
}
@@ -570,10 +608,17 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
/// Returns true if the cursor is above the canvas, or if we're dragging something.
/// Pass in the position in browser viewport coordinates (usually event.clientX/Y).
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
let document = web_sys::window().unwrap().document().unwrap();
let is_hovering_canvas = document
.element_from_point(pos.x, pos.y)
.is_some_and(|element| element.eq(runner.canvas()));
let root_node = runner.canvas().get_root_node();
let element_at_point = if let Some(document) = root_node.dyn_ref::<Document>() {
document.element_from_point(pos.x, pos.y)
} else if let Some(shadow) = root_node.dyn_ref::<ShadowRoot>() {
shadow.element_from_point(pos.x, pos.y)
} else {
None
};
let is_hovering_canvas = element_at_point.is_some_and(|element| element.eq(runner.canvas()));
let is_pointer_down = runner
.egui_ctx()
.input(|i| i.pointer.any_down() || i.any_touches());
@@ -593,15 +638,19 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
egui::pos2(event.client_x() as f32, event.client_y() as f32),
) {
let egui_event = egui::Event::PointerMoved(pos);
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
runner.needs_repaint.repaint();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
}
})
}
@@ -615,10 +664,13 @@ fn install_mouseleave(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !(runner.web_options.should_propagate_event)(&egui::Event::PointerGone) {
if (runner.web_options.should_stop_propagation)(&egui::Event::PointerGone) {
event.stop_propagation();
}
event.prevent_default();
if (runner.web_options.should_prevent_default)(&egui::Event::PointerGone) {
event.prevent_default();
}
},
)
}
@@ -628,7 +680,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
target,
"touchstart",
|event: web_sys::TouchEvent, runner| {
let mut should_propagate = false;
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if let Some((pos, _)) = primary_touch_pos(runner, &event) {
let egui_event = egui::Event::PointerButton {
pos,
@@ -636,7 +689,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
pressed: true,
modifiers: runner.input.raw.modifiers,
};
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
}
@@ -644,10 +698,13 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
},
)
}
@@ -660,17 +717,23 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
) {
let egui_event = egui::Event::PointerMoved(pos);
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation =
(runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default =
(runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
push_touches(runner, egui::TouchPhase::Move, &event);
runner.needs_repaint.repaint_asap();
runner.needs_repaint.repaint();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
}
}
})
@@ -684,18 +747,23 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
) {
// First release mouse to click:
let mut should_propagate = false;
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
let egui_event = egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: runner.input.raw.modifiers,
};
should_propagate |= (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation &=
(runner.web_options.should_stop_propagation)(&egui_event);
should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
// Then remove hover effect:
should_propagate |=
(runner.web_options.should_propagate_event)(&egui::Event::PointerGone);
should_stop_propagation &=
(runner.web_options.should_stop_propagation)(&egui::Event::PointerGone);
should_prevent_default &=
(runner.web_options.should_prevent_default)(&egui::Event::PointerGone);
runner.input.raw.events.push(egui::Event::PointerGone);
push_touches(runner, egui::TouchPhase::End, &event);
@@ -703,10 +771,13 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
// Fix virtual keyboard IOS
// Need call focus at the same time of event
@@ -748,7 +819,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
let egui_event = if modifiers.ctrl && !runner.input.raw.modifiers.ctrl {
// The browser is saying the ctrl key is down, but it isn't _really_.
// This happens on pinch-to-zoom on a Mac trackpad.
// This happens on pinch-to-zoom on multitouch trackpads
// egui will treat ctrl+scroll as zoom, so it all works.
// However, we explicitly handle it here in order to better match the pinch-to-zoom
// speed of a native app, without being sensitive to egui's `scroll_zoom_speed` setting.
@@ -760,19 +831,91 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
unit,
delta,
modifiers,
phase: egui::TouchPhase::Move,
}
};
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if should_stop_propagation {
event.stop_propagation();
}
if should_prevent_default {
event.prevent_default();
}
})
}
fn install_gesture(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "gesturestart", |event: web_sys::Event, runner| {
runner.input.accumulated_scale = 1.0;
runner.input.accumulated_rotation = 0.0;
handle_gesture(event, runner);
})?;
runner_ref.add_event_listener(target, "gesturechange", handle_gesture)?;
runner_ref.add_event_listener(target, "gestureend", |event: web_sys::Event, runner| {
handle_gesture(event, runner);
runner.input.accumulated_scale = 1.0;
runner.input.accumulated_rotation = 0.0;
})?;
Ok(())
}
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
fn handle_gesture(event: web_sys::Event, runner: &mut AppRunner) {
// GestureEvent is a non-standard API, so this attempts to get the relevant fields if they exist.
let new_scale = Reflect::get(&event, &JsValue::from_str("scale"))
.ok()
.and_then(|scale| scale.as_f64())
.map_or(1.0, |scale| scale as f32);
let new_rotation = Reflect::get(&event, &JsValue::from_str("rotation"))
.ok()
.and_then(|rotation| rotation.as_f64())
.map_or(0.0, |rotation| rotation.to_radians() as f32);
let scale_delta = new_scale / runner.input.accumulated_scale;
let rotation_delta = new_rotation - runner.input.accumulated_rotation;
runner.input.accumulated_scale *= scale_delta;
runner.input.accumulated_rotation += rotation_delta;
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if scale_delta != 1.0 {
let zoom_event = egui::Event::Zoom(scale_delta);
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&zoom_event);
should_prevent_default &= (runner.web_options.should_prevent_default)(&zoom_event);
runner.input.raw.events.push(zoom_event);
}
if rotation_delta != 0.0 {
let rotate_event = egui::Event::Rotate(rotation_delta);
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&rotate_event);
should_prevent_default &= (runner.web_options.should_prevent_default)(&rotate_event);
runner.input.raw.events.push(rotate_event);
}
if scale_delta != 1.0 || rotation_delta != 0.0 {
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
})
if should_prevent_default {
// Prevents a simulated ctrl-scroll event for zoom
event.prevent_default();
}
}
}
fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
@@ -856,7 +999,10 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
}
}
Err(err) => {
log::error!("Failed to read file: {:?}", err);
log::error!(
"Failed to read file: {}",
string_from_js_value(&err)
);
}
}
};
@@ -925,7 +1071,7 @@ impl ResizeObserverContext {
// we rely on the resize observer to trigger the first `request_animation_frame`:
if let Err(err) = runner_ref.request_animation_frame() {
log::error!("{}", super::string_from_js_value(&err));
};
}
} else {
log::warn!("ResizeObserverContext callback: failed to lock runner");
}

View File

@@ -1,4 +1,4 @@
use super::{canvas_content_rect, AppRunner};
use super::{AppRunner, canvas_content_rect};
pub fn pos_from_mouse_event(
canvas: &web_sys::HtmlCanvasElement,

View File

@@ -23,25 +23,22 @@ pub use panic_handler::{PanicHandler, PanicSummary};
pub use web_logger::WebLogger;
pub use web_runner::WebRunner;
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
#[cfg(not(any(feature = "glow", feature = "wgpu_no_default_features")))]
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
mod web_painter;
#[cfg(feature = "glow")]
mod web_painter_glow;
#[cfg(feature = "glow")]
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
mod web_painter_wgpu;
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
pub use backend::*;
use egui::Theme;
use wasm_bindgen::prelude::*;
use web_sys::MediaQueryList;
use web_sys::{Document, MediaQueryList, Node};
use input::{
button_from_mouse_event, modifiers_from_kb_event, modifiers_from_mouse_event,
@@ -64,18 +61,22 @@ pub(crate) fn string_from_js_value(value: &JsValue) -> String {
/// - `<a>`/`<area>` with an `href` attribute
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
/// - any other element with a `tabindex` attribute
pub(crate) fn focused_element() -> Option<web_sys::Element> {
web_sys::window()?
.document()?
.active_element()?
.dyn_into()
.ok()
pub(crate) fn focused_element(root: &Node) -> Option<web_sys::Element> {
if let Some(document) = root.dyn_ref::<Document>() {
document.active_element()
} else if let Some(shadow) = root.dyn_ref::<web_sys::ShadowRoot>() {
shadow.active_element()
} else {
None
}
}
pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
let element = element.dyn_ref::<web_sys::Element>()?;
let focused_element = focused_element()?;
let root = element.get_root_node();
let focused_element = focused_element(&root)?;
Some(element == &focused_element)
}
try_has_focus(element).unwrap_or(false)
@@ -109,24 +110,31 @@ pub fn native_pixels_per_point() -> f32 {
///
/// `None` means unknown.
pub fn system_theme() -> Option<egui::Theme> {
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
.ok()??
.matches();
Some(theme_from_dark_mode(dark_mode))
}
fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQueryList>, JsValue> {
window.match_media("(prefers-color-scheme: dark)")
}
fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme {
if dark_mode {
egui::Theme::Dark
let window = web_sys::window()?;
if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) {
Some(Theme::Dark)
} else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) {
Some(Theme::Light)
} else {
egui::Theme::Light
None
}
}
fn does_prefer_color_scheme(window: &web_sys::Window, theme: Theme) -> Option<bool> {
Some(prefers_color_scheme(window, theme).ok()??.matches())
}
fn prefers_color_scheme(
window: &web_sys::Window,
theme: Theme,
) -> Result<Option<MediaQueryList>, JsValue> {
let theme = match theme {
Theme::Dark => "dark",
Theme::Light => "light",
};
window.match_media(format!("(prefers-color-scheme: {theme})").as_str())
}
/// Returns the canvas in client coordinates.
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
let bounding_rect = canvas.get_bounding_client_rect();
@@ -277,7 +285,7 @@ fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result<web_sys::ClipboardI
let items = js_sys::Object::new();
// SAFETY: I hope so
#[allow(unsafe_code, unused_unsafe)] // Weird false positive
#[expect(unsafe_code, unused_unsafe)] // Weird false positive
unsafe {
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
};

View File

@@ -4,6 +4,7 @@
use std::cell::Cell;
use wasm_bindgen::prelude::*;
use web_sys::{Document, Node};
use super::{AppRunner, WebRunner};
@@ -14,7 +15,7 @@ pub struct TextAgent {
impl TextAgent {
/// Attach the agent to the document.
pub fn attach(runner_ref: &WebRunner) -> Result<Self, JsValue> {
pub fn attach(runner_ref: &WebRunner, root: Node) -> Result<Self, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
// create an `<input>` element
@@ -37,7 +38,17 @@ impl TextAgent {
style.set_property("position", "absolute")?;
style.set_property("top", "0")?;
style.set_property("left", "0")?;
document.body().unwrap().append_child(&input)?;
if root.has_type::<Document>() {
// root object is a document, append to its body
root.dyn_into::<Document>()?
.body()
.unwrap()
.append_child(&input)?;
} else {
// append input into root directly
root.append_child(&input)?;
}
// attach event listeners
@@ -168,7 +179,7 @@ impl TextAgent {
if let Err(err) = self.input.focus() {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
};
}
}
pub fn blur(&self) {
@@ -180,7 +191,7 @@ impl TextAgent {
if let Err(err) = self.input.blur() {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
};
}
}
}

View File

@@ -110,7 +110,7 @@ mod console {
/// * `tokio-1.24.1/src/runtime/runtime.rs`
/// * `rerun/src/main.rs`
/// * `core/src/ops/function.rs`
#[allow(dead_code)] // only used on web and in tests
#[allow(dead_code, clippy::allow_attributes)] // only used on web and in tests
fn shorten_file_path(file_path: &str) -> &str {
if let Some(i) = file_path.rfind("/src/") {
if let Some(prev_slash) = file_path[..i].rfind('/') {
@@ -126,12 +126,17 @@ fn shorten_file_path(file_path: &str) -> &str {
#[test]
fn test_shorten_file_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
(
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
"tokio-1.24.1/src/runtime/runtime.rs",
),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
(
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
"core/src/ops/function.rs",
),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
] {
assert_eq!(shorten_file_path(before), after);
}
}

View File

@@ -1,7 +1,7 @@
use egui::{Event, UserData, ViewportId};
use egui_glow::glow;
use std::sync::Arc;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsCast as _;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
@@ -20,14 +20,15 @@ impl WebPainterGlow {
self.painter.gl()
}
pub async fn new(
pub fn new(
_ctx: egui::Context,
canvas: HtmlCanvasElement,
options: &WebOptions,
) -> Result<Self, String> {
let (gl, shader_prefix) =
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
let gl = std::sync::Arc::new(gl);
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)

View File

@@ -1,20 +1,22 @@
use std::sync::Arc;
use super::web_painter::WebPainter;
use crate::WebOptions;
use egui::{Event, UserData, ViewportId};
use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use egui_wgpu::{RenderState, SurfaceErrorAction};
use egui_wgpu::{
RenderState, SurfaceErrorAction,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
};
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use super::web_painter::WebPainter;
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
surface: wgpu::Surface<'static>,
surface_configuration: wgpu::SurfaceConfiguration,
render_state: Option<RenderState>,
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
depth_format: Option<wgpu::TextureFormat>,
depth_stencil_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
screen_capture_state: Option<CaptureState>,
capture_tx: CaptureSender,
@@ -23,7 +25,6 @@ pub(crate) struct WebPainterWgpu {
}
impl WebPainterWgpu {
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
@@ -35,7 +36,7 @@ impl WebPainterWgpu {
height_in_pixels: u32,
) -> Option<wgpu::TextureView> {
let device = &render_state.device;
self.depth_format.map(|depth_format| {
self.depth_stencil_format.map(|depth_stencil_format| {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_depth_texture"),
@@ -47,19 +48,18 @@ impl WebPainterWgpu {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: depth_format,
format: depth_stencil_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[depth_format],
view_formats: &[depth_stencil_format],
})
.create_view(&wgpu::TextureViewDescriptor::default())
})
}
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub async fn new(
ctx: egui::Context,
canvas: web_sys::HtmlCanvasElement,
options: &WebOptions,
options: &crate::WebOptions,
) -> Result<Self, String> {
log::debug!("Creating wgpu painter");
@@ -68,15 +68,17 @@ impl WebPainterWgpu {
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
let depth_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
let render_state = RenderState::create(
&options.wgpu_options,
&instance,
Some(&surface),
depth_format,
1,
options.dithering,
egui_wgpu::RendererOptions {
dithering: options.dithering,
depth_stencil_format,
..Default::default()
},
)
.await
.map_err(|err| err.to_string())?;
@@ -101,7 +103,7 @@ impl WebPainterWgpu {
render_state: Some(render_state),
surface,
surface_configuration,
depth_format,
depth_stencil_format,
depth_texture_view: None,
on_surface_error: options.wgpu_options.on_surface_error.clone(),
screen_capture_state: None,
@@ -236,6 +238,7 @@ impl WebPainter for WebPainterWgpu {
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
wgpu::RenderPassDepthStencilAttachment {
@@ -274,18 +277,11 @@ impl WebPainter for WebPainterWgpu {
&mut encoder,
));
}
};
}
Some((output_frame, capture_buffer))
};
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
// Submit the commands: both the main buffer and user-defined ones.
render_state
.queue
@@ -307,6 +303,16 @@ impl WebPainter for WebPainterWgpu {
frame.present();
}
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
Ok(())
}

View File

@@ -2,12 +2,12 @@ use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::prelude::*;
use crate::{epi, App};
use crate::{App, epi};
use super::{
AppRunner, PanicHandler,
events::{self, ResizeObserverContext},
text_agent::TextAgent,
AppRunner, PanicHandler,
};
/// This is how `eframe` runs your web application
@@ -37,7 +37,7 @@ pub struct WebRunner {
impl WebRunner {
/// Will install a panic handler that will catch and log any panics
#[allow(clippy::new_without_default)]
#[expect(clippy::new_without_default)]
pub fn new() -> Self {
let panic_handler = PanicHandler::install();
@@ -73,7 +73,7 @@ impl WebRunner {
{
// First set up the app runner:
let text_agent = TextAgent::attach(self)?;
let text_agent = TextAgent::attach(self, canvas.get_root_node())?;
let app_runner =
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
self.app_runner.replace(Some(app_runner));
@@ -280,7 +280,7 @@ struct TargetEvent {
closure: Closure<dyn FnMut(web_sys::Event)>,
}
#[allow(unused)]
#[expect(unused)]
struct IntervalHandle {
handle: i32,
closure: Closure<dyn FnMut()>,
@@ -289,7 +289,7 @@ struct IntervalHandle {
enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(unused)]
#[expect(unused)]
IntervalHandle(IntervalHandle),
}

View File

@@ -6,6 +6,42 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.2 - 2025-11-13
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
## 0.33.0 - 2025-10-09
### 🔧 Changed
* Update wgpu to 26 and wasm-bindgen to 0.2.100 [#7540](https://github.com/emilk/egui/pull/7540) by [@Kumpelinus](https://github.com/Kumpelinus)
* Warn if `DYLD_LIBRARY_PATH` is set and we find no wgpu adapter [#7572](https://github.com/emilk/egui/pull/7572) by [@emilk](https://github.com/emilk)
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
* Update wgpu to 27.0.0 [#7580](https://github.com/emilk/egui/pull/7580) by [@Wumpf](https://github.com/Wumpf)
* Create `egui_wgpu::RendererOptions` [#7601](https://github.com/emilk/egui/pull/7601) by [@emilk](https://github.com/emilk)
* Use software texture filtering in kittest [#7602](https://github.com/emilk/egui/pull/7602) by [@emilk](https://github.com/emilk)
## 0.32.3 - 2025-09-12
Nothing new
## 0.32.2 - 2025-09-04
Nothing new
## 0.32.1 - 2025-08-15
* Enable wgpu default features in eframe / egui_wgpu default features [#7344](https://github.com/emilk/egui/pull/7344) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.32.0 - 2025-07-10
* Update to wgpu 25 [#6744](https://github.com/emilk/egui/pull/6744) by [@torokati44](https://github.com/torokati44)
* Free textures after submitting queue instead of before with wgpu renderer on Web [#7291](https://github.com/emilk/egui/pull/7291) by [@Wumpf](https://github.com/Wumpf)
* Improve texture filtering by doing it in gamma space [#7311](https://github.com/emilk/egui/pull/7311) by [@emilk](https://github.com/emilk)
## 0.31.1 - 2025-03-05
Nothing new
## 0.31.0 - 2025-02-04
* Upgrade to wgpu 24 [#5610](https://github.com/emilk/egui/pull/5610) by [@torokati44](https://github.com/torokati44)
* Extend `WgpuSetup`, `egui_kittest` now prefers software rasterizers for testing [#5506](https://github.com/emilk/egui/pull/5506) by [@Wumpf](https://github.com/Wumpf)

View File

@@ -9,19 +9,13 @@ authors = [
]
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
include = [
"../LICENSE-APACHE",
"../LICENSE-MIT",
"**/*.rs",
"**/*.wgsl",
"Cargo.toml",
]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
[lints]
workspace = true
@@ -31,10 +25,13 @@ all-features = true
rustdoc-args = ["--generate-link-to-definition"]
[features]
default = ["fragile-send-sync-non-atomic-wasm"]
default = ["fragile-send-sync-non-atomic-wasm", "macos-window-resize-jitter-fix", "wgpu/default"]
## Enables the `capture` module for capturing screenshots.
capture = ["dep:egui"]
## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11`
winit = ["dep:winit", "winit/rwh_06"]
winit = ["dep:winit", "winit/rwh_06", "dep:egui", "capture"]
## Enables Wayland support for winit.
wayland = ["winit?/wayland"]
@@ -43,14 +40,16 @@ wayland = ["winit?/wayland"]
x11 = ["winit?/x11"]
## Make the renderer `Sync` on wasm, exploiting that by default wasm isn't multithreaded.
## It may make code easier, expecially when targeting both native and web.
## It may make code easier, especially when targeting both native and web.
## On native most wgpu objects are send and sync, on the web they are not (by nature of the WebGPU specification).
## This is not supported in [multithreaded WASM](https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer).
## Thus that usage is guarded against with compiler errors in wgpu.
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"]
## Enables `present_with_transaction` surface flag temporary during window resize on MacOS.
macos-window-resize-jitter-fix = ["wgpu/metal"]
[dependencies]
egui = { workspace = true, default-features = false }
epaint = { workspace = true, default-features = false, features = ["bytemuck"] }
ahash.workspace = true
@@ -65,4 +64,5 @@ wgpu = { workspace = true, features = ["wgsl"] }
# Optional dependencies:
egui = { workspace = true, optional = true, default-features = false }
winit = { workspace = true, optional = true, default-features = false }

View File

@@ -1,9 +1,10 @@
use egui::{UserData, ViewportId};
use epaint::ColorImage;
use std::sync::{mpsc, Arc};
use std::sync::{Arc, mpsc};
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
/// A texture and a buffer for reading the rendered frame back to the cpu.
///
/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
/// flag for the surface texture on all platforms. This means that anytime we want to
/// capture the frame, we first render it to this texture, and then we can copy it to
@@ -125,7 +126,7 @@ impl CaptureState {
// It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
// for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
// it might make sense to revisit this and implement a more efficient solution.
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_screen_capture_buffer"),
size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
@@ -159,6 +160,7 @@ impl CaptureState {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
@@ -184,7 +186,7 @@ impl CaptureState {
tx: CaptureSender,
viewport_id: ViewportId,
) {
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
let buffer = Arc::new(buffer);
let buffer_clone = buffer.clone();
let buffer_slice = buffer_clone.slice(..);
@@ -195,13 +197,15 @@ impl CaptureState {
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
_ => {
log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", format);
log::error!(
"Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {format:?}"
);
return;
}
};
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
if let Err(err) = result {
log::error!("Failed to map buffer for reading: {:?}", err);
log::error!("Failed to map buffer for reading: {err}");
return;
}
let buffer_slice = buffer.slice(..);
@@ -226,10 +230,10 @@ impl CaptureState {
tx.send((
viewport_id,
data,
ColorImage {
size: [tex_extent.width as usize, tex_extent.height as usize],
ColorImage::new(
[tex_extent.width as usize, tex_extent.height as usize],
pixels,
},
),
))
.ok();
ctx.request_repaint();

View File

@@ -8,10 +8,13 @@ struct VertexOutput {
struct Locals {
screen_size: vec2<f32>,
dithering: u32, // 1 if dithering is enabled, 0 otherwise
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: u32,
/// 1 if dithering is enabled, 0 otherwise
dithering: u32,
/// 1 to do manual filtering for more predictable kittest snapshot images.
/// See also https://github.com/emilk/egui/issues/5295
predictable_texture_filtering: u32,
};
@group(0) @binding(0) var<uniform> r_locals: Locals;
@@ -95,11 +98,42 @@ fn vs_main(
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
@group(1) @binding(1) var r_tex_sampler: sampler;
fn sample_texture(in: VertexOutput) -> vec4<f32> {
if r_locals.predictable_texture_filtering == 0 {
// Hardware filtering: fast, but varies across GPUs and drivers.
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
} else {
// Manual bilinear filtering with four taps at pixel centers using textureLoad
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
let texture_size_f = vec2<f32>(texture_size);
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
let pixel_fract = fract(pixel_coord);
let pixel_floor = vec2<i32>(floor(pixel_coord));
// Manual texture clamping
let max_coord = texture_size - vec2<i32>(1, 1);
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
let p11 = clamp(pixel_floor + vec2<i32>(1, 1), vec2<i32>(0, 0), max_coord);
// Load at pixel centers
let tl = textureLoad(r_tex_color, p00, 0);
let tr = textureLoad(r_tex_color, p10, 0);
let bl = textureLoad(r_tex_color, p01, 0);
let br = textureLoad(r_tex_color, p11, 0);
// Manual bilinear interpolation
let top = mix(tl, tr, pixel_fract.x);
let bottom = mix(bl, br, pixel_fract.x);
return mix(top, bottom, pixel_fract.y);
}
}
@fragment
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
// We expect "normal" textures that are NOT sRGB-aware.
let tex_gamma = sample_texture(in);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
@@ -115,9 +149,8 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
@fragment
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
// We expect "normal" textures that are NOT sRGB-aware.
let tex_gamma = sample_texture(in);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.

View File

@@ -29,6 +29,7 @@ pub use renderer::*;
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
/// Helpers for capturing screenshots of the UI.
#[cfg(feature = "capture")]
pub mod capture;
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
@@ -42,8 +43,11 @@ use epaint::mutex::RwLock;
/// An error produced by egui-wgpu.
#[derive(thiserror::Error, Debug)]
pub enum WgpuError {
#[error("Failed to create wgpu adapter, no suitable adapter found: {0}")]
NoSuitableAdapterFound(String),
#[error(transparent)]
RequestAdapterError(#[from] wgpu::RequestAdapterError),
#[error("Adapter selection failed: {0}")]
CustomNativeAdapterSelectionError(String),
#[error("There was no valid format for the surface at all.")]
NoSurfaceFormatsAvailable,
@@ -89,7 +93,7 @@ async fn request_adapter(
instance: &wgpu::Instance,
power_preference: wgpu::PowerPreference,
compatible_surface: Option<&wgpu::Surface<'_>>,
_available_adapters: &[wgpu::Adapter],
available_adapters: &[wgpu::Adapter],
) -> Result<wgpu::Adapter, WgpuError> {
profiling::function_scope!();
@@ -104,48 +108,58 @@ async fn request_adapter(
force_fallback_adapter: false,
})
.await
.ok_or_else(|| {
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.is_empty() {
log::info!("No wgpu adapters found");
} else if _available_adapters.len() == 1 {
.inspect_err(|_err| {
if cfg!(target_arch = "wasm32") {
// Nothing to add here
} else if available_adapters.is_empty() {
if std::env::var("DYLD_LIBRARY_PATH").is_ok() {
// DYLD_LIBRARY_PATH can sometimes lead to loading dylibs that cause
// us to find zero adapters. Very strange.
// I don't want to debug this again.
// See https://github.com/rerun-io/rerun/issues/11351 for more
log::warn!(
"No wgpu adapter found. This could be because DYLD_LIBRARY_PATH causes dylibs to be loaded that interfere with Metal device creation. Try restarting with DYLD_LIBRARY_PATH=''"
);
} else {
log::info!("No wgpu adapter found");
}
} else if available_adapters.len() == 1 {
log::info!(
"The only available wgpu adapter was not suitable: {}",
adapter_info_summary(&_available_adapters[0].get_info())
adapter_info_summary(&available_adapters[0].get_info())
);
} else {
log::info!(
"No suitable wgpu adapter found out of the {} available ones: {}",
_available_adapters.len(),
describe_adapters(_available_adapters)
available_adapters.len(),
describe_adapters(available_adapters)
);
}
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
})?;
#[cfg(target_arch = "wasm32")]
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.len() == 1 {
log::debug!(
"Picked the only available wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
log::info!(
"There were {} available wgpu adapters: {}",
_available_adapters.len(),
describe_adapters(_available_adapters)
);
if cfg!(target_arch = "wasm32") {
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
// native:
if available_adapters.len() == 1 {
log::debug!(
"Picked the only available wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
log::info!(
"There were {} available wgpu adapters: {}",
available_adapters.len(),
describe_adapters(available_adapters)
);
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
}
}
Ok(adapter)
@@ -160,9 +174,7 @@ impl RenderState {
config: &WgpuConfiguration,
instance: &wgpu::Instance,
compatible_surface: Option<&wgpu::Surface<'static>>,
depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32,
dithering: bool,
options: RendererOptions,
) -> Result<Self, WgpuError> {
profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
@@ -184,7 +196,6 @@ impl RenderState {
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
trace_path,
}) => {
let adapter = {
#[cfg(target_arch = "wasm32")]
@@ -194,7 +205,7 @@ impl RenderState {
#[cfg(not(target_arch = "wasm32"))]
if let Some(native_adapter_selector) = _native_adapter_selector {
native_adapter_selector(&available_adapters, compatible_surface)
.map_err(WgpuError::NoSuitableAdapterFound)
.map_err(WgpuError::CustomNativeAdapterSelectionError)
} else {
request_adapter(
instance,
@@ -209,7 +220,7 @@ impl RenderState {
let (device, queue) = {
profiling::scope!("request_device");
adapter
.request_device(&(*device_descriptor)(&adapter), trace_path.as_deref())
.request_device(&(*device_descriptor)(&adapter))
.await?
};
@@ -232,17 +243,11 @@ impl RenderState {
};
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
let renderer = Renderer::new(
&device,
target_format,
depth_format,
msaa_samples,
dithering,
);
let renderer = Renderer::new(&device, target_format, options);
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
Ok(Self {
adapter,
#[cfg(not(target_arch = "wasm32"))]
@@ -255,7 +260,6 @@ impl RenderState {
}
}
#[cfg(not(target_arch = "wasm32"))]
fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
if adapters.is_empty() {
"(none)".to_owned()

View File

@@ -3,7 +3,8 @@
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
use bytemuck::Zeroable as _;
use epaint::{PaintCallbackInfo, Primitive, Vertex, emath::NumExt as _};
use wgpu::util::DeviceExt as _;
@@ -84,7 +85,7 @@ impl Callback {
///
/// # Example
///
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/main/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
pub trait CallbackTrait: Send + Sync {
fn prepare(
&self,
@@ -140,21 +141,16 @@ impl ScreenDescriptor {
}
/// Uniform buffer used when rendering.
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct UniformBuffer {
screen_size_in_points: [f32; 2],
dithering: u32,
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: u32,
}
impl PartialEq for UniformBuffer {
fn eq(&self, other: &Self) -> bool {
self.screen_size_in_points == other.screen_size_in_points
&& self.dithering == other.dithering
}
/// 1 to do manual filtering for more predictable kittest snapshot images.
///
/// See also <https://github.com/emilk/egui/issues/5295>.
predictable_texture_filtering: u32,
}
struct SlicedBuffer {
@@ -175,6 +171,69 @@ pub struct Texture {
pub options: Option<epaint::textures::TextureOptions>,
}
/// Ways to configure [`Renderer`] during creation.
#[derive(Clone, Copy, Debug)]
pub struct RendererOptions {
/// Set the level of the multisampling anti-aliasing (MSAA).
///
/// Must be a power-of-two. Higher = more smooth 3D.
///
/// A value of `0` or `1` turns it off (default).
///
/// `egui` already performs anti-aliasing via "feathering"
/// (controlled by [`egui::epaint::TessellationOptions`]),
/// but if you are embedding 3D in egui you may want to turn on multisampling.
pub msaa_samples: u32,
/// What format to use for the depth and stencil buffers,
/// e.g. [`wgpu::TextureFormat::Depth32FloatStencil8`].
///
/// egui doesn't need depth/stencil, so the default value is `None` (no depth or stancil buffers).
pub depth_stencil_format: Option<wgpu::TextureFormat>,
/// Controls whether to apply dithering to minimize banding artifacts.
///
/// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between
/// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space".
/// This means that only inputs from texture interpolation and vertex colors should be affected in practice.
///
/// Defaults to true.
pub dithering: bool,
/// Perform texture filtering in software?
///
/// This is useful when you want predictable rendering across
/// different hardware, e.g. for kittest snapshots.
///
/// Default is `false`.
///
/// See also <https://github.com/emilk/egui/issues/5295>.
pub predictable_texture_filtering: bool,
}
impl RendererOptions {
/// Set options that produce the most predicatable output.
///
/// Useful for image snapshot tests.
pub const PREDICTABLE: Self = Self {
msaa_samples: 1,
depth_stencil_format: None,
dithering: false,
predictable_texture_filtering: true,
};
}
impl Default for RendererOptions {
fn default() -> Self {
Self {
msaa_samples: 0,
depth_stencil_format: None,
dithering: true,
predictable_texture_filtering: false,
}
}
}
/// Renderer for a egui based GUI.
pub struct Renderer {
pipeline: wgpu::RenderPipeline,
@@ -194,7 +253,7 @@ pub struct Renderer {
next_user_texture_id: u64,
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
dithering: bool,
options: RendererOptions,
/// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
///
@@ -210,9 +269,7 @@ impl Renderer {
pub fn new(
device: &wgpu::Device,
output_color_format: wgpu::TextureFormat,
output_depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32,
dithering: bool,
options: RendererOptions,
) -> Self {
profiling::function_scope!();
@@ -229,8 +286,8 @@ impl Renderer {
label: Some("egui_uniform_buffer"),
contents: bytemuck::cast_slice(&[UniformBuffer {
screen_size_in_points: [0.0, 0.0],
dithering: u32::from(dithering),
_padding: Default::default(),
dithering: u32::from(options.dithering),
predictable_texture_filtering: u32::from(options.predictable_texture_filtering),
}]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
@@ -299,13 +356,15 @@ impl Renderer {
push_constant_ranges: &[],
});
let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
let depth_stencil = options
.depth_stencil_format
.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
let pipeline = {
profiling::scope!("create_render_pipeline");
@@ -337,14 +396,14 @@ impl Renderer {
depth_stencil,
multisample: wgpu::MultisampleState {
alpha_to_coverage_enabled: false,
count: msaa_samples,
count: options.msaa_samples.max(1),
mask: !0,
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some(if output_color_format.is_srgb() {
log::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format);
log::warn!("Detected a linear (sRGBA aware) framebuffer {output_color_format:?}. egui prefers Rgba8Unorm or Bgra8Unorm");
"fs_main_linear_framebuffer"
} else {
"fs_main_gamma_framebuffer" // this is what we prefer
@@ -392,17 +451,13 @@ impl Renderer {
},
uniform_buffer,
// Buffers on wgpu are zero initialized, so this is indeed its current state!
previous_uniform_buffer_content: UniformBuffer {
screen_size_in_points: [0.0, 0.0],
dithering: 0,
_padding: 0,
},
previous_uniform_buffer_content: UniformBuffer::zeroed(),
uniform_bind_group,
texture_bind_group_layout,
textures: HashMap::default(),
next_user_texture_id: 0,
samplers: HashMap::default(),
dithering,
options,
callback_resources: CallbackResources::default(),
}
}
@@ -564,15 +619,6 @@ impl Renderer {
);
Cow::Borrowed(&image.pixels)
}
epaint::ImageData::Font(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
"Mismatch between texture size and texel count"
);
profiling::scope!("font -> sRGBA");
Cow::Owned(image.srgba_pixels(None).collect::<Vec<epaint::Color32>>())
}
};
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
@@ -638,9 +684,9 @@ impl Renderer {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
})
};
let origin = wgpu::Origin3d::ZERO;
@@ -699,7 +745,7 @@ impl Renderer {
///
/// This enables the application to reference the texture inside an image ui element.
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
/// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// the texture format [`wgpu::TextureFormat::Rgba8Unorm`].
pub fn register_native_texture(
&mut self,
device: &wgpu::Device,
@@ -747,9 +793,9 @@ impl Renderer {
/// This allows applications to specify individual minification/magnification filters as well as
/// custom mipmap and tiling options.
///
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// The texture must have the format [`wgpu::TextureFormat::Rgba8Unorm`].
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
#[allow(clippy::needless_pass_by_value)] // false positive
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn register_native_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
@@ -796,7 +842,7 @@ impl Renderer {
/// [`wgpu::SamplerDescriptor`] options.
///
/// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler options.
#[allow(clippy::needless_pass_by_value)] // false positive
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
@@ -855,8 +901,8 @@ impl Renderer {
let uniform_buffer_content = UniformBuffer {
screen_size_in_points,
dithering: u32::from(self.dithering),
_padding: Default::default(),
dithering: u32::from(self.options.dithering),
predictable_texture_filtering: u32::from(self.options.predictable_texture_filtering),
};
if uniform_buffer_content != self.previous_uniform_buffer_content {
profiling::scope!("update uniforms");
@@ -882,7 +928,7 @@ impl Renderer {
callbacks.push(c.0.as_ref());
} else {
log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
};
}
acc
}
}
@@ -909,7 +955,11 @@ impl Renderer {
);
let Some(mut index_buffer_staging) = index_buffer_staging else {
panic!("Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity);
panic!(
"Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)",
self.index_buffer.buffer.size(),
self.index_buffer.capacity
);
};
let mut index_offset = 0;
@@ -948,7 +998,11 @@ impl Renderer {
);
let Some(mut vertex_buffer_staging) = vertex_buffer_staging else {
panic!("Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity);
panic!(
"Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)",
self.vertex_buffer.buffer.size(),
self.vertex_buffer.capacity
);
};
let mut vertex_offset = 0;

View File

@@ -48,7 +48,7 @@ impl WgpuSetup {
pub async fn new_instance(&self) -> wgpu::Instance {
match self {
Self::CreateNew(create_new) => {
#[allow(unused_mut)]
#[allow(unused_mut, clippy::allow_attributes)]
let mut backends = create_new.instance_descriptor.backends;
// Don't try WebGPU if we're not in a secure context.
@@ -64,7 +64,7 @@ impl WgpuSetup {
}
}
log::debug!("Creating wgpu instance with backends {:?}", backends);
log::debug!("Creating wgpu instance with backends {backends:?}");
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
.await
}
@@ -126,13 +126,6 @@ pub struct WgpuSetupCreateNew {
/// Configuration passed on device request, given an adapter
pub device_descriptor:
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
/// Option path to output a wgpu trace file.
///
/// This only works if this feature is enabled in `wgpu-core`.
/// Does not work when running with WebGPU.
/// Defaults to the path set in the `WGPU_TRACE` environment variable.
pub trace_path: Option<std::path::PathBuf>,
}
impl Clone for WgpuSetupCreateNew {
@@ -142,7 +135,6 @@ impl Clone for WgpuSetupCreateNew {
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: self.device_descriptor.clone(),
trace_path: self.trace_path.clone(),
}
}
}
@@ -156,7 +148,6 @@ impl std::fmt::Debug for WgpuSetupCreateNew {
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
)
.field("trace_path", &self.trace_path)
.finish()
}
}
@@ -171,6 +162,7 @@ impl Default for WgpuSetupCreateNew {
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
flags: wgpu::InstanceFlags::from_build_config().with_env(),
backend_options: wgpu::BackendOptions::from_env_or_default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
},
power_preference: wgpu::PowerPreference::from_env()
@@ -187,20 +179,15 @@ impl Default for WgpuSetupCreateNew {
wgpu::DeviceDescriptor {
label: Some("egui wgpu device"),
required_features: wgpu::Features::default(),
required_limits: wgpu::Limits {
// When using a depth buffer, we have to be able to create a texture
// large enough for the entire surface, and we want to support 4k+ displays.
max_texture_dimension_2d: 8192,
..base_limits
},
memory_hints: wgpu::MemoryHints::default(),
..Default::default()
}
}),
trace_path: std::env::var("WGPU_TRACE")
.ok()
.map(std::path::PathBuf::from),
}
}
}

View File

@@ -1,8 +1,11 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::undocumented_unsafe_blocks)]
use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration};
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
use crate::{
RendererOptions,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
};
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
use std::{num::NonZeroU32, sync::Arc};
@@ -11,6 +14,7 @@ struct SurfaceState {
alpha_mode: wgpu::CompositeAlphaMode,
width: u32,
height: u32,
resizing: bool,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
@@ -21,10 +25,8 @@ struct SurfaceState {
pub struct Painter {
context: Context,
configuration: WgpuConfiguration,
msaa_samples: u32,
options: RendererOptions,
support_transparent_backbuffer: bool,
dithering: bool,
depth_format: Option<wgpu::TextureFormat>,
screen_capture_state: Option<CaptureState>,
instance: wgpu::Instance,
@@ -54,10 +56,8 @@ impl Painter {
pub async fn new(
context: Context,
configuration: WgpuConfiguration,
msaa_samples: u32,
depth_format: Option<wgpu::TextureFormat>,
support_transparent_backbuffer: bool,
dithering: bool,
options: RendererOptions,
) -> Self {
let (capture_tx, capture_rx) = capture_channel();
let instance = configuration.wgpu_setup.new_instance().await;
@@ -65,10 +65,8 @@ impl Painter {
Self {
context,
configuration,
msaa_samples,
options,
support_transparent_backbuffer,
dithering,
depth_format,
screen_capture_state: None,
instance,
@@ -204,9 +202,7 @@ impl Painter {
&self.configuration,
&self.instance,
Some(&surface),
self.depth_format,
self.msaa_samples,
self.dithering,
self.options,
)
.await?;
self.render_state.get_or_insert(render_state)
@@ -220,7 +216,9 @@ impl Painter {
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
wgpu::CompositeAlphaMode::PostMultiplied
} else {
log::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
log::warn!(
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
);
wgpu::CompositeAlphaMode::Auto
}
} else {
@@ -233,6 +231,7 @@ impl Painter {
width: size.width,
height: size.height,
alpha_mode,
resizing: false,
},
);
let Some(width) = NonZeroU32::new(size.width) else {
@@ -277,7 +276,7 @@ impl Painter {
Self::configure_surface(surface_state, render_state, &self.configuration);
if let Some(depth_format) = self.depth_format {
if let Some(depth_format) = self.options.depth_stencil_format {
self.depth_texture_view.insert(
viewport_id,
render_state
@@ -290,7 +289,7 @@ impl Painter {
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.msaa_samples,
sample_count: self.options.msaa_samples.max(1),
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
@@ -301,7 +300,7 @@ impl Painter {
);
}
if let Some(render_state) = (self.msaa_samples > 1)
if let Some(render_state) = (self.options.msaa_samples > 1)
.then_some(self.render_state.as_ref())
.flatten()
{
@@ -318,7 +317,7 @@ impl Painter {
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.msaa_samples,
sample_count: self.options.msaa_samples.max(1),
dimension: wgpu::TextureDimension::D2,
format: texture_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
@@ -326,7 +325,60 @@ impl Painter {
})
.create_view(&wgpu::TextureViewDescriptor::default()),
);
}
}
/// Handles changes of the resizing state.
///
/// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
/// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
profiling::function_scope!();
let Some(state) = self.surfaces.get_mut(&viewport_id) else {
return;
};
if state.resizing == resizing {
if resizing {
log::debug!(
"Painter::on_window_resize_state_change() redundant call while resizing"
);
} else {
log::debug!(
"Painter::on_window_resize_state_change() redundant call after resizing"
);
}
return;
}
// Resizing is a bit tricky on macOS.
// It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
// flag to avoid jittering during the resize. Even though resize jittering on macOS
// is common across rendering backends, the solution for wgpu/metal is known.
//
// See https://github.com/emilk/egui/issues/903
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
{
// SAFETY: The cast is checked with if condition. If the used backend is not metal
// it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping.
// This is how wgpu currently exposes this backend-specific flag.
unsafe {
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
let raw =
std::ptr::from_ref::<wgpu::hal::metal::Surface>(&*hal_surface).cast_mut();
(*raw).present_with_transaction = resizing;
Self::configure_surface(
state,
self.render_state.as_ref().unwrap(),
&self.configuration,
);
}
}
}
state.resizing = resizing;
}
pub fn on_window_resized(
@@ -344,7 +396,9 @@ impl Painter {
height_in_pixels,
);
} else {
log::warn!("Ignoring window resize notification with no surface created via Painter::set_window()");
log::warn!(
"Ignoring window resize notification with no surface created via Painter::set_window()"
);
}
}
@@ -446,7 +500,7 @@ impl Painter {
};
let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
let (view, resolve_target) = (self.msaa_samples > 1)
let (view, resolve_target) = (self.options.msaa_samples > 1)
.then_some(self.msaa_texture_view.get(&viewport_id))
.flatten()
.map_or((&target_view, None), |texture_view| {
@@ -467,6 +521,7 @@ impl Painter {
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
wgpu::RenderPassDepthStencilAttachment {
@@ -493,14 +548,12 @@ impl Painter {
&screen_descriptor,
);
if capture {
if let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
if capture && let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
}
@@ -530,16 +583,16 @@ impl Painter {
}
}
if let Some(capture_buffer) = capture_buffer {
if let Some(screen_capture_state) = &mut self.screen_capture_state {
screen_capture_state.read_screen_rgba(
self.context.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
viewport_id,
);
}
if let Some(capture_buffer) = capture_buffer
&& let Some(screen_capture_state) = &mut self.screen_capture_state
{
screen_capture_state.read_screen_rgba(
self.context.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
viewport_id,
);
}
{
@@ -575,7 +628,7 @@ impl Painter {
.retain(|id, _| active_viewports.contains(id));
}
#[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
#[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}

View File

@@ -5,6 +5,49 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.2 - 2025-11-13
* Don't enable `arboard` on iOS [#7663](https://github.com/emilk/egui/pull/7663) by [@irh](https://github.com/irh)
## 0.33.0 - 2025-10-09
### ⭐ Added
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)
* Add support for the safe area on iOS [#7578](https://github.com/emilk/egui/pull/7578) by [@irh](https://github.com/irh)
### 🔧 Changed
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
* Create `egui_wgpu::RendererOptions` [#7601](https://github.com/emilk/egui/pull/7601) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Fix build error in egui-winit with profiling enabled [#7557](https://github.com/emilk/egui/pull/7557) by [@torokati44](https://github.com/torokati44)
* Properly end winit event loop [#7565](https://github.com/emilk/egui/pull/7565) by [@tye-exe](https://github.com/tye-exe)
* Fix eframe window not being focused on mac on startup [#7593](https://github.com/emilk/egui/pull/7593) by [@emilk](https://github.com/emilk)
## 0.32.3 - 2025-09-12
Nothing new
## 0.32.2 - 2025-09-04
Nothing new
## 0.32.1 - 2025-08-15
* Update to winit 0.30.12 [#7420](https://github.com/emilk/egui/pull/7420) by [@emilk](https://github.com/emilk)
## 0.32.0 - 2025-07-10
* Mark all keys as released if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
* Support for back-button on Android [#7073](https://github.com/emilk/egui/pull/7073) by [@ardocrat](https://github.com/ardocrat)
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
## 0.31.1 - 2025-03-05
Nothing new
## 0.31.0 - 2025-02-04
* Re-enable IME support on Linux [#5198](https://github.com/emilk/egui/pull/5198) by [@YgorSouza](https://github.com/YgorSouza)
* Update to winit 0.30.7 [#5516](https://github.com/emilk/egui/pull/5516) by [@emilk](https://github.com/emilk)

View File

@@ -5,10 +5,10 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
@@ -24,7 +24,7 @@ rustdoc-args = ["--generate-link-to-definition"]
default = ["clipboard", "links", "wayland", "winit/default", "x11"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["dep:accesskit_winit", "egui/accesskit"]
accesskit = ["dep:accesskit_winit"]
# Allow crates to choose an android-activity backend via Winit
# - It's important that most applications should not have to depend on android-activity directly, and can
@@ -55,9 +55,8 @@ wayland = ["winit/wayland", "bytemuck"]
x11 = ["winit/x11", "bytemuck"]
[dependencies]
egui = { workspace = true, default-features = false, features = ["log"] }
egui = { workspace = true, default-features = false }
ahash.workspace = true
log.workspace = true
profiling.workspace = true
raw-window-handle.workspace = true
@@ -67,7 +66,7 @@ winit = { workspace = true, default-features = false }
#! ### Optional dependencies
# feature accesskit
accesskit_winit = { version = "0.23", optional = true }
accesskit_winit = { workspace = true, optional = true }
bytemuck = { workspace = true, optional = true }
@@ -75,17 +74,30 @@ bytemuck = { workspace = true, optional = true }
document-features = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
webbrowser = { version = "1.0.0", optional = true }
webbrowser = { workspace = true, optional = true }
[target.'cfg(target_os = "ios")'.dependencies]
objc2.workspace = true
objc2-foundation = { workspace = true, features = ["std", "NSThread"] }
objc2-ui-kit = { workspace = true, features = [
"std",
"UIApplication",
"UIGeometry",
"UIResponder",
"UIScene",
"UISceneDefinitions",
"UIView",
"UIWindow",
"UIWindowScene",
] }
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
smithay-clipboard = { version = "0.7.2", optional = true }
smithay-clipboard = { workspace = true, optional = true }
# The wayland-cursor normally selected doesn't properly enable all the features it uses
# and thus doesn't compile as it is used in egui-winit. This is fixed upstream, so force
# a slightly newer version. Remove this when winit upgrades past this version.
wayland-cursor = { version = "0.31.1", default-features = false, optional = true }
wayland-cursor = { workspace = true, optional = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "3.3", optional = true, default-features = false, features = [
"image-data",
] }
arboard = { workspace = true, optional = true, features = ["image-data"] }

View File

@@ -5,7 +5,10 @@ use raw_window_handle::RawDisplayHandle;
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
/// then a fallback clipboard that just works within the same app is used instead.
pub struct Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
arboard: Option<arboard::Clipboard>,
#[cfg(all(
@@ -28,7 +31,10 @@ impl Clipboard {
/// Construct a new instance
pub fn new(_raw_display_handle: Option<RawDisplayHandle>) -> Self {
Self {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
arboard: init_arboard(),
#[cfg(all(
@@ -68,7 +74,10 @@ impl Clipboard {
};
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
return match clipboard.get_text() {
Ok(text) => Some(text),
@@ -98,7 +107,10 @@ impl Clipboard {
return;
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_text(text) {
log::error!("arboard copy/cut error: {err}");
@@ -110,7 +122,10 @@ impl Clipboard {
}
pub fn set_image(&mut self, image: &egui::ColorImage) {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_image(arboard::ImageData {
width: image.width(),
@@ -123,12 +138,17 @@ impl Clipboard {
return;
}
log::error!("Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it.");
log::error!(
"Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it."
);
_ = image;
}
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
fn init_arboard() -> Option<arboard::Clipboard> {
profiling::function_scope!();
@@ -161,7 +181,7 @@ fn init_smithay_clipboard(
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
log::trace!("Initializing smithay clipboard…");
#[allow(unsafe_code)]
#[expect(unsafe_code)]
Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
} else {
#[cfg(feature = "wayland")]

View File

@@ -18,11 +18,11 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
pub use winit;
pub mod clipboard;
mod safe_area;
mod window_settings;
pub use window_settings::WindowSettings;
use ahash::HashSet;
use raw_window_handle::HasDisplayHandle;
use winit::{
@@ -166,12 +166,14 @@ impl State {
#[cfg(feature = "accesskit")]
pub fn init_accesskit<T: From<accesskit_winit::Event> + Send>(
&mut self,
event_loop: &ActiveEventLoop,
window: &Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
) {
profiling::function_scope!();
self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy(
event_loop,
window,
event_loop_proxy,
));
@@ -273,6 +275,21 @@ impl State {
}
use winit::event::WindowEvent;
#[cfg(target_os = "ios")]
match &event {
WindowEvent::Resized(_)
| WindowEvent::ScaleFactorChanged { .. }
| WindowEvent::Focused(true)
| WindowEvent::Occluded(false) => {
// Once winit v0.31 has been released this can be reworked to get the safe area from
// `Window::safe_area`, and updated from a new event which is being discussed in
// https://github.com/rust-windowing/winit/issues/3911.
self.egui_input_mut().safe_area_insets = Some(safe_area::get_safe_area_insets());
}
_ => {}
}
match event {
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let native_pixels_per_point = *scale_factor as f32;
@@ -295,8 +312,8 @@ impl State {
consumed: self.egui_ctx.wants_pointer_input(),
}
}
WindowEvent::MouseWheel { delta, .. } => {
self.on_mouse_wheel(window, *delta);
WindowEvent::MouseWheel { delta, phase, .. } => {
self.on_mouse_wheel(window, *delta, *phase);
EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_pointer_input(),
@@ -371,7 +388,7 @@ impl State {
winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => {
self.ime_event_disable();
}
};
}
EventResponse {
repaint: true,
@@ -406,10 +423,19 @@ impl State {
}
}
WindowEvent::Focused(focused) => {
self.egui_input.focused = *focused;
let focused = if cfg!(target_os = "macos") {
// TODO(emilk): remove this work-around once we update winit
// https://github.com/rust-windowing/winit/issues/4371
// https://github.com/emilk/egui/issues/7588
window.has_focus()
} else {
*focused
};
self.egui_input.focused = focused;
self.egui_input
.events
.push(egui::Event::WindowFocused(*focused));
.push(egui::Event::WindowFocused(focused));
EventResponse {
repaint: true,
consumed: false,
@@ -490,9 +516,7 @@ impl State {
// Things we completely ignore:
WindowEvent::ActivationTokenDone { .. }
| WindowEvent::AxisMotion { .. }
| WindowEvent::DoubleTapGesture { .. }
| WindowEvent::RotationGesture { .. }
| WindowEvent::PanGesture { .. } => EventResponse {
| WindowEvent::DoubleTapGesture { .. } => EventResponse {
repaint: false,
consumed: false,
},
@@ -507,6 +531,34 @@ impl State {
consumed: self.egui_ctx.wants_pointer_input(),
}
}
WindowEvent::RotationGesture { delta, .. } => {
// Positive delta values indicate counterclockwise rotation
// Negative delta values indicate clockwise rotation
// This is opposite of egui's sign convention for angles
self.egui_input
.events
.push(egui::Event::Rotate(-delta.to_radians()));
EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_pointer_input(),
}
}
WindowEvent::PanGesture { delta, phase, .. } => {
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
self.egui_input.events.push(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
phase: to_egui_touch_phase(*phase),
modifiers: self.egui_input.modifiers,
});
EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_pointer_input(),
}
}
}
}
@@ -548,41 +600,41 @@ impl State {
state: winit::event::ElementState,
button: winit::event::MouseButton,
) {
if let Some(pos) = self.pointer_pos_in_points {
if let Some(button) = translate_mouse_button(button) {
let pressed = state == winit::event::ElementState::Pressed;
if let Some(pos) = self.pointer_pos_in_points
&& let Some(button) = translate_mouse_button(button)
{
let pressed = state == winit::event::ElementState::Pressed;
self.egui_input.events.push(egui::Event::PointerButton {
pos,
button,
pressed,
modifiers: self.egui_input.modifiers,
});
self.egui_input.events.push(egui::Event::PointerButton {
pos,
button,
pressed,
modifiers: self.egui_input.modifiers,
});
if self.simulate_touch_screen {
if pressed {
self.any_pointer_button_down = true;
if self.simulate_touch_screen {
if pressed {
self.any_pointer_button_down = true;
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::Start,
pos,
force: None,
});
} else {
self.any_pointer_button_down = false;
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::Start,
pos,
force: None,
});
} else {
self.any_pointer_button_down = false;
self.egui_input.events.push(egui::Event::PointerGone);
self.egui_input.events.push(egui::Event::PointerGone);
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::End,
pos,
force: None,
});
};
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::End,
pos,
force: None,
});
}
}
}
@@ -629,12 +681,7 @@ impl State {
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
id: egui::TouchId::from(touch.id),
phase: match touch.phase {
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
},
phase: to_egui_touch_phase(touch.phase),
pos: egui::pos2(
touch.location.x as f32 / pixels_per_point,
touch.location.y as f32 / pixels_per_point,
@@ -687,7 +734,12 @@ impl State {
}
}
fn on_mouse_wheel(&mut self, window: &Window, delta: winit::event::MouseScrollDelta) {
fn on_mouse_wheel(
&mut self,
window: &Window,
delta: winit::event::MouseScrollDelta,
phase: winit::event::TouchPhase,
) {
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
{
@@ -703,10 +755,12 @@ impl State {
egui::vec2(x as f32, y as f32) / pixels_per_point,
),
};
let phase = to_egui_touch_phase(phase);
let modifiers = self.egui_input.modifiers;
self.egui_input.events.push(egui::Event::MouseWheel {
unit,
delta,
phase,
modifiers,
});
}
@@ -727,7 +781,7 @@ impl State {
// When telling users "Press Ctrl-F to find", this is where we should
// look for the "F" key, because they may have a dvorak layout on
// a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position.
logical_key,
logical_key: winit_logical_key,
text,
@@ -746,7 +800,7 @@ impl State {
None
};
let logical_key = key_from_winit_key(logical_key);
let logical_key = key_from_winit_key(winit_logical_key);
// Helpful logging to enable when adding new key support
log::trace!(
@@ -789,7 +843,11 @@ impl State {
});
}
if let Some(text) = &text {
if let Some(text) = text
.as_ref()
.map(|t| t.as_str())
.or_else(|| winit_logical_key.to_text())
{
// Make sure there is text, and that it is not control characters
// (e.g. delete is sent as "\u{f728}" on macOS).
if !text.is_empty() && text.chars().all(is_printable_char) {
@@ -803,7 +861,7 @@ impl State {
if pressed && !is_cmd {
self.egui_input
.events
.push(egui::Event::Text(text.to_string()));
.push(egui::Event::Text(text.to_owned()));
}
}
}
@@ -822,18 +880,14 @@ impl State {
window: &Window,
platform_output: egui::PlatformOutput,
) {
#![allow(deprecated)]
profiling::function_scope!();
let egui::PlatformOutput {
commands,
cursor_icon,
open_url,
copied_text,
events: _, // handled elsewhere
mutable_text_under_cursor: _, // only used in eframe web
ime,
#[cfg(feature = "accesskit")]
accesskit_update,
num_completed_passes: _, // `egui::Context::run` handles this
request_discard_reasons: _, // `egui::Context::run` handles this
@@ -855,14 +909,6 @@ impl State {
self.set_cursor_icon(window, cursor_icon);
if let Some(open_url) = open_url {
open_url_in_browser(&open_url.url);
}
if !copied_text.is_empty() {
self.clipboard.set_text(copied_text);
}
let allow_ime = ime.is_some();
if self.allow_ime != allow_ime {
self.allow_ime = allow_ime;
@@ -894,12 +940,15 @@ impl State {
}
#[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_mut() {
if let Some(update) = accesskit_update {
profiling::scope!("accesskit");
accesskit.update_if_active(|| update);
}
if let Some(accesskit) = self.accesskit.as_mut()
&& let Some(update) = accesskit_update
{
profiling::scope!("accesskit");
accesskit.update_if_active(|| update);
}
#[cfg(not(feature = "accesskit"))]
let _ = accesskit_update;
}
fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
@@ -926,6 +975,15 @@ impl State {
}
}
fn to_egui_touch_phase(phase: winit::event::TouchPhase) -> egui::TouchPhase {
match phase {
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
}
}
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
match theme {
winit::window::Theme::Dark => Theme::Dark,
@@ -1020,7 +1078,7 @@ pub fn update_viewport_info(
fn open_url_in_browser(_url: &str) {
#[cfg(feature = "webbrowser")]
if let Err(err) = webbrowser::open(_url) {
log::warn!("Failed to open url: {}", err);
log::warn!("Failed to open url: {err}");
}
#[cfg(not(feature = "webbrowser"))]
@@ -1138,6 +1196,8 @@ fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option<egui::Key>
NamedKey::F33 => Key::F33,
NamedKey::F34 => Key::F34,
NamedKey::F35 => Key::F35,
NamedKey::BrowserBack => Key::BrowserBack,
_ => {
log::trace!("Unknown key: {named_key:?}");
return None;
@@ -1326,7 +1386,7 @@ pub fn process_viewport_commands(
info: &mut ViewportInfo,
commands: impl IntoIterator<Item = ViewportCommand>,
window: &Window,
actions_requested: &mut HashSet<ActionRequested>,
actions_requested: &mut Vec<ActionRequested>,
) {
for command in commands {
process_viewport_command(egui_ctx, window, command, info, actions_requested);
@@ -1338,9 +1398,9 @@ fn process_viewport_command(
window: &Window,
command: ViewportCommand,
info: &mut ViewportInfo,
actions_requested: &mut HashSet<ActionRequested>,
actions_requested: &mut Vec<ActionRequested>,
) {
profiling::function_scope!();
profiling::function_scope!(&format!("{command:?}"));
use winit::window::ResizeDirection;
@@ -1357,10 +1417,10 @@ fn process_viewport_command(
}
ViewportCommand::StartDrag => {
// If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed!
if window.has_focus() {
if let Err(err) = window.drag_window() {
log::warn!("{command:?}: {err}");
}
if window.has_focus()
&& let Err(err) = window.drag_window()
{
log::warn!("{command:?}: {err}");
}
}
ViewportCommand::InnerSize(size) => {
@@ -1531,16 +1591,16 @@ fn process_viewport_command(
}
}
ViewportCommand::Screenshot(user_data) => {
actions_requested.insert(ActionRequested::Screenshot(user_data));
actions_requested.push(ActionRequested::Screenshot(user_data));
}
ViewportCommand::RequestCut => {
actions_requested.insert(ActionRequested::Cut);
actions_requested.push(ActionRequested::Cut);
}
ViewportCommand::RequestCopy => {
actions_requested.insert(ActionRequested::Copy);
actions_requested.push(ActionRequested::Copy);
}
ViewportCommand::RequestPaste => {
actions_requested.insert(ActionRequested::Paste);
actions_requested.push(ActionRequested::Paste);
}
}
}
@@ -1558,8 +1618,7 @@ pub fn create_window(
) -> Result<Window, winit::error::OsError> {
profiling::function_scope!();
let window_attributes =
create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone());
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
let window = event_loop.create_window(window_attributes)?;
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
Ok(window)
@@ -1567,28 +1626,10 @@ pub fn create_window(
pub fn create_winit_window_attributes(
egui_ctx: &egui::Context,
event_loop: &ActiveEventLoop,
viewport_builder: ViewportBuilder,
) -> winit::window::WindowAttributes {
profiling::function_scope!();
// We set sizes and positions in egui:s own ui points, which depends on the egui
// zoom_factor and the native pixels per point, so we need to know that here.
// We don't know what monitor the window will appear on though, but
// we'll try to fix that after the window is created in the call to `apply_viewport_builder_to_window`.
let native_pixels_per_point = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
.map_or_else(
|| {
log::debug!("Failed to find a monitor - assuming native_pixels_per_point of 1.0");
1.0
},
|m| m.scale_factor() as f32,
);
let zoom_factor = egui_ctx.zoom_factor();
let pixels_per_point = zoom_factor * native_pixels_per_point;
let ViewportBuilder {
title,
position,
@@ -1610,9 +1651,11 @@ pub fn create_winit_window_attributes(
// macOS:
fullsize_content_view: _fullsize_content_view,
movable_by_window_background: _movable_by_window_background,
title_shown: _title_shown,
titlebar_buttons_shown: _titlebar_buttons_shown,
titlebar_shown: _titlebar_shown,
has_shadow: _has_shadow,
// Windows:
drag_and_drop: _drag_and_drop,
@@ -1662,40 +1705,46 @@ pub fn create_winit_window_attributes(
})
.with_active(active.unwrap_or(true));
// Here and below: we create `LogicalSize` / `LogicalPosition` taking
// zoom factor into account. We don't have a good way to get physical size here,
// and trying to do it anyway leads to weird bugs on Wayland, see:
// https://github.com/emilk/egui/issues/7095#issuecomment-2920545377
// https://github.com/rust-windowing/winit/issues/4266
#[expect(
clippy::disallowed_types,
reason = "zoom factor is manually accounted for"
)]
#[cfg(not(target_os = "ios"))]
if let Some(size) = inner_size {
window_attributes = window_attributes.with_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
{
use winit::dpi::{LogicalPosition, LogicalSize};
let zoom_factor = egui_ctx.zoom_factor();
#[cfg(not(target_os = "ios"))]
if let Some(size) = min_inner_size {
window_attributes = window_attributes.with_min_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
if let Some(size) = inner_size {
window_attributes = window_attributes
.with_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
#[cfg(not(target_os = "ios"))]
if let Some(size) = max_inner_size {
window_attributes = window_attributes.with_max_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
if let Some(size) = min_inner_size {
window_attributes = window_attributes
.with_min_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
#[cfg(not(target_os = "ios"))]
if let Some(pos) = position {
window_attributes = window_attributes.with_position(PhysicalPosition::new(
pixels_per_point * pos.x,
pixels_per_point * pos.y,
));
if let Some(size) = max_inner_size {
window_attributes = window_attributes
.with_max_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
if let Some(pos) = position {
window_attributes = window_attributes.with_position(LogicalPosition::new(
zoom_factor * pos.x,
zoom_factor * pos.y,
));
}
}
#[cfg(target_os = "ios")]
{
// Unused:
_ = egui_ctx;
_ = pixels_per_point;
_ = position;
_ = inner_size;
@@ -1756,7 +1805,9 @@ pub fn create_winit_window_attributes(
.with_title_hidden(!_title_shown.unwrap_or(true))
.with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
.with_titlebar_transparent(!_titlebar_shown.unwrap_or(true))
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false));
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false))
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false))
.with_has_shadow(_has_shadow.unwrap_or(true));
}
window_attributes
@@ -1783,10 +1834,10 @@ pub fn apply_viewport_builder_to_window(
window: &Window,
builder: &ViewportBuilder,
) {
if let Some(mouse_passthrough) = builder.mouse_passthrough {
if let Err(err) = window.set_cursor_hittest(!mouse_passthrough) {
log::warn!("set_cursor_hittest failed: {err}");
}
if let Some(mouse_passthrough) = builder.mouse_passthrough
&& let Err(err) = window.set_cursor_hittest(!mouse_passthrough)
{
log::warn!("set_cursor_hittest failed: {err}");
}
{
@@ -1797,16 +1848,15 @@ pub fn apply_viewport_builder_to_window(
let pixels_per_point = pixels_per_point(egui_ctx, window);
if let Some(size) = builder.inner_size {
if window
if let Some(size) = builder.inner_size
&& window
.request_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
))
.is_some()
{
log::debug!("Failed to set window size");
}
{
log::debug!("Failed to set window size");
}
if let Some(size) = builder.min_inner_size {
window.set_min_inner_size(Some(PhysicalSize::new(
@@ -1838,8 +1888,8 @@ pub fn short_device_event_description(event: &winit::event::DeviceEvent) -> &'st
use winit::event::DeviceEvent;
match event {
DeviceEvent::Added { .. } => "DeviceEvent::Added",
DeviceEvent::Removed { .. } => "DeviceEvent::Removed",
DeviceEvent::Added => "DeviceEvent::Added",
DeviceEvent::Removed => "DeviceEvent::Removed",
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
@@ -1857,11 +1907,11 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone",
WindowEvent::Resized { .. } => "WindowEvent::Resized",
WindowEvent::Moved { .. } => "WindowEvent::Moved",
WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested",
WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed",
WindowEvent::CloseRequested => "WindowEvent::CloseRequested",
WindowEvent::Destroyed => "WindowEvent::Destroyed",
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled",
WindowEvent::HoveredFileCancelled => "WindowEvent::HoveredFileCancelled",
WindowEvent::Focused { .. } => "WindowEvent::Focused",
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
@@ -1872,7 +1922,7 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture",
WindowEvent::RedrawRequested { .. } => "WindowEvent::RedrawRequested",
WindowEvent::RedrawRequested => "WindowEvent::RedrawRequested",
WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture",
WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture",
WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",

View 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 isnt covered by a navigation bar, tab bar,
/// toolbar, or other views a window might provide. Safe areas are essential for avoiding a
/// devices 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()
}
}

View File

@@ -26,10 +26,6 @@ rustdoc-args = ["--generate-link-to-definition"]
[features]
default = ["default_fonts"]
## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration.
accesskit = ["dep:accesskit"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`.
bytemuck = ["epaint/bytemuck"]
@@ -44,18 +40,10 @@ cint = ["epaint/cint"]
## Enable the [`hex_color`] macro.
color-hex = ["epaint/color-hex"]
## This will automatically detect deadlocks due to double-locking on the same thread.
## If your app freezes, you may want to enable this!
## Only affects [`epaint::mutex::RwLock`] (which egui uses a lot).
deadlock_detection = ["epaint/deadlock_detection"]
## If set, egui will use `include_bytes!` to bundle some fonts.
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["epaint/default_fonts"]
## Turn on the `log` feature, that makes egui log some errors using the [`log`](https://docs.rs/log) crate.
log = ["dep:log", "epaint/log"]
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
mint = ["epaint/mint"]
@@ -69,7 +57,7 @@ persistence = ["serde", "epaint/serde", "ron"]
rayon = ["epaint/rayon"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
serde = ["dep:serde", "epaint/serde", "accesskit/serde"]
## Change Vertex layout to be compatible with unity
unity = ["epaint/unity"]
@@ -83,19 +71,21 @@ _override_unity = ["epaint/_override_unity"]
emath = { workspace = true, default-features = false }
epaint = { workspace = true, default-features = false }
accesskit.workspace = true
ahash.workspace = true
bitflags.workspace = true
log.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true
smallvec.workspace = true
unicode-segmentation.workspace = true
#! ### Optional dependencies
accesskit = { version = "0.17.0", optional = true }
backtrace = { workspace = true, optional = true }
## Enable this when generating docs.
document-features = { workspace = true, optional = true }
log = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive", "rc"] }

View File

@@ -1,7 +1,7 @@
There are no stand-alone egui examples, because egui is not stand-alone!
See the top-level [examples](https://github.com/emilk/egui/tree/master/examples/) folder instead.
See the top-level [examples](https://github.com/emilk/egui/tree/main/examples/) folder instead.
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/crates/egui_demo_lib>.
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/main/crates/egui_demo_lib>.
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!

View File

@@ -1,6 +1,6 @@
use crate::{
emath::{remap_clamp, NumExt as _},
Id, IdMap, InputState,
emath::{NumExt as _, remap_clamp},
};
#[derive(Clone, Default)]

View 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()
}
}
}

View 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
}
}

View 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())
}
}

View 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
}
}

View 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());
}
}

View 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::*;

View 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
}
}

View 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,
}
}
}

View File

@@ -1,5 +1,5 @@
/// A cache, storing some value for some length of time.
#[allow(clippy::len_without_is_empty)]
#[expect(clippy::len_without_is_empty)]
pub trait CacheTrait: 'static + Send + Sync {
/// Call once per frame to evict cache.
fn update(&mut self);

View File

@@ -20,10 +20,10 @@ pub fn capture() -> String {
backtrace::resolve_frame(frame, |symbol| {
let mut file_and_line = symbol.filename().map(shorten_source_file_path);
if let Some(file_and_line) = &mut file_and_line {
if let Some(line_nr) = symbol.lineno() {
file_and_line.push_str(&format!(":{line_nr}"));
}
if let Some(file_and_line) = &mut file_and_line
&& let Some(line_nr) = symbol.lineno()
{
file_and_line.push_str(&format!(":{line_nr}"));
}
let file_and_line = file_and_line.unwrap_or_default();
@@ -204,12 +204,17 @@ fn shorten_source_file_path(path: &std::path::Path) -> String {
#[test]
fn test_shorten_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
(
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
"tokio-1.24.1/src/runtime/runtime.rs",
),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
(
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
"core/src/ops/function.rs",
),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
] {
use std::str::FromStr as _;
let before = std::path::PathBuf::from_str(before).unwrap();
assert_eq!(shorten_source_file_path(&before), after);

View File

@@ -5,8 +5,8 @@
use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
};
/// State of an [`Area`] that is persisted between frames.
@@ -103,10 +103,10 @@ impl AreaState {
///
/// The previous rectangle used by this area can be obtained through [`crate::Memory::area_rect()`].
#[must_use = "You should call .show()"]
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub struct Area {
pub(crate) id: Id,
kind: UiKind,
info: UiStackInfo,
sense: Option<Sense>,
movable: bool,
interactable: bool,
@@ -120,6 +120,8 @@ pub struct Area {
anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>,
fade_in: bool,
layout: Layout,
sizing_pass: bool,
}
impl WidgetWithState for Area {
@@ -131,7 +133,7 @@ impl Area {
pub fn new(id: Id) -> Self {
Self {
id,
kind: UiKind::GenericArea,
info: UiStackInfo::new(UiKind::GenericArea),
sense: None,
movable: true,
interactable: true,
@@ -145,6 +147,8 @@ impl Area {
pivot: Align2::LEFT_TOP,
anchor: None,
fade_in: true,
layout: Layout::default(),
sizing_pass: false,
}
}
@@ -162,7 +166,16 @@ impl Area {
/// Default to [`UiKind::GenericArea`].
#[inline]
pub fn kind(mut self, kind: UiKind) -> Self {
self.kind = kind;
self.info = UiStackInfo::new(kind);
self
}
/// Set the [`UiStackInfo`] of the area's [`Ui`].
///
/// Default to [`UiStackInfo::new(UiKind::GenericArea)`].
#[inline]
pub fn info(mut self, info: UiStackInfo) -> Self {
self.info = info;
self
}
@@ -339,10 +352,38 @@ impl Area {
self.fade_in = fade_in;
self
}
/// Set the layout for the child Ui.
#[inline]
pub fn layout(mut self, layout: Layout) -> Self {
self.layout = layout;
self
}
/// While true, a sizing pass will be done. This means the area will be invisible
/// and the contents will be laid out to estimate the proper containing size of the area.
/// If false, there will be no change to the default area behavior. This is useful if the
/// area contents area dynamic and you need to need to make sure the area adjusts its size
/// accordingly.
///
/// This should only be set to true during the specific frames you want force a sizing pass.
/// Do NOT hard-code this as `.sizing_pass(true)`, as it will cause the area to never be
/// visible.
///
/// # Arguments
/// - resize: If true, the area will be resized to fit its contents. False will keep the
/// default area resizing behavior.
///
/// Default: `false`.
#[inline]
pub fn sizing_pass(mut self, resize: bool) -> Self {
self.sizing_pass = resize;
self
}
}
pub(crate) struct Prepared {
kind: UiKind,
info: Option<UiStackInfo>,
layer_id: LayerId,
state: AreaState,
move_response: Response,
@@ -358,6 +399,7 @@ pub(crate) struct Prepared {
sizing_pass: bool,
fade_in: bool,
layout: Layout,
}
impl Area {
@@ -366,7 +408,7 @@ impl Area {
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let prepared = self.begin(ctx);
let mut prepared = self.begin(ctx);
let mut content_ui = prepared.content_ui(ctx);
let inner = add_contents(&mut content_ui);
let response = prepared.end(ctx, content_ui);
@@ -376,7 +418,7 @@ impl Area {
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
let Self {
id,
kind,
info,
sense,
movable,
order,
@@ -390,9 +432,11 @@ impl Area {
constrain,
constrain_rect,
fade_in,
layout,
sizing_pass: force_sizing_pass,
} = self;
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.content_rect());
let layer_id = LayerId::new(order, id);
@@ -405,6 +449,10 @@ impl Area {
interactable,
last_became_visible_at: None,
});
if force_sizing_pass {
sizing_pass = true;
state.size = None;
}
state.pivot = pivot;
state.interactable = interactable;
if let Some(new_pos) = new_pos {
@@ -477,10 +525,21 @@ impl Area {
true,
);
if movable && move_response.dragged() {
if let Some(pivot_pos) = &mut state.pivot_pos {
*pivot_pos += move_response.drag_delta();
}
// Used to prevent drift
let pivot_at_start_of_drag_id = id.with("pivot_at_drag_start");
if movable
&& move_response.dragged()
&& let Some(pivot_pos) = &mut state.pivot_pos
{
let pivot_at_start_of_drag = ctx.data_mut(|data| {
*data.get_temp_mut_or::<Pos2>(pivot_at_start_of_drag_id, *pivot_pos)
});
*pivot_pos =
pivot_at_start_of_drag + move_response.total_drag_delta().unwrap_or_default();
} else {
ctx.data_mut(|data| data.remove::<Pos2>(pivot_at_start_of_drag_id));
}
if (move_response.dragged() || move_response.clicked())
@@ -494,20 +553,21 @@ impl Area {
move_response
};
if constrain {
state.set_left_top_pos(
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
);
}
state.set_left_top_pos(state.left_top_pos());
state.set_left_top_pos(round_area_position(
ctx,
if constrain {
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
} else {
state.left_top_pos()
},
));
// Update response with possibly moved/constrained rect:
move_response.rect = state.rect();
move_response.interact_rect = state.rect();
Prepared {
kind,
info: Some(info),
layer_id,
state,
move_response,
@@ -516,10 +576,21 @@ impl Area {
constrain_rect,
sizing_pass,
fade_in,
layout,
}
}
}
fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
// We round a lot of rendering to pixels, so we round the whole
// area positions to pixels too, so avoid widgets appearing to float
// around independently of each other when the area is dragged.
// But just in case pixels_per_point is irrational,
// we then also round to ui coordinates:
pos.round_to_pixels(ctx.pixels_per_point()).round_ui()
}
impl Prepared {
pub(crate) fn state(&self) -> &AreaState {
&self.state
@@ -537,13 +608,16 @@ impl Prepared {
self.constrain_rect
}
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
pub(crate) fn content_ui(&mut self, ctx: &Context) -> Ui {
let max_rect = self.state.rect();
let mut ui_builder = UiBuilder::new()
.ui_stack_info(UiStackInfo::new(self.kind))
.ui_stack_info(self.info.take().unwrap_or_default())
.layer_id(self.layer_id)
.max_rect(max_rect);
.max_rect(max_rect)
.layout(self.layout)
.accessibility_parent(self.move_response.id)
.closable();
if !self.enabled {
ui_builder = ui_builder.disabled();
@@ -555,16 +629,16 @@ impl Prepared {
let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
ui.set_clip_rect(self.constrain_rect); // Don't paint outside our bounds
if self.fade_in {
if let Some(last_became_visible_at) = self.state.last_became_visible_at {
let age =
ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
ui.multiply_opacity(opacity);
if opacity < 1.0 {
ctx.request_repaint();
}
if self.fade_in
&& let Some(last_became_visible_at) = self.state.last_became_visible_at
{
let age =
ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
ui.multiply_opacity(opacity);
if opacity < 1.0 {
ctx.request_repaint();
}
}
@@ -579,10 +653,10 @@ impl Prepared {
self.move_response.id
}
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
#[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
let Self {
kind: _,
info: _,
layer_id,
mut state,
move_response: mut response,
@@ -598,6 +672,12 @@ impl Prepared {
response.rect = final_rect;
response.interact_rect = final_rect;
// TODO(lucasmerlin): Can the area response be based on Ui::response? Then this won't be needed
// Bubble up the close event
if content_ui.should_close() {
response.set_close();
}
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
if sizing_pass {
@@ -648,7 +728,7 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
let current_column_bb = column_bbs.last_mut().unwrap();
if rect.left() < current_column_bb.right() {
// same column
*current_column_bb = current_column_bb.union(rect);
*current_column_bb |= rect;
} else {
// new column
column_bbs.push(rect);

View 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)
}
}

View File

@@ -1,8 +1,9 @@
use std::hash::Hash;
use crate::{
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, WidgetInfo, WidgetText, WidgetType,
Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle,
TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType,
emath, epaint, pos2, remap, remap_clamp, vec2,
};
use emath::GuiRounding as _;
use epaint::{Shape, StrokeKind};
@@ -203,11 +204,16 @@ impl CollapsingState {
add_body: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
let openness = self.openness(ui.ctx());
let builder = UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::Collapsible))
.closable();
if openness <= 0.0 {
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
None
} else if openness < 1.0 {
Some(ui.scope(|child_ui| {
Some(ui.scope_builder(builder, |child_ui| {
let max_height = if self.state.open && self.state.open_height.is_none() {
// First frame of expansion.
// We don't know full height yet, but we will next frame.
@@ -226,6 +232,9 @@ impl CollapsingState {
let mut min_rect = child_ui.min_rect();
self.state.open_height = Some(min_rect.height());
if child_ui.should_close() {
self.state.open = false;
}
self.store(child_ui.ctx()); // remember the height
// Pretend children took up at most `max_height` space:
@@ -234,7 +243,10 @@ impl CollapsingState {
ret
}))
} else {
let ret_response = ui.scope(add_body);
let ret_response = ui.scope_builder(builder, add_body);
if ret_response.response.should_close() {
self.state.open = false;
}
let full_size = ret_response.response.rect.size();
self.state.open_height = Some(full_size.y);
self.store(ui.ctx()); // remember the height

View File

@@ -1,31 +1,25 @@
use epaint::Shape;
use crate::{
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter,
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
Align2, Context, Id, InnerResponse, NumExt as _, Painter, Popup, PopupCloseBehavior, Rect,
Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo,
WidgetText, WidgetType, epaint, style::StyleModifier, style::WidgetVisuals, vec2,
};
#[allow(unused_imports)] // Documentation
#[expect(unused_imports)] // Documentation
use crate::style::Spacing;
/// Indicate whether a popup will be shown above or below the box.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AboveOrBelow {
Above,
Below,
}
/// A function that paints the [`ComboBox`] icon
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
/// A drop-down selection menu with a descriptive label.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[derive(Debug, PartialEq)]
/// # #[derive(Debug, PartialEq, Copy, Clone)]
/// # enum Enum { First, Second, Third }
/// # let mut selected = Enum::First;
/// let before = selected;
/// egui::ComboBox::from_label("Select one!")
/// .selected_text(format!("{:?}", selected))
/// .show_ui(ui, |ui| {
@@ -34,6 +28,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBe
/// ui.selectable_value(&mut selected, Enum::Third, "Third");
/// }
/// );
///
/// if selected != before {
/// // Handle selection change
/// }
/// # });
/// ```
#[must_use = "You should call .show*"]
@@ -46,6 +44,7 @@ pub struct ComboBox {
icon: Option<IconPainter>,
wrap_mode: Option<TextWrapMode>,
close_behavior: Option<PopupCloseBehavior>,
popup_style: StyleModifier,
}
impl ComboBox {
@@ -60,6 +59,7 @@ impl ComboBox {
icon: None,
wrap_mode: None,
close_behavior: None,
popup_style: StyleModifier::default(),
}
}
@@ -75,6 +75,7 @@ impl ComboBox {
icon: None,
wrap_mode: None,
close_behavior: None,
popup_style: StyleModifier::default(),
}
}
@@ -89,11 +90,12 @@ impl ComboBox {
icon: None,
wrap_mode: None,
close_behavior: None,
popup_style: StyleModifier::default(),
}
}
/// Without label.
#[deprecated = "Renamed id_salt"]
#[deprecated = "Renamed from_id_salt"]
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
Self::from_id_salt(id_salt)
}
@@ -135,7 +137,6 @@ impl ComboBox {
/// rect: egui::Rect,
/// visuals: &egui::style::WidgetVisuals,
/// _is_open: bool,
/// _above_or_below: egui::AboveOrBelow,
/// ) {
/// let rect = egui::Rect::from_center_size(
/// rect.center(),
@@ -154,10 +155,8 @@ impl ComboBox {
/// .show_ui(ui, |_ui| {});
/// # });
/// ```
pub fn icon(
mut self,
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
) -> Self {
#[inline]
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self {
self.icon = Some(Box::new(icon_fn));
self
}
@@ -196,6 +195,16 @@ impl ComboBox {
self
}
/// Set the style of the popup menu.
///
/// Could for example be used with [`crate::containers::menu::menu_style`] to get the frame-less
/// menu button style.
#[inline]
pub fn popup_style(mut self, popup_style: StyleModifier) -> Self {
self.popup_style = popup_style;
self
}
/// Show the combo box, with the given ui code for the menu contents.
///
/// Returns `InnerResponse { inner: None }` if the combo box is closed.
@@ -221,6 +230,7 @@ impl ComboBox {
icon,
wrap_mode,
close_behavior,
popup_style,
} = self;
let button_id = ui.make_persistent_id(id_salt);
@@ -229,21 +239,24 @@ impl ComboBox {
let mut ir = combo_box_dyn(
ui,
button_id,
selected_text,
selected_text.clone(),
menu_contents,
icon,
wrap_mode,
close_behavior,
popup_style,
(width, height),
);
ir.response.widget_info(|| {
let mut info = WidgetInfo::new(WidgetType::ComboBox);
info.enabled = ui.is_enabled();
info.current_text_value = Some(selected_text.text().to_owned());
info
});
if let Some(label) = label {
ir.response.widget_info(|| {
WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text())
});
ir.response |= ui.label(label);
} else {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), ""));
let label_response = ui.label(label);
ir.response = ir.response.labelled_by(label_response.id);
ir.response |= label_response;
}
ir
})
@@ -298,7 +311,7 @@ impl ComboBox {
/// Check if the [`ComboBox`] with the given id has its popup menu currently opened.
pub fn is_open(ctx: &Context, id: Id) -> bool {
ctx.memory(|m| m.is_popup_open(Self::widget_to_popup_id(id)))
Popup::is_id_open(ctx, Self::widget_to_popup_id(id))
}
/// Convert a [`ComboBox`] id to the id used to store it's popup state.
@@ -307,7 +320,7 @@ impl ComboBox {
}
}
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
fn combo_box_dyn<'c, R>(
ui: &mut Ui,
button_id: Id,
@@ -316,27 +329,12 @@ fn combo_box_dyn<'c, R>(
icon: Option<IconPainter>,
wrap_mode: Option<TextWrapMode>,
close_behavior: Option<PopupCloseBehavior>,
popup_style: StyleModifier,
(width, height): (Option<f32>, Option<f32>),
) -> InnerResponse<Option<R>> {
let popup_id = ComboBox::widget_to_popup_id(button_id);
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let popup_height = ui.memory(|m| {
m.areas()
.get(popup_id)
.and_then(|state| state.size)
.map_or(100.0, |size| size.y)
});
let above_or_below =
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
< ui.ctx().screen_rect().bottom()
{
AboveOrBelow::Below
} else {
AboveOrBelow::Above
};
let is_popup_open = Popup::is_id_open(ui.ctx(), popup_id);
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
@@ -385,15 +383,9 @@ fn combo_box_dyn<'c, R>(
icon_rect.expand(visuals.expansion),
visuals,
is_popup_open,
above_or_below,
);
} else {
paint_default_icon(
ui.painter(),
icon_rect.expand(visuals.expansion),
visuals,
above_or_below,
);
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
}
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
@@ -402,19 +394,16 @@ fn combo_box_dyn<'c, R>(
}
});
if button_response.clicked() {
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
let height = height.unwrap_or_else(|| ui.spacing().combo_height);
let inner = crate::popup::popup_above_or_below_widget(
ui,
popup_id,
&button_response,
above_or_below,
close_behavior,
|ui| {
let inner = Popup::menu(&button_response)
.id(popup_id)
.width(button_response.rect.width())
.close_behavior(close_behavior)
.style(popup_style)
.show(|ui| {
ui.set_min_width(ui.available_width());
ScrollArea::vertical()
.max_height(height)
.show(ui, |ui| {
@@ -427,8 +416,8 @@ fn combo_box_dyn<'c, R>(
menu_contents(ui)
})
.inner
},
);
})
.map(|r| r.inner);
InnerResponse {
inner,
@@ -484,33 +473,19 @@ fn button_frame(
response
}
fn paint_default_icon(
painter: &Painter,
rect: Rect,
visuals: &WidgetVisuals,
above_or_below: AboveOrBelow,
) {
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
let rect = Rect::from_center_size(
rect.center(),
vec2(rect.width() * 0.7, rect.height() * 0.45),
);
match above_or_below {
AboveOrBelow::Above => {
// Upward pointing triangle
painter.add(Shape::convex_polygon(
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
visuals.fg_stroke.color,
Stroke::NONE,
));
}
AboveOrBelow::Below => {
// Downward pointing triangle
painter.add(Shape::convex_polygon(
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
visuals.fg_stroke.color,
Stroke::NONE,
));
}
}
// Downward pointing triangle
// Previously, we would show an up arrow when we expected the popup to open upwards
// (due to lack of space below the button), but this could look weird in edge cases, so this
// feature was removed. (See https://github.com/emilk/egui/pull/5713#issuecomment-2654420245)
painter.add(Shape::convex_polygon(
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
visuals.fg_stroke.color,
Stroke::NONE,
));
}

View File

@@ -1,10 +1,10 @@
//! Frame container
use crate::{
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
UiStackInfo,
InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, epaint,
layers::ShapeIdx,
};
use epaint::{Color32, CornerRadius, Margin, Marginf, Rect, Shadow, Shape, Stroke};
use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stroke};
/// A frame around some content, including margin, colors, etc.
///
@@ -46,7 +46,7 @@ use epaint::{Color32, CornerRadius, Margin, Marginf, Rect, Shadow, Shape, Stroke
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::Frame::none()
/// egui::Frame::NONE
/// .fill(egui::Color32::RED)
/// .show(ui, |ui| {
/// ui.label("Label with red background");
@@ -143,7 +143,8 @@ pub struct Frame {
#[test]
fn frame_size() {
assert_eq!(
std::mem::size_of::<Frame>(), 32,
std::mem::size_of::<Frame>(),
32,
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
assert!(
@@ -337,10 +338,10 @@ impl Frame {
///
/// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`].
#[inline]
pub fn total_margin(&self) -> Marginf {
Marginf::from(self.inner_margin)
+ Marginf::from(self.stroke.width)
+ Marginf::from(self.outer_margin)
pub fn total_margin(&self) -> MarginF32 {
MarginF32::from(self.inner_margin)
+ MarginF32::from(self.stroke.width)
+ MarginF32::from(self.outer_margin)
}
/// Calculate the `fill_rect` from the `content_rect`.
@@ -354,14 +355,14 @@ impl Frame {
///
/// This is the visible and interactive rectangle.
pub fn widget_rect(&self, content_rect: Rect) -> Rect {
content_rect + self.inner_margin + Marginf::from(self.stroke.width)
content_rect + self.inner_margin + MarginF32::from(self.stroke.width)
}
/// Calculate the `outer_rect` from the `content_rect`.
///
/// This is what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`].
pub fn outer_rect(&self, content_rect: Rect) -> Rect {
content_rect + self.inner_margin + Marginf::from(self.stroke.width) + self.outer_margin
content_rect + self.inner_margin + MarginF32::from(self.stroke.width) + self.outer_margin
}
}
@@ -463,7 +464,7 @@ impl Prepared {
let content_rect = self.content_ui.min_rect();
content_rect
+ self.frame.inner_margin
+ Marginf::from(self.frame.stroke.width)
+ MarginF32::from(self.frame.stroke.width)
+ self.frame.outer_margin
}

View 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
}
}

View File

@@ -3,29 +3,36 @@
//! For instance, a [`Frame`] adds a frame and background to some contained UI.
pub(crate) mod area;
mod close_tag;
pub mod collapsing_header;
mod combo_box;
pub mod frame;
pub mod menu;
pub mod modal;
pub mod old_popup;
pub mod panel;
pub mod popup;
mod popup;
pub(crate) mod resize;
mod scene;
pub mod scroll_area;
mod sides;
mod tooltip;
pub(crate) mod window;
pub use {
area::{Area, AreaState},
close_tag::ClosableTag,
collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*,
frame::Frame,
modal::{Modal, ModalResponse},
panel::{CentralPanel, Panel, PanelSide, VerticalSide, HorizontalSide},
old_popup::*,
panel::{CentralPanel, HorizontalSide, Panel, PanelSide, VerticalSide},
popup::*,
resize::Resize,
scene::Scene,
scene::{DragPanButtons, Scene},
scroll_area::ScrollArea,
sides::Sides,
tooltip::*,
window::Window,
};

View File

@@ -1,14 +1,18 @@
use emath::{Align2, Vec2};
use crate::{
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
};
use emath::{Align2, Vec2};
/// A modal dialog.
///
/// Similar to a [`crate::Window`] but centered and with a backdrop that
/// blocks input to the rest of the UI.
///
/// You can show multiple modals on top of each other. The topmost modal will always be
/// the most recently shown one.
/// If multiple modals are newly shown in the same frame, the order of the modals is undefined
/// (either first or second could be top).
pub struct Modal {
pub area: Area,
pub backdrop_color: Color32,
@@ -16,7 +20,9 @@ pub struct Modal {
}
impl Modal {
/// Create a new Modal. The id is passed to the area.
/// Create a new Modal.
///
/// The id is passed to the area.
pub fn new(id: Id) -> Self {
Self {
area: Self::default_area(id),
@@ -26,6 +32,7 @@ impl Modal {
}
/// Returns an area customized for a modal.
///
/// Makes these changes to the default area:
/// - sense: hover
/// - anchor: center
@@ -74,18 +81,16 @@ impl Modal {
frame,
} = self;
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
let is_top_modal = ctx.memory_mut(|mem| {
mem.set_modal_layer(area.layer());
(
mem.top_modal_layer() == Some(area.layer()),
mem.any_popup_open(),
)
mem.top_modal_layer() == Some(area.layer())
});
let any_popup_open = crate::Popup::is_any_open(ctx);
let InnerResponse {
inner: (inner, backdrop_response),
response,
} = area.show(ctx, |ui| {
let bg_rect = ui.ctx().screen_rect();
let bg_rect = ui.ctx().content_rect();
let bg_sense = Sense::CLICK | Sense::DRAG;
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
backdrop.set_min_size(bg_rect.size());
@@ -150,7 +155,10 @@ impl<T> ModalResponse<T> {
let escape_clicked =
|| ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
let ui_close_called = self.response.should_close();
self.backdrop_response.clicked()
|| ui_close_called
|| (self.is_top_modal && !self.any_popup_open && escape_clicked())
}
}

View 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

View File

@@ -1,6 +1,6 @@
use crate::{
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt, Rect, Response, Sense, Shape, Ui,
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense, Shape, Ui,
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
};
#[derive(Clone, Copy, Debug)]
@@ -220,7 +220,7 @@ impl Resize {
.at_least(self.min_size)
.at_most(self.max_size)
.at_most(
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
ui.ctx().content_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
)
.round_ui();
@@ -241,14 +241,12 @@ impl Resize {
let corner_id = self.resizable.any().then(|| id.with("__resize_corner"));
if let Some(corner_id) = corner_id {
if let Some(corner_response) = ui.ctx().read_response(corner_id) {
if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
// Respond to the interaction early to avoid frame delay.
user_requested_size =
Some(pointer_pos - position + 0.5 * corner_response.rect.size());
}
}
if let Some(corner_id) = corner_id
&& let Some(corner_response) = ui.ctx().read_response(corner_id)
&& let Some(pointer_pos) = corner_response.interact_pointer_pos()
{
// Respond to the interaction early to avoid frame delay.
user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size());
}
if let Some(user_requested_size) = user_requested_size {

View File

@@ -1,9 +1,10 @@
use core::f32;
use emath::{GuiRounding, Pos2};
use emath::{GuiRounding as _, Pos2};
use crate::{
emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
emath::TSTransform,
};
/// Creates a transformation that fits a given scene rectangle into the available screen size.
@@ -44,14 +45,41 @@ fn fit_to_rect_in_scene(
#[must_use = "You should call .show()"]
pub struct Scene {
zoom_range: Rangef,
sense: Sense,
max_inner_size: Vec2,
drag_pan_buttons: DragPanButtons,
}
/// Specifies which pointer buttons can be used to pan the scene by dragging.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DragPanButtons(u8);
bitflags::bitflags! {
impl DragPanButtons: u8 {
/// [PointerButton::Primary]
const PRIMARY = 1 << 0;
/// [PointerButton::Secondary]
const SECONDARY = 1 << 1;
/// [PointerButton::Middle]
const MIDDLE = 1 << 2;
/// [PointerButton::Extra1]
const EXTRA_1 = 1 << 3;
/// [PointerButton::Extra2]
const EXTRA_2 = 1 << 4;
}
}
impl Default for Scene {
fn default() -> Self {
Self {
zoom_range: Rangef::new(f32::EPSILON, 1.0),
sense: Sense::click_and_drag(),
max_inner_size: Vec2::splat(1000.0),
drag_pan_buttons: DragPanButtons::all(),
}
}
}
@@ -62,6 +90,17 @@ impl Scene {
Default::default()
}
/// Specify what type of input the scene should respond to.
///
/// The default is `Sense::click_and_drag()`.
///
/// Set this to `Sense::hover()` to disable panning via clicking and dragging.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the allowed zoom range.
///
/// The default zoom range is `0.0..=1.0`,
@@ -82,6 +121,15 @@ impl Scene {
self
}
/// Specify which pointer buttons can be used to pan by clicking and dragging.
///
/// By default, this is `DragPanButtons::all()`.
#[inline]
pub fn drag_pan_buttons(mut self, flags: DragPanButtons) -> Self {
self.drag_pan_buttons = flags;
self
}
/// `scene_rect` contains the view bounds of the inner [`Ui`].
///
/// `scene_rect` will be mutated by any panning/zooming done by the user.
@@ -118,8 +166,10 @@ impl Scene {
}
if !scene_rect_was_good {
// Auto-reset if the trsnsformation goes bad somehow (or started bad).
*scene_rect = inner_rect;
// Auto-reset if the transformation goes bad somehow (or started bad).
// Recalculates transform based on inner_rect, resulting in a rect that's the full size of outer_rect but centered on inner_rect.
let to_global = fit_to_rect_in_scene(outer_rect, inner_rect, self.zoom_range);
*scene_rect = to_global.inverse() * outer_rect;
}
ret
@@ -147,7 +197,7 @@ impl Scene {
UiBuilder::new()
.layer_id(scene_layer_id)
.max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size))
.sense(Sense::click_and_drag()),
.sense(self.sense),
);
let mut pan_response = local_ui.response();
@@ -158,17 +208,17 @@ impl Scene {
// Set a correct global clip rect:
local_ui.set_clip_rect(to_global.inverse() * outer_rect);
// Tell egui to apply the transform on the layer:
local_ui
.ctx()
.set_transform_layer(scene_layer_id, *to_global);
// Add the actual contents to the area:
let ret = add_contents(&mut local_ui);
// This ensures we catch clicks/drags/pans anywhere on the background.
local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui());
// Tell egui to apply the transform on the layer:
local_ui
.ctx()
.set_transform_layer(scene_layer_id, *to_global);
InnerResponse {
response: pan_response,
inner: ret,
@@ -177,43 +227,51 @@ impl Scene {
/// Helper function to handle pan and zoom interactions on a response.
pub fn register_pan_and_zoom(&self, ui: &Ui, resp: &mut Response, to_global: &mut TSTransform) {
if resp.dragged() {
let dragged = self.drag_pan_buttons.iter().any(|button| match button {
DragPanButtons::PRIMARY => resp.dragged_by(PointerButton::Primary),
DragPanButtons::SECONDARY => resp.dragged_by(PointerButton::Secondary),
DragPanButtons::MIDDLE => resp.dragged_by(PointerButton::Middle),
DragPanButtons::EXTRA_1 => resp.dragged_by(PointerButton::Extra1),
DragPanButtons::EXTRA_2 => resp.dragged_by(PointerButton::Extra2),
_ => false,
});
if dragged {
to_global.translation += to_global.scaling * resp.drag_delta();
resp.mark_changed();
}
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos()) {
if resp.contains_pointer() {
let pointer_in_scene = to_global.inverse() * mouse_pos;
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos())
&& resp.contains_pointer()
{
let pointer_in_scene = to_global.inverse() * mouse_pos;
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta());
// Most of the time we can return early. This is also important to
// avoid `ui_from_scene` to change slightly due to floating point errors.
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
return;
}
if zoom_delta != 1.0 {
// Zoom in on pointer, but only if we are not zoomed in or out too far.
let zoom_delta = zoom_delta.clamp(
self.zoom_range.min / to_global.scaling,
self.zoom_range.max / to_global.scaling,
);
*to_global = *to_global
* TSTransform::from_translation(pointer_in_scene.to_vec2())
* TSTransform::from_scaling(zoom_delta)
* TSTransform::from_translation(-pointer_in_scene.to_vec2());
// Clamp to exact zoom range.
to_global.scaling = self.zoom_range.clamp(to_global.scaling);
}
// Pan:
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
resp.mark_changed();
// Most of the time we can return early. This is also important to
// avoid `ui_from_scene` to change slightly due to floating point errors.
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
return;
}
if zoom_delta != 1.0 {
// Zoom in on pointer, but only if we are not zoomed in or out too far.
let zoom_delta = zoom_delta.clamp(
self.zoom_range.min / to_global.scaling,
self.zoom_range.max / to_global.scaling,
);
*to_global = *to_global
* TSTransform::from_translation(pointer_in_scene.to_vec2())
* TSTransform::from_scaling(zoom_delta)
* TSTransform::from_translation(-pointer_in_scene.to_vec2());
// Clamp to exact zoom range.
to_global.scaling = self.zoom_range.clamp(to_global.scaling);
}
// Pan:
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
resp.mark_changed();
}
}
}

View File

@@ -1,8 +1,12 @@
#![allow(clippy::needless_range_loop)]
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use emath::GuiRounding as _;
use crate::{
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2,
Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
UiKind, UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
};
#[derive(Clone, Copy, Debug)]
@@ -133,6 +137,113 @@ impl ScrollBarVisibility {
];
}
/// What is the source of scrolling for a [`ScrollArea`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ScrollSource {
/// Scroll the area by dragging a scroll bar.
///
/// By default the scroll bars remain visible to show current position.
/// To hide them use [`ScrollArea::scroll_bar_visibility()`].
pub scroll_bar: bool,
/// Scroll the area by dragging the contents.
pub drag: bool,
/// Scroll the area by scrolling (or shift scrolling) the mouse wheel with
/// the mouse cursor over the [`ScrollArea`].
pub mouse_wheel: bool,
}
impl Default for ScrollSource {
fn default() -> Self {
Self::ALL
}
}
impl ScrollSource {
pub const NONE: Self = Self {
scroll_bar: false,
drag: false,
mouse_wheel: false,
};
pub const ALL: Self = Self {
scroll_bar: true,
drag: true,
mouse_wheel: true,
};
pub const SCROLL_BAR: Self = Self {
scroll_bar: true,
drag: false,
mouse_wheel: false,
};
pub const DRAG: Self = Self {
scroll_bar: false,
drag: true,
mouse_wheel: false,
};
pub const MOUSE_WHEEL: Self = Self {
scroll_bar: false,
drag: false,
mouse_wheel: true,
};
/// Is everything disabled?
#[inline]
pub fn is_none(&self) -> bool {
self == &Self::NONE
}
/// Is anything enabled?
#[inline]
pub fn any(&self) -> bool {
self.scroll_bar | self.drag | self.mouse_wheel
}
/// Is everything enabled?
#[inline]
pub fn is_all(&self) -> bool {
self.scroll_bar & self.drag & self.mouse_wheel
}
}
impl BitOr for ScrollSource {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
Self {
scroll_bar: self.scroll_bar | rhs.scroll_bar,
drag: self.drag | rhs.drag,
mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
}
}
}
#[expect(clippy::suspicious_arithmetic_impl)]
impl Add for ScrollSource {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self::Output {
self | rhs
}
}
impl BitOrAssign for ScrollSource {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
*self = *self | rhs;
}
}
impl AddAssign for ScrollSource {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
///
/// By default, scroll bars only show up when needed, i.e. when the contents
@@ -168,7 +279,7 @@ impl ScrollBarVisibility {
#[must_use = "You should call .show()"]
pub struct ScrollArea {
/// Do we have horizontal/vertical scrolling enabled?
scroll_enabled: Vec2b,
direction_enabled: Vec2b,
auto_shrink: Vec2b,
max_size: Vec2,
@@ -178,10 +289,10 @@ pub struct ScrollArea {
id_salt: Option<Id>,
offset_x: Option<f32>,
offset_y: Option<f32>,
/// If false, we ignore scroll events.
scrolling_enabled: bool,
drag_to_scroll: bool,
on_hover_cursor: Option<CursorIcon>,
on_drag_cursor: Option<CursorIcon>,
scroll_source: ScrollSource,
wheel_scroll_multiplier: Vec2,
/// If true for vertical or horizontal the scroll wheel will stick to the
/// end position until user manually changes position. It will become true
@@ -220,9 +331,9 @@ impl ScrollArea {
/// Create a scroll area where you decide which axis has scrolling enabled.
/// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling.
pub fn new(scroll_enabled: impl Into<Vec2b>) -> Self {
pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
Self {
scroll_enabled: scroll_enabled.into(),
direction_enabled: direction_enabled.into(),
auto_shrink: Vec2b::TRUE,
max_size: Vec2::INFINITY,
min_scrolled_size: Vec2::splat(64.0),
@@ -231,8 +342,10 @@ impl ScrollArea {
id_salt: None,
offset_x: None,
offset_y: None,
scrolling_enabled: true,
drag_to_scroll: true,
on_hover_cursor: None,
on_drag_cursor: None,
scroll_source: ScrollSource::default(),
wheel_scroll_multiplier: Vec2::splat(1.0),
stick_to_end: Vec2b::FALSE,
animated: true,
}
@@ -355,17 +468,41 @@ impl ScrollArea {
self
}
/// Set the cursor used when the mouse pointer is hovering over the [`ScrollArea`].
///
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
///
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
/// override this setting.
#[inline]
pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
self.on_hover_cursor = Some(cursor);
self
}
/// Set the cursor used when the [`ScrollArea`] is being dragged.
///
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
///
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
/// override this setting.
#[inline]
pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
self.on_drag_cursor = Some(cursor);
self
}
/// Turn on/off scrolling on the horizontal axis.
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self {
self.scroll_enabled[0] = hscroll;
self.direction_enabled[0] = hscroll;
self
}
/// Turn on/off scrolling on the vertical axis.
#[inline]
pub fn vscroll(mut self, vscroll: bool) -> Self {
self.scroll_enabled[1] = vscroll;
self.direction_enabled[1] = vscroll;
self
}
@@ -373,16 +510,8 @@ impl ScrollArea {
///
/// You can pass in `false`, `true`, `[false, true]` etc.
#[inline]
pub fn scroll(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
self.scroll_enabled = scroll_enabled.into();
self
}
/// Turn on/off scrolling on the horizontal/vertical axes.
#[deprecated = "Renamed to `scroll`"]
#[inline]
pub fn scroll2(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
self.scroll_enabled = scroll_enabled.into();
pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
self.direction_enabled = direction_enabled.into();
self
}
@@ -395,9 +524,14 @@ impl ScrollArea {
/// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
///
/// This controls both scrolling directions.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn enable_scrolling(mut self, enable: bool) -> Self {
self.scrolling_enabled = enable;
self.scroll_source = if enable {
ScrollSource::ALL
} else {
ScrollSource::NONE
};
self
}
@@ -408,9 +542,28 @@ impl ScrollArea {
/// If `true`, the [`ScrollArea`] will sense drags.
///
/// Default: `true`.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.drag_to_scroll = drag_to_scroll;
self.scroll_source.drag = drag_to_scroll;
self
}
/// What sources does the [`ScrollArea`] use for scrolling the contents.
#[inline]
pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
self.scroll_source = scroll_source;
self
}
/// The scroll amount caused by a mouse wheel scroll is multiplied by this amount.
///
/// Independent for each scroll direction. Defaults to `Vec2{x: 1.0, y: 1.0}`.
///
/// This can invert or effectively disable mouse scrolling.
#[inline]
pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
self.wheel_scroll_multiplier = multiplier;
self
}
@@ -437,7 +590,7 @@ impl ScrollArea {
/// Is any scrolling enabled?
pub(crate) fn is_any_scroll_enabled(&self) -> bool {
self.scroll_enabled[0] || self.scroll_enabled[1]
self.direction_enabled[0] || self.direction_enabled[1]
}
/// The scroll handle will stick to the rightmost position even while the content size
@@ -472,7 +625,7 @@ struct Prepared {
auto_shrink: Vec2b,
/// Does this `ScrollArea` have horizontal/vertical scrolling enabled?
scroll_enabled: Vec2b,
direction_enabled: Vec2b,
/// Smoothly interpolated boolean of whether or not to show the scroll bars.
show_bars_factor: Vec2,
@@ -500,20 +653,24 @@ struct Prepared {
/// `viewport.min == ZERO` means we scrolled to the top.
viewport: Rect,
scrolling_enabled: bool,
scroll_source: ScrollSource,
wheel_scroll_multiplier: Vec2,
stick_to_end: Vec2b,
/// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
/// The response from dragging the background (if enabled)
background_drag_response: Option<Response>,
animated: bool,
}
impl ScrollArea {
fn begin(self, ui: &mut Ui) -> Prepared {
let Self {
scroll_enabled,
direction_enabled,
auto_shrink,
max_size,
min_scrolled_size,
@@ -522,14 +679,15 @@ impl ScrollArea {
id_salt,
offset_x,
offset_y,
scrolling_enabled,
drag_to_scroll,
on_hover_cursor,
on_drag_cursor,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
animated,
} = self;
let ctx = ui.ctx().clone();
let scrolling_enabled = scrolling_enabled && ui.is_enabled();
let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
let id = ui.make_persistent_id(id_salt);
@@ -546,7 +704,7 @@ impl ScrollArea {
let show_bars: Vec2b = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
ScrollBarVisibility::AlwaysVisible => direction_enabled,
};
let show_bars_factor = Vec2::new(
@@ -568,7 +726,7 @@ impl ScrollArea {
// one shouldn't collapse into nothingness.
// See https://github.com/emilk/egui/issues/1097
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
}
}
@@ -585,13 +743,19 @@ impl ScrollArea {
} else {
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
content_max_size[d] = f32::INFINITY;
}
}
}
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
// Round to pixels to avoid widgets appearing to "float" when scrolling fractional amounts:
let content_max_rect = content_max_rect
.round_to_pixels(ui.pixels_per_point())
.round_ui();
let mut content_ui = ui.new_child(
UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
@@ -603,7 +767,7 @@ impl ScrollArea {
let clip_rect_margin = ui.visuals().clip_rect_margin;
let mut content_clip_rect = ui.clip_rect();
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
} else {
@@ -619,56 +783,72 @@ impl ScrollArea {
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
if (scrolling_enabled && drag_to_scroll)
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
let content_response_option = state
.interact_rect
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
let background_drag_response =
if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
let content_response_option = state
.interact_rect
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
if content_response_option
.as_ref()
.is_some_and(|response| response.dragged())
{
for d in 0..2 {
if scroll_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
}
}
} else {
// Apply the cursor velocity to the scroll area when the user releases the drag.
if content_response_option
.as_ref()
.is_some_and(|response| response.drag_stopped())
.is_some_and(|response| response.dragged())
{
state.vel =
scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
for d in 0..2 {
if direction_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
}
}
} else {
// Apply the cursor velocity to the scroll area when the user releases the drag.
if content_response_option
.as_ref()
.is_some_and(|response| response.drag_stopped())
{
state.vel = direction_enabled.to_vec2()
* ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
} else {
state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint();
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
} else {
state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint();
}
}
}
}
}
// Set the desired mouse cursors.
if let Some(response) = &content_response_option {
if response.dragged()
&& let Some(cursor) = on_drag_cursor
{
ui.ctx().set_cursor_icon(cursor);
} else if response.hovered()
&& let Some(cursor) = on_hover_cursor
{
ui.ctx().set_cursor_icon(cursor);
}
}
content_response_option
} else {
None
};
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
// above).
@@ -709,7 +889,7 @@ impl ScrollArea {
id,
state,
auto_shrink,
scroll_enabled,
direction_enabled,
show_bars_factor,
current_bar_use,
scroll_bar_visibility,
@@ -717,9 +897,11 @@ impl ScrollArea {
inner_rect,
content_ui,
viewport,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
background_drag_response,
animated,
}
}
@@ -776,7 +958,7 @@ impl ScrollArea {
let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |viewport_ui| {
ui.scope_builder(UiBuilder::new().max_rect(rect), |viewport_ui| {
viewport_ui.skip_ahead_auto_ids(min_row); // Make sure we get consistent IDs.
add_contents(viewport_ui, min_row..max_row)
})
@@ -824,16 +1006,18 @@ impl Prepared {
mut state,
inner_rect,
auto_shrink,
scroll_enabled,
direction_enabled,
mut show_bars_factor,
current_bar_use,
scroll_bar_visibility,
scroll_bar_rect,
content_ui,
viewport: _,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
background_drag_response,
animated,
} = self;
@@ -854,7 +1038,7 @@ impl Prepared {
.ctx()
.pass_state_mut(|state| state.scroll_target[d].take());
if scroll_enabled[d] {
if direction_enabled[d] {
if let Some(target) = scroll_target {
let pass_state::ScrollTarget {
range,
@@ -890,7 +1074,7 @@ impl Prepared {
delta += delta_update;
animation = animation_update;
};
}
if delta != 0.0 {
let target_offset = state.offset[d] + delta;
@@ -921,7 +1105,7 @@ impl Prepared {
for d in 0..2 {
if saved_scroll_target[d].is_some() {
state.scroll_target[d] = saved_scroll_target[d].clone();
};
}
}
});
@@ -930,7 +1114,7 @@ impl Prepared {
let mut inner_size = inner_rect.size();
for d in 0..2 {
inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) {
inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
(true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
(true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
(false, true) => content_size[d], // Follow the content (expand/contract to fit it).
@@ -944,25 +1128,35 @@ impl Prepared {
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
let content_is_too_large = Vec2b::new(
scroll_enabled[0] && inner_rect.width() < content_size.x,
scroll_enabled[1] && inner_rect.height() < content_size.y,
direction_enabled[0] && inner_rect.width() < content_size.x,
direction_enabled[1] && inner_rect.height() < content_size.y,
);
let max_offset = content_size - inner_rect.size();
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
if scrolling_enabled && is_hovering_outer_rect {
// Drag-to-scroll?
let is_dragging_background = background_drag_response
.as_ref()
.is_some_and(|r| r.dragged());
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
&& ui.ctx().dragged_id().is_none()
|| is_dragging_background;
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
&& scroll_enabled[0] != scroll_enabled[1];
&& direction_enabled[0] != direction_enabled[1];
for d in 0..2 {
if scroll_enabled[d] {
let scroll_delta = ui.ctx().input_mut(|input| {
if direction_enabled[d] {
let scroll_delta = ui.ctx().input(|input| {
if always_scroll_enabled_direction {
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
} else {
input.smooth_scroll_delta[d]
input.smooth_scroll_delta()[d]
}
});
let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
@@ -973,10 +1167,10 @@ impl Prepared {
// Clear scroll delta so no parent scroll will use it:
ui.ctx().input_mut(|input| {
if always_scroll_enabled_direction {
input.smooth_scroll_delta[0] = 0.0;
input.smooth_scroll_delta[1] = 0.0;
input.smooth_scroll_delta()[0] = 0.0;
input.smooth_scroll_delta()[1] = 0.0;
} else {
input.smooth_scroll_delta[d] = 0.0;
input.smooth_scroll_delta()[d] = 0.0;
}
});
@@ -990,7 +1184,7 @@ impl Prepared {
let show_scroll_this_frame = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
ScrollBarVisibility::AlwaysVisible => direction_enabled,
};
// Avoid frame delay; start showing scroll bar right away:
@@ -1034,6 +1228,7 @@ impl Prepared {
let is_hovering_bar_area = is_hovering_outer_rect
&& ui.rect_contains_pointer(max_bar_rect)
&& !is_dragging_background
|| state.scroll_bar_interaction[d];
let is_hovering_bar_area_t = ui
@@ -1090,23 +1285,37 @@ impl Prepared {
)
};
let handle_rect = if d == 0 {
Rect::from_min_max(
pos2(from_content(state.offset.x), cross.min),
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
)
} else {
Rect::from_min_max(
pos2(cross.min, from_content(state.offset.y)),
pos2(
cross.max,
from_content(state.offset.y + inner_rect.height()),
),
)
let calculate_handle_rect = |d, offset: &Vec2| {
let handle_size = if d == 0 {
from_content(offset.x + inner_rect.width()) - from_content(offset.x)
} else {
from_content(offset.y + inner_rect.height()) - from_content(offset.y)
}
.max(scroll_style.handle_min_length);
let handle_start_point = remap_clamp(
offset[d],
0.0..=max_offset[d],
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
);
if d == 0 {
Rect::from_min_max(
pos2(handle_start_point, cross.min),
pos2(handle_start_point + handle_size, cross.max),
)
} else {
Rect::from_min_max(
pos2(cross.min, handle_start_point),
pos2(cross.max, handle_start_point + handle_size),
)
}
};
let handle_rect = calculate_handle_rect(d, &state.offset);
let interact_id = id.with(d);
let sense = if self.scrolling_enabled {
let sense = if scroll_source.scroll_bar && ui.is_enabled() {
Sense::click_and_drag()
} else {
Sense::hover()
@@ -1131,11 +1340,13 @@ impl Prepared {
});
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
state.offset[d] = remap(
new_handle_top,
scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
0.0..=content_size[d],
);
let handle_travel =
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
state.offset[d] = if handle_travel.start() == handle_travel.end() {
0.0
} else {
remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
};
// some manual action taken, scroll not stuck
state.scroll_stuck_to_end[d] = false;
@@ -1154,33 +1365,9 @@ impl Prepared {
if ui.is_rect_visible(outer_scroll_bar_rect) {
// Avoid frame-delay by calculating a new handle rect:
let mut handle_rect = if d == 0 {
Rect::from_min_max(
pos2(from_content(state.offset.x), cross.min),
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
)
} else {
Rect::from_min_max(
pos2(cross.min, from_content(state.offset.y)),
pos2(
cross.max,
from_content(state.offset.y + inner_rect.height()),
),
)
};
let min_handle_size = scroll_style.handle_min_length;
if handle_rect.size()[d] < min_handle_size {
handle_rect = Rect::from_center_size(
handle_rect.center(),
if d == 0 {
vec2(min_handle_size, handle_rect.size().y)
} else {
vec2(handle_rect.size().x, min_handle_size)
},
);
}
let handle_rect = calculate_handle_rect(d, &state.offset);
let visuals = if scrolling_enabled {
let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
// Pick visuals based on interaction with the handle.
// Remember that the response is for the whole scroll bar!
let is_hovering_handle = response.hovered()

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