mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Merge branch 'emilk:main' into master
This commit is contained in:
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -10,13 +10,13 @@ assignees: ''
|
||||
<!--
|
||||
First look if there is already a similar bug report. If there is, upvote the issue with 👍
|
||||
|
||||
Please also check if the bug is still present in latest master! Do so by adding the following lines to your Cargo.toml:
|
||||
Please also check if the bug is still present in latest main! Do so by adding the following lines to your Cargo.toml:
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
egui = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||
egui = { git = "https://github.com/emilk/egui", branch = "main" }
|
||||
# if you're using eframe:
|
||||
eframe = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||
eframe = { git = "https://github.com/emilk/egui", branch = "main" }
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) before opening a Pull Request!
|
||||
Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request!
|
||||
|
||||
* Keep your PR:s small and focused.
|
||||
* The PR title is what ends up in the changelog, so make it descriptive!
|
||||
|
||||
15
.github/workflows/deploy_web_demo.yml
vendored
15
.github/workflows/deploy_web_demo.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: Deploy web demo
|
||||
|
||||
on:
|
||||
# We only run this on merges to master
|
||||
# We only run this on merges to main
|
||||
push:
|
||||
branches: ["master"]
|
||||
branches: ["main"]
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
|
||||
@@ -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
|
||||
|
||||
34
.github/workflows/enforce_branch_name.yml
vendored
Normal file
34
.github/workflows/enforce_branch_name.yml
vendored
Normal 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.'
|
||||
})
|
||||
2
.github/workflows/png_only_on_lfs.yml
vendored
2
.github/workflows/png_only_on_lfs.yml
vendored
@@ -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
|
||||
|
||||
11
.github/workflows/preview_build.yml
vendored
11
.github/workflows/preview_build.yml
vendored
@@ -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
|
||||
|
||||
20
.github/workflows/rust.yml
vendored
20
.github/workflows/rust.yml
vendored
@@ -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
|
||||
|
||||
@@ -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)).
|
||||
|
||||
@@ -35,7 +35,7 @@ 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
|
||||
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 git lfs](#working-with-git-lfs) for more info.
|
||||
@@ -125,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
|
||||
|
||||
|
||||
|
||||
496
Cargo.lock
496
Cargo.lock
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
@@ -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,8 +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.18.0"
|
||||
accesskit_winit = "0.24"
|
||||
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",
|
||||
@@ -87,6 +87,7 @@ home = "0.5.9"
|
||||
image = { version = "0.25", default-features = false }
|
||||
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"
|
||||
@@ -94,16 +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.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 }
|
||||
|
||||
@@ -133,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"
|
||||
@@ -194,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"
|
||||
@@ -221,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"
|
||||
@@ -239,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"
|
||||
@@ -251,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"
|
||||
@@ -260,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"
|
||||
@@ -267,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"
|
||||
@@ -285,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
|
||||
|
||||
39
README.md
39
README.md
@@ -5,8 +5,8 @@
|
||||
[](https://docs.rs/egui)
|
||||
[](https://github.com/rust-secure-code/safety-dance/)
|
||||
[](https://github.com/emilk/egui/actions/workflows/rust.yml)
|
||||
[](https://github.com/emilk/egui/blob/master/LICENSE-MIT)
|
||||
[](https://github.com/emilk/egui/blob/master/LICENSE-APACHE)
|
||||
[](https://github.com/emilk/egui/blob/main/LICENSE-MIT)
|
||||
[](https://github.com/emilk/egui/blob/main/LICENSE-APACHE)
|
||||
[](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
|
||||
@@ -357,7 +358,7 @@ Notable contributions by:
|
||||
* [@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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Section identical to scripts/clippy_wasm/clippy.toml:
|
||||
|
||||
msrv = "1.81"
|
||||
msrv = "1.84"
|
||||
|
||||
allow-unwrap-in-tests = true
|
||||
|
||||
|
||||
@@ -102,9 +102,6 @@ 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 {
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -33,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) {
|
||||
|
||||
@@ -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.
|
||||
##
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
@@ -662,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> {
|
||||
@@ -671,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> {
|
||||
@@ -703,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")
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -27,7 +27,7 @@ impl Drop for EventLoopGuard {
|
||||
}
|
||||
|
||||
// Helper function to safely use the current event loop
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
pub fn with_current_event_loop<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&ActiveEventLoop) -> R,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::Write,
|
||||
io::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -42,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);
|
||||
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,6 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
|
||||
log::debug!("Exiting with return code 0");
|
||||
|
||||
#[allow(clippy::exit)]
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
@@ -317,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)…");
|
||||
|
||||
@@ -363,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")]
|
||||
@@ -387,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),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -336,9 +336,6 @@ impl AppRunner {
|
||||
egui::OutputCommand::OpenUrl(open_url) => {
|
||||
super::open_url(&open_url.url, open_url.new_tab);
|
||||
}
|
||||
egui::OutputCommand::SetPointerPosition(_) => {
|
||||
// Not supported on the web.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -163,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 {
|
||||
@@ -261,7 +261,7 @@ 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;
|
||||
|
||||
@@ -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)?
|
||||
};
|
||||
|
||||
@@ -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('/') {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -856,9 +856,6 @@ impl State {
|
||||
egui::OutputCommand::OpenUrl(open_url) => {
|
||||
open_url_in_browser(&open_url.url);
|
||||
}
|
||||
egui::OutputCommand::SetPointerPosition(egui::Pos2 { x, y }) => {
|
||||
let _ = window.set_cursor_position(winit::dpi::LogicalPosition { x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1147,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;
|
||||
@@ -1623,6 +1622,7 @@ pub fn create_winit_window_attributes(
|
||||
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,
|
||||
@@ -1767,7 +1767,8 @@ pub fn create_winit_window_attributes(
|
||||
.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_movable_by_window_background(_movable_by_window_background.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
|
||||
@@ -1849,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",
|
||||
@@ -1868,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",
|
||||
@@ -1883,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",
|
||||
|
||||
@@ -87,6 +87,8 @@ ahash.workspace = true
|
||||
bitflags.workspace = true
|
||||
nohash-hasher.workspace = true
|
||||
profiling.workspace = true
|
||||
smallvec.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
accesskit = { workspace = true, optional = true }
|
||||
|
||||
@@ -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!
|
||||
|
||||
109
crates/egui/src/atomics/atom.rs
Normal file
109
crates/egui/src/atomics/atom.rs
Normal 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,
|
||||
grow: self.grow,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Atom<'a>
|
||||
where
|
||||
T: Into<AtomKind<'a>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Atom {
|
||||
kind: value.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
107
crates/egui/src/atomics/atom_ext.rs
Normal file
107
crates/egui/src/atomics/atom_ext.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::{Atom, FontSelection, Ui};
|
||||
use emath::Vec2;
|
||||
|
||||
/// A trait for conveniently building [`Atom`]s.
|
||||
///
|
||||
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
|
||||
pub trait AtomExt<'a> {
|
||||
/// Set the atom to a fixed size.
|
||||
///
|
||||
/// If [`Atom::grow`] is `true`, this will be the minimum width.
|
||||
/// If [`Atom::shrink`] is `true`, this will be the maximum width.
|
||||
/// If both are true, the width will have no effect.
|
||||
///
|
||||
/// [`Self::atom_max_size`] will limit size.
|
||||
///
|
||||
/// See [`crate::AtomKind`] docs to see how the size affects the different types.
|
||||
fn atom_size(self, size: Vec2) -> Atom<'a>;
|
||||
|
||||
/// Grow this atom to the available space.
|
||||
///
|
||||
/// This will affect the size of the [`Atom`] in the main direction. Since
|
||||
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
|
||||
///
|
||||
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
|
||||
/// remaining space.
|
||||
fn atom_grow(self, grow: bool) -> Atom<'a>;
|
||||
|
||||
/// Shrink this atom if there isn't enough space.
|
||||
///
|
||||
/// This will affect the size of the [`Atom`] in the main direction. Since
|
||||
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
|
||||
///
|
||||
/// NOTE: Only a single [`Atom`] may shrink for each widget.
|
||||
///
|
||||
/// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first
|
||||
/// `AtomKind::Text` is set to shrink.
|
||||
fn atom_shrink(self, shrink: bool) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum size of this atom.
|
||||
///
|
||||
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
|
||||
/// equally to fill the available space).
|
||||
fn atom_max_size(self, max_size: Vec2) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum width of this atom.
|
||||
///
|
||||
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
|
||||
/// equally to fill the available space).
|
||||
fn atom_max_width(self, max_width: f32) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum height of this atom.
|
||||
fn atom_max_height(self, max_height: f32) -> Atom<'a>;
|
||||
|
||||
/// Set the max height of this atom to match the font size.
|
||||
///
|
||||
/// This is useful for e.g. limiting the height of icons in buttons.
|
||||
fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let font_selection = FontSelection::default();
|
||||
let font_id = font_selection.resolve(ui.style());
|
||||
let height = ui.fonts(|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
|
||||
}
|
||||
}
|
||||
120
crates/egui/src/atomics/atom_kind.rs
Normal file
120
crates/egui/src/atomics/atom_kind.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
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 galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button);
|
||||
(
|
||||
galley.size(), // TODO(#5762): calculate the preferred 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())
|
||||
}
|
||||
}
|
||||
493
crates/egui/src/atomics/atom_layout.rs
Normal file
493
crates/egui/src/atomics/atom_layout.rs
Normal 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
|
||||
}
|
||||
}
|
||||
259
crates/egui/src/atomics/atoms.rs
Normal file
259
crates/egui/src/atomics/atoms.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use crate::{Atom, AtomKind, Image, WidgetText};
|
||||
use smallvec::SmallVec;
|
||||
use std::borrow::Cow;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
// Rarely there should be more than 2 atoms in one Widget.
|
||||
// I guess it could happen in a menu button with Image and right text...
|
||||
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
|
||||
|
||||
/// A list of [`Atom`]s.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
|
||||
|
||||
impl<'a> Atoms<'a> {
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
atoms.into_atoms()
|
||||
}
|
||||
|
||||
/// Insert a new [`Atom`] at the end of the list (right side).
|
||||
pub fn push_right(&mut self, atom: impl Into<Atom<'a>>) {
|
||||
self.0.push(atom.into());
|
||||
}
|
||||
|
||||
/// Insert a new [`Atom`] at the beginning of the list (left side).
|
||||
pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
|
||||
self.0.insert(0, atom.into());
|
||||
}
|
||||
|
||||
/// Concatenate and return the text contents.
|
||||
// TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g.
|
||||
// in a submenu button there is a right text '⏵' which is now passed to the screen reader.
|
||||
pub fn text(&self) -> Option<Cow<'_, str>> {
|
||||
let mut string: Option<Cow<'_, str>> = None;
|
||||
for atom in &self.0 {
|
||||
if let AtomKind::Text(text) = &atom.kind {
|
||||
if let Some(string) = &mut string {
|
||||
let string = string.to_mut();
|
||||
string.push(' ');
|
||||
string.push_str(text.text());
|
||||
} else {
|
||||
string = Some(Cow::Borrowed(text.text()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no text, try to find an image with alt text.
|
||||
if string.is_none() {
|
||||
string = self.iter().find_map(|a| match &a.kind {
|
||||
AtomKind::Image(image) => image.alt_text.as_deref().map(Cow::Borrowed),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
|
||||
string
|
||||
}
|
||||
|
||||
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
|
||||
self.0.iter().map(|atom| &atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut AtomKind<'a>> {
|
||||
self.0.iter_mut().map(|atom| &mut atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'a>> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'a>> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts(&self) -> impl Iterator<Item = &WidgetText> + use<'_, 'a> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut WidgetText> + use<'a, '_> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
|
||||
self.iter_mut()
|
||||
.for_each(|atom| *atom = f(std::mem::take(atom)));
|
||||
}
|
||||
|
||||
pub fn map_kind<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
|
||||
{
|
||||
for kind in self.iter_kinds_mut() {
|
||||
*kind = f(std::mem::take(kind));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_images<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(Image<'a>) -> Image<'a>,
|
||||
{
|
||||
self.map_kind(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
AtomKind::Image(f(image))
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn map_texts<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(WidgetText) -> WidgetText,
|
||||
{
|
||||
self.map_kind(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
AtomKind::Text(f(text))
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Atoms<'a> {
|
||||
type Item = Atom<'a>;
|
||||
type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait to convert a tuple of atoms into [`Atoms`].
|
||||
///
|
||||
/// ```
|
||||
/// use egui::{Atoms, Image, IntoAtoms, RichText};
|
||||
/// let atoms: Atoms = (
|
||||
/// "Some text",
|
||||
/// RichText::new("Some RichText"),
|
||||
/// Image::new("some_image_url"),
|
||||
/// ).into_atoms();
|
||||
/// ```
|
||||
impl<'a, T> IntoAtoms<'a> for T
|
||||
where
|
||||
T: Into<Atom<'a>>,
|
||||
{
|
||||
fn collect(self, atoms: &mut Atoms<'a>) {
|
||||
atoms.push_right(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
|
||||
pub trait IntoAtoms<'a> {
|
||||
fn collect(self, atoms: &mut Atoms<'a>);
|
||||
|
||||
fn into_atoms(self) -> Atoms<'a>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut atoms = Atoms::default();
|
||||
self.collect(&mut atoms);
|
||||
atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAtoms<'a> for Atoms<'a> {
|
||||
fn collect(self, atoms: &mut Self) {
|
||||
atoms.0.extend(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! all_the_atoms {
|
||||
($($T:ident),*) => {
|
||||
impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*)
|
||||
where
|
||||
$($T: IntoAtoms<'a>),*
|
||||
{
|
||||
fn collect(self, _atoms: &mut Atoms<'a>) {
|
||||
#[allow(clippy::allow_attributes)]
|
||||
#[allow(non_snake_case)]
|
||||
let ($($T),*) = self;
|
||||
$($T.collect(_atoms);)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
all_the_atoms!();
|
||||
all_the_atoms!(T0, T1);
|
||||
all_the_atoms!(T0, T1, T2);
|
||||
all_the_atoms!(T0, T1, T2, T3);
|
||||
all_the_atoms!(T0, T1, T2, T3, T4);
|
||||
all_the_atoms!(T0, T1, T2, T3, T4, T5);
|
||||
|
||||
impl<'a> Deref for Atoms<'a> {
|
||||
type Target = [Atom<'a>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Atoms<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Into<Atom<'a>>> From<Vec<T>> for Atoms<'a> {
|
||||
fn from(vec: Vec<T>) -> Self {
|
||||
Atoms(vec.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Into<Atom<'a>> + Clone> From<&[T]> for Atoms<'a> {
|
||||
fn from(slice: &[T]) -> Self {
|
||||
Atoms(slice.iter().cloned().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Item: Into<Atom<'a>>> FromIterator<Item> for Atoms<'a> {
|
||||
fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
|
||||
Atoms(iter.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Atoms;
|
||||
|
||||
#[test]
|
||||
fn collect_atoms() {
|
||||
let _: Atoms<'_> = ["Hello", "World"].into_iter().collect();
|
||||
let _ = Atoms::from(vec!["Hi"]);
|
||||
let _ = Atoms::from(["Hi"].as_slice());
|
||||
}
|
||||
}
|
||||
15
crates/egui/src/atomics/mod.rs
Normal file
15
crates/egui/src/atomics/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
mod atom;
|
||||
mod atom_ext;
|
||||
mod atom_kind;
|
||||
mod atom_layout;
|
||||
mod atoms;
|
||||
mod sized_atom;
|
||||
mod sized_atom_kind;
|
||||
|
||||
pub use atom::*;
|
||||
pub use atom_ext::*;
|
||||
pub use atom_kind::*;
|
||||
pub use atom_layout::*;
|
||||
pub use atoms::*;
|
||||
pub use sized_atom::*;
|
||||
pub use sized_atom_kind::*;
|
||||
26
crates/egui/src/atomics/sized_atom.rs
Normal file
26
crates/egui/src/atomics/sized_atom.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use crate::SizedAtomKind;
|
||||
use emath::Vec2;
|
||||
|
||||
/// A [`crate::Atom`] which has been sized.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SizedAtom<'a> {
|
||||
pub(crate) grow: bool,
|
||||
|
||||
/// The size of the atom.
|
||||
///
|
||||
/// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by
|
||||
/// size.x + gap.
|
||||
pub size: Vec2,
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
25
crates/egui/src/atomics/sized_atom_kind.rs
Normal file
25
crates/egui/src/atomics/sized_atom_kind.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::{Id, Image};
|
||||
use emath::Vec2;
|
||||
use epaint::Galley;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A sized [`crate::AtomKind`].
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub enum SizedAtomKind<'a> {
|
||||
#[default]
|
||||
Empty,
|
||||
Text(Arc<Galley>),
|
||||
Image(Image<'a>, Vec2),
|
||||
Custom(Id),
|
||||
}
|
||||
|
||||
impl SizedAtomKind<'_> {
|
||||
/// Get the calculated size.
|
||||
pub fn size(&self) -> Vec2 {
|
||||
match self {
|
||||
SizedAtomKind::Text(galley) => galley.size(),
|
||||
SizedAtomKind::Image(_, size) => *size,
|
||||
SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
2
crates/egui/src/cache/cache_trait.rs
vendored
2
crates/egui/src/cache/cache_trait.rs
vendored
@@ -1,5 +1,5 @@
|
||||
/// A cache, storing some value for some length of time.
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
#[expect(clippy::len_without_is_empty)]
|
||||
pub trait CacheTrait: 'static + Send + Sync {
|
||||
/// Call once per frame to evict cache.
|
||||
fn update(&mut self);
|
||||
|
||||
@@ -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: _,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,6 +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`].
|
||||
@@ -242,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.
|
||||
@@ -292,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`].
|
||||
|
||||
@@ -29,7 +29,7 @@ pub use {
|
||||
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
||||
popup::*,
|
||||
resize::Resize,
|
||||
scene::Scene,
|
||||
scene::{DragPanButtons, Scene},
|
||||
scroll_area::ScrollArea,
|
||||
sides::Sides,
|
||||
tooltip::*,
|
||||
|
||||
@@ -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::always_open(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::always_open(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::always_open(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:");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -466,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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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.
|
||||
@@ -149,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();
|
||||
@@ -179,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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -7,26 +7,49 @@ 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.
|
||||
#[deprecated = "Use `Tooltip::always_open` instead."]
|
||||
pub fn new(
|
||||
widget_id: Id,
|
||||
parent_widget: Id,
|
||||
ctx: Context,
|
||||
anchor: impl Into<PopupAnchor>,
|
||||
layer_id: LayerId,
|
||||
parent_layer: LayerId,
|
||||
) -> Self {
|
||||
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)
|
||||
.sense(Sense::hover()),
|
||||
layer_id,
|
||||
widget_id,
|
||||
parent_layer,
|
||||
parent_widget,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a tooltip that is always open.
|
||||
pub fn always_open(
|
||||
ctx: Context,
|
||||
parent_layer: LayerId,
|
||||
parent_widget: Id,
|
||||
anchor: impl Into<PopupAnchor>,
|
||||
) -> Self {
|
||||
let width = ctx.style().spacing.tooltip_width;
|
||||
Self {
|
||||
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
|
||||
.kind(PopupKind::Tooltip)
|
||||
.gap(4.0)
|
||||
.width(width)
|
||||
.sense(Sense::hover()),
|
||||
parent_layer,
|
||||
parent_widget,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +62,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 +119,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 +134,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 +146,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 +167,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")]
|
||||
@@ -314,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();
|
||||
@@ -327,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();
|
||||
@@ -493,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;
|
||||
|
||||
@@ -556,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.
|
||||
@@ -1160,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()
|
||||
@@ -1189,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")]
|
||||
@@ -1233,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;
|
||||
@@ -1434,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)
|
||||
}
|
||||
@@ -1494,9 +1488,37 @@ impl Context {
|
||||
self.send_cmd(crate::OutputCommand::CopyImage(image));
|
||||
}
|
||||
|
||||
/// Set the mouse cursor position (if the platform supports it).
|
||||
pub fn set_pointer_position(&self, position: Pos2) {
|
||||
self.send_cmd(crate::OutputCommand::SetPointerPosition(position));
|
||||
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`).
|
||||
@@ -1505,29 +1527,9 @@ impl Context {
|
||||
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)
|
||||
@@ -2322,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);
|
||||
@@ -2355,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")]
|
||||
@@ -2400,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();
|
||||
@@ -2656,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| {
|
||||
@@ -2746,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.
|
||||
@@ -3070,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| {
|
||||
@@ -3114,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)
|
||||
@@ -3128,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));
|
||||
@@ -3152,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
|
||||
@@ -3210,7 +3271,7 @@ impl Context {
|
||||
}
|
||||
});
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!(
|
||||
"{} menu bars",
|
||||
@@ -3268,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
|
||||
@@ -3452,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3495,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,
|
||||
}
|
||||
@@ -3541,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,
|
||||
}
|
||||
}
|
||||
@@ -3553,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
|
||||
@@ -3601,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,
|
||||
) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -95,9 +95,6 @@ pub enum OutputCommand {
|
||||
|
||||
/// Open this url in a browser.
|
||||
OpenUrl(OpenUrl),
|
||||
|
||||
/// Set the mouse cursor position (if the platform supports it).
|
||||
SetPointerPosition(emath::Pos2),
|
||||
}
|
||||
|
||||
/// The non-rendering part of what egui emits each frame.
|
||||
@@ -255,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(),
|
||||
@@ -263,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(),
|
||||
@@ -610,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,
|
||||
@@ -620,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,
|
||||
@@ -638,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 {
|
||||
@@ -649,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,
|
||||
@@ -673,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>,
|
||||
@@ -742,7 +739,7 @@ impl WidgetInfo {
|
||||
if text_value.is_empty() {
|
||||
"blank".into()
|
||||
} else {
|
||||
text_value.to_string()
|
||||
text_value.clone()
|
||||
}
|
||||
} else {
|
||||
"blank".into()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -938,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 {
|
||||
@@ -976,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 {
|
||||
@@ -1283,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;
|
||||
}
|
||||
}
|
||||
@@ -1319,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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1379,7 +1479,7 @@ impl InputState {
|
||||
modifiers,
|
||||
keys_down,
|
||||
events,
|
||||
input_options: _,
|
||||
options: _,
|
||||
} = self;
|
||||
|
||||
ui.style_mut()
|
||||
@@ -1465,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:?}"));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Showing UI:s for egui/epaint types.
|
||||
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,
|
||||
};
|
||||
|
||||
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl DefaultBytesLoader {
|
||||
}
|
||||
|
||||
impl BytesLoader for DefaultBytesLoader {
|
||||
fn id(&self) -> &str {
|
||||
fn id(&self) -> &'static str {
|
||||
generate_loader_id!(DefaultBytesLoader)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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<OpenPopup>,
|
||||
|
||||
#[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();
|
||||
|
||||
@@ -809,11 +773,11 @@ impl Memory {
|
||||
self.focus_mut().end_pass(used_ids);
|
||||
|
||||
// Clean up abandoned popups.
|
||||
if let Some(popup) = &mut self.popup {
|
||||
if let Some(popup) = self.popups.get_mut(&self.viewport_id) {
|
||||
if popup.open_this_frame {
|
||||
popup.open_this_frame = false;
|
||||
} else {
|
||||
self.popup = None;
|
||||
self.popups.remove(&self.viewport_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -992,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) {
|
||||
@@ -1107,19 +1018,23 @@ impl OpenPopup {
|
||||
impl Memory {
|
||||
/// Is the given popup open?
|
||||
pub fn is_popup_open(&self, popup_id: Id) -> bool {
|
||||
self.popup.is_some_and(|state| state.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(OpenPopup::new(popup_id, None));
|
||||
self.popups
|
||||
.insert(self.viewport_id, OpenPopup::new(popup_id, None));
|
||||
}
|
||||
|
||||
/// Popups must call this every frame while open.
|
||||
@@ -1128,7 +1043,7 @@ impl Memory {
|
||||
/// 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.popup.as_mut() {
|
||||
if let Some(state) = self.popups.get_mut(&self.viewport_id) {
|
||||
if state.id == popup_id {
|
||||
state.open_this_frame = true;
|
||||
}
|
||||
@@ -1137,18 +1052,20 @@ impl Memory {
|
||||
|
||||
/// 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(OpenPopup::new(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
|
||||
self.popups
|
||||
.get(&self.viewport_id)
|
||||
.and_then(|state| if state.id == id { state.pos } else { None })
|
||||
}
|
||||
|
||||
/// Close any currently open popup.
|
||||
pub fn close_all_popups(&mut self) {
|
||||
self.popup = None;
|
||||
self.popups.clear();
|
||||
}
|
||||
|
||||
/// Close the given popup, if it is open.
|
||||
@@ -1156,7 +1073,7 @@ impl Memory {
|
||||
/// 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.popup = None;
|
||||
self.popups.remove(&self.viewport_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1318,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);
|
||||
}
|
||||
}
|
||||
@@ -1339,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);
|
||||
}
|
||||
}
|
||||
@@ -1351,7 +1268,7 @@ impl Areas {
|
||||
self.order
|
||||
.iter()
|
||||
.filter(|layer| layer.order == order && !self.is_sublayer(layer))
|
||||
.last()
|
||||
.next_back()
|
||||
.copied()
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -88,6 +88,7 @@ fn set_menu_style(style: &mut Style) {
|
||||
/// 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());
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -218,6 +218,14 @@ 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,
|
||||
@@ -387,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 {
|
||||
|
||||
@@ -747,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)]
|
||||
@@ -1259,7 +1260,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,
|
||||
@@ -1561,7 +1562,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,
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn update_accesskit_for_text_widget(
|
||||
let row_id = parent_id.with(row_index);
|
||||
ctx.accesskit_node_builder(row_id, |builder| {
|
||||
builder.set_role(accesskit::Role::TextRun);
|
||||
let rect = global_from_galley * row.rect();
|
||||
let rect = global_from_galley * row.rect_without_leading_space();
|
||||
builder.set_bounds(accesskit::Rect {
|
||||
x0: rect.min.x.into(),
|
||||
y0: rect.min.y.into(),
|
||||
|
||||
@@ -530,7 +530,7 @@ impl LabelSelectionState {
|
||||
|
||||
let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley);
|
||||
|
||||
let old_range = cursor_state.char_range();
|
||||
let old_range = cursor_state.range(galley);
|
||||
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
if response.contains_pointer() {
|
||||
@@ -544,7 +544,7 @@ impl LabelSelectionState {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut cursor_range) = cursor_state.char_range() {
|
||||
if let Some(mut cursor_range) = cursor_state.range(galley) {
|
||||
let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
|
||||
self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
|
||||
|
||||
@@ -562,7 +562,7 @@ impl LabelSelectionState {
|
||||
}
|
||||
|
||||
// Look for changes due to keyboard and/or mouse interaction:
|
||||
let new_range = cursor_state.char_range();
|
||||
let new_range = cursor_state.range(galley);
|
||||
let selection_changed = old_range != new_range;
|
||||
|
||||
if let (true, Some(range)) = (selection_changed, new_range) {
|
||||
@@ -616,7 +616,7 @@ impl LabelSelectionState {
|
||||
let old_primary = old_selection.map(|s| s.primary);
|
||||
let new_primary = self.selection.as_ref().map(|s| s.primary);
|
||||
if let Some(new_primary) = new_primary {
|
||||
let primary_changed = old_primary.map_or(true, |old| {
|
||||
let primary_changed = old_primary.is_none_or(|old| {
|
||||
old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor
|
||||
});
|
||||
if primary_changed && new_primary.widget_id == widget_id {
|
||||
@@ -632,7 +632,7 @@ impl LabelSelectionState {
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_range = cursor_state.char_range();
|
||||
let cursor_range = cursor_state.range(galley);
|
||||
|
||||
let mut new_vertex_indices = vec![];
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Text cursor changes/interaction, without modifying the text.
|
||||
|
||||
use epaint::text::{cursor::CCursor, Galley};
|
||||
use unicode_segmentation::UnicodeSegmentation as _;
|
||||
|
||||
use crate::{epaint, NumExt, Rect, Response, Ui};
|
||||
use crate::{epaint, NumExt as _, Rect, Response, Ui};
|
||||
|
||||
use super::CCursorRange;
|
||||
|
||||
@@ -34,6 +35,16 @@ impl TextCursorState {
|
||||
self.ccursor_range
|
||||
}
|
||||
|
||||
/// The currently selected range of characters, clamped within the character
|
||||
/// range of the given [`Galley`].
|
||||
pub fn range(&self, galley: &Galley) -> Option<CCursorRange> {
|
||||
self.ccursor_range.map(|mut range| {
|
||||
range.primary = galley.clamp_cursor(&range.primary);
|
||||
range.secondary = galley.clamp_cursor(&range.secondary);
|
||||
range
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the currently selected range of characters.
|
||||
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
|
||||
self.ccursor_range = ccursor_range;
|
||||
@@ -68,7 +79,7 @@ impl TextCursorState {
|
||||
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
|
||||
// The start of a drag (or a click).
|
||||
if ui.input(|i| i.modifiers.shift) {
|
||||
if let Some(mut cursor_range) = self.char_range() {
|
||||
if let Some(mut cursor_range) = self.range(galley) {
|
||||
cursor_range.primary = cursor_at_pointer;
|
||||
self.set_char_range(Some(cursor_range));
|
||||
} else {
|
||||
@@ -80,7 +91,7 @@ impl TextCursorState {
|
||||
true
|
||||
} else if is_being_dragged {
|
||||
// Drag to select text:
|
||||
if let Some(mut cursor_range) = self.char_range() {
|
||||
if let Some(mut cursor_range) = self.range(galley) {
|
||||
cursor_range.primary = cursor_at_pointer;
|
||||
self.set_char_range(Some(cursor_range));
|
||||
}
|
||||
@@ -166,7 +177,7 @@ fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
|
||||
|
||||
pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||
CCursor {
|
||||
index: next_word_boundary_char_index(text.chars(), ccursor.index),
|
||||
index: next_word_boundary_char_index(text, ccursor.index),
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
@@ -180,9 +191,10 @@ fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
|
||||
|
||||
pub fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||
let num_chars = text.chars().count();
|
||||
let reversed: String = text.graphemes(true).rev().collect();
|
||||
CCursor {
|
||||
index: num_chars
|
||||
- next_word_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
|
||||
- next_word_boundary_char_index(&reversed, num_chars - ccursor.index).min(num_chars),
|
||||
prefer_next_row: true,
|
||||
}
|
||||
}
|
||||
@@ -196,22 +208,25 @@ fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
|
||||
}
|
||||
}
|
||||
|
||||
fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
||||
let mut it = it.skip(index);
|
||||
if let Some(_first) = it.next() {
|
||||
index += 1;
|
||||
|
||||
if let Some(second) = it.next() {
|
||||
index += 1;
|
||||
for next in it {
|
||||
if is_word_char(next) != is_word_char(second) {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
fn next_word_boundary_char_index(text: &str, index: usize) -> usize {
|
||||
for word in text.split_word_bound_indices() {
|
||||
// Splitting considers contiguous whitespace as one word, such words must be skipped,
|
||||
// this handles cases for example ' abc' (a space and a word), the cursor is at the beginning
|
||||
// (before space) - this jumps at the end of 'abc' (this is consistent with text editors
|
||||
// or browsers)
|
||||
let ci = char_index_from_byte_index(text, word.0);
|
||||
if ci > index && !skip_word(word.1) {
|
||||
return ci;
|
||||
}
|
||||
}
|
||||
index
|
||||
|
||||
char_index_from_byte_index(text, text.len())
|
||||
}
|
||||
|
||||
fn skip_word(text: &str) -> bool {
|
||||
// skip words that contain anything other than alphanumeric characters and underscore
|
||||
// (i.e. whitespace, dashes, etc.)
|
||||
!text.chars().any(|c| !is_word_char(c))
|
||||
}
|
||||
|
||||
fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
||||
@@ -233,7 +248,7 @@ fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usiz
|
||||
}
|
||||
|
||||
pub fn is_word_char(c: char) -> bool {
|
||||
c.is_ascii_alphanumeric() || c == '_'
|
||||
c.is_alphanumeric() || c == '_'
|
||||
}
|
||||
|
||||
fn is_linebreak(c: char) -> bool {
|
||||
@@ -270,6 +285,16 @@ pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
|
||||
s.len()
|
||||
}
|
||||
|
||||
pub fn char_index_from_byte_index(input: &str, byte_index: usize) -> usize {
|
||||
for (ci, (bi, _)) in input.char_indices().enumerate() {
|
||||
if bi == byte_index {
|
||||
return ci;
|
||||
}
|
||||
}
|
||||
|
||||
input.char_indices().last().map_or(0, |(i, _)| i + 1)
|
||||
}
|
||||
|
||||
pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
|
||||
assert!(
|
||||
char_range.start <= char_range.end,
|
||||
@@ -293,3 +318,38 @@ pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
|
||||
|
||||
cursor_pos
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::text_selection::text_cursor_state::next_word_boundary_char_index;
|
||||
|
||||
#[test]
|
||||
fn test_next_word_boundary_char_index() {
|
||||
// ASCII only
|
||||
let text = "abc d3f g_h i-j";
|
||||
assert_eq!(next_word_boundary_char_index(text, 1), 3);
|
||||
assert_eq!(next_word_boundary_char_index(text, 3), 7);
|
||||
assert_eq!(next_word_boundary_char_index(text, 9), 11);
|
||||
assert_eq!(next_word_boundary_char_index(text, 12), 13);
|
||||
assert_eq!(next_word_boundary_char_index(text, 13), 15);
|
||||
assert_eq!(next_word_boundary_char_index(text, 15), 15);
|
||||
|
||||
assert_eq!(next_word_boundary_char_index("", 0), 0);
|
||||
assert_eq!(next_word_boundary_char_index("", 1), 0);
|
||||
|
||||
// Unicode graphemes, some of which consist of multiple Unicode characters,
|
||||
// !!! Unicode character is not always what is tranditionally considered a character,
|
||||
// the values below are correct despite not seeming that way on the first look,
|
||||
// handling of and around emojis is kind of weird and is not consistent across
|
||||
// text editors and browsers
|
||||
let text = "❤️👍 skvělá knihovna 👍❤️";
|
||||
assert_eq!(next_word_boundary_char_index(text, 0), 2);
|
||||
assert_eq!(next_word_boundary_char_index(text, 2), 3); // this does not skip the space between thumbs-up and 'skvělá'
|
||||
assert_eq!(next_word_boundary_char_index(text, 6), 10);
|
||||
assert_eq!(next_word_boundary_char_index(text, 9), 10);
|
||||
assert_eq!(next_word_boundary_char_index(text, 12), 19);
|
||||
assert_eq!(next_word_boundary_char_index(text, 15), 19);
|
||||
assert_eq!(next_word_boundary_char_index(text, 19), 20);
|
||||
assert_eq!(next_word_boundary_char_index(text, 20), 21);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ use crate::{
|
||||
color_picker, Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link,
|
||||
RadioButton, SelectableLabel, Separator, Spinner, TextEdit, Widget,
|
||||
},
|
||||
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, LayerId,
|
||||
Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense,
|
||||
Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, WidgetRect,
|
||||
WidgetText,
|
||||
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtoms,
|
||||
LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText,
|
||||
Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2,
|
||||
WidgetRect, WidgetText,
|
||||
};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -98,7 +98,7 @@ pub struct Ui {
|
||||
sizing_pass: bool,
|
||||
|
||||
/// Indicates whether this Ui belongs to a Menu.
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
|
||||
|
||||
/// The [`UiStack`] for this [`Ui`].
|
||||
@@ -666,7 +666,7 @@ impl Ui {
|
||||
///
|
||||
/// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`].
|
||||
pub fn wrap_mode(&self) -> TextWrapMode {
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
if let Some(wrap_mode) = self.style.wrap_mode {
|
||||
wrap_mode
|
||||
}
|
||||
@@ -2055,8 +2055,8 @@ impl Ui {
|
||||
/// ```
|
||||
#[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "]
|
||||
#[inline]
|
||||
pub fn button(&mut self, text: impl Into<WidgetText>) -> Response {
|
||||
Button::new(text).ui(self)
|
||||
pub fn button<'a>(&mut self, atoms: impl IntoAtoms<'a>) -> Response {
|
||||
Button::new(atoms).ui(self)
|
||||
}
|
||||
|
||||
/// A button as small as normal body text.
|
||||
@@ -2073,8 +2073,8 @@ impl Ui {
|
||||
///
|
||||
/// See also [`Self::toggle_value`].
|
||||
#[inline]
|
||||
pub fn checkbox(&mut self, checked: &mut bool, text: impl Into<WidgetText>) -> Response {
|
||||
Checkbox::new(checked, text).ui(self)
|
||||
pub fn checkbox<'a>(&mut self, checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Response {
|
||||
Checkbox::new(checked, atoms).ui(self)
|
||||
}
|
||||
|
||||
/// Acts like a checkbox, but looks like a [`SelectableLabel`].
|
||||
@@ -2095,8 +2095,8 @@ impl Ui {
|
||||
/// Often you want to use [`Self::radio_value`] instead.
|
||||
#[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "]
|
||||
#[inline]
|
||||
pub fn radio(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
|
||||
RadioButton::new(selected, text).ui(self)
|
||||
pub fn radio<'a>(&mut self, selected: bool, atoms: impl IntoAtoms<'a>) -> Response {
|
||||
RadioButton::new(selected, atoms).ui(self)
|
||||
}
|
||||
|
||||
/// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`.
|
||||
@@ -2118,13 +2118,13 @@ impl Ui {
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn radio_value<Value: PartialEq>(
|
||||
pub fn radio_value<'a, Value: PartialEq>(
|
||||
&mut self,
|
||||
current_value: &mut Value,
|
||||
alternative: Value,
|
||||
text: impl Into<WidgetText>,
|
||||
atoms: impl IntoAtoms<'a>,
|
||||
) -> Response {
|
||||
let mut response = self.radio(*current_value == alternative, text);
|
||||
let mut response = self.radio(*current_value == alternative, atoms);
|
||||
if response.clicked() && *current_value != alternative {
|
||||
*current_value = alternative;
|
||||
response.mark_changed();
|
||||
@@ -3015,7 +3015,7 @@ impl Ui {
|
||||
self.close_kind(UiKind::Menu);
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
pub(crate) fn set_menu_state(
|
||||
&mut self,
|
||||
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
|
||||
@@ -3041,15 +3041,15 @@ impl Ui {
|
||||
/// ```
|
||||
///
|
||||
/// See also: [`Self::close`] and [`Response::context_menu`].
|
||||
pub fn menu_button<R>(
|
||||
pub fn menu_button<'a, R>(
|
||||
&mut self,
|
||||
title: impl Into<WidgetText>,
|
||||
atoms: impl IntoAtoms<'a>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let (response, inner) = if menu::is_in_menu(self) {
|
||||
menu::SubMenuButton::new(title).ui(self, add_contents)
|
||||
menu::SubMenuButton::new(atoms).ui(self, add_contents)
|
||||
} else {
|
||||
menu::MenuButton::new(title).ui(self, add_contents)
|
||||
menu::MenuButton::new(atoms).ui(self, add_contents)
|
||||
};
|
||||
InnerResponse::new(inner.map(|i| i.inner), response)
|
||||
}
|
||||
@@ -3156,7 +3156,7 @@ impl Drop for Ui {
|
||||
/// Show this rectangle to the user if certain debug options are set.
|
||||
#[cfg(debug_assertions)]
|
||||
fn register_rect(ui: &Ui, rect: Rect) {
|
||||
use emath::{Align2, GuiRounding};
|
||||
use emath::{Align2, GuiRounding as _};
|
||||
|
||||
let debug = ui.style().debug;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{hash::Hash, sync::Arc};
|
||||
|
||||
use crate::close_tag::ClosableTag;
|
||||
#[allow(unused_imports)] // Used for doclinks
|
||||
#[expect(unused_imports)] // Used for doclinks
|
||||
use crate::Ui;
|
||||
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo};
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ impl UiStack {
|
||||
// these methods act on the entire stack
|
||||
impl UiStack {
|
||||
/// Return an iterator that walks the stack from this node to the root.
|
||||
#[allow(clippy::iter_without_into_iter)]
|
||||
#[expect(clippy::iter_without_into_iter)]
|
||||
pub fn iter(&self) -> UiStackIterator<'_> {
|
||||
UiStackIterator { next: Some(self) }
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ impl IdTypeMap {
|
||||
|
||||
/// For tests
|
||||
#[cfg(feature = "persistence")]
|
||||
#[allow(unused)]
|
||||
#[allow(unused, clippy::allow_attributes)]
|
||||
fn get_generation<T: SerializableAny>(&self, id: Id) -> Option<usize> {
|
||||
let element = self.map.get(&hash(TypeId::of::<T>(), id))?;
|
||||
match element {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
//! a [`ViewportCommand`] to it using [`Context::send_viewport_cmd`].
|
||||
//! You can interact with other viewports using [`Context::send_viewport_cmd_to`].
|
||||
//!
|
||||
//! There is an example in <https://github.com/emilk/egui/tree/master/examples/multiple_viewports/src/main.rs>.
|
||||
//! There is an example in <https://github.com/emilk/egui/tree/main/examples/multiple_viewports/src/main.rs>.
|
||||
//!
|
||||
//! You can find all available viewports in [`crate::RawInput::viewports`] and the active viewport in
|
||||
//! [`crate::InputState::viewport`]:
|
||||
@@ -260,7 +260,6 @@ pub type ImmediateViewportRendererCallback = dyn for<'a> Fn(&Context, ImmediateV
|
||||
/// The default values are implementation defined, so you may want to explicitly
|
||||
/// configure the size of the window, and what buttons are shown.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
#[allow(clippy::option_option)]
|
||||
pub struct ViewportBuilder {
|
||||
/// The title of the viewport.
|
||||
/// `eframe` will use this as the title of the native window.
|
||||
@@ -295,6 +294,7 @@ pub struct ViewportBuilder {
|
||||
pub title_shown: Option<bool>,
|
||||
pub titlebar_buttons_shown: Option<bool>,
|
||||
pub titlebar_shown: Option<bool>,
|
||||
pub has_shadow: Option<bool>,
|
||||
|
||||
// windows:
|
||||
pub drag_and_drop: Option<bool>,
|
||||
@@ -381,6 +381,10 @@ impl ViewportBuilder {
|
||||
/// The default is `false`.
|
||||
/// If this is not working, it's because the graphic context doesn't support transparency,
|
||||
/// you will need to set the transparency in the eframe!
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// **macOS:** When using this feature to create an overlay-like UI, you likely want to combine this with [`Self::with_has_shadow`] set to `false` in order to avoid ghosting artifacts.
|
||||
#[inline]
|
||||
pub fn with_transparent(mut self, transparent: bool) -> Self {
|
||||
self.transparent = Some(transparent);
|
||||
@@ -434,7 +438,6 @@ impl ViewportBuilder {
|
||||
}
|
||||
|
||||
/// macOS: Set to `true` to allow the window to be moved by dragging the background.
|
||||
///
|
||||
/// Enabling this feature can result in unexpected behaviour with draggable UI widgets such as sliders.
|
||||
#[inline]
|
||||
pub fn with_movable_by_background(mut self, value: bool) -> Self {
|
||||
@@ -463,6 +466,19 @@ impl ViewportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// macOS: Set to `false` to make the window render without a drop shadow.
|
||||
///
|
||||
/// The default is `true`.
|
||||
///
|
||||
/// Disabling this feature can solve ghosting issues experienced if using [`Self::with_transparent`].
|
||||
///
|
||||
/// Look at winit for more details
|
||||
#[inline]
|
||||
pub fn with_has_shadow(mut self, has_shadow: bool) -> Self {
|
||||
self.has_shadow = Some(has_shadow);
|
||||
self
|
||||
}
|
||||
|
||||
/// windows: Whether show or hide the window icon in the taskbar.
|
||||
#[inline]
|
||||
pub fn with_taskbar(mut self, show: bool) -> Self {
|
||||
@@ -654,6 +670,7 @@ impl ViewportBuilder {
|
||||
title_shown: new_title_shown,
|
||||
titlebar_buttons_shown: new_titlebar_buttons_shown,
|
||||
titlebar_shown: new_titlebar_shown,
|
||||
has_shadow: new_has_shadow,
|
||||
close_button: new_close_button,
|
||||
minimize_button: new_minimize_button,
|
||||
maximize_button: new_maximize_button,
|
||||
@@ -824,6 +841,11 @@ impl ViewportBuilder {
|
||||
recreate_window = true;
|
||||
}
|
||||
|
||||
if new_has_shadow.is_some() && self.has_shadow != new_has_shadow {
|
||||
self.has_shadow = new_has_shadow;
|
||||
recreate_window = true;
|
||||
}
|
||||
|
||||
if new_taskbar.is_some() && self.taskbar != new_taskbar {
|
||||
self.taskbar = new_taskbar;
|
||||
recreate_window = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::text::TextFormat;
|
||||
use std::fmt::Formatter;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
text::{LayoutJob, TextWrapping},
|
||||
@@ -488,7 +489,16 @@ impl RichText {
|
||||
/// which will be replaced with a color chosen by the widget that paints the text.
|
||||
#[derive(Clone)]
|
||||
pub enum WidgetText {
|
||||
RichText(RichText),
|
||||
/// Plain unstyled text.
|
||||
///
|
||||
/// We have this as a special case, as it is the common-case,
|
||||
/// and it uses less memory than [`Self::RichText`].
|
||||
Text(String),
|
||||
|
||||
/// Text and optional style choices for it.
|
||||
///
|
||||
/// Prefer [`Self::Text`] if there is no styling, as it will be faster.
|
||||
RichText(Arc<RichText>),
|
||||
|
||||
/// Use this [`LayoutJob`] when laying out the text.
|
||||
///
|
||||
@@ -502,7 +512,7 @@ pub enum WidgetText {
|
||||
///
|
||||
/// You can color the text however you want, or use [`Color32::PLACEHOLDER`]
|
||||
/// which will be replaced with a color chosen by the widget that paints the text.
|
||||
LayoutJob(LayoutJob),
|
||||
LayoutJob(Arc<LayoutJob>),
|
||||
|
||||
/// Use exactly this galley when painting the text.
|
||||
///
|
||||
@@ -511,9 +521,21 @@ pub enum WidgetText {
|
||||
Galley(Arc<Galley>),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WidgetText {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let text = self.text();
|
||||
match self {
|
||||
Self::Text(_) => write!(f, "Text({text:?})"),
|
||||
Self::RichText(_) => write!(f, "RichText({text:?})"),
|
||||
Self::LayoutJob(_) => write!(f, "LayoutJob({text:?})"),
|
||||
Self::Galley(_) => write!(f, "Galley({text:?})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WidgetText {
|
||||
fn default() -> Self {
|
||||
Self::RichText(RichText::default())
|
||||
Self::Text(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,6 +543,7 @@ impl WidgetText {
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Text(text) => text.is_empty(),
|
||||
Self::RichText(text) => text.is_empty(),
|
||||
Self::LayoutJob(job) => job.is_empty(),
|
||||
Self::Galley(galley) => galley.is_empty(),
|
||||
@@ -530,21 +553,36 @@ impl WidgetText {
|
||||
#[inline]
|
||||
pub fn text(&self) -> &str {
|
||||
match self {
|
||||
Self::Text(text) => text,
|
||||
Self::RichText(text) => text.text(),
|
||||
Self::LayoutJob(job) => &job.text,
|
||||
Self::Galley(galley) => galley.text(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the contents based on the provided closure.
|
||||
///
|
||||
/// - [`Self::Text`] => convert to [`RichText`] and call f
|
||||
/// - [`Self::RichText`] => call f
|
||||
/// - else do nothing
|
||||
#[must_use]
|
||||
fn map_rich_text<F>(self, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(RichText) -> RichText,
|
||||
{
|
||||
match self {
|
||||
Self::Text(text) => Self::RichText(Arc::new(f(RichText::new(text)))),
|
||||
Self::RichText(text) => Self::RichText(Arc::new(f(Arc::unwrap_or_clone(text)))),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Override the [`TextStyle`] if, and only if, this is a [`RichText`].
|
||||
///
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn text_style(self, text_style: TextStyle) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.text_style(text_style)),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.text_style(text_style))
|
||||
}
|
||||
|
||||
/// Set the [`TextStyle`] unless it has already been set
|
||||
@@ -552,10 +590,7 @@ impl WidgetText {
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.fallback_text_style(text_style))
|
||||
}
|
||||
|
||||
/// Override text color if, and only if, this is a [`RichText`].
|
||||
@@ -563,111 +598,85 @@ impl WidgetText {
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn color(self, color: impl Into<Color32>) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.color(color)),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.color(color))
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn heading(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.heading()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.heading())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn monospace(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.monospace()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.monospace())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn code(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.code()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.code())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn strong(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.strong()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.strong())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn weak(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.weak()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.weak())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn underline(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.underline()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.underline())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn strikethrough(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.strikethrough()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.strikethrough())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn italics(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.italics()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.italics())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn small(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.small()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.small())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn small_raised(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.small_raised()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.small_raised())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn raised(self) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.raised()),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.raised())
|
||||
}
|
||||
|
||||
/// Prefer using [`RichText`] directly!
|
||||
#[inline]
|
||||
pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
|
||||
match self {
|
||||
Self::RichText(text) => Self::RichText(text.background_color(background_color)),
|
||||
Self::LayoutJob(_) | Self::Galley(_) => self,
|
||||
}
|
||||
self.map_rich_text(|text| text.background_color(background_color))
|
||||
}
|
||||
|
||||
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
|
||||
pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
|
||||
match self {
|
||||
Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)),
|
||||
Self::RichText(text) => text.font_height(fonts, style),
|
||||
Self::LayoutJob(job) => job.font_height(fonts),
|
||||
Self::Galley(galley) => {
|
||||
@@ -685,11 +694,24 @@ impl WidgetText {
|
||||
style: &Style,
|
||||
fallback_font: FontSelection,
|
||||
default_valign: Align,
|
||||
) -> LayoutJob {
|
||||
) -> Arc<LayoutJob> {
|
||||
match self {
|
||||
Self::RichText(text) => text.into_layout_job(style, fallback_font, default_valign),
|
||||
Self::Text(text) => Arc::new(LayoutJob::simple_format(
|
||||
text,
|
||||
TextFormat {
|
||||
font_id: FontSelection::Default.resolve(style),
|
||||
color: crate::Color32::PLACEHOLDER,
|
||||
valign: default_valign,
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
Self::RichText(text) => Arc::new(Arc::unwrap_or_clone(text).into_layout_job(
|
||||
style,
|
||||
fallback_font,
|
||||
default_valign,
|
||||
)),
|
||||
Self::LayoutJob(job) => job,
|
||||
Self::Galley(galley) => (*galley.job).clone(),
|
||||
Self::Galley(galley) => galley.job.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,12 +743,30 @@ impl WidgetText {
|
||||
default_valign: Align,
|
||||
) -> Arc<Galley> {
|
||||
match self {
|
||||
Self::RichText(text) => {
|
||||
let mut layout_job = text.into_layout_job(style, fallback_font, default_valign);
|
||||
Self::Text(text) => {
|
||||
let mut layout_job = LayoutJob::simple_format(
|
||||
text,
|
||||
TextFormat {
|
||||
font_id: FontSelection::Default.resolve(style),
|
||||
color: crate::Color32::PLACEHOLDER,
|
||||
valign: default_valign,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
layout_job.wrap = text_wrapping;
|
||||
ctx.fonts(|f| f.layout_job(layout_job))
|
||||
}
|
||||
Self::LayoutJob(mut job) => {
|
||||
Self::RichText(text) => {
|
||||
let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job(
|
||||
style,
|
||||
fallback_font,
|
||||
default_valign,
|
||||
);
|
||||
layout_job.wrap = text_wrapping;
|
||||
ctx.fonts(|f| f.layout_job(layout_job))
|
||||
}
|
||||
Self::LayoutJob(job) => {
|
||||
let mut job = Arc::unwrap_or_clone(job);
|
||||
job.wrap = text_wrapping;
|
||||
ctx.fonts(|f| f.layout_job(job))
|
||||
}
|
||||
@@ -738,48 +778,55 @@ impl WidgetText {
|
||||
impl From<&str> for WidgetText {
|
||||
#[inline]
|
||||
fn from(text: &str) -> Self {
|
||||
Self::RichText(RichText::new(text))
|
||||
Self::Text(text.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for WidgetText {
|
||||
#[inline]
|
||||
fn from(text: &String) -> Self {
|
||||
Self::RichText(RichText::new(text))
|
||||
Self::Text(text.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for WidgetText {
|
||||
#[inline]
|
||||
fn from(text: String) -> Self {
|
||||
Self::RichText(RichText::new(text))
|
||||
Self::Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Box<str>> for WidgetText {
|
||||
#[inline]
|
||||
fn from(text: &Box<str>) -> Self {
|
||||
Self::RichText(RichText::new(text.clone()))
|
||||
Self::Text(text.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<str>> for WidgetText {
|
||||
#[inline]
|
||||
fn from(text: Box<str>) -> Self {
|
||||
Self::RichText(RichText::new(text))
|
||||
Self::Text(text.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'_, str>> for WidgetText {
|
||||
#[inline]
|
||||
fn from(text: Cow<'_, str>) -> Self {
|
||||
Self::RichText(RichText::new(text))
|
||||
Self::Text(text.into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RichText> for WidgetText {
|
||||
#[inline]
|
||||
fn from(rich_text: RichText) -> Self {
|
||||
Self::RichText(Arc::new(rich_text))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<RichText>> for WidgetText {
|
||||
#[inline]
|
||||
fn from(rich_text: Arc<RichText>) -> Self {
|
||||
Self::RichText(rich_text)
|
||||
}
|
||||
}
|
||||
@@ -787,6 +834,13 @@ impl From<RichText> for WidgetText {
|
||||
impl From<LayoutJob> for WidgetText {
|
||||
#[inline]
|
||||
fn from(layout_job: LayoutJob) -> Self {
|
||||
Self::LayoutJob(Arc::new(layout_job))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<LayoutJob>> for WidgetText {
|
||||
#[inline]
|
||||
fn from(layout_job: Arc<LayoutJob>) -> Self {
|
||||
Self::LayoutJob(layout_job)
|
||||
}
|
||||
}
|
||||
@@ -797,3 +851,13 @@ impl From<Arc<Galley>> for WidgetText {
|
||||
Self::Galley(galley)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::WidgetText;
|
||||
|
||||
#[test]
|
||||
fn ensure_small_widget_text() {
|
||||
assert_eq!(size_of::<WidgetText>(), size_of::<String>());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user