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

Merge branch 'lucas/atoms-preferred-size' into lucas/experiments/measure-widget-size

# Conflicts:
#	crates/egui/src/ui.rs
#	crates/egui/src/widgets/button.rs
#	crates/egui/src/widgets/label.rs
#	crates/egui_demo_lib/src/demo/popups.rs
#	crates/egui_extras/src/layout.rs
#	crates/epaint/src/text/text_layout_types.rs
This commit is contained in:
lucasmerlin
2025-06-16 09:52:22 +02:00
389 changed files with 8660 additions and 3857 deletions

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

@@ -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.
@@ -39,16 +39,19 @@ jobs:
with:
profile: minimal
target: wasm32-unknown-unknown
toolchain: 1.81.0
toolchain: 1.84.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,34 @@
name: PR Branch Name Check
on:
pull_request_target:
types: [opened, reopened, synchronize]
jobs:
check-source-branch:
runs-on: ubuntu-latest
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

@@ -25,7 +25,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 https://github.com/emilk/egui/blob/master/CONTRIBUTING.md#working-with-git-lfs"
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

@@ -19,15 +19,18 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.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

@@ -5,7 +5,7 @@ name: Rust
env:
RUSTFLAGS: -D warnings
RUSTDOCFLAGS: -D warnings
NIGHTLY_VERSION: nightly-2024-09-11
NIGHTLY_VERSION: nightly-2025-04-22
jobs:
fmt-crank-check-test:
@@ -18,7 +18,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
- name: Install packages (Linux)
if: runner.os == 'Linux'
@@ -83,7 +83,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
targets: wasm32-unknown-unknown
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
rust-version: "1.81.0"
rust-version: "1.84.0"
log-level: error
command: check
arguments: --target ${{ matrix.target }}
@@ -170,13 +170,15 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.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
# ---------------------------------------------------------------------------
@@ -189,7 +191,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
targets: aarch64-apple-ios
- name: Set up cargo cache
@@ -208,7 +210,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
@@ -232,7 +234,7 @@ jobs:
lfs: true
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2

View File

@@ -15,16 +15,20 @@ jobs:
- name: Check spelling of entire workspace
uses: crate-ci/typos@master
linkinator:
name: linkinator
lychee:
name: lychee
runs-on: ubuntu-latest
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

@@ -33,7 +33,11 @@
"--all-features",
],
"rust-analyzer.showUnlinkedFileNotification": false,
"rust-analyzer.cargo.extraEnv": {
// rust-analyzer is only guaranteed to support the latest stable version of Rust. Use it instead of whatever is
// specified in rust-toolchain.
"RUSTUP_TOOLCHAIN": "stable"
},
// 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

@@ -108,7 +108,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!).
@@ -326,7 +326,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
@@ -942,7 +942,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,8 +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` artefact).
For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md).
Snapshots and other big files are stored with git lfs. See [Working with lfs](#working-with-lfs) for more info.
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!
@@ -122,6 +125,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

1331
Cargo.lock

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ members = [
[workspace.package]
edition = "2021"
license = "MIT OR Apache-2.0"
rust-version = "1.81"
rust-version = "1.84"
version = "0.31.1"
@@ -68,6 +68,8 @@ egui_glow = { version = "0.31.1", path = "crates/egui_glow", default-features =
egui_kittest = { version = "0.31.1", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.31.1", path = "crates/eframe", default-features = false }
accesskit = "0.19.0"
accesskit_winit = "0.27"
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",
@@ -83,8 +85,9 @@ glutin = { version = "0.32.0", default-features = false }
glutin-winit = { version = "0.5.0", default-features = false }
home = "0.5.9"
image = { version = "0.25", default-features = false }
kittest = { version = "0.1" }
kittest = { version = "0.1.0", git = "https://github.com/rerun-io/kittest", branch = "main" }
log = { version = "0.4", features = ["std"] }
mimalloc = "0.1.46"
nohash-hasher = "0.2"
parking_lot = "0.12"
pollster = "0.4"
@@ -92,15 +95,18 @@ profiling = { version = "1.0.16", default-features = false }
puffin = "0.19"
puffin_http = "0.16"
raw-window-handle = "0.6.0"
ron = "0.8"
ron = "0.10.1"
serde = { version = "1", features = ["derive"] }
similar-asserts = "1.4.2"
smallvec = "1"
thiserror = "1.0.37"
type-map = "0.5.0"
unicode-segmentation = "1.12.0"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = "0.3.70"
web-sys = "0.3.73"
web-time = "1.1.0" # Timekeeping for native and web
wgpu = { version = "24.0.0", default-features = false }
wgpu = { version = "25.0.0", default-features = false }
windows-sys = "0.59"
winit = { version = "0.30.7", default-features = false }
@@ -130,6 +136,7 @@ broken_intra_doc_links = "warn"
# See also clippy.toml
[workspace.lints.clippy]
allow_attributes = "warn"
as_ptr_cast_mut = "warn"
await_holding_lock = "warn"
bool_to_int_with_if = "warn"
@@ -191,13 +198,13 @@ 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_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"
@@ -205,6 +212,7 @@ 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 +225,13 @@ needless_for_each = "warn"
needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
negative_feature_names = "warn"
non_zero_suggestions = "warn"
nonstandard_macro_braces = "warn"
option_as_ref_cloned = "warn"
option_option = "warn"
path_buf_push_overwrite = "warn"
print_stderr = "warn"
pathbuf_init_then_push = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
pub_underscore_fields = "warn"
@@ -235,6 +245,7 @@ ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "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"
str_split_at_newline = "warn"
@@ -247,6 +258,7 @@ 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"
tuple_array_conversions = "warn"
@@ -256,6 +268,7 @@ unimplemented = "warn"
uninhabited_references = "warn"
uninlined_format_args = "warn"
unnecessary_box_returns = "warn"
unnecessary_literal_bound = "warn"
unnecessary_safety_doc = "warn"
unnecessary_struct_initialization = "warn"
unnecessary_wraps = "warn"
@@ -263,6 +276,7 @@ unnested_or_patterns = "warn"
unused_peekable = "warn"
unused_rounding = "warn"
unused_self = "warn"
unused_trait_names = "warn"
use_self = "warn"
useless_transmute = "warn"
verbose_file_reads = "warn"
@@ -272,7 +286,6 @@ zero_sized_map_values = "warn"
# TODO(emilk): maybe enable more of these lints?
iter_over_hash_type = "allow"
missing_assert_message = "allow"
should_panic_without_expect = "allow"
too_many_lines = "allow"
unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one
@@ -282,6 +295,7 @@ assigning_clones = "allow" # No please
let_underscore_must_use = "allow"
let_underscore_untyped = "allow"
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

View File

@@ -4,9 +4,9 @@
[![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)
@@ -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
@@ -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
@@ -154,9 +154,9 @@ Light Theme:
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?
@@ -177,16 +177,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
@@ -286,7 +286,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.
@@ -303,15 +303,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
@@ -353,11 +354,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).

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.
@@ -53,14 +53,14 @@ We don't update the MSRV in a patch release, unless we really, really need to.
* [ ] bump version numbers in workspace `Cargo.toml`
## 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.
I usually do this all on the `main` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `main` later.
* [ ] Run `typos`
* [ ] `git commit -m 'Release 0.x.0 - <release title>'`
* [ ] `cargo publish` (see below)
* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - <release title>'`
* [ ] `git pull --tags ; git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force ; git push --tags`
* [ ] merge release PR or push to `master`
* [ ] merge release PR or push to `main`
* [ ] check that CI is green
* [ ] do a GitHub release: https://github.com/emilk/egui/releases/new
* Follow the format of the last release

View File

@@ -3,7 +3,7 @@
# -----------------------------------------------------------------------------
# Section identical to scripts/clippy_wasm/clippy.toml:
msrv = "1.81"
msrv = "1.84"
allow-unwrap-in-tests = 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

@@ -5,10 +5,24 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
/// 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)]
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -16,6 +30,7 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
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 +102,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 +156,26 @@ impl Color32 {
}
}
/// 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 +186,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 +241,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 +270,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 +306,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 +342,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 +384,131 @@ 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 rgba = Rgba::from(original);
let back = Color32::from(rgba);
assert_eq!(back, 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

@@ -208,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,
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32, Color32, Rgba,
};
/// 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.

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),
])
}
}
}

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

@@ -5,10 +5,10 @@ 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 = [
@@ -59,7 +59,7 @@ 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).
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
## Enable saving app state to disk.
@@ -90,7 +90,7 @@ web_screen_reader = [
"web-sys/SpeechSynthesisUtterance",
]
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
## Use [`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.
##

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:
@@ -26,12 +26,12 @@ sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev lib
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 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

@@ -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> {
@@ -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.
///
@@ -499,10 +499,16 @@ 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>,
}
#[cfg(target_arch = "wasm32")]
@@ -519,7 +525,8 @@ impl Default for WebOptions {
dithering: true,
should_propagate_event: Box::new(|_| false),
should_stop_propagation: Box::new(|_| true),
should_prevent_default: Box::new(|_| true),
}
}
}
@@ -655,7 +662,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 +671,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> {
@@ -696,7 +703,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")
}

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,6 +144,15 @@
#![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};
@@ -182,6 +191,14 @@ pub use web::{WebLogger, WebRunner};
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod native;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use native::run::EframeWinitApplication;
#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use native::run::EframePumpStatus;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(feature = "persistence")]
@@ -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
@@ -236,32 +253,13 @@ pub mod icon_data;
/// 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)]
#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
pub fn run_native(
app_name: &str,
mut native_options: NativeOptions,
app_creator: AppCreator<'_>,
) -> Result {
#[cfg(not(feature = "__screenshot"))]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
);
if native_options.viewport.title.is_none() {
native_options.viewport.title = Some(app_name.to_owned());
}
let renderer = native_options.renderer;
#[cfg(all(feature = "glow", feature = "wgpu"))]
{
match renderer {
Renderer::Glow => "glow",
Renderer::Wgpu => "wgpu",
};
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
}
let renderer = init_native(app_name, &mut native_options);
match renderer {
#[cfg(feature = "glow")]
@@ -278,6 +276,113 @@ pub fn run_native(
}
}
/// 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"))]
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")]
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"))]
fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
#[cfg(not(feature = "__screenshot"))]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
);
if native_options.viewport.title.is_none() {
native_options.viewport.title = Some(app_name.to_owned());
}
let renderer = native_options.renderer;
#[cfg(all(feature = "glow", feature = "wgpu"))]
{
match native_options.renderer {
Renderer::Glow => "glow",
Renderer::Wgpu => "wgpu",
};
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
}
renderer
}
// ----------------------------------------------------------------------------
/// The simplest way to get started when writing a native app.

View File

@@ -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,13 +71,13 @@ 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;
@@ -198,12 +198,12 @@ 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};

View File

@@ -136,7 +136,7 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
None
}
#[allow(clippy::unnecessary_wraps)]
#[expect(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 +169,7 @@ pub struct EpiIntegration {
}
impl EpiIntegration {
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
pub fn new(
egui_ctx: egui::Context,
window: &winit::window::Window,
@@ -326,7 +326,7 @@ impl EpiIntegration {
}
}
#[allow(clippy::unused_self)]
#[allow(clippy::unused_self, clippy::allow_attributes)]
pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
#[cfg(feature = "persistence")]
if let Some(storage) = self.frame.storage_mut() {

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,10 +42,10 @@ 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;
@@ -207,7 +207,8 @@ 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);

View File

@@ -11,13 +11,13 @@ 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},
@@ -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,
@@ -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);
}
}
@@ -901,7 +901,7 @@ fn change_gl_context(
}
impl GlutinWindowContext {
#[allow(unsafe_code)]
#[expect(unsafe_code)]
unsafe fn new(
egui_ctx: &egui::Context,
viewport_builder: ViewportBuilder,
@@ -1094,7 +1094,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,
@@ -1566,6 +1566,6 @@ fn save_screenshot_and_exit(
});
log::info!("Screenshot saved to {path:?}.");
#[allow(clippy::exit)]
#[expect(clippy::exit)]
std::process::exit(0);
}

View File

@@ -93,48 +93,52 @@ 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") {
if 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
}
});
@@ -159,7 +163,6 @@ impl<T: WinitApp> WinitAppWrapper<T> {
log::debug!("Exiting with return code 0");
#[allow(clippy::exit)]
std::process::exit(0);
}
}
@@ -313,7 +316,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,6 +362,19 @@ 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")]
@@ -383,3 +399,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")]
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,7 +15,7 @@ use winit::{
window::{Window, WindowId},
};
use ahash::{HashMap, HashSet, HashSetExt};
use ahash::{HashMap, HashSet, HashSetExt as _};
use egui::{
DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass,
ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput,
@@ -182,7 +182,6 @@ 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(),
@@ -236,7 +235,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 +248,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)

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

@@ -2,10 +2,10 @@ use egui::{TexturesDelta, UserData, ViewportCommand};
use crate::{epi, App};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter as _, NeedRepaint};
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,

View File

@@ -4,7 +4,7 @@ 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,
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast as _, JsValue, WebRunner,
DEBUG_RESIZE,
};
@@ -139,15 +139,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 +163,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 +189,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 +206,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();
}
}
@@ -256,12 +261,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 +276,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);
}
@@ -290,7 +295,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,7 +304,7 @@ 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();
}
}
@@ -310,19 +315,26 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
if let Ok(text) = data.get_data("text") {
let text = text.replace("\r\n", "\n");
let mut should_propagate = false;
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if !text.is_empty() && runner.input.raw.focused {
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();
}
}
}
})?;
@@ -340,10 +352,13 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
}
// 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| {
@@ -359,10 +374,13 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
}
// 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(())
@@ -484,7 +502,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 +512,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 +524,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 +554,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 +576,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();
}
}
@@ -600,15 +623,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();
// 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();
}
}
})
}
@@ -622,10 +649,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();
}
},
)
}
@@ -635,7 +665,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,
@@ -643,7 +674,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);
}
@@ -651,10 +683,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();
}
},
)
}
@@ -667,17 +702,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();
// 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();
}
}
}
})
@@ -691,18 +732,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);
@@ -710,10 +756,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
@@ -769,16 +818,20 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
modifiers,
}
};
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.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();
}
})
}

View File

@@ -281,7 +281,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

@@ -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('/') {

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;
@@ -27,7 +27,8 @@ impl WebPainterGlow {
) -> 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

@@ -23,7 +23,7 @@ pub(crate) struct WebPainterWgpu {
}
impl WebPainterWgpu {
#[allow(unused)] // only used if `wgpu` is the only active feature.
#[expect(unused)] // only used if `wgpu` is the only active feature.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
@@ -55,7 +55,7 @@ impl WebPainterWgpu {
})
}
#[allow(unused)] // only used if `wgpu` is the only active feature.
#[expect(unused)] // only used if `wgpu` is the only active feature.
pub async fn new(
ctx: egui::Context,
canvas: web_sys::HtmlCanvasElement,

View File

@@ -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();
@@ -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

@@ -9,10 +9,10 @@ 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 = [
@@ -43,7 +43,7 @@ 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.

View File

@@ -4,6 +4,7 @@ use std::sync::{mpsc, Arc};
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
/// A texture and a buffer for reading the rendered frame back to the cpu.
///
/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
/// flag for the surface texture on all platforms. This means that anytime we want to
/// capture the frame, we first render it to this texture, and then we can copy it to
@@ -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,
@@ -184,7 +185,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(..);
@@ -226,10 +227,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

@@ -42,8 +42,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,
@@ -104,7 +107,7 @@ async fn request_adapter(
force_fallback_adapter: false,
})
.await
.ok_or_else(|| {
.inspect_err(|_err| {
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.is_empty() {
log::info!("No wgpu adapters found");
@@ -120,8 +123,6 @@ async fn request_adapter(
describe_adapters(_available_adapters)
);
}
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
})?;
#[cfg(target_arch = "wasm32")]
@@ -184,7 +185,6 @@ impl RenderState {
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
trace_path,
}) => {
let adapter = {
#[cfg(target_arch = "wasm32")]
@@ -194,7 +194,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 +209,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?
};
@@ -242,7 +242,7 @@ impl RenderState {
// 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"))]

View File

@@ -3,7 +3,7 @@
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
use epaint::{emath::NumExt as _, PaintCallbackInfo, Primitive, Vertex};
use wgpu::util::DeviceExt as _;
@@ -84,7 +84,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,
@@ -749,7 +749,7 @@ impl Renderer {
///
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// 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 +796,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,

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.
@@ -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()
}
}
@@ -195,12 +186,9 @@ impl Default for WgpuSetupCreateNew {
..base_limits
},
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
}
}),
trace_path: std::env::var("WGPU_TRACE")
.ok()
.map(std::path::PathBuf::from),
}
}
}

View File

@@ -575,7 +575,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,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"]
@@ -67,7 +67,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 }

View File

@@ -161,7 +161,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

@@ -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,
));
@@ -727,7 +729,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 +748,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 +791,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 +809,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()));
}
}
}
@@ -1138,6 +1144,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;
@@ -1610,9 +1618,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,
@@ -1756,7 +1766,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
@@ -1838,8 +1850,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 +1869,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 +1884,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

@@ -87,9 +87,11 @@ ahash.workspace = true
bitflags.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 }
accesskit = { workspace = true, optional = true }
backtrace = { workspace = true, optional = true }

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

@@ -0,0 +1,109 @@
use crate::{AtomKind, 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>,
) -> 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 (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
let size = self
.size
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));
SizedAtom {
size,
preferred_size: preferred.at_least(size),
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(|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,132 @@
use crate::{Id, Image, ImageSource, SizedAtomKind, TextStyle, 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::put`] 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.put(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>,
) -> (Vec2, SizedAtomKind<'a>) {
match self {
AtomKind::Text(text) => {
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
let desired_size = matches!(wrap_mode, TextWrapMode::Truncate).then(|| {
text.clone()
.into_galley(
ui,
Some(TextWrapMode::Extend),
available_size.x,
TextStyle::Button,
)
.desired_size()
});
let galley =
text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button);
(
desired_size.unwrap_or_else(|| galley.desired_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,493 @@
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
use crate::{
AtomKind, Atoms, 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>,
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,
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 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,
} = self;
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;
// Preferred 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 preferred_width = 0.0;
let mut preferred_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;
preferred_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));
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_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));
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_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(preferred_width, preferred_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,
/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
pub preferred_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

@@ -5,8 +5,8 @@
use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect,
Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2,
Rect, Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
};
/// State of an [`Area`] that is persisted between frames.
@@ -602,7 +602,7 @@ 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 {
info: _,

View File

@@ -1,7 +1,7 @@
use std::hash::Hash;
use crate::{
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt as _, Rect,
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
WidgetInfo, WidgetText, WidgetType,
};

View File

@@ -2,11 +2,11 @@ use epaint::Shape;
use crate::{
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
NumExt as _, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
};
#[allow(unused_imports)] // Documentation
#[expect(unused_imports)] // Documentation
use crate::style::Spacing;
/// A function that paints the [`ComboBox`] icon
@@ -16,9 +16,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
///
/// ```
/// # 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| {
@@ -27,6 +28,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
/// ui.selectable_value(&mut selected, Enum::Third, "Third");
/// }
/// );
///
/// if selected != before {
/// // Handle selection change
/// }
/// # });
/// ```
#[must_use = "You should call .show*"]
@@ -86,7 +91,7 @@ impl ComboBox {
}
/// 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)
}
@@ -297,7 +302,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,

View File

@@ -1,7 +1,7 @@
use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior,
Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, WidgetText,
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{vec2, Align, RectAlign, Vec2};
use epaint::Stroke;
@@ -159,7 +159,7 @@ impl MenuState {
}
/// 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`].
@@ -243,8 +243,8 @@ pub struct MenuButton<'a> {
}
impl<'a> MenuButton<'a> {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::from_button(Button::new(text))
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self::from_button(Button::new(atoms.into_atoms()))
}
/// Set the config for the menu.
@@ -293,8 +293,8 @@ impl<'a> SubMenuButton<'a> {
/// The default right arrow symbol: `"⏵"`
pub const RIGHT_ARROW: &'static str = "";
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::from_button(Button::new(text).right_text(""))
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`].

View File

@@ -29,7 +29,7 @@ pub use {
panel::{CentralPanel, SidePanel, TopBottomPanel},
popup::*,
resize::Resize,
scene::Scene,
scene::{DragPanButtons, Scene},
scroll_area::ScrollArea,
sides::Sides,
tooltip::*,

View File

@@ -4,11 +4,14 @@ use crate::{
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 not undefined
/// (either first or second could be top).
pub struct Modal {
pub area: Area,
pub backdrop_color: Color32,
@@ -16,7 +19,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 +31,7 @@ impl Modal {
}
/// Returns an area customized for a modal.
///
/// Makes these changes to the default area:
/// - sense: hover
/// - anchor: center

View File

@@ -4,7 +4,7 @@
use crate::containers::tooltip::Tooltip;
use crate::{
Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect,
Response, Ui, Widget, WidgetText,
Response, Ui, Widget as _, WidgetText,
};
use emath::RectAlign;
// ----------------------------------------------------------------------------
@@ -19,7 +19,7 @@ use emath::RectAlign;
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[allow(deprecated)]
/// # #[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");
@@ -61,7 +61,7 @@ pub fn show_tooltip_at_pointer<R>(
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(widget_id, ctx.clone(), PopupAnchor::Pointer, parent_layer)
Tooltip::new(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
.gap(12.0)
.show(add_contents)
.map(|response| response.inner)
@@ -78,7 +78,7 @@ pub fn show_tooltip_for<R>(
widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(widget_id, ctx.clone(), *widget_rect, parent_layer)
Tooltip::new(ctx.clone(), parent_layer, widget_id, *widget_rect)
.show(add_contents)
.map(|response| response.inner)
}
@@ -94,7 +94,7 @@ pub fn show_tooltip_at<R>(
suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(widget_id, ctx.clone(), suggested_position, parent_layer)
Tooltip::new(ctx.clone(), parent_layer, widget_id, suggested_position)
.show(add_contents)
.map(|response| response.inner)
}
@@ -177,7 +177,7 @@ pub fn popup_below_widget<R>(
/// }
/// let below = egui::AboveOrBelow::Below;
/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside;
/// # #[allow(deprecated)]
/// # #[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:");

View File

@@ -18,7 +18,7 @@
use emath::GuiRounding as _;
use crate::{
lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt,
lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _,
Rangef, Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
};

View File

@@ -8,6 +8,7 @@ use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2};
use std::iter::once;
/// What should we anchor the popup to?
///
/// The final position for the popup will be calculated based on [`RectAlign`]
/// and can be customized with [`Popup::align`] and [`Popup::align_alternatives`].
/// [`PopupAnchor`] is the parent rect of [`RectAlign`].
@@ -84,7 +85,7 @@ pub enum PopupCloseBehavior {
/// but in the popup's body
CloseOnClickOutside,
/// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_popup`]
/// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_all_popups`]
/// or by pressing the escape button
IgnoreClicks,
}
@@ -272,11 +273,15 @@ impl<'a> Popup<'a> {
/// In contrast to [`Self::menu`], this will open at the pointer position.
pub fn context_menu(response: &Response) -> Self {
Self::menu(response)
.open_memory(
response
.secondary_clicked()
.then_some(SetOpenCommand::Bool(true)),
)
.open_memory(if response.secondary_clicked() {
Some(SetOpenCommand::Bool(true))
} else if response.clicked() {
// Explicitly close the menu if the widget was clicked
// Without this, the context menu would stay open if the user clicks the widget
Some(SetOpenCommand::Bool(false))
} else {
None
})
.at_pointer_fixed()
}
@@ -461,7 +466,7 @@ impl<'a> Popup<'a> {
};
RectAlign::find_best_align(
#[allow(clippy::iter_on_empty_collections)]
#[expect(clippy::iter_on_empty_collections)]
once(self.rect_align).chain(
self.alternative_aligns
// Need the empty slice so the iters have the same type so we can unwrap_or
@@ -519,13 +524,15 @@ impl<'a> Popup<'a> {
_ => mem.open_popup(id),
}
} else {
mem.close_popup();
mem.close_popup(id);
}
}
Some(SetOpenCommand::Toggle) => {
mem.toggle_popup(id);
}
None => {}
None => {
mem.keep_popup_open(id);
}
});
}
@@ -556,7 +563,9 @@ impl<'a> Popup<'a> {
.info(info.unwrap_or_else(|| {
UiStackInfo::new(kind.into()).with_tag_value(
MenuConfig::MENU_CONFIG_TAG,
MenuConfig::new().close_behavior(close_behavior),
MenuConfig::new()
.close_behavior(close_behavior)
.style(style.clone()),
)
}));
@@ -599,7 +608,7 @@ impl<'a> Popup<'a> {
}
OpenKind::Memory { .. } => {
if should_close {
ctx.memory_mut(|mem| mem.close_popup());
ctx.memory_mut(|mem| mem.close_popup(id));
}
}
}

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,
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense,
Shape, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
};
#[derive(Clone, Copy, Debug)]

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,
emath::TSTransform, InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui,
UiBuilder, Vec2,
};
/// 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.
@@ -119,7 +167,9 @@ impl Scene {
if !scene_rect_was_good {
// Auto-reset if the transformation goes bad somehow (or started bad).
*scene_rect = inner_rect;
// Recalculates transform based on inner_rect, resulting in a rect that's the full size of outer_rect but centered on inner_rect.
let to_global = fit_to_rect_in_scene(outer_rect, inner_rect, self.zoom_range);
*scene_rect = to_global.inverse() * outer_rect;
}
ret
@@ -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,7 +227,15 @@ 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();
}

View File

@@ -1,8 +1,10 @@
#![allow(clippy::needless_range_loop)]
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use crate::{
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt, Pos2, Rangef,
Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, CursorIcon, Id,
NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
};
#[derive(Clone, Copy, Debug)]
@@ -133,6 +135,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 +277,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 +287,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 +329,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 +340,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 +466,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 +508,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 +522,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 +540,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 +588,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 +623,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,7 +651,8 @@ 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
@@ -513,7 +665,7 @@ struct Prepared {
impl ScrollArea {
fn begin(self, ui: &mut Ui) -> Prepared {
let Self {
scroll_enabled,
direction_enabled,
auto_shrink,
max_size,
min_scrolled_size,
@@ -522,14 +674,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 +699,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 +721,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,7 +738,7 @@ 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;
}
}
@@ -603,7 +756,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,7 +772,8 @@ 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)
if scroll_source.drag
&& ui.is_enabled()
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
// Drag contents to scroll (for touch screens mostly).
@@ -634,7 +788,7 @@ impl ScrollArea {
.is_some_and(|response| response.dragged())
{
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
@@ -649,7 +803,7 @@ impl ScrollArea {
.is_some_and(|response| response.drag_stopped())
{
state.vel =
scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
@@ -668,6 +822,19 @@ impl ScrollArea {
}
}
}
// Set the desired mouse cursors.
if let Some(response) = content_response_option {
if response.dragged() {
if let Some(cursor) = on_drag_cursor {
response.on_hover_cursor(cursor);
}
} else if response.hovered() {
if let Some(cursor) = on_hover_cursor {
response.on_hover_cursor(cursor);
}
}
}
}
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
@@ -709,7 +876,7 @@ impl ScrollArea {
id,
state,
auto_shrink,
scroll_enabled,
direction_enabled,
show_bars_factor,
current_bar_use,
scroll_bar_visibility,
@@ -717,7 +884,8 @@ impl ScrollArea {
inner_rect,
content_ui,
viewport,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
animated,
@@ -824,14 +992,15 @@ 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,
animated,
@@ -854,7 +1023,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,
@@ -930,7 +1099,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,18 +1113,18 @@ 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 {
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]
@@ -963,6 +1132,7 @@ impl Prepared {
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;
@@ -990,7 +1160,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:
@@ -1120,7 +1290,7 @@ impl Prepared {
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()
@@ -1170,7 +1340,7 @@ impl Prepared {
// Avoid frame-delay by calculating a new handle rect:
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()

View File

@@ -7,26 +7,31 @@ use emath::Vec2;
pub struct Tooltip<'a> {
pub popup: Popup<'a>,
layer_id: LayerId,
widget_id: Id,
/// The layer of the parent widget.
parent_layer: LayerId,
/// The id of the widget that owns this tooltip.
parent_widget: Id,
}
impl Tooltip<'_> {
/// Show a tooltip that is always open
/// Show a tooltip that is always open.
pub fn new(
widget_id: Id,
ctx: Context,
parent_layer: LayerId,
parent_widget: Id,
anchor: impl Into<PopupAnchor>,
layer_id: LayerId,
) -> Self {
let width = ctx.style().spacing.tooltip_width;
Self {
// TODO(lucasmerlin): Set width somehow (we're missing context here)
popup: Popup::new(widget_id, ctx, anchor.into(), layer_id)
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.width(width)
.sense(Sense::hover()),
layer_id,
widget_id,
parent_layer,
parent_widget,
}
}
@@ -39,8 +44,8 @@ impl Tooltip<'_> {
.sense(Sense::hover());
Self {
popup,
layer_id: response.layer_id,
widget_id: response.id,
parent_layer: response.layer_id,
parent_widget: response.id,
}
}
@@ -96,8 +101,8 @@ impl Tooltip<'_> {
pub fn show<R>(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option<InnerResponse<R>> {
let Self {
mut popup,
layer_id: parent_layer,
widget_id,
parent_layer,
parent_widget,
} = self;
if !popup.is_open() {
@@ -111,11 +116,11 @@ impl Tooltip<'_> {
fs.layers
.entry(parent_layer)
.or_default()
.widget_with_tooltip = Some(widget_id);
.widget_with_tooltip = Some(parent_widget);
fs.tooltips
.widget_tooltips
.get(&widget_id)
.get(&parent_widget)
.copied()
.unwrap_or(PerWidgetTooltipState {
bounding_rect: rect,
@@ -123,7 +128,7 @@ impl Tooltip<'_> {
})
});
let tooltip_area_id = Self::tooltip_id(widget_id, state.tooltip_count);
let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
let response = popup.show(|ui| {
@@ -144,7 +149,7 @@ impl Tooltip<'_> {
response
.response
.ctx
.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(parent_widget, state));
Self::remember_that_tooltip_was_shown(&response.response.ctx);
}

View File

@@ -8,7 +8,7 @@ use epaint::{CornerRadiusF32, RectShape};
use crate::collapsing_header::CollapsingState;
use crate::*;
use super::scroll_area::ScrollBarVisibility;
use super::scroll_area::{ScrollBarVisibility, ScrollSource};
use super::{area, resize, Area, Frame, Resize, ScrollArea};
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
@@ -376,14 +376,6 @@ impl<'open> Window<'open> {
self
}
/// Enable/disable horizontal/vertical scrolling. `false` by default.
#[deprecated = "Renamed to `scroll`"]
#[inline]
pub fn scroll2(mut self, scroll: impl Into<Vec2b>) -> Self {
self.scroll = self.scroll.scroll(scroll);
self
}
/// Enable/disable horizontal scrolling. `false` by default.
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self {
@@ -403,7 +395,10 @@ impl<'open> Window<'open> {
/// See [`ScrollArea::drag_to_scroll`] for more.
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.scroll = self.scroll.drag_to_scroll(drag_to_scroll);
self.scroll = self.scroll.scroll_source(ScrollSource {
drag: drag_to_scroll,
..Default::default()
});
self
}

View File

@@ -2,29 +2,26 @@
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
use containers::area::AreaState;
use emath::GuiRounding as _;
use emath::{GuiRounding as _, OrderedFloat};
use epaint::{
emath::{self, TSTransform},
mutex::RwLock,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
util::OrderedFloat,
vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
TessellationOptions, TextureAtlas, TextureId, Vec2,
};
use crate::{
animation_manager::AnimationManager,
containers,
containers::{self, area::AreaState},
data::output::PlatformOutput,
epaint, hit_test,
input_state::{InputState, MultiTouchInfo, PointerEvent},
interaction,
layers::GraphicLayers,
load,
load::{Bytes, Loaders, SizedTexture},
load::{self, Bytes, Loaders, SizedTexture},
memory::{Options, Theme},
os::OperatingSystem,
output::FullOutput,
@@ -34,9 +31,10 @@ use crate::{
viewport::ViewportClass,
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
ModifierNames, NumExt, Order, Painter, RawInput, Response, RichText, ScrollArea, Sense, Style,
TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, ViewportCommand, ViewportId,
ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, Widget, WidgetRect, WidgetText,
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText,
ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder,
ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput,
Widget as _, WidgetRect, WidgetText,
};
#[cfg(feature = "accesskit")]
@@ -82,7 +80,11 @@ impl Default for WrappedTextureManager {
epaint::FontImage::new([0, 0]).into(),
Default::default(),
);
assert_eq!(font_id, TextureId::default());
assert_eq!(
font_id,
TextureId::default(),
"font id should be equal to TextureId::default(), but was {font_id:?}",
);
Self(Arc::new(RwLock::new(tex_mngr)))
}
@@ -310,7 +312,7 @@ impl std::fmt::Display for RepaintCause {
impl RepaintCause {
/// Capture the file and line number of the call site.
#[allow(clippy::new_without_default)]
#[expect(clippy::new_without_default)]
#[track_caller]
pub fn new() -> Self {
let caller = Location::caller();
@@ -323,7 +325,6 @@ impl RepaintCause {
/// Capture the file and line number of the call site,
/// as well as add a reason.
#[allow(clippy::new_without_default)]
#[track_caller]
pub fn new_reason(reason: impl Into<Cow<'static, str>>) -> Self {
let caller = Location::caller();
@@ -489,8 +490,9 @@ impl ContextImpl {
new_raw_input,
viewport.repaint.requested_immediate_repaint_prev_pass(),
pixels_per_point,
&self.memory.options,
self.memory.options.input_options,
);
let repaint_after = viewport.input.wants_repaint_after();
let screen_rect = viewport.input.screen_rect;
@@ -552,6 +554,10 @@ impl ContextImpl {
}
self.update_fonts_mut();
if let Some(delay) = repaint_after {
self.request_repaint_after(delay, viewport_id, RepaintCause::new());
}
}
/// Load fonts unless already loaded.
@@ -804,7 +810,11 @@ impl Context {
let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get());
let mut output = FullOutput::default();
debug_assert_eq!(output.platform_output.num_completed_passes, 0);
debug_assert_eq!(
output.platform_output.num_completed_passes, 0,
"output must be fresh, but had {} passes",
output.platform_output.num_completed_passes
);
loop {
profiling::scope!(
@@ -828,7 +838,11 @@ impl Context {
self.begin_pass(new_input.take());
run_ui(self);
output.append(self.end_pass());
debug_assert!(0 < output.platform_output.num_completed_passes);
debug_assert!(
0 < output.platform_output.num_completed_passes,
"Completed passes was lower than 0, was {}",
output.platform_output.num_completed_passes
);
if !output.platform_output.requested_discard() {
break; // no need for another pass
@@ -1148,7 +1162,6 @@ impl Context {
///
/// `allow_focus` should usually be true, unless you call this function multiple times with the
/// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)).
#[allow(clippy::too_many_arguments)]
pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response {
let interested_in_focus = w.enabled
&& w.sense.is_focusable()
@@ -1177,7 +1190,7 @@ impl Context {
self.check_for_id_clash(w.id, w.rect, "widget");
}
#[allow(clippy::let_and_return)]
#[allow(clippy::let_and_return, clippy::allow_attributes)]
let res = self.get_response(w);
#[cfg(feature = "accesskit")]
@@ -1221,13 +1234,6 @@ impl Context {
.map(|widget_rect| self.get_response(widget_rect))
}
/// Returns `true` if the widget with the given `Id` contains the pointer.
#[deprecated = "Use Response.contains_pointer or Context::read_response instead"]
pub fn widget_contains_pointer(&self, id: Id) -> bool {
self.read_response(id)
.is_some_and(|response| response.contains_pointer())
}
/// Do all interaction for an existing widget, without (re-)registering it.
pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response {
use response::Flags;
@@ -1422,7 +1428,7 @@ impl Context {
/// figured out from the `target_os`.
///
/// For web, this can be figured out from the user-agent,
/// and is done so by [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
/// and is done so by [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
pub fn os(&self) -> OperatingSystem {
self.read(|ctx| ctx.os)
}
@@ -1482,35 +1488,48 @@ impl Context {
self.send_cmd(crate::OutputCommand::CopyImage(image));
}
fn can_show_modifier_symbols(&self) -> bool {
let ModifierNames {
alt,
ctrl,
shift,
mac_cmd,
..
} = ModifierNames::SYMBOLS;
let font_id = TextStyle::Body.resolve(&self.style());
self.fonts(|f| {
let mut lock = f.lock();
let font = lock.fonts.font(&font_id);
font.has_glyphs(alt)
&& font.has_glyphs(ctrl)
&& font.has_glyphs(shift)
&& font.has_glyphs(mac_cmd)
})
}
/// Format the given modifiers in a human-readable way (e.g. `Ctrl+Shift+X`).
pub fn format_modifiers(&self, modifiers: Modifiers) -> String {
let os = self.os();
let is_mac = os.is_mac();
if is_mac && self.can_show_modifier_symbols() {
ModifierNames::SYMBOLS.format(&modifiers, is_mac)
} else {
ModifierNames::NAMES.format(&modifiers, is_mac)
}
}
/// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`).
///
/// Can be used to get the text for [`crate::Button::shortcut_text`].
pub fn format_shortcut(&self, shortcut: &KeyboardShortcut) -> String {
let os = self.os();
let is_mac = matches!(os, OperatingSystem::Mac | OperatingSystem::IOS);
let is_mac = os.is_mac();
let can_show_symbols = || {
let ModifierNames {
alt,
ctrl,
shift,
mac_cmd,
..
} = ModifierNames::SYMBOLS;
let font_id = TextStyle::Body.resolve(&self.style());
self.fonts(|f| {
let mut lock = f.lock();
let font = lock.fonts.font(&font_id);
font.has_glyphs(alt)
&& font.has_glyphs(ctrl)
&& font.has_glyphs(shift)
&& font.has_glyphs(mac_cmd)
})
};
if is_mac && can_show_symbols() {
if is_mac && self.can_show_modifier_symbols() {
shortcut.format(&ModifierNames::SYMBOLS, is_mac)
} else {
shortcut.format(&ModifierNames::NAMES, is_mac)
@@ -2305,6 +2324,8 @@ impl ContextImpl {
let viewport = self.viewports.entry(ended_viewport_id).or_default();
let pixels_per_point = viewport.input.pixels_per_point;
self.loaders.end_pass(viewport.repaint.cumulative_pass_nr);
viewport.repaint.cumulative_pass_nr += 1;
self.memory.end_pass(&viewport.this_pass.used_ids);
@@ -2338,7 +2359,6 @@ impl ContextImpl {
// Inform the backend of all textures that have been updated (including font atlas).
let textures_delta = self.tex_manager.0.write().take_delta();
#[cfg_attr(not(feature = "accesskit"), allow(unused_mut))]
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
#[cfg(feature = "accesskit")]
@@ -2383,10 +2403,7 @@ impl ContextImpl {
if repaint_needed {
self.request_repaint(ended_viewport_id, RepaintCause::new());
} else if let Some(delay) = viewport.input.wants_repaint_after() {
self.request_repaint_after(delay, ended_viewport_id, RepaintCause::new());
}
// -------------------
let all_viewport_ids = self.all_viewport_ids();
@@ -2639,7 +2656,7 @@ impl Context {
/// Is an egui context menu open?
///
/// This only works with the old, deprecated [`crate::menu`] API.
#[allow(deprecated)]
#[expect(deprecated)]
#[deprecated = "Use `is_popup_open` instead"]
pub fn is_context_menu_open(&self) -> bool {
self.data(|d| {
@@ -2729,21 +2746,6 @@ impl Context {
.map(|t| t.inverse())
}
/// Move all the graphics at the given layer.
///
/// Is used to implement drag-and-drop preview.
///
/// This only applied to the existing graphics at the layer, not to new graphics added later.
///
/// For a persistent transform, use [`Self::set_transform_layer`] instead.
#[deprecated = "Use `transform_layer_shapes` instead"]
pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) {
if delta != Vec2::ZERO {
let transform = emath::TSTransform::from_translation(delta);
self.transform_layer_shapes(layer_id, transform);
}
}
/// Transform all the graphics at the given layer.
///
/// Is used to implement drag-and-drop preview.
@@ -3053,6 +3055,12 @@ impl Context {
self.texture_ui(ui);
});
CollapsingHeader::new("🖼 Image loaders")
.default_open(false)
.show(ui, |ui| {
self.loaders_ui(ui);
});
CollapsingHeader::new("🔠 Font texture")
.default_open(false)
.show(ui, |ui| {
@@ -3097,6 +3105,8 @@ impl Context {
));
let max_preview_size = vec2(48.0, 32.0);
let pixels_per_point = self.pixels_per_point();
ui.group(|ui| {
ScrollArea::vertical()
.max_height(300.0)
@@ -3111,15 +3121,16 @@ impl Context {
.show(ui, |ui| {
for (&texture_id, meta) in textures {
let [w, h] = meta.size;
let point_size = vec2(w as f32, h as f32) / pixels_per_point;
let mut size = vec2(w as f32, h as f32);
let mut size = point_size;
size *= (max_preview_size.x / size.x).min(1.0);
size *= (max_preview_size.y / size.y).min(1.0);
ui.image(SizedTexture::new(texture_id, size))
.on_hover_ui(|ui| {
// show larger on hover
let max_size = 0.5 * ui.ctx().screen_rect().size();
let mut size = vec2(w as f32, h as f32);
let mut size = point_size;
size *= max_size.x / size.x.max(max_size.x);
size *= max_size.y / size.y.max(max_size.y);
ui.image(SizedTexture::new(texture_id, size));
@@ -3135,6 +3146,73 @@ impl Context {
});
}
/// Show stats about different image loaders.
pub fn loaders_ui(&self, ui: &mut crate::Ui) {
struct LoaderInfo {
id: String,
byte_size: usize,
}
let mut byte_loaders = vec![];
let mut image_loaders = vec![];
let mut texture_loaders = vec![];
{
let loaders = self.loaders();
let Loaders {
include: _,
bytes,
image,
texture,
} = loaders.as_ref();
for loader in bytes.lock().iter() {
byte_loaders.push(LoaderInfo {
id: loader.id().to_owned(),
byte_size: loader.byte_size(),
});
}
for loader in image.lock().iter() {
image_loaders.push(LoaderInfo {
id: loader.id().to_owned(),
byte_size: loader.byte_size(),
});
}
for loader in texture.lock().iter() {
texture_loaders.push(LoaderInfo {
id: loader.id().to_owned(),
byte_size: loader.byte_size(),
});
}
}
fn loaders_ui(ui: &mut crate::Ui, title: &str, loaders: &[LoaderInfo]) {
let heading = format!("{} {title} loaders", loaders.len());
crate::CollapsingHeader::new(heading)
.default_open(true)
.show(ui, |ui| {
Grid::new("loaders")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
ui.label("ID");
ui.label("Size");
ui.end_row();
for loader in loaders {
ui.label(&loader.id);
ui.label(format!("{:.3} MB", loader.byte_size as f64 * 1e-6));
ui.end_row();
}
});
});
}
loaders_ui(ui, "byte", &byte_loaders);
loaders_ui(ui, "image", &image_loaders);
loaders_ui(ui, "texture", &texture_loaders);
}
/// Shows the contents of [`Self::memory`].
pub fn memory_ui(&self, ui: &mut crate::Ui) {
if ui
@@ -3193,7 +3271,7 @@ impl Context {
}
});
#[allow(deprecated)]
#[expect(deprecated)]
ui.horizontal(|ui| {
ui.label(format!(
"{} menu bars",
@@ -3251,7 +3329,7 @@ impl Context {
/// the function is still called, but with no other effect.
///
/// No locks are held while the given closure is called.
#[allow(clippy::unused_self, clippy::let_and_return)]
#[allow(clippy::unused_self, clippy::let_and_return, clippy::allow_attributes)]
#[inline]
pub fn with_accessibility_parent<R>(&self, _id: Id, f: impl FnOnce() -> R) -> R {
// TODO(emilk): this isn't thread-safe - another thread can call this function between the push/pop calls
@@ -3267,7 +3345,11 @@ impl Context {
#[cfg(feature = "accesskit")]
self.pass_state_mut(|fs| {
if let Some(state) = fs.accesskit_state.as_mut() {
assert_eq!(state.parent_stack.pop(), Some(_id));
assert_eq!(
state.parent_stack.pop(),
Some(_id),
"Mismatched push/pop in with_accessibility_parent"
);
}
});
@@ -3431,9 +3513,10 @@ impl Context {
// Try most recently added loaders first (hence `.rev()`)
for loader in bytes_loaders.iter().rev() {
match loader.load(self, uri) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
let result = loader.load(self, uri);
match result {
Err(load::LoadError::NotSupported) => {}
_ => return result,
}
}
@@ -3474,10 +3557,9 @@ impl Context {
// Try most recently added loaders first (hence `.rev()`)
for loader in image_loaders.iter().rev() {
match loader.load(self, uri, size_hint) {
Err(load::LoadError::NotSupported) => continue,
Err(load::LoadError::NotSupported) => {}
Err(load::LoadError::FormatNotSupported { detected_format }) => {
format = format.or(detected_format);
continue;
}
result => return result,
}
@@ -3520,7 +3602,7 @@ impl Context {
// Try most recently added loaders first (hence `.rev()`)
for loader in texture_loaders.iter().rev() {
match loader.load(self, uri, texture_options, size_hint) {
Err(load::LoadError::NotSupported) => continue,
Err(load::LoadError::NotSupported) => {}
result => return result,
}
}
@@ -3532,6 +3614,14 @@ impl Context {
pub fn loaders(&self) -> Arc<Loaders> {
self.read(|this| this.loaders.clone())
}
/// Returns `true` if any image is currently being loaded.
pub fn has_pending_images(&self) -> bool {
self.read(|this| {
this.loaders.image.lock().iter().any(|i| i.has_pending())
|| this.loaders.bytes.lock().iter().any(|i| i.has_pending())
})
}
}
/// ## Viewports
@@ -3580,7 +3670,6 @@ impl Context {
/// * Set the window attributes (position, size, …) based on [`ImmediateViewport::builder`].
/// * Call [`Context::run`] with [`ImmediateViewport::viewport_ui_cb`].
/// * Handle the output from [`Context::run`], including rendering
#[allow(clippy::unused_self)]
pub fn set_immediate_viewport_renderer(
callback: impl for<'a> Fn(&Self, ImmediateViewport<'a>) + 'static,
) {

View File

@@ -256,9 +256,7 @@ impl ViewportInfo {
/// If this is not the root viewport,
/// it is up to the user to hide this viewport the next frame.
pub fn close_requested(&self) -> bool {
self.events
.iter()
.any(|&event| event == ViewportEvent::Close)
self.events.contains(&ViewportEvent::Close)
}
/// Helper: move [`Self::events`], clone the other fields.
@@ -592,7 +590,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
/// State of the modifier keys. These must be fed to egui.
///
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches`].
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`].
///
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
/// as on mac that is how you type special characters,
@@ -775,8 +773,8 @@ impl Modifiers {
/// ```
/// # use egui::Modifiers;
/// # let pressed_modifiers = Modifiers::default();
/// if pressed_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, and nothing else
/// if pressed_modifiers.matches_logically(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, but not ctrl/command
/// }
/// ```
///
@@ -817,7 +815,7 @@ impl Modifiers {
/// ```
/// # use egui::Modifiers;
/// # let pressed_modifiers = Modifiers::default();
/// if pressed_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) {
/// if pressed_modifiers.matches_exact(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, and nothing else
/// }
/// ```
@@ -825,13 +823,13 @@ impl Modifiers {
/// ## Behavior:
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches(Modifiers::MAC_CMD));
/// assert!(Modifiers::CTRL.matches_exact(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches_exact(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches_exact(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches_exact(Modifiers::MAC_CMD));
/// ```
pub fn matches_exact(&self, pattern: Self) -> bool {
// alt and shift must always match the pattern:
@@ -842,9 +840,35 @@ impl Modifiers {
self.cmd_ctrl_matches(pattern)
}
#[deprecated = "Renamed `matches_exact`, but maybe you want to use `matches_logically` instead"]
pub fn matches(&self, pattern: Self) -> bool {
self.matches_exact(pattern)
/// Check if any of the modifiers match exactly.
///
/// Returns true if the same modifier is pressed in `self` as in `pattern`,
/// for at least one modifier.
///
/// ## Behavior:
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL));
/// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL));
/// ```
pub fn matches_any(&self, pattern: Self) -> bool {
if self.alt && pattern.alt {
return true;
}
if self.shift && pattern.shift {
return true;
}
if self.ctrl && pattern.ctrl {
return true;
}
if self.mac_cmd && pattern.mac_cmd {
return true;
}
if (self.mac_cmd || self.command || self.ctrl) && pattern.command {
return true;
}
false
}
/// Checks only cmd/ctrl, not alt/shift.
@@ -960,6 +984,12 @@ impl std::ops::BitOrAssign for Modifiers {
}
}
impl Modifiers {
pub fn ui(&self, ui: &mut crate::Ui) {
ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac()));
}
}
// ----------------------------------------------------------------------------
/// Names of different modifier keys.
@@ -1233,7 +1263,7 @@ pub struct EventFilter {
pub escape: bool,
}
#[allow(clippy::derivable_impls)] // let's be explicit
#[expect(clippy::derivable_impls)] // let's be explicit
impl Default for EventFilter {
fn default() -> Self {
Self {

View File

@@ -183,6 +183,11 @@ pub enum Key {
F33,
F34,
F35,
/// Back navigation key from multimedia keyboard.
/// Android sends this key on Back button press.
/// Does not work on Web.
BrowserBack,
// When adding keys, remember to also update:
// * crates/egui-winit/src/lib.rs
// * Key::ALL
@@ -307,6 +312,8 @@ impl Key {
Self::F33,
Self::F34,
Self::F35,
// Navigation keys:
Self::BrowserBack,
];
/// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc.
@@ -435,6 +442,8 @@ impl Key {
"F34" => Self::F34,
"F35" => Self::F35,
"BrowserBack" => Self::BrowserBack,
_ => return None,
})
}
@@ -588,6 +597,8 @@ impl Key {
Self::F33 => "F33",
Self::F34 => "F34",
Self::F35 => "F35",
Self::BrowserBack => "BrowserBack",
}
}
}
@@ -596,7 +607,7 @@ impl Key {
fn test_key_from_name() {
assert_eq!(
Key::ALL.len(),
Key::F35 as usize + 1,
Key::BrowserBack as usize + 1,
"Some keys are missing in Key::ALL"
);

View File

@@ -252,7 +252,7 @@ pub struct OpenUrl {
}
impl OpenUrl {
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn same_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
@@ -260,7 +260,7 @@ impl OpenUrl {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn new_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
@@ -607,7 +607,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self {
Self {
enabled,
@@ -617,7 +617,7 @@ impl WidgetInfo {
}
/// checkboxes, radio-buttons etc
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self {
Self {
enabled,
@@ -635,7 +635,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self {
let label = label.to_string();
Self {
@@ -646,7 +646,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn text_edit(
enabled: bool,
prev_text_value: impl ToString,
@@ -670,7 +670,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn text_selection_changed(
enabled: bool,
text_selection: std::ops::RangeInclusive<usize>,
@@ -739,7 +739,7 @@ impl WidgetInfo {
if text_value.is_empty() {
"blank".into()
} else {
text_value.to_string()
text_value.clone()
}
} else {
"blank".into()

View File

@@ -13,7 +13,7 @@ use crate::{Context, CursorIcon, Id};
/// - [`crate::Response::dnd_hover_payload`]
/// - [`crate::Response::dnd_release_payload`]
///
/// See [this example](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/drag_and_drop.rs).
/// See [this example](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/drag_and_drop.rs).
#[doc(alias = "drag and drop")]
#[derive(Clone, Default)]
pub struct DragAndDrop {

View File

@@ -1,8 +1,8 @@
use emath::GuiRounding as _;
use crate::{
vec2, Align2, Color32, Context, Id, InnerResponse, NumExt, Painter, Rect, Region, Style, Ui,
UiBuilder, Vec2,
vec2, Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style,
Ui, UiBuilder, Vec2,
};
#[cfg(debug_assertions)]
@@ -184,7 +184,7 @@ impl GridLayout {
Rect::from_min_size(cursor.min, size).round_ui()
}
#[allow(clippy::unused_self)]
#[expect(clippy::unused_self)]
pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect {
// TODO(emilk): allow this alignment to be customized
Align2::LEFT_CENTER

View File

@@ -65,7 +65,7 @@ pub fn hit_test(
.filter(|layer| layer.order.allow_interaction())
.flat_map(|&layer_id| widgets.get_layer(layer_id))
.filter(|&w| {
if w.interact_rect.is_negative() {
if w.interact_rect.is_negative() || w.interact_rect.any_nan() {
return false;
}
@@ -175,11 +175,17 @@ pub fn hit_test(
restore_widget_rect(wr);
}
if let Some(wr) = &mut hits.drag {
debug_assert!(wr.sense.senses_drag());
debug_assert!(
wr.sense.senses_drag(),
"We should only return drag hits if they sense drag"
);
restore_widget_rect(wr);
}
if let Some(wr) = &mut hits.click {
debug_assert!(wr.sense.senses_click());
debug_assert!(
wr.sense.senses_click(),
"We should only return click hits if they sense click"
);
restore_widget_rect(wr);
}
}

View File

@@ -59,7 +59,7 @@ impl Id {
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
pub fn with(self, child: impl std::hash::Hash) -> Self {
use std::hash::{BuildHasher, Hasher};
use std::hash::{BuildHasher as _, Hasher as _};
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.0.get());
child.hash(&mut hasher);

View File

@@ -5,7 +5,7 @@ use crate::data::input::{
TouchDeviceId, ViewportInfo, NUM_POINTER_BUTTONS,
};
use crate::{
emath::{vec2, NumExt, Pos2, Rect, Vec2},
emath::{vec2, NumExt as _, Pos2, Rect, Vec2},
util::History,
};
use std::{
@@ -18,9 +18,15 @@ pub use touch_state::MultiTouchInfo;
use touch_state::TouchState;
/// Options for input state handling.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct InputOptions {
/// Multiplier for the scroll speed when reported in [`crate::MouseWheelUnit::Line`]s.
pub line_scroll_speed: f32,
/// Controls the speed at which we zoom in when doing ctrl/cmd + scroll.
pub scroll_zoom_speed: f32,
/// After a pointer-down event, if the pointer moves more than this, it won't become a click.
pub max_click_dist: f32,
@@ -35,14 +41,44 @@ pub struct InputOptions {
/// The new pointer press must come within this many seconds from previous pointer release
/// for double click (or when this value is doubled, triple click) to count.
pub max_double_click_delay: f64,
/// When this modifier is down, all scroll events are treated as zoom events.
///
/// The default is CTRL/CMD, and it is STRONGLY recommended to NOT change this.
pub zoom_modifier: Modifiers,
/// When this modifier is down, all scroll events are treated as horizontal scrolls,
/// and when combined with [`Self::zoom_modifier`] it will result in zooming
/// on only the horizontal axis.
///
/// The default is SHIFT, and it is STRONGLY recommended to NOT change this.
pub horizontal_scroll_modifier: Modifiers,
/// When this modifier is down, all scroll events are treated as vertical scrolls,
/// and when combined with [`Self::zoom_modifier`] it will result in zooming
/// on only the vertical axis.
pub vertical_scroll_modifier: Modifiers,
}
impl Default for InputOptions {
fn default() -> Self {
// TODO(emilk): figure out why these constants need to be different on web and on native (winit).
let is_web = cfg!(target_arch = "wasm32");
let line_scroll_speed = if is_web {
8.0
} else {
40.0 // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461
};
Self {
line_scroll_speed,
scroll_zoom_speed: 1.0 / 200.0,
max_click_dist: 6.0,
max_click_duration: 0.8,
max_double_click_delay: 0.3,
zoom_modifier: Modifiers::COMMAND,
horizontal_scroll_modifier: Modifiers::SHIFT,
vertical_scroll_modifier: Modifiers::ALT,
}
}
}
@@ -51,39 +87,74 @@ impl InputOptions {
/// Show the options in the ui.
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
line_scroll_speed,
scroll_zoom_speed,
max_click_dist,
max_click_duration,
max_double_click_delay,
zoom_modifier,
horizontal_scroll_modifier,
vertical_scroll_modifier,
} = self;
crate::containers::CollapsingHeader::new("InputOptions")
.default_open(false)
crate::Grid::new("InputOptions")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Max click distance");
ui.add(
crate::DragValue::new(max_click_dist)
.range(0.0..=f32::INFINITY)
ui.label("Line scroll speed");
ui.add(crate::DragValue::new(line_scroll_speed).range(0.0..=f32::INFINITY))
.on_hover_text(
"How many lines to scroll with each tick of the mouse wheel",
);
ui.end_row();
ui.label("Scroll zoom speed");
ui.add(
crate::DragValue::new(scroll_zoom_speed)
.range(0.0..=f32::INFINITY)
.speed(0.001),
)
.on_hover_text("How fast to zoom with ctrl/cmd + scroll");
ui.end_row();
ui.label("Max click distance");
ui.add(crate::DragValue::new(max_click_dist).range(0.0..=f32::INFINITY))
.on_hover_text(
"If the pointer moves more than this, it won't become a click",
);
ui.end_row();
ui.label("Max click duration");
ui.add(
crate::DragValue::new(max_click_duration)
.range(0.1..=f64::INFINITY)
.speed(0.1),
)
.on_hover_text("If the pointer moves more than this, it won't become a click");
});
ui.horizontal(|ui| {
ui.label("Max click duration");
ui.add(
crate::DragValue::new(max_click_duration)
.range(0.1..=f64::INFINITY)
.speed(0.1),
)
.on_hover_text("If the pointer is down for longer than this it will no longer register as a click");
});
ui.horizontal(|ui| {
ui.label("Max double click delay");
ui.add(
crate::DragValue::new(max_double_click_delay)
.range(0.01..=f64::INFINITY)
.speed(0.1),
)
.on_hover_text("Max time interval for double click to count");
});
.on_hover_text(
"If the pointer is down for longer than this it will no longer register as a click",
);
ui.end_row();
ui.label("Max double click delay");
ui.add(
crate::DragValue::new(max_double_click_delay)
.range(0.01..=f64::INFINITY)
.speed(0.1),
)
.on_hover_text("Max time interval for double click to count");
ui.end_row();
ui.label("zoom_modifier");
zoom_modifier.ui(ui);
ui.end_row();
ui.label("horizontal_scroll_modifier");
horizontal_scroll_modifier.ui(ui);
ui.end_row();
ui.label("vertical_scroll_modifier");
vertical_scroll_modifier.ui(ui);
ui.end_row();
});
}
}
@@ -227,7 +298,7 @@ pub struct InputState {
/// Input state management configuration.
///
/// This gets copied from `egui::Options` at the start of each frame for convenience.
input_options: InputOptions,
options: InputOptions,
}
impl Default for InputState {
@@ -255,7 +326,7 @@ impl Default for InputState {
modifiers: Default::default(),
keys_down: Default::default(),
events: Default::default(),
input_options: Default::default(),
options: Default::default(),
}
}
}
@@ -267,7 +338,7 @@ impl InputState {
mut new: RawInput,
requested_immediate_repaint_prev_frame: bool,
pixels_per_point: f32,
options: &crate::Options,
options: InputOptions,
) -> Self {
profiling::function_scope!();
@@ -324,11 +395,18 @@ impl InputState {
MouseWheelUnit::Page => screen_rect.height() * *delta,
};
if modifiers.shift {
// Treat as horizontal scrolling.
let is_horizontal = modifiers.matches_any(options.horizontal_scroll_modifier);
let is_vertical = modifiers.matches_any(options.vertical_scroll_modifier);
if is_horizontal && !is_vertical {
// Treat all scrolling as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
delta = vec2(delta.x + delta.y, 0.0);
}
if !is_horizontal && is_vertical {
// Treat all scrolling as vertical scrolling.
delta = vec2(0.0, delta.x + delta.y);
}
raw_scroll_delta += delta;
@@ -342,14 +420,14 @@ impl InputState {
MouseWheelUnit::Line | MouseWheelUnit::Page => false,
};
let is_zoom = modifiers.ctrl || modifiers.mac_cmd || modifiers.command;
let is_zoom = modifiers.matches_any(options.zoom_modifier);
#[allow(clippy::collapsible_else_if)]
#[expect(clippy::collapsible_else_if)]
if is_zoom {
if is_smooth {
smooth_scroll_delta_for_zoom += delta.y;
smooth_scroll_delta_for_zoom += delta.x + delta.y;
} else {
unprocessed_scroll_delta_for_zoom += delta.y;
unprocessed_scroll_delta_for_zoom += delta.x + delta.y;
}
} else {
if is_smooth {
@@ -437,7 +515,7 @@ impl InputState {
keys_down,
events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events
raw: new,
input_options: options.input_options.clone(),
options,
}
}
@@ -452,10 +530,13 @@ impl InputState {
self.screen_rect
}
/// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
/// Uniform zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
/// * `zoom = 1`: no change
/// * `zoom < 1`: pinch together
/// * `zoom > 1`: pinch spread
///
/// If your application supports non-proportional zooming,
/// then you probably want to use [`Self::zoom_delta_2d`] instead.
#[inline(always)]
pub fn zoom_delta(&self) -> f32 {
// If a multi touch gesture is detected, it measures the exact and linear proportions of
@@ -485,10 +566,29 @@ impl InputState {
// the distances of the finger tips. It is therefore potentially more accurate than
// `zoom_factor_delta` which is based on the `ctrl-scroll` event which, in turn, may be
// synthesized from an original touch gesture.
self.multi_touch().map_or_else(
|| Vec2::splat(self.zoom_factor_delta),
|touch| touch.zoom_delta_2d,
)
if let Some(multi_touch) = self.multi_touch() {
multi_touch.zoom_delta_2d
} else {
let mut zoom = Vec2::splat(self.zoom_factor_delta);
let is_horizontal = self
.modifiers
.matches_any(self.options.horizontal_scroll_modifier);
let is_vertical = self
.modifiers
.matches_any(self.options.vertical_scroll_modifier);
if is_horizontal && !is_vertical {
// Horizontal-only zooming.
zoom.y = 1.0;
}
if !is_horizontal && is_vertical {
// Vertical-only zooming.
zoom.x = 1.0;
}
zoom
}
}
/// How long has it been (in seconds) since the use last scrolled?
@@ -497,10 +597,14 @@ impl InputState {
(self.time - self.last_scroll_time) as f32
}
/// The [`crate::Context`] will call this at the end of each frame to see if we need a repaint.
/// The [`crate::Context`] will call this at the beginning of each frame to see if we need a repaint.
///
/// Returns how long to wait for a repaint.
pub fn wants_repaint_after(&self) -> Option<Duration> {
///
/// NOTE: It's important to call this immediately after [`Self::begin_pass`] since calls to
/// [`Self::consume_key`] will remove events from the vec, meaning those key presses wouldn't
/// cause a repaint.
pub(crate) fn wants_repaint_after(&self) -> Option<Duration> {
if self.pointer.wants_repaint()
|| self.unprocessed_scroll_delta.abs().max_elem() > 0.2
|| self.unprocessed_scroll_delta_for_zoom.abs() > 0.2
@@ -514,10 +618,10 @@ impl InputState {
// We need to wake up and check for press-and-hold for the context menu.
if let Some(press_start_time) = self.pointer.press_start_time {
let press_duration = self.time - press_start_time;
if self.input_options.max_click_duration.is_finite()
&& press_duration < self.input_options.max_click_duration
if self.options.max_click_duration.is_finite()
&& press_duration < self.options.max_click_duration
{
let secs_until_menu = self.input_options.max_click_duration - press_duration;
let secs_until_menu = self.options.max_click_duration - press_duration;
return Some(Duration::from_secs_f64(secs_until_menu));
}
}
@@ -878,7 +982,7 @@ pub struct PointerState {
/// Input state management configuration.
///
/// This gets copied from `egui::Options` at the start of each frame for convenience.
input_options: InputOptions,
options: InputOptions,
}
impl Default for PointerState {
@@ -901,23 +1005,18 @@ impl Default for PointerState {
last_last_click_time: f64::NEG_INFINITY,
last_move_time: f64::NEG_INFINITY,
pointer_events: vec![],
input_options: Default::default(),
options: Default::default(),
}
}
}
impl PointerState {
#[must_use]
pub(crate) fn begin_pass(
mut self,
time: f64,
new: &RawInput,
options: &crate::Options,
) -> Self {
pub(crate) fn begin_pass(mut self, time: f64, new: &RawInput, options: InputOptions) -> Self {
let was_decidedly_dragging = self.is_decidedly_dragging();
self.time = time;
self.input_options = options.input_options.clone();
self.options = options;
self.pointer_events.clear();
@@ -927,6 +1026,7 @@ impl PointerState {
self.motion = Some(Vec2::ZERO);
}
let mut clear_history_after_velocity_calculation = false;
for event in &new.events {
match event {
Event::PointerMoved(pos) => {
@@ -937,9 +1037,10 @@ impl PointerState {
if let Some(press_origin) = self.press_origin {
self.has_moved_too_much_for_a_click |=
press_origin.distance(pos) > self.input_options.max_click_dist;
press_origin.distance(pos) > self.options.max_click_dist;
}
self.last_move_time = time;
self.pointer_events.push(PointerEvent::Moved(pos));
}
Event::PointerButton {
@@ -975,10 +1076,10 @@ impl PointerState {
let clicked = self.could_any_button_be_click();
let click = if clicked {
let double_click = (time - self.last_click_time)
< self.input_options.max_double_click_delay;
let double_click =
(time - self.last_click_time) < self.options.max_double_click_delay;
let triple_click = (time - self.last_last_click_time)
< (self.input_options.max_double_click_delay * 2.0);
< (self.options.max_double_click_delay * 2.0);
let count = if triple_click {
3
} else if double_click {
@@ -1013,7 +1114,10 @@ impl PointerState {
// When dragging a slider and the mouse leaves the viewport, we still want the drag to work,
// so we don't treat this as a `PointerEvent::Released`.
// NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame.
self.pos_history.clear();
// Delay the clearing until after the final velocity calculation, so we can
// get the final velocity when `drag_stopped` is true.
clear_history_after_velocity_calculation = true;
}
Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta,
_ => {}
@@ -1044,6 +1148,9 @@ impl PointerState {
if self.velocity != Vec2::ZERO {
self.last_move_time = time;
}
if clear_history_after_velocity_calculation {
self.pos_history.clear();
}
self.direction = self.pos_history.velocity().unwrap_or_default().normalized();
@@ -1276,7 +1383,7 @@ impl PointerState {
}
if let Some(press_start_time) = self.press_start_time {
if self.time - press_start_time > self.input_options.max_click_duration {
if self.time - press_start_time > self.options.max_click_duration {
return false;
}
}
@@ -1312,7 +1419,7 @@ impl PointerState {
&& !self.has_moved_too_much_for_a_click
&& self.button_down(PointerButton::Primary)
&& self.press_start_time.is_some_and(|press_start_time| {
self.time - press_start_time > self.input_options.max_click_duration
self.time - press_start_time > self.options.max_click_duration
})
}
@@ -1372,7 +1479,7 @@ impl InputState {
modifiers,
keys_down,
events,
input_options: _,
options: _,
} = self;
ui.style_mut()
@@ -1458,7 +1565,7 @@ impl PointerState {
last_last_click_time,
pointer_events,
last_move_time,
input_options: _,
options: _,
} = self;
ui.label(format!("latest_pos: {latest_pos:?}"));

View File

@@ -194,7 +194,7 @@ impl TouchState {
let zoom_delta = state.current.avg_distance / state_previous.avg_distance;
let zoom_delta2 = match state.pinch_type {
let zoom_delta_2d = match state.pinch_type {
PinchType::Horizontal => Vec2::new(
state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x,
1.0,
@@ -213,7 +213,7 @@ impl TouchState {
start_pos: state.start_pointer_pos,
num_touches: self.active_touches.len(),
zoom_delta,
zoom_delta_2d: zoom_delta2,
zoom_delta_2d,
rotation_delta: normalized_angle(state.current.heading - state_previous.heading),
translation_delta: state.current.avg_pos - state_previous.avg_pos,
force: state.current.avg_force,

View File

@@ -2,7 +2,7 @@
use crate::{
epaint, memory, pos2, remap_clamp, vec2, Color32, CursorIcon, FontFamily, FontId, Label, Mesh,
NumExt, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget,
NumExt as _, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget,
};
use emath::Vec2;

View File

@@ -1,7 +1,7 @@
use emath::GuiRounding as _;
use crate::{
emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2},
emath::{pos2, vec2, Align2, NumExt as _, Pos2, Rect, Vec2},
Align,
};
const INFINITY: f32 = f32::INFINITY;
@@ -75,9 +75,17 @@ impl Region {
}
pub fn sanity_check(&self) {
debug_assert!(!self.min_rect.any_nan());
debug_assert!(!self.max_rect.any_nan());
debug_assert!(!self.cursor.any_nan());
debug_assert!(
!self.min_rect.any_nan(),
"min rect has Nan: {:?}",
self.min_rect
);
debug_assert!(
!self.max_rect.any_nan(),
"max rect has Nan: {:?}",
self.max_rect
);
debug_assert!(!self.cursor.any_nan(), "cursor has Nan: {:?}", self.cursor);
}
}
@@ -398,8 +406,8 @@ impl Layout {
/// ## Doing layout
impl Layout {
pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect {
debug_assert!(size.x >= 0.0 && size.y >= 0.0);
debug_assert!(!outer.is_negative());
debug_assert!(size.x >= 0.0 && size.y >= 0.0, "Negative size: {size:?}");
debug_assert!(!outer.is_negative(), "Negative outer: {outer:?}");
self.align2().align_size_within_rect(size, outer).round_ui()
}
@@ -425,7 +433,7 @@ impl Layout {
}
pub(crate) fn region_from_max_rect(&self, max_rect: Rect) -> Region {
debug_assert!(!max_rect.any_nan());
debug_assert!(!max_rect.any_nan(), "max_rect is not NaN: {max_rect:?}");
let mut region = Region {
min_rect: Rect::NOTHING, // temporary
max_rect,
@@ -461,8 +469,8 @@ impl Layout {
/// Given the cursor in the region, how much space is available
/// for the next widget?
fn available_from_cursor_max_rect(&self, cursor: Rect, max_rect: Rect) -> Rect {
debug_assert!(!cursor.any_nan());
debug_assert!(!max_rect.any_nan());
debug_assert!(!cursor.any_nan(), "cursor is NaN: {cursor:?}");
debug_assert!(!max_rect.any_nan(), "max_rect is NaN: {max_rect:?}");
// NOTE: in normal top-down layout the cursor has moved below the current max_rect,
// but the available shouldn't be negative.
@@ -516,7 +524,7 @@ impl Layout {
avail.max.y = y;
}
debug_assert!(!avail.any_nan());
debug_assert!(!avail.any_nan(), "avail is NaN: {avail:?}");
avail
}
@@ -527,7 +535,10 @@ impl Layout {
/// Use `justify_and_align` to get the inner `widget_rect`.
pub(crate) fn next_frame(&self, region: &Region, child_size: Vec2, spacing: Vec2) -> Rect {
region.sanity_check();
debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
debug_assert!(
child_size.x >= 0.0 && child_size.y >= 0.0,
"Negative size: {child_size:?}"
);
if self.main_wrap {
let available_size = self.available_rect_before_wrap(region).size();
@@ -613,7 +624,10 @@ impl Layout {
fn next_frame_ignore_wrap(&self, region: &Region, child_size: Vec2) -> Rect {
region.sanity_check();
debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
debug_assert!(
child_size.x >= 0.0 && child_size.y >= 0.0,
"Negative size: {child_size:?}"
);
let available_rect = self.available_rect_before_wrap(region);
@@ -646,16 +660,19 @@ impl Layout {
frame_rect = frame_rect.translate(Vec2::Y * (region.cursor.top() - frame_rect.top()));
}
debug_assert!(!frame_rect.any_nan());
debug_assert!(!frame_rect.is_negative());
debug_assert!(!frame_rect.any_nan(), "frame_rect is NaN: {frame_rect:?}");
debug_assert!(!frame_rect.is_negative(), "frame_rect is negative");
frame_rect.round_ui()
}
/// Apply justify (fill width/height) and/or alignment after calling `next_space`.
pub(crate) fn justify_and_align(&self, frame: Rect, mut child_size: Vec2) -> Rect {
debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
debug_assert!(!frame.is_negative());
debug_assert!(
child_size.x >= 0.0 && child_size.y >= 0.0,
"Negative size: {child_size:?}"
);
debug_assert!(!frame.is_negative(), "frame is negative");
if self.horizontal_justify() {
child_size.x = child_size.x.at_least(frame.width()); // fill full width
@@ -673,8 +690,8 @@ impl Layout {
) -> Rect {
let frame = self.next_frame_ignore_wrap(region, size);
let rect = self.align_size_within_rect(size, frame);
debug_assert!(!rect.any_nan());
debug_assert!(!rect.is_negative());
debug_assert!(!rect.any_nan(), "rect is NaN: {rect:?}");
debug_assert!(!rect.is_negative(), "rect is negative: {rect:?}");
rect
}
@@ -717,7 +734,7 @@ impl Layout {
widget_rect: Rect,
item_spacing: Vec2,
) {
debug_assert!(!cursor.any_nan());
debug_assert!(!cursor.any_nan(), "cursor is NaN: {cursor:?}");
if self.main_wrap {
if cursor.intersects(frame_rect.shrink(1.0)) {
// make row/column larger if necessary

View File

@@ -3,7 +3,7 @@
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//!
//! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.81.0 or later to use `egui`.
//! You need to have rust 1.84.0 or later to use `egui`.
//!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe).
@@ -143,7 +143,7 @@
//! }
//! ```
//!
//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs).
//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/main/crates/egui_glow/src/painter.rs).
//!
//!
//! ### Debugging your renderer
@@ -219,7 +219,7 @@
//! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames.
//!
//! It can be useful to read the code for the toggle switch example widget to get a better understanding
//! of how egui works: <https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
//! of how egui works: <https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
//!
//! Read more about the pros and cons of immediate mode at <https://github.com/emilk/egui#why-immediate-mode>.
//!
@@ -400,6 +400,9 @@
//! profile-with-puffin = ["profiling/profile-with-puffin"]
//! ```
//!
//! ## Custom allocator
//! egui apps can run significantly (~20%) faster by using a custom allocator, like [mimalloc](https://crates.io/crates/mimalloc) or [talc](https://crates.io/crates/talc).
//!
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
@@ -441,6 +444,7 @@ mod widget_rect;
pub mod widget_text;
pub mod widgets;
mod atomics;
#[cfg(feature = "callstack")]
#[cfg(debug_assertions)]
mod callstack;
@@ -471,7 +475,7 @@ pub use epaint::{
};
pub mod text {
pub use crate::text_selection::{CCursorRange, CursorRange};
pub use crate::text_selection::CCursorRange;
pub use epaint::text::{
cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob,
LayoutSection, TextFormat, TextWrapping, TAB_SIZE,
@@ -479,6 +483,7 @@ pub mod text {
}
pub use self::{
atomics::*,
containers::*,
context::{Context, RepaintCause, RequestRepaintInfo},
data::{
@@ -493,7 +498,7 @@ pub use self::{
epaint::text::TextWrapMode,
grid::Grid,
id::{Id, IdMap},
input_state::{InputState, MultiTouchInfo, PointerState},
input_state::{InputOptions, InputState, MultiTouchInfo, PointerState},
layers::{LayerId, Order},
layout::*,
load::SizeHint,
@@ -564,7 +569,7 @@ macro_rules! include_image {
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.add(egui::github_link_file_line!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));
/// ui.add(egui::github_link_file_line!("https://github.com/YOUR/PROJECT/blob/main/", "(source code)"));
/// # });
/// ```
#[macro_export]
@@ -579,7 +584,7 @@ macro_rules! github_link_file_line {
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.add(egui::github_link_file!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));
/// ui.add(egui::github_link_file!("https://github.com/YOUR/PROJECT/blob/main/", "(source code)"));
/// # });
/// ```
#[macro_export]

View File

@@ -64,7 +64,7 @@ use std::{
use ahash::HashMap;
use emath::{Float, OrderedFloat};
use emath::{Float as _, OrderedFloat};
use epaint::{mutex::Mutex, textures::TextureOptions, ColorImage, TextureHandle, TextureId, Vec2};
use crate::Context;
@@ -143,22 +143,53 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
/// Given as a hint for image loading requests.
///
/// Used mostly for rendering SVG:s to a good size.
/// The size is measured in texels, with the pixels per point already factored in.
///
/// All variants will preserve the original aspect ratio.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// The [`SizeHint`] determines at what resolution the image should be rasterized.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SizeHint {
/// Scale original size by some factor.
/// Scale original size by some factor, keeping the original aspect ratio.
///
/// The original size of the image is usually its texel resolution,
/// but for an SVG it's the point size of the SVG.
///
/// For instance, setting `Scale(2.0)` will rasterize SVG:s to twice their original size,
/// which is useful for high-DPI displays.
Scale(OrderedFloat<f32>),
/// Scale to width.
/// Scale to exactly this pixel width, keeping the original aspect ratio.
Width(u32),
/// Scale to height.
/// Scale to exactly this pixel height, keeping the original aspect ratio.
Height(u32),
/// Scale to size.
Size(u32, u32),
/// Scale to this pixel size.
Size {
width: u32,
height: u32,
/// If true, the image will be as large as possible
/// while still fitting within the given width/height.
maintain_aspect_ratio: bool,
},
}
impl SizeHint {
/// Multiply size hint by a factor.
pub fn scale_by(self, factor: f32) -> Self {
match self {
Self::Scale(scale) => Self::Scale(OrderedFloat(factor * scale.0)),
Self::Width(width) => Self::Width((factor * width as f32).round() as _),
Self::Height(height) => Self::Height((factor * height as f32).round() as _),
Self::Size {
width,
height,
maintain_aspect_ratio,
} => Self::Size {
width: (factor * width as f32).round() as _,
height: (factor * height as f32).round() as _,
maintain_aspect_ratio,
},
}
}
}
impl Default for SizeHint {
@@ -168,13 +199,6 @@ impl Default for SizeHint {
}
}
impl From<Vec2> for SizeHint {
#[inline]
fn from(value: Vec2) -> Self {
Self::Size(value.x.round() as u32, value.y.round() as u32)
}
}
/// Represents a byte buffer.
///
/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`.
@@ -249,12 +273,16 @@ impl Deref for Bytes {
pub enum BytesPoll {
/// Bytes are being loaded.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
/// Bytes are loaded.
Ready {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
@@ -322,12 +350,17 @@ pub trait BytesLoader {
/// Implementations may use this to perform work at the end of a frame,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
/// Returns `true` if some data is currently being loaded.
fn has_pending(&self) -> bool {
false
}
}
/// Represents an image which is currently being loaded.
@@ -339,6 +372,8 @@ pub trait BytesLoader {
pub enum ImagePoll {
/// Image is loading.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
@@ -389,18 +424,27 @@ pub trait ImageLoader {
/// Implementations may use this to perform work at the end of a pass,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
/// Returns `true` if some image is currently being loaded.
///
/// NOTE: You probably also want to check [`BytesLoader::has_pending`].
fn has_pending(&self) -> bool {
false
}
}
/// A texture with a known size.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SizedTexture {
pub id: TextureId,
/// Point size of the original SVG, or the size of the image in texels.
pub size: Vec2,
}
@@ -446,6 +490,8 @@ impl<'a> From<&'a TextureHandle> for SizedTexture {
pub enum TexturePoll {
/// Texture is loading.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
@@ -455,6 +501,7 @@ pub enum TexturePoll {
}
impl TexturePoll {
/// Point size of the original SVG, or the size of the image in texels.
#[inline]
pub fn size(&self) -> Option<Vec2> {
match self {
@@ -527,8 +574,8 @@ pub trait TextureLoader {
/// Implementations may use this to perform work at the end of a pass,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
@@ -560,3 +607,26 @@ impl Default for Loaders {
}
}
}
impl Loaders {
/// The given pass has just ended.
pub fn end_pass(&self, pass_index: u64) {
let Self {
include,
bytes,
image,
texture,
} = self;
include.end_pass(pass_index);
for loader in bytes.lock().iter() {
loader.end_pass(pass_index);
}
for loader in image.lock().iter() {
loader.end_pass(pass_index);
}
for loader in texture.lock().iter() {
loader.end_pass(pass_index);
}
}
}

View File

@@ -28,7 +28,7 @@ impl DefaultBytesLoader {
}
impl BytesLoader for DefaultBytesLoader {
fn id(&self) -> &str {
fn id(&self) -> &'static str {
generate_loader_id!(DefaultBytesLoader)
}

View File

@@ -1,17 +1,38 @@
use std::borrow::Cow;
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
use emath::Vec2;
use super::{
BytesLoader, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle,
BytesLoader as _, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle,
TextureLoadResult, TextureLoader, TextureOptions, TexturePoll,
};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct PrimaryKey {
uri: String,
texture_options: TextureOptions,
}
/// SVG:s might have several different sizes loaded
type Bucket = HashMap<Option<SizeHint>, Entry>;
struct Entry {
last_used: AtomicU64,
/// Size of the original SVG, if any, or the texel size of the image if not an SVG.
source_size: Vec2,
handle: TextureHandle,
}
#[derive(Default)]
pub struct DefaultTextureLoader {
cache: Mutex<HashMap<(Cow<'static, str>, TextureOptions), TextureHandle>>,
pass_index: AtomicU64,
cache: Mutex<HashMap<PrimaryKey, Bucket>>,
}
impl TextureLoader for DefaultTextureLoader {
fn id(&self) -> &str {
fn id(&self) -> &'static str {
crate::generate_loader_id!(DefaultTextureLoader)
}
@@ -22,17 +43,47 @@ impl TextureLoader for DefaultTextureLoader {
texture_options: TextureOptions,
size_hint: SizeHint,
) -> TextureLoadResult {
let svg_size_hint = if is_svg(uri) {
// For SVGs it's important that we render at the desired size,
// or we might get a blurry image when we scale it up.
// So we make the size hint a part of the cache key.
// This might lead to a lot of extra entries for the same SVG file,
// which is potentially wasteful of RAM, but better that than blurry images.
Some(size_hint)
} else {
// For other images we just use one cache value, no matter what the size we render at.
None
};
let mut cache = self.cache.lock();
if let Some(handle) = cache.get(&(Cow::Borrowed(uri), texture_options)) {
let texture = SizedTexture::from_handle(handle);
let bucket = cache
.entry(PrimaryKey {
uri: uri.to_owned(),
texture_options,
})
.or_default();
if let Some(texture) = bucket.get(&svg_size_hint) {
texture
.last_used
.store(self.pass_index.load(Relaxed), Relaxed);
let texture = SizedTexture::new(texture.handle.id(), texture.source_size);
Ok(TexturePoll::Ready { texture })
} else {
match ctx.try_load_image(uri, size_hint)? {
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
ImagePoll::Ready { image } => {
let source_size = image.source_size;
let handle = ctx.load_texture(uri, image, texture_options);
let texture = SizedTexture::from_handle(&handle);
cache.insert((Cow::Owned(uri.to_owned()), texture_options), handle);
let texture = SizedTexture::new(handle.id(), source_size);
bucket.insert(
svg_size_hint,
Entry {
last_used: AtomicU64::new(self.pass_index.load(Relaxed)),
source_size,
handle,
},
);
let reduce_texture_memory = ctx.options(|o| o.reduce_texture_memory);
if reduce_texture_memory {
let loaders = ctx.loaders();
@@ -54,7 +105,7 @@ impl TextureLoader for DefaultTextureLoader {
#[cfg(feature = "log")]
log::trace!("forget {uri:?}");
self.cache.lock().retain(|(u, _), _| u != uri);
self.cache.lock().retain(|key, _value| key.uri != uri);
}
fn forget_all(&self) {
@@ -64,13 +115,35 @@ impl TextureLoader for DefaultTextureLoader {
self.cache.lock().clear();
}
fn end_pass(&self, _: usize) {}
fn end_pass(&self, pass_index: u64) {
self.pass_index.store(pass_index, Relaxed);
let mut cache = self.cache.lock();
cache.retain(|_key, bucket| {
if 2 <= bucket.len() {
// There are multiple textures of the same URI (e.g. SVGs of different scales).
// This could be because someone has an SVG in a resizable container,
// and so we get a lot of different sizes of it.
// This could wast VRAM, so we remove the ones that are not used in this frame.
bucket.retain(|_, texture| pass_index <= texture.last_used.load(Relaxed) + 1);
}
!bucket.is_empty()
});
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|texture| texture.byte_size())
.map(|bucket| {
bucket
.values()
.map(|texture| texture.handle.byte_size())
.sum::<usize>()
})
.sum()
}
}
fn is_svg(uri: &str) -> bool {
uri.ends_with(".svg")
}

View File

@@ -87,15 +87,6 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) viewport_id: ViewportId,
/// Which popup-window is open (if any)?
/// Could be a combo box, color picker, menu, etc.
/// Optionally stores the position of the popup (usually this would be the position where
/// the user clicked).
/// If position is [`None`], the popup position will be calculated based on some configuration
/// (e.g. relative to some other widget).
#[cfg_attr(feature = "persistence", serde(skip))]
popup: Option<(Id, Option<Pos2>)>,
#[cfg_attr(feature = "persistence", serde(skip))]
everything_is_visible: bool,
@@ -116,6 +107,15 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) focus: ViewportIdMap<Focus>,
/// Which popup-window is open on a viewport (if any)?
/// Could be a combo box, color picker, menu, etc.
/// Optionally stores the position of the popup (usually this would be the position where
/// the user clicked).
/// If position is [`None`], the popup position will be calculated based on some configuration
/// (e.g. relative to some other widget).
#[cfg_attr(feature = "persistence", serde(skip))]
popups: ViewportIdMap<OpenPopup>,
}
impl Default for Memory {
@@ -130,7 +130,7 @@ impl Default for Memory {
viewport_id: Default::default(),
areas: Default::default(),
to_global: Default::default(),
popup: Default::default(),
popups: Default::default(),
everything_is_visible: Default::default(),
add_fonts: Default::default(),
};
@@ -284,14 +284,6 @@ pub struct Options {
/// By default this is `true` in debug builds.
pub warn_on_id_clash: bool,
// ------------------------------
// Input:
/// Multiplier for the scroll speed when reported in [`crate::MouseWheelUnit::Line`]s.
pub line_scroll_speed: f32,
/// Controls the speed at which we zoom in when doing ctrl/cmd + scroll.
pub scroll_zoom_speed: f32,
/// Options related to input state handling.
pub input_options: crate::input_state::InputOptions,
@@ -311,14 +303,6 @@ pub struct Options {
impl Default for Options {
fn default() -> Self {
// TODO(emilk): figure out why these constants need to be different on web and on native (winit).
let is_web = cfg!(target_arch = "wasm32");
let line_scroll_speed = if is_web {
8.0
} else {
40.0 // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461
};
Self {
dark_style: std::sync::Arc::new(Theme::Dark.default_style()),
light_style: std::sync::Arc::new(Theme::Light.default_style()),
@@ -335,8 +319,6 @@ impl Default for Options {
warn_on_id_clash: cfg!(debug_assertions),
// Input:
line_scroll_speed,
scroll_zoom_speed: 1.0 / 200.0,
input_options: Default::default(),
reduce_texture_memory: false,
}
@@ -391,9 +373,6 @@ impl Options {
screen_reader: _, // needs to come from the integration
preload_font_glyphs: _,
warn_on_id_clash,
line_scroll_speed,
scroll_zoom_speed,
input_options,
reduce_texture_memory,
} = self;
@@ -448,22 +427,6 @@ impl Options {
CollapsingHeader::new("🖱 Input")
.default_open(false)
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Line scroll speed");
ui.add(crate::DragValue::new(line_scroll_speed).range(0.0..=f32::INFINITY))
.on_hover_text(
"How many lines to scroll with each tick of the mouse wheel",
);
});
ui.horizontal(|ui| {
ui.label("Scroll zoom speed");
ui.add(
crate::DragValue::new(scroll_zoom_speed)
.range(0.0..=f32::INFINITY)
.speed(0.001),
)
.on_hover_text("How fast to zoom with ctrl/cmd + scroll");
});
input_options.ui(ui);
});
@@ -790,6 +753,7 @@ impl Memory {
// Cleanup
self.interactions.retain(|id, _| viewports.contains(id));
self.areas.retain(|id, _| viewports.contains(id));
self.popups.retain(|id, _| viewports.contains(id));
self.areas.entry(self.viewport_id).or_default();
@@ -807,6 +771,15 @@ impl Memory {
self.caches.update();
self.areas_mut().end_pass();
self.focus_mut().end_pass(used_ids);
// Clean up abandoned popups.
if let Some(popup) = self.popups.get_mut(&self.viewport_id) {
if popup.open_this_frame {
popup.open_this_frame = false;
} else {
self.popups.remove(&self.viewport_id);
}
}
}
pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) {
@@ -983,59 +956,6 @@ impl Memory {
self.focus_mut().focused_widget = None;
}
/// Is any widget being dragged?
#[deprecated = "Use `Context::dragged_id` instead"]
#[inline(always)]
pub fn is_anything_being_dragged(&self) -> bool {
self.interaction().potential_drag_id.is_some()
}
/// Is this specific widget being dragged?
///
/// A widget that sense both clicks and drags is only marked as "dragged"
/// when the mouse has moved a bit, but `is_being_dragged` will return true immediately.
#[deprecated = "Use `Context::is_being_dragged` instead"]
#[inline(always)]
pub fn is_being_dragged(&self, id: Id) -> bool {
self.interaction().potential_drag_id == Some(id)
}
/// Get the id of the widget being dragged, if any.
///
/// Note that this is set as soon as the mouse is pressed,
/// so the widget may not yet be marked as "dragged",
/// as that can only happen after the mouse has moved a bit
/// (at least if the widget is interesated in both clicks and drags).
#[deprecated = "Use `Context::dragged_id` instead"]
#[inline(always)]
pub fn dragged_id(&self) -> Option<Id> {
self.interaction().potential_drag_id
}
/// Set which widget is being dragged.
#[inline(always)]
#[deprecated = "Use `Context::set_dragged_id` instead"]
pub fn set_dragged_id(&mut self, id: Id) {
self.interaction_mut().potential_drag_id = Some(id);
}
/// Stop dragging any widget.
#[inline(always)]
#[deprecated = "Use `Context::stop_dragging` instead"]
pub fn stop_dragging(&mut self) {
self.interaction_mut().potential_drag_id = None;
}
/// Is something else being dragged?
///
/// Returns true if we are dragging something, but not the given widget.
#[inline(always)]
#[deprecated = "Use `Context::dragging_something_else` instead"]
pub fn dragging_something_else(&self, not_this: Id) -> bool {
let drag_id = self.interaction().potential_drag_id;
drag_id.is_some() && drag_id != Some(not_this)
}
/// Forget window positions, sizes etc.
/// Can be used to auto-layout windows.
pub fn reset_areas(&mut self) {
@@ -1068,39 +988,93 @@ impl Memory {
}
}
/// State of an open popup.
#[derive(Clone, Copy, Debug)]
struct OpenPopup {
/// Id of the popup.
id: Id,
/// Optional position of the popup.
pos: Option<Pos2>,
/// Whether this popup was still open this frame. Otherwise it's considered abandoned and `Memory::popup` will be cleared.
open_this_frame: bool,
}
impl OpenPopup {
/// Create a new `OpenPopup`.
fn new(id: Id, pos: Option<Pos2>) -> Self {
Self {
id,
pos,
open_this_frame: true,
}
}
}
/// ## Popups
/// Popups are things like combo-boxes, color pickers, menus etc.
/// Only one can be open at a time.
impl Memory {
/// Is the given popup open?
pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popup.is_some_and(|(id, _)| id == popup_id) || self.everything_is_visible()
self.popups
.get(&self.viewport_id)
.is_some_and(|state| state.id == popup_id)
|| self.everything_is_visible()
}
/// Is any popup open?
pub fn any_popup_open(&self) -> bool {
self.popup.is_some() || self.everything_is_visible()
self.popups.contains_key(&self.viewport_id) || self.everything_is_visible()
}
/// Open the given popup and close all others.
///
/// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open.
pub fn open_popup(&mut self, popup_id: Id) {
self.popup = Some((popup_id, None));
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, None));
}
/// Popups must call this every frame while open.
///
/// This is needed because in some cases popups can go away without `close_popup` being
/// called. For example, when a context menu is open and the underlying widget stops
/// being rendered.
pub fn keep_popup_open(&mut self, popup_id: Id) {
if let Some(state) = self.popups.get_mut(&self.viewport_id) {
if state.id == popup_id {
state.open_this_frame = true;
}
}
}
/// Open the popup and remember its position.
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
self.popup = Some((popup_id, pos.into()));
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, pos.into()));
}
/// Get the position for this popup.
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
self.popup
.and_then(|(popup_id, pos)| if popup_id == id { pos } else { None })
self.popups
.get(&self.viewport_id)
.and_then(|state| if state.id == id { state.pos } else { None })
}
/// Close the open popup, if any.
pub fn close_popup(&mut self) {
self.popup = None;
/// Close any currently open popup.
pub fn close_all_popups(&mut self) {
self.popups.clear();
}
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
pub fn close_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.popups.remove(&self.viewport_id);
}
}
/// Toggle the given popup between closed and open.
@@ -1108,7 +1082,7 @@ impl Memory {
/// Note: At most, only one popup can be open at a time.
pub fn toggle_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.close_popup();
self.close_popup(popup_id);
} else {
self.open_popup(popup_id);
}
@@ -1261,7 +1235,7 @@ impl Areas {
self.visible_areas_current_frame.insert(layer_id);
self.wants_to_be_on_top.insert(layer_id);
if !self.order.iter().any(|x| *x == layer_id) {
if !self.order.contains(&layer_id) {
self.order.push(layer_id);
}
}
@@ -1282,10 +1256,10 @@ impl Areas {
self.sublayers.entry(parent).or_default().insert(child);
// Make sure the layers are in the order list:
if !self.order.iter().any(|x| *x == parent) {
if !self.order.contains(&parent) {
self.order.push(parent);
}
if !self.order.iter().any(|x| *x == child) {
if !self.order.contains(&child) {
self.order.push(child);
}
}
@@ -1294,7 +1268,7 @@ impl Areas {
self.order
.iter()
.filter(|layer| layer.order == order && !self.is_sublayer(layer))
.last()
.next_back()
.copied()
}

View File

@@ -1,5 +1,5 @@
#![allow(deprecated)]
//! Menu bar functionality (very basic so far).
//! Deprecated menu API - Use [`crate::containers::menu`] instead.
//!
//! Usage:
//! ```
@@ -23,8 +23,8 @@ use super::{
use crate::{
epaint, vec2,
widgets::{Button, ImageButton},
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt, Order, Stroke, Style, TextWrapMode,
UiKind, WidgetText,
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt as _, Order, Stroke, Style,
TextWrapMode, UiKind, WidgetText,
};
use epaint::mutex::RwLock;
use std::sync::Arc;
@@ -76,16 +76,19 @@ impl std::ops::DerefMut for BarState {
}
fn set_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.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
if style.compact_menu_style {
style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.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;
}
}
/// 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`].
#[deprecated = "Use `crate::containers::menu::Bar` instead"]
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
ui.horizontal(|ui| {
set_menu_style(ui.style_mut());

View File

@@ -1,5 +1,5 @@
/// An `enum` of common operating systems.
#[allow(clippy::upper_case_acronyms)] // `Ios` looks too ugly
#[expect(clippy::upper_case_acronyms)] // `Ios` looks too ugly
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum OperatingSystem {
/// Unknown OS - could be wasm
@@ -76,4 +76,9 @@ impl OperatingSystem {
Self::Unknown
}
}
/// Are we either macOS or iOS?
pub fn is_mac(&self) -> bool {
matches!(self, Self::Mac | Self::IOS)
}
}

View File

@@ -294,7 +294,7 @@ impl Painter {
/// ## Debug painting
impl Painter {
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
self.rect(
rect,
@@ -320,7 +320,7 @@ impl Painter {
/// Text with a background.
///
/// See also [`Context::debug_text`].
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn debug_text(
&self,
pos: Pos2,
@@ -497,7 +497,7 @@ impl Painter {
/// [`Self::layout`] or [`Self::layout_no_wrap`].
///
/// Returns where the text ended up.
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn text(
&self,
pos: Pos2,
@@ -582,16 +582,6 @@ impl Painter {
));
}
}
#[deprecated = "Use `Painter::galley` or `Painter::galley_with_override_text_color` instead"]
#[inline]
pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
if !galley.is_empty() {
self.add(Shape::galley_with_override_text_color(
pos, galley, text_color,
));
}
}
}
fn tint_shape_towards(shape: &mut Shape, target: Color32) {

View File

@@ -3,7 +3,7 @@ use ahash::HashMap;
use crate::{id::IdSet, style, Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects};
#[cfg(debug_assertions)]
use crate::{pos2, Align2, Color32, FontId, NumExt, Painter};
use crate::{pos2, Align2, Color32, FontId, NumExt as _, Painter};
/// Reset at the start of each frame.
#[derive(Clone, Debug, Default)]

View File

@@ -133,8 +133,8 @@ impl Placer {
/// Apply justify or alignment after calling `next_space`.
pub(crate) fn justify_and_align(&self, rect: Rect, child_size: Vec2) -> Rect {
debug_assert!(!rect.any_nan());
debug_assert!(!child_size.any_nan());
debug_assert!(!rect.any_nan(), "rect: {rect:?}");
debug_assert!(!child_size.any_nan(), "child_size is NaN: {child_size:?}");
if let Some(grid) = &self.grid {
grid.justify_and_align(rect, child_size)
@@ -165,8 +165,11 @@ impl Placer {
item_spacing: Vec2,
intrinsic_size: Vec2,
) {
debug_assert!(!frame_rect.any_nan());
debug_assert!(!widget_rect.any_nan());
debug_assert!(!frame_rect.any_nan(), "frame_rect: {frame_rect:?}");
debug_assert!(
!widget_rect.any_nan(),
"widget_rect is NaN: {widget_rect:?}"
);
self.region.sanity_check();
if let Some(grid) = &mut self.grid {

View File

@@ -218,29 +218,44 @@ impl Response {
&& self.ctx.input(|i| i.pointer.button_triple_clicked(button))
}
/// Was this widget middle-clicked or clicked while holding down a modifier key?
///
/// This is used by [`crate::Hyperlink`] to check if a URL should be opened
/// in a new tab, using [`crate::OpenUrl::new_tab`].
pub fn clicked_with_open_in_background(&self) -> bool {
self.middle_clicked() || self.clicked() && self.ctx.input(|i| i.modifiers.any())
}
/// `true` if there was a click *outside* the rect of this widget.
///
/// Clicks on widgets contained in this one counts as clicks inside this widget,
/// so that clicking a button in an area will not be considered as clicking "elsewhere" from the area.
///
/// Clicks on other layers above this widget *will* be considered as clicking elsewhere.
pub fn clicked_elsewhere(&self) -> bool {
let (pointer_interact_pos, any_click) = self
.ctx
.input(|i| (i.pointer.interact_pos(), i.pointer.any_click()));
// We do not use self.clicked(), because we want to catch all clicks within our frame,
// even if we aren't clickable (or even enabled).
// This is important for windows and such that should close then the user clicks elsewhere.
self.ctx.input(|i| {
let pointer = &i.pointer;
if pointer.any_click() {
if self.contains_pointer() || self.hovered() {
false
} else if let Some(pos) = pointer.interact_pos() {
!self.interact_rect.contains(pos)
if any_click {
if self.contains_pointer() || self.hovered() {
false
} else if let Some(pos) = pointer_interact_pos {
let layer_under_pointer = self.ctx.layer_id_at(pos);
if layer_under_pointer != Some(self.layer_id) {
true
} else {
false // clicked without a pointer, weird
!self.interact_rect.contains(pos)
}
} else {
false
false // clicked without a pointer, weird
}
})
} else {
false
}
}
/// Was the widget enabled?
@@ -380,19 +395,6 @@ impl Response {
self.drag_stopped() && self.ctx.input(|i| i.pointer.button_released(button))
}
/// The widget was being dragged, but now it has been released.
#[inline]
#[deprecated = "Renamed 'drag_stopped'"]
pub fn drag_released(&self) -> bool {
self.drag_stopped()
}
/// The widget was being dragged by the button, but now it has been released.
#[deprecated = "Renamed 'drag_stopped_by'"]
pub fn drag_released_by(&self, button: PointerButton) -> bool {
self.drag_stopped_by(button)
}
/// If dragged, how many points were we dragged and in what direction?
#[inline]
pub fn drag_delta(&self) -> Vec2 {
@@ -978,7 +980,10 @@ impl Response {
///
/// You may not call [`Self::interact`] on the resulting `Response`.
pub fn union(&self, other: Self) -> Self {
assert!(self.ctx == other.ctx);
assert!(
self.ctx == other.ctx,
"Responses must be from the same `Context`"
);
debug_assert!(
self.layer_id == other.layer_id,
"It makes no sense to combine Responses from two different layers"

View File

@@ -332,6 +332,9 @@ pub struct Style {
/// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [`Ui::scroll_to_rect`].
pub scroll_animation: ScrollAnimation,
/// Use a more compact style for menus.
pub compact_menu_style: bool,
}
#[test]
@@ -744,7 +747,8 @@ impl ScrollStyle {
// ----------------------------------------------------------------------------
/// Scroll animation configuration, used when programmatically scrolling somewhere (e.g. with `[crate::Ui::scroll_to_cursor]`)
/// Scroll animation configuration, used when programmatically scrolling somewhere (e.g. with `[crate::Ui::scroll_to_cursor]`).
///
/// The animation duration is calculated based on the distance to be scrolled via `[ScrollAnimation::points_per_second]`
/// and can be clamped to a min / max duration via `[ScrollAnimation::duration]`.
#[derive(Copy, Clone, Debug, PartialEq)]
@@ -986,7 +990,7 @@ pub struct Visuals {
/// Show a background behind collapsing headers.
pub collapsing_header_frame: bool,
/// Draw a vertical lien left of indented region, in e.g. [`crate::CollapsingHeader`].
/// Draw a vertical line left of indented region, in e.g. [`crate::CollapsingHeader`].
pub indent_has_left_vline: bool,
/// Whether or not Grids and Tables should be striped by default
@@ -1257,7 +1261,7 @@ pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
impl Default for Style {
fn default() -> Self {
#[allow(deprecated)]
#[expect(deprecated)]
Self {
override_font_id: None,
override_text_style: None,
@@ -1277,6 +1281,7 @@ impl Default for Style {
url_in_tooltip: false,
always_scroll_the_only_direction: false,
scroll_animation: ScrollAnimation::default(),
compact_menu_style: true,
}
}
}
@@ -1558,7 +1563,7 @@ use crate::{
impl Style {
pub fn ui(&mut self, ui: &mut crate::Ui) {
#[allow(deprecated)]
#[expect(deprecated)]
let Self {
override_font_id,
override_text_style,
@@ -1578,6 +1583,7 @@ impl Style {
url_in_tooltip,
always_scroll_the_only_direction,
scroll_animation,
compact_menu_style,
} = self;
crate::Grid::new("_options").show(ui, |ui| {
@@ -1683,6 +1689,8 @@ impl Style {
#[cfg(debug_assertions)]
ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
ui.checkbox(compact_menu_style, "Compact menu style");
ui.checkbox(explanation_tooltips, "Explanation tooltips")
.on_hover_text(
"Show explanatory text when hovering DragValue:s and other egui widgets",

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