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

Merge remote-tracking branch 'origin/master' into default_affects_opacity

This commit is contained in:
tye-exe
2025-06-17 13:24:41 +01:00
285 changed files with 5692 additions and 2237 deletions

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
name: Deploy web demo
on:
# We only run this on merges to master
# We only run this on merges to main
push:
branches: ["master"]
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
@@ -39,16 +39,19 @@ jobs:
with:
profile: minimal
target: wasm32-unknown-unknown
toolchain: 1.81.0
toolchain: 1.84.0
override: true
- uses: Swatinem/rust-cache@v2
with:
prefix-key: "web-demo-"
- name: "Install wasmopt / binaryen"
run: |
sudo apt-get update && sudo apt-get install binaryen
- name: Install wasm-opt
uses: sigoden/install-binary@v1
with:
repo: WebAssembly/binaryen
tag: version_123
name: wasm-opt
- run: |
scripts/build_demo_web.sh --release

View File

@@ -0,0 +1,34 @@
name: PR Branch Name Check
on:
pull_request_target:
types: [opened, reopened, synchronize]
jobs:
check-source-branch:
runs-on: ubuntu-latest
steps:
- name: Check PR source branch
run: |
# Check if PR is from a fork
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Check if PR is from the master/main branch of a fork
if [[ "${{ github.event.pull_request.head.ref }}" == "master" || "${{ github.event.pull_request.head.ref }}" == "main" ]]; then
echo "ERROR: Pull requests from the master/main branch of forks are not allowed, because it prevents maintainers from contributing to your PR"
echo "Please create a feature branch in your fork and submit the PR from that branch instead."
exit 1
fi
fi
- name: Leave comment if PR is from master/main branch of fork d
if: ${{ failure() }}
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '⚠️ **ERROR:** Pull requests from the `master`/`main` branch of forks are not allowed, because it prevents maintainers from contributing to your PR. Please create a feature branch in your fork and submit the PR from that branch instead.'
})

View File

@@ -25,7 +25,7 @@ jobs:
exclude_pattern=$(printf "|^%s" "${exclude_paths[@]}" | sed 's/^|//')
if comm -23 <(git ls-files | grep -Ev "$exclude_pattern" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then
echo "Error: Found binary file with extension .$ext not tracked by git LFS. See https://github.com/emilk/egui/blob/master/CONTRIBUTING.md#working-with-git-lfs"
echo "Error: Found binary file with extension .$ext not tracked by git LFS. See https://github.com/emilk/egui/blob/main/CONTRIBUTING.md#working-with-git-lfs"
exit 1
fi
done

View File

@@ -19,15 +19,18 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
prefix-key: "pr-preview-"
- name: "Install wasmopt / binaryen"
run: |
sudo apt-get update && sudo apt-get install binaryen
- name: Install wasm-opt
uses: sigoden/install-binary@v1
with:
repo: WebAssembly/binaryen
tag: version_123
name: wasm-opt
- run: |
scripts/build_demo_web.sh --release

View File

@@ -5,7 +5,7 @@ name: Rust
env:
RUSTFLAGS: -D warnings
RUSTDOCFLAGS: -D warnings
NIGHTLY_VERSION: nightly-2024-09-11
NIGHTLY_VERSION: nightly-2025-04-22
jobs:
fmt-crank-check-test:
@@ -18,7 +18,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
- name: Install packages (Linux)
if: runner.os == 'Linux'
@@ -83,7 +83,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
targets: wasm32-unknown-unknown
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
rust-version: "1.81.0"
rust-version: "1.84.0"
log-level: error
command: check
arguments: --target ${{ matrix.target }}
@@ -170,13 +170,15 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
targets: aarch64-linux-android
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- run: cargo check --features wgpu,android-native-activity --target aarch64-linux-android
# Default features disabled to turn off accesskit, which does not work
# with NativeActivity.
- run: cargo check --features wgpu,android-native-activity --target aarch64-linux-android --no-default-features
working-directory: crates/eframe
# ---------------------------------------------------------------------------
@@ -189,7 +191,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
targets: aarch64-apple-ios
- name: Set up cargo cache
@@ -208,7 +210,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
@@ -232,7 +234,7 @@ jobs:
lfs: true
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.81.0
toolchain: 1.84.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2

View File

@@ -108,7 +108,7 @@ In order to pave the path for more complex and customizable styling solutions, w
* Improved support for transform layers ([#5465](https://github.com/emilk/egui/pull/5465), [#5468](https://github.com/emilk/egui/pull/5468), [#5429](https://github.com/emilk/egui/pull/5429))
#### `egui_kittest`
This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/master/crates/egui_kittest).
This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/main/crates/egui_kittest).
`egui_kittest` is a testing framework for egui, allowing you to test both automation (simulated clicks and other events),
and also do screenshot testing (useful for regression tests).
`egui_kittest` is built using [`kittest`](https://github.com/rerun-io/kittest), which is a general GUI testing framework that aims to work with any Rust GUI (not just egui!).
@@ -326,7 +326,7 @@ There also has been several small improvements to the look of egui:
* The `extra_asserts` and `extra_debug_asserts` feature flags have been removed ([#4478](https://github.com/emilk/egui/pull/4478))
* Remove `Event::Scroll` and handle it in egui. Use `Event::MouseWheel` instead ([#4524](https://github.com/emilk/egui/pull/4524))
* `Event::Zoom` is no longer emitted on ctrl+scroll. Use `InputState::smooth_scroll_delta` instead ([#4524](https://github.com/emilk/egui/pull/4524))
* `ui.set_enabled` and `set_visbile` have been deprecated ([#4614](https://github.com/emilk/egui/pull/4614))
* `ui.set_enabled` and `set_visible` have been deprecated ([#4614](https://github.com/emilk/egui/pull/4614))
* `DragValue::clamp_range` renamed to `range` (([#4728](https://github.com/emilk/egui/pull/4728))
### ⭐ Added
@@ -942,7 +942,7 @@ egui_extras::install_image_loaders(egui_ctx);
## 0.18.0 - 2022-04-30
### ⭐ Added
* Added `Shape::Callback` for backend-specific painting, [with an example](https://github.com/emilk/egui/tree/master/examples/custom_3d_glow) ([#1351](https://github.com/emilk/egui/pull/1351)).
* Added `Shape::Callback` for backend-specific painting, [with an example](https://github.com/emilk/egui/tree/main/examples/custom_3d_glow) ([#1351](https://github.com/emilk/egui/pull/1351)).
* Added `Frame::canvas` ([#1362](https://github.com/emilk/egui/pull/1362)).
* `Context::request_repaint` will now wake up UI thread, if integrations has called `Context::set_request_repaint_callback` ([#1366](https://github.com/emilk/egui/pull/1366)).
* Added `Plot::allow_scroll`, `Plot::allow_zoom` no longer affects scrolling ([#1382](https://github.com/emilk/egui/pull/1382)).

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ members = [
[workspace.package]
edition = "2021"
license = "MIT OR Apache-2.0"
rust-version = "1.81"
rust-version = "1.84"
version = "0.31.1"
@@ -68,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

View File

@@ -5,8 +5,8 @@
[![Documentation](https://docs.rs/egui/badge.svg)](https://docs.rs/egui)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
[![Build Status](https://github.com/emilk/egui/workflows/Rust/badge.svg)](https://github.com/emilk/egui/actions/workflows/rust.yml)
[![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-MIT)
[![Apache](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-APACHE)
[![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/emilk/egui/blob/main/LICENSE-MIT)
[![Apache](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/emilk/egui/blob/main/LICENSE-APACHE)
[![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq)
@@ -27,7 +27,7 @@ egui aims to be the easiest-to-use Rust GUI library, and the simplest way to mak
egui can be used anywhere you can draw textured triangles, which means you can easily integrate it into your game engine of choice.
[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) is the official egui framework, which supports writing apps for Web, Linux, Mac, Windows, and Android.
[`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) is the official egui framework, which supports writing apps for Web, Linux, Mac, Windows, and Android.
## Example
@@ -68,19 +68,19 @@ ui.image(egui::include_image!("ferris.png"));
## Quick start
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/master/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md).
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md).
## Demo
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with Wasm and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with Wasm and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
@@ -99,7 +99,7 @@ On Fedora Rawhide you need to run:
* Easy to integrate into any environment
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
* Pure immediate mode: no callbacks
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs)
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/toggle_switch.rs)
* Modular: You should be able to use small parts of egui and combine them in new ways
* Safe: there is no `unsafe` code in egui
* Minimal dependencies
@@ -154,9 +154,9 @@ Light Theme:
Heavier dependencies are kept out of `egui`, even as opt-in.
All code in `egui` is Wasm-friendly (even outside a browser).
To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/master/crates/egui_extras) crate.
To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/main/crates/egui_extras) crate.
[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc,
[`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc,
## Who is egui for?
@@ -177,16 +177,16 @@ An integration needs to do the following each frame:
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
* Call into the application GUI code
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs))
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/main/crates/egui_glow/src/painter.rs))
### Official integrations
These are the official egui integrations:
* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`
* [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps
* [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API)
* [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit)
* [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`
* [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps
* [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API)
* [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit)
### 3rd party integrations
@@ -286,7 +286,7 @@ egui includes optional support for [AccessKit](https://accesskit.dev/), which cu
The original discussion of accessibility in egui is at <https://github.com/emilk/egui/issues/167>. Now that AccessKit support is merged, providing a strong foundation for future accessibility work, please open new issues on specific accessibility problems.
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/main/crates/eframe)?
`egui` is a 2D user interface library for laying out and interacting with buttons, sliders, etc.
`egui` has no idea if it is running on the web or natively, and does not know how to collect input or show things on screen.
@@ -303,15 +303,16 @@ If you want to embed 3D into an egui view there are two options:
#### `Shape::Callback`
Example:
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_glow/src/main.rs>
* <https://github.com/emilk/egui/blob/main/examples/custom_3d_glow/src/main.rs>
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
#### Render-to-texture
You can also render your 3D scene to a texture and display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use.
Examples:
* Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs
* Using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe) + [`VTK (C++)`](https://vtk.org/): https://github.com/Gerharddc/vtk-egui-demo
## Other
@@ -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).

View File

@@ -9,7 +9,7 @@ All crates under the [`crates/`](crates/) folder are published in lock-step, wit
The only exception to this are patch releases, where we sometimes only patch a single crate.
The egui version in egui `master` is always the version of the last published crates. This is so that users can easily patch their egui crates to egui `master` if they want to.
The egui version in egui `main` is always the version of the last published crates. This is so that users can easily patch their egui crates to egui `main` if they want to.
## Governance
Releases are generally done by [emilk](https://github.com/emilk/), but the [rerun-io](https://github.com/rerun-io/) organization (where emilk is CTO) also has publish rights to all the crates.
@@ -53,14 +53,14 @@ We don't update the MSRV in a patch release, unless we really, really need to.
* [ ] bump version numbers in workspace `Cargo.toml`
## Actual release
I usually do this all on the `master` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `master` later.
I usually do this all on the `main` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `main` later.
* [ ] Run `typos`
* [ ] `git commit -m 'Release 0.x.0 - <release title>'`
* [ ] `cargo publish` (see below)
* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - <release title>'`
* [ ] `git pull --tags ; git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force ; git push --tags`
* [ ] merge release PR or push to `master`
* [ ] merge release PR or push to `main`
* [ ] check that CI is green
* [ ] do a GitHub release: https://github.com/emilk/egui/releases/new
* Follow the format of the last release

View File

@@ -3,7 +3,7 @@
# -----------------------------------------------------------------------------
# Section identical to scripts/clippy_wasm/clippy.toml:
msrv = "1.81"
msrv = "1.84"
allow-unwrap-in-tests = true

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) {

View File

@@ -5,10 +5,10 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively"
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
homepage = "https://github.com/emilk/egui/tree/main/crates/eframe"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"]
include = [
@@ -59,7 +59,7 @@ android-native-activity = ["egui-winit/android-native-activity"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
## Enable saving app state to disk.
@@ -90,7 +90,7 @@ web_screen_reader = [
"web-sys/SpeechSynthesisUtterance",
]
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)).
##
## This overrides the `glow` feature.
##

View File

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

View File

@@ -91,7 +91,7 @@ pub struct CreationContext<'s> {
pub(crate) raw_display_handle: Result<RawDisplayHandle, HandleError>,
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasWindowHandle for CreationContext<'_> {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
@@ -100,7 +100,7 @@ impl HasWindowHandle for CreationContext<'_> {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasDisplayHandle for CreationContext<'_> {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
@@ -133,7 +133,7 @@ impl CreationContext<'_> {
// ----------------------------------------------------------------------------
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
pub trait App {
/// Called each time the UI needs repainting, which may be many times per second.
///
@@ -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")
}

View File

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

View File

@@ -47,7 +47,7 @@ enum AppIconStatus {
NotSetTryAgain,
/// We successfully set the icon and it should be visible now.
#[allow(dead_code)] // Not used on Linux
#[allow(dead_code, clippy::allow_attributes)] // Not used on Linux
Set,
}
@@ -71,13 +71,13 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta
#[cfg(target_os = "macos")]
return set_title_and_icon_mac(_title, _icon_data);
#[allow(unreachable_code)]
#[allow(unreachable_code, clippy::allow_attributes)]
AppIconStatus::NotSetIgnored
}
/// Set icon for Windows applications.
#[cfg(target_os = "windows")]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
use winapi::um::winuser;
@@ -198,12 +198,12 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
/// Set icon & app title for `MacOS` applications.
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
profiling::function_scope!();
use objc2::ClassType;
use objc2::ClassType as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::{NSData, NSString};

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
use std::{
collections::HashMap,
io::Write,
io::Write as _,
path::{Path, PathBuf},
};
@@ -42,10 +42,10 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
// Adapted from
// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37
#[cfg(all(windows, not(target_vendor = "uwp")))]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn roaming_appdata() -> Option<PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::ffi::OsStringExt as _;
use std::ptr;
use std::slice;
@@ -207,7 +207,8 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
let config = Default::default();
profiling::scope!("ron::serialize");
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
if let Err(err) = ron::Options::default()
.to_io_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
{
log::warn!("Failed to serialize app state: {}", err);

View File

@@ -11,13 +11,13 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
use egui_winit::ActionRequested;
use glutin::{
config::GlConfig,
context::NotCurrentGlContext,
display::GetGlDisplay,
prelude::{GlDisplay, PossiblyCurrentGlContext},
surface::GlSurface,
config::GlConfig as _,
context::NotCurrentGlContext as _,
display::GetGlDisplay as _,
prelude::{GlDisplay as _, PossiblyCurrentGlContext as _},
surface::GlSurface as _,
};
use raw_window_handle::HasWindowHandle;
use raw_window_handle::HasWindowHandle as _;
use winit::{
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
window::{Window, WindowId},
@@ -139,7 +139,7 @@ impl<'app> GlowWinitApp<'app> {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn create_glutin_windowed_context(
egui_ctx: &egui::Context,
event_loop: &ActiveEventLoop,
@@ -901,7 +901,7 @@ fn change_gl_context(
}
impl GlutinWindowContext {
#[allow(unsafe_code)]
#[expect(unsafe_code)]
unsafe fn new(
egui_ctx: &egui::Context,
viewport_builder: ViewportBuilder,
@@ -1094,7 +1094,7 @@ impl GlutinWindowContext {
}
/// Create a surface, window, and winit integration for the viewport, if missing.
#[allow(unsafe_code)]
#[expect(unsafe_code)]
pub(crate) fn initialize_window(
&mut self,
viewport_id: ViewportId,
@@ -1566,6 +1566,6 @@ fn save_screenshot_and_exit(
});
log::info!("Screenshot saved to {path:?}.");
#[allow(clippy::exit)]
#[expect(clippy::exit)]
std::process::exit(0);
}

View File

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

View File

@@ -15,7 +15,7 @@ use winit::{
window::{Window, WindowId},
};
use ahash::{HashMap, HashSet, HashSetExt};
use ahash::{HashMap, HashSet, HashSetExt as _};
use egui::{
DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass,
ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput,
@@ -182,7 +182,6 @@ impl<'app> WgpuWinitApp<'app> {
builder: ViewportBuilder,
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
profiling::function_scope!();
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
self.native_options.wgpu_options.clone(),
@@ -236,7 +235,7 @@ impl<'app> WgpuWinitApp<'app> {
});
}
#[allow(unused_mut)] // used for accesskit
#[allow(unused_mut, clippy::allow_attributes)] // used for accesskit
let mut egui_winit = egui_winit::State::new(
egui_ctx.clone(),
ViewportId::ROOT,

View File

@@ -2,10 +2,10 @@ use egui::{TexturesDelta, UserData, ViewportCommand};
use crate::{epi, App};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter as _, NeedRepaint};
pub struct AppRunner {
#[allow(dead_code)]
#[allow(dead_code, clippy::allow_attributes)]
pub(crate) web_options: crate::WebOptions,
pub(crate) frame: epi::Frame,
egui_ctx: egui::Context,
@@ -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.
}
}
}

View File

@@ -4,7 +4,7 @@ use super::{
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast, JsValue, WebRunner,
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast as _, JsValue, WebRunner,
DEBUG_RESIZE,
};
@@ -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;

View File

@@ -281,7 +281,7 @@ fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result<web_sys::ClipboardI
let items = js_sys::Object::new();
// SAFETY: I hope so
#[allow(unsafe_code, unused_unsafe)] // Weird false positive
#[expect(unsafe_code, unused_unsafe)] // Weird false positive
unsafe {
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
};

View File

@@ -110,7 +110,7 @@ mod console {
/// * `tokio-1.24.1/src/runtime/runtime.rs`
/// * `rerun/src/main.rs`
/// * `core/src/ops/function.rs`
#[allow(dead_code)] // only used on web and in tests
#[allow(dead_code, clippy::allow_attributes)] // only used on web and in tests
fn shorten_file_path(file_path: &str) -> &str {
if let Some(i) = file_path.rfind("/src/") {
if let Some(prev_slash) = file_path[..i].rfind('/') {

View File

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

View File

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

View File

@@ -37,7 +37,7 @@ pub struct WebRunner {
impl WebRunner {
/// Will install a panic handler that will catch and log any panics
#[allow(clippy::new_without_default)]
#[expect(clippy::new_without_default)]
pub fn new() -> Self {
let panic_handler = PanicHandler::install();
@@ -280,7 +280,7 @@ struct TargetEvent {
closure: Closure<dyn FnMut(web_sys::Event)>,
}
#[allow(unused)]
#[expect(unused)]
struct IntervalHandle {
handle: i32,
closure: Closure<dyn FnMut()>,
@@ -289,7 +289,7 @@ struct IntervalHandle {
enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(unused)]
#[expect(unused)]
IntervalHandle(IntervalHandle),
}

View File

@@ -9,10 +9,10 @@ authors = [
]
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
include = [
@@ -43,7 +43,7 @@ wayland = ["winit?/wayland"]
x11 = ["winit?/x11"]
## Make the renderer `Sync` on wasm, exploiting that by default wasm isn't multithreaded.
## It may make code easier, expecially when targeting both native and web.
## It may make code easier, especially when targeting both native and web.
## On native most wgpu objects are send and sync, on the web they are not (by nature of the WebGPU specification).
## This is not supported in [multithreaded WASM](https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer).
## Thus that usage is guarded against with compiler errors in wgpu.

View File

@@ -4,6 +4,7 @@ use std::sync::{mpsc, Arc};
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
/// A texture and a buffer for reading the rendered frame back to the cpu.
///
/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
/// flag for the surface texture on all platforms. This means that anytime we want to
/// capture the frame, we first render it to this texture, and then we can copy it to
@@ -125,7 +126,7 @@ impl CaptureState {
// It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
// for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
// it might make sense to revisit this and implement a more efficient solution.
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_screen_capture_buffer"),
size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
@@ -184,7 +185,7 @@ impl CaptureState {
tx: CaptureSender,
viewport_id: ViewportId,
) {
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
let buffer = Arc::new(buffer);
let buffer_clone = buffer.clone();
let buffer_slice = buffer_clone.slice(..);
@@ -226,10 +227,10 @@ impl CaptureState {
tx.send((
viewport_id,
data,
ColorImage {
size: [tex_extent.width as usize, tex_extent.height as usize],
ColorImage::new(
[tex_extent.width as usize, tex_extent.height as usize],
pixels,
},
),
))
.ok();
ctx.request_repaint();

View File

@@ -42,8 +42,11 @@ use epaint::mutex::RwLock;
/// An error produced by egui-wgpu.
#[derive(thiserror::Error, Debug)]
pub enum WgpuError {
#[error("Failed to create wgpu adapter, no suitable adapter found: {0}")]
NoSuitableAdapterFound(String),
#[error(transparent)]
RequestAdapterError(#[from] wgpu::RequestAdapterError),
#[error("Adapter selection failed: {0}")]
CustomNativeAdapterSelectionError(String),
#[error("There was no valid format for the surface at all.")]
NoSurfaceFormatsAvailable,
@@ -104,7 +107,7 @@ async fn request_adapter(
force_fallback_adapter: false,
})
.await
.ok_or_else(|| {
.inspect_err(|_err| {
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.is_empty() {
log::info!("No wgpu adapters found");
@@ -120,8 +123,6 @@ async fn request_adapter(
describe_adapters(_available_adapters)
);
}
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
})?;
#[cfg(target_arch = "wasm32")]
@@ -184,7 +185,6 @@ impl RenderState {
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
trace_path,
}) => {
let adapter = {
#[cfg(target_arch = "wasm32")]
@@ -194,7 +194,7 @@ impl RenderState {
#[cfg(not(target_arch = "wasm32"))]
if let Some(native_adapter_selector) = _native_adapter_selector {
native_adapter_selector(&available_adapters, compatible_surface)
.map_err(WgpuError::NoSuitableAdapterFound)
.map_err(WgpuError::CustomNativeAdapterSelectionError)
} else {
request_adapter(
instance,
@@ -209,7 +209,7 @@ impl RenderState {
let (device, queue) = {
profiling::scope!("request_device");
adapter
.request_device(&(*device_descriptor)(&adapter), trace_path.as_deref())
.request_device(&(*device_descriptor)(&adapter))
.await?
};
@@ -242,7 +242,7 @@ impl RenderState {
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
Ok(Self {
adapter,
#[cfg(not(target_arch = "wasm32"))]

View File

@@ -3,7 +3,7 @@
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
use epaint::{emath::NumExt as _, PaintCallbackInfo, Primitive, Vertex};
use wgpu::util::DeviceExt as _;
@@ -84,7 +84,7 @@ impl Callback {
///
/// # Example
///
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/main/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
pub trait CallbackTrait: Send + Sync {
fn prepare(
&self,
@@ -749,7 +749,7 @@ impl Renderer {
///
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
#[allow(clippy::needless_pass_by_value)] // false positive
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn register_native_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
@@ -796,7 +796,7 @@ impl Renderer {
/// [`wgpu::SamplerDescriptor`] options.
///
/// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler options.
#[allow(clippy::needless_pass_by_value)] // false positive
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,

View File

@@ -48,7 +48,7 @@ impl WgpuSetup {
pub async fn new_instance(&self) -> wgpu::Instance {
match self {
Self::CreateNew(create_new) => {
#[allow(unused_mut)]
#[allow(unused_mut, clippy::allow_attributes)]
let mut backends = create_new.instance_descriptor.backends;
// Don't try WebGPU if we're not in a secure context.
@@ -126,13 +126,6 @@ pub struct WgpuSetupCreateNew {
/// Configuration passed on device request, given an adapter
pub device_descriptor:
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
/// Option path to output a wgpu trace file.
///
/// This only works if this feature is enabled in `wgpu-core`.
/// Does not work when running with WebGPU.
/// Defaults to the path set in the `WGPU_TRACE` environment variable.
pub trace_path: Option<std::path::PathBuf>,
}
impl Clone for WgpuSetupCreateNew {
@@ -142,7 +135,6 @@ impl Clone for WgpuSetupCreateNew {
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: self.device_descriptor.clone(),
trace_path: self.trace_path.clone(),
}
}
}
@@ -156,7 +148,6 @@ impl std::fmt::Debug for WgpuSetupCreateNew {
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
)
.field("trace_path", &self.trace_path)
.finish()
}
}
@@ -195,12 +186,9 @@ impl Default for WgpuSetupCreateNew {
..base_limits
},
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
}
}),
trace_path: std::env::var("WGPU_TRACE")
.ok()
.map(std::path::PathBuf::from),
}
}
}

View File

@@ -575,7 +575,7 @@ impl Painter {
.retain(|id, _| active_viewports.contains(id));
}
#[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
#[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}

View File

@@ -5,10 +5,10 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]

View File

@@ -161,7 +161,7 @@ fn init_smithay_clipboard(
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
log::trace!("Initializing smithay clipboard…");
#[allow(unsafe_code)]
#[expect(unsafe_code)]
Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
} else {
#[cfg(feature = "wayland")]

View File

@@ -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",

View File

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

View File

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

View File

@@ -0,0 +1,109 @@
use crate::{AtomKind, Id, SizedAtom, Ui};
use emath::{NumExt as _, Vec2};
use epaint::text::TextWrapMode;
/// A low-level ui building block.
///
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
/// ```
/// # use egui::{Image, emath::Vec2};
/// use egui::AtomExt as _;
/// let string_atom = "Hello".atom_grow(true);
/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0));
/// ```
#[derive(Clone, Debug)]
pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_size`]
pub size: Option<Vec2>,
/// See [`crate::AtomExt::atom_max_size`]
pub max_size: Vec2,
/// See [`crate::AtomExt::atom_grow`]
pub grow: bool,
/// See [`crate::AtomExt::atom_shrink`]
pub shrink: bool,
/// The atom type
pub kind: AtomKind<'a>,
}
impl Default for Atom<'_> {
fn default() -> Self {
Atom {
size: None,
max_size: Vec2::INFINITY,
grow: false,
shrink: false,
kind: AtomKind::Empty,
}
}
}
impl<'a> Atom<'a> {
/// Create an empty [`Atom`] marked as `grow`.
///
/// This will expand in size, allowing all preceding atoms to be left-aligned,
/// and all following atoms to be right-aligned
pub fn grow() -> Self {
Atom {
grow: true,
..Default::default()
}
}
/// Create a [`AtomKind::Custom`] with a specific size.
pub fn custom(id: Id, size: impl Into<Vec2>) -> Self {
Atom {
size: Some(size.into()),
kind: AtomKind::Custom(id),
..Default::default()
}
}
/// Turn this into a [`SizedAtom`].
pub fn into_sized(
self,
ui: &Ui,
mut available_size: Vec2,
mut wrap_mode: Option<TextWrapMode>,
) -> SizedAtom<'a> {
if !self.shrink && self.max_size.x.is_infinite() {
wrap_mode = Some(TextWrapMode::Extend);
}
available_size = available_size.at_most(self.max_size);
if let Some(size) = self.size {
available_size = available_size.at_most(size);
}
if self.max_size.x.is_finite() {
wrap_mode = Some(TextWrapMode::Truncate);
}
let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
let size = self
.size
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));
SizedAtom {
size,
preferred_size: preferred,
grow: self.grow,
kind,
}
}
}
impl<'a, T> From<T> for Atom<'a>
where
T: Into<AtomKind<'a>>,
{
fn from(value: T) -> Self {
Atom {
kind: value.into(),
..Default::default()
}
}
}

View File

@@ -0,0 +1,107 @@
use crate::{Atom, FontSelection, Ui};
use emath::Vec2;
/// A trait for conveniently building [`Atom`]s.
///
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
pub trait AtomExt<'a> {
/// Set the atom to a fixed size.
///
/// If [`Atom::grow`] is `true`, this will be the minimum width.
/// If [`Atom::shrink`] is `true`, this will be the maximum width.
/// If both are true, the width will have no effect.
///
/// [`Self::atom_max_size`] will limit size.
///
/// See [`crate::AtomKind`] docs to see how the size affects the different types.
fn atom_size(self, size: Vec2) -> Atom<'a>;
/// Grow this atom to the available space.
///
/// This will affect the size of the [`Atom`] in the main direction. Since
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
///
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
/// remaining space.
fn atom_grow(self, grow: bool) -> Atom<'a>;
/// Shrink this atom if there isn't enough space.
///
/// This will affect the size of the [`Atom`] in the main direction. Since
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
///
/// NOTE: Only a single [`Atom`] may shrink for each widget.
///
/// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first
/// `AtomKind::Text` is set to shrink.
fn atom_shrink(self, shrink: bool) -> Atom<'a>;
/// Set the maximum size of this atom.
///
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
/// equally to fill the available space).
fn atom_max_size(self, max_size: Vec2) -> Atom<'a>;
/// Set the maximum width of this atom.
///
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
/// equally to fill the available space).
fn atom_max_width(self, max_width: f32) -> Atom<'a>;
/// Set the maximum height of this atom.
fn atom_max_height(self, max_height: f32) -> Atom<'a>;
/// Set the max height of this atom to match the font size.
///
/// This is useful for e.g. limiting the height of icons in buttons.
fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a>
where
Self: Sized,
{
let font_selection = FontSelection::default();
let font_id = font_selection.resolve(ui.style());
let height = ui.fonts(|f| f.row_height(&font_id));
self.atom_max_height(height)
}
}
impl<'a, T> AtomExt<'a> for T
where
T: Into<Atom<'a>> + Sized,
{
fn atom_size(self, size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.size = Some(size);
atom
}
fn atom_grow(self, grow: bool) -> Atom<'a> {
let mut atom = self.into();
atom.grow = grow;
atom
}
fn atom_shrink(self, shrink: bool) -> Atom<'a> {
let mut atom = self.into();
atom.shrink = shrink;
atom
}
fn atom_max_size(self, max_size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.max_size = max_size;
atom
}
fn atom_max_width(self, max_width: f32) -> Atom<'a> {
let mut atom = self.into();
atom.max_size.x = max_width;
atom
}
fn atom_max_height(self, max_height: f32) -> Atom<'a> {
let mut atom = self.into();
atom.max_size.y = max_height;
atom
}
}

View File

@@ -0,0 +1,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())
}
}

View File

@@ -0,0 +1,493 @@
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
use crate::{
AtomKind, Atoms, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom, SizedAtomKind, Ui,
Widget,
};
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
use epaint::text::TextWrapMode;
use epaint::{Color32, Galley};
use smallvec::SmallVec;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
/// Intra-widget layout utility.
///
/// Used to lay out and paint [`crate::Atom`]s.
/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`].
/// You can use it to make your own widgets.
///
/// Painting the atoms can be split in two phases:
/// - [`AtomLayout::allocate`]
/// - calculates sizes
/// - converts texts to [`Galley`]s
/// - allocates a [`Response`]
/// - returns a [`AllocatedAtomLayout`]
/// - [`AllocatedAtomLayout::paint`]
/// - paints the [`Frame`]
/// - calculates individual [`crate::Atom`] positions
/// - paints each single atom
///
/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
/// [`AllocatedAtomLayout`] for interaction styling.
pub struct AtomLayout<'a> {
id: Option<Id>,
pub atoms: Atoms<'a>,
gap: Option<f32>,
pub(crate) frame: Frame,
pub(crate) sense: Sense,
fallback_text_color: Option<Color32>,
min_size: Vec2,
wrap_mode: Option<TextWrapMode>,
align2: Option<Align2>,
}
impl Default for AtomLayout<'_> {
fn default() -> Self {
Self::new(())
}
}
impl<'a> AtomLayout<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
id: None,
atoms: atoms.into_atoms(),
gap: None,
frame: Frame::default(),
sense: Sense::hover(),
fallback_text_color: None,
min_size: Vec2::ZERO,
wrap_mode: None,
align2: None,
}
}
/// Set the gap between atoms.
///
/// Default: `Spacing::icon_spacing`
#[inline]
pub fn gap(mut self, gap: f32) -> Self {
self.gap = Some(gap);
self
}
/// Set the [`Frame`].
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = frame;
self
}
/// Set the [`Sense`] used when allocating the [`Response`].
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the fallback (default) text color.
///
/// Default: [`crate::Visuals::text_color`]
#[inline]
pub fn fallback_text_color(mut self, color: Color32) -> Self {
self.fallback_text_color = Some(color);
self
}
/// Set the minimum size of the Widget.
///
/// This will find and expand atoms with `grow: true`.
/// If there are no growable atoms then everything will be left-aligned.
#[inline]
pub fn min_size(mut self, size: Vec2) -> Self {
self.min_size = size;
self
}
/// Set the [`Id`] used to allocate a [`Response`].
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
/// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
///
/// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
/// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most)
/// [`AtomKind::Text`] will be set to shrink.
#[inline]
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self
}
/// Set the [`Align2`].
///
/// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`].
///
/// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See
/// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png)
/// for info on how the [`crate::Layout`] affects the alignment.
#[inline]
pub fn align2(mut self, align2: Align2) -> Self {
self.align2 = Some(align2);
self
}
/// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go.
pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
self.allocate(ui).paint(ui)
}
/// Calculate sizes, create [`Galley`]s and allocate a [`Response`].
///
/// Use the returned [`AllocatedAtomLayout`] for painting.
pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
let Self {
id,
mut atoms,
gap,
frame,
sense,
fallback_text_color,
min_size,
wrap_mode,
align2,
} = self;
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
// If none is found, mark the first text item as `shrink`.
if wrap_mode != TextWrapMode::Extend {
let any_shrink = atoms.iter().any(|a| a.shrink);
if !any_shrink {
let first_text = atoms
.iter_mut()
.find(|a| matches!(a.kind, AtomKind::Text(..)));
if let Some(atom) = first_text {
atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode
}
}
}
let id = id.unwrap_or_else(|| ui.next_auto_id());
let fallback_text_color =
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
let gap = gap.unwrap_or(ui.spacing().icon_spacing);
// The size available for the content
let available_inner_size = ui.available_size() - frame.total_margin().sum();
let mut desired_width = 0.0;
// Preferred width / height is the ideal size of the widget, e.g. the size where the
// text is not wrapped. Used to set Response::intrinsic_size.
let mut preferred_width = 0.0;
let mut preferred_height = 0.0;
let mut height: f32 = 0.0;
let mut sized_items = SmallVec::new();
let mut grow_count = 0;
let mut shrink_item = None;
let align2 = align2.unwrap_or_else(|| {
Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()])
});
if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
desired_width += gap_space;
preferred_width += gap_space;
}
for (idx, item) in atoms.into_iter().enumerate() {
if item.grow {
grow_count += 1;
}
if item.shrink {
debug_assert!(
shrink_item.is_none(),
"Only one atomic may be marked as shrink. {item:?}"
);
if shrink_item.is_none() {
shrink_item = Some((idx, item));
continue;
}
}
let sized = item.into_sized(ui, available_inner_size, Some(wrap_mode));
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
sized_items.push(sized);
}
if let Some((index, item)) = shrink_item {
// The `shrink` item gets the remaining space
let available_size_for_shrink_item = Vec2::new(
available_inner_size.x - desired_width,
available_inner_size.y,
);
let sized = item.into_sized(ui, available_size_for_shrink_item, Some(wrap_mode));
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
sized_items.insert(index, sized);
}
let margin = frame.total_margin();
let desired_size = Vec2::new(desired_width, height);
let frame_size = (desired_size + margin.sum()).at_least(min_size);
let (_, rect) = ui.allocate_space(frame_size);
let mut response = ui.interact(rect, id, sense);
response.intrinsic_size =
Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size));
AllocatedAtomLayout {
sized_atoms: sized_items,
frame,
fallback_text_color,
response,
grow_count,
desired_size,
align2,
gap,
}
}
}
/// Instructions for painting an [`AtomLayout`].
#[derive(Clone, Debug)]
pub struct AllocatedAtomLayout<'a> {
pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>,
pub frame: Frame,
pub fallback_text_color: Color32,
pub response: Response,
grow_count: usize,
// The size of the inner content, before any growing.
desired_size: Vec2,
align2: Align2,
gap: f32,
}
impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
self.sized_atoms.iter().map(|atom| &atom.kind)
}
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
}
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn map_kind<F>(&mut self, mut f: F)
where
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
{
for kind in self.iter_kinds_mut() {
*kind = f(std::mem::take(kind));
}
}
pub fn map_images<F>(&mut self, mut f: F)
where
F: FnMut(Image<'atom>) -> Image<'atom>,
{
self.map_kind(|kind| {
if let SizedAtomKind::Image(image, size) = kind {
SizedAtomKind::Image(f(image), size)
} else {
kind
}
});
}
/// Paint the [`Frame`] and individual [`crate::Atom`]s.
pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
let Self {
sized_atoms,
frame,
fallback_text_color,
response,
grow_count,
desired_size,
align2,
gap,
} = self;
let inner_rect = response.rect - self.frame.total_margin();
ui.painter().add(frame.paint(inner_rect));
let width_to_fill = inner_rect.width();
let extra_space = f32::max(width_to_fill - desired_size.x, 0.0);
let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
let aligned_rect = if grow_count > 0 {
align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect)
} else {
align2.align_size_within_rect(desired_size, inner_rect)
};
let mut cursor = aligned_rect.left();
let mut response = AtomLayoutResponse::empty(response);
for sized in sized_atoms {
let size = sized.size;
// TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
// https://github.com/emilk/egui/pull/5830#discussion_r2079627864
let growth = if sized.is_grow() { grow_width } else { 0.0 };
let frame = aligned_rect
.with_min_x(cursor)
.with_max_x(cursor + size.x + growth);
cursor = frame.right() + gap;
let align = Align2::CENTER_CENTER;
let rect = align.align_size_within_rect(size, frame);
match sized.kind {
SizedAtomKind::Text(galley) => {
ui.painter().galley(rect.min, galley, fallback_text_color);
}
SizedAtomKind::Image(image, _) => {
image.paint_at(ui, rect);
}
SizedAtomKind::Custom(id) => {
debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
response.custom_rects.push((id, rect));
}
SizedAtomKind::Empty => {}
}
}
response
}
}
/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
///
/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`].
#[derive(Clone, Debug)]
pub struct AtomLayoutResponse {
pub response: Response,
// There should rarely be more than one custom rect.
custom_rects: SmallVec<[(Id, Rect); 1]>,
}
impl AtomLayoutResponse {
pub fn empty(response: Response) -> Self {
Self {
response,
custom_rects: Default::default(),
}
}
pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
self.custom_rects.iter().copied()
}
/// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets.
///
/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
pub fn rect(&self, id: Id) -> Option<Rect> {
self.custom_rects
.iter()
.find_map(|(i, r)| if *i == id { Some(*r) } else { None })
}
}
impl Widget for AtomLayout<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response
}
}
impl<'a> Deref for AtomLayout<'a> {
type Target = Atoms<'a>;
fn deref(&self) -> &Self::Target {
&self.atoms
}
}
impl DerefMut for AtomLayout<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.atoms
}
}
impl<'a> Deref for AllocatedAtomLayout<'a> {
type Target = [SizedAtom<'a>];
fn deref(&self) -> &Self::Target {
&self.sized_atoms
}
}
impl DerefMut for AllocatedAtomLayout<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sized_atoms
}
}

View File

@@ -0,0 +1,259 @@
use crate::{Atom, AtomKind, Image, WidgetText};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::ops::{Deref, DerefMut};
// Rarely there should be more than 2 atoms in one Widget.
// I guess it could happen in a menu button with Image and right text...
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
/// A list of [`Atom`]s.
#[derive(Clone, Debug, Default)]
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
impl<'a> Atoms<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
atoms.into_atoms()
}
/// Insert a new [`Atom`] at the end of the list (right side).
pub fn push_right(&mut self, atom: impl Into<Atom<'a>>) {
self.0.push(atom.into());
}
/// Insert a new [`Atom`] at the beginning of the list (left side).
pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
self.0.insert(0, atom.into());
}
/// Concatenate and return the text contents.
// TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g.
// in a submenu button there is a right text '⏵' which is now passed to the screen reader.
pub fn text(&self) -> Option<Cow<'_, str>> {
let mut string: Option<Cow<'_, str>> = None;
for atom in &self.0 {
if let AtomKind::Text(text) = &atom.kind {
if let Some(string) = &mut string {
let string = string.to_mut();
string.push(' ');
string.push_str(text.text());
} else {
string = Some(Cow::Borrowed(text.text()));
}
}
}
// If there is no text, try to find an image with alt text.
if string.is_none() {
string = self.iter().find_map(|a| match &a.kind {
AtomKind::Image(image) => image.alt_text.as_deref().map(Cow::Borrowed),
_ => None,
});
}
string
}
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
self.0.iter().map(|atom| &atom.kind)
}
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut AtomKind<'a>> {
self.0.iter_mut().map(|atom| &mut atom.kind)
}
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'a>> {
self.iter_kinds().filter_map(|kind| {
if let AtomKind::Image(image) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'a>> {
self.iter_kinds_mut().filter_map(|kind| {
if let AtomKind::Image(image) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_texts(&self) -> impl Iterator<Item = &WidgetText> + use<'_, 'a> {
self.iter_kinds().filter_map(|kind| {
if let AtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut WidgetText> + use<'a, '_> {
self.iter_kinds_mut().filter_map(|kind| {
if let AtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
self.iter_mut()
.for_each(|atom| *atom = f(std::mem::take(atom)));
}
pub fn map_kind<F>(&mut self, mut f: F)
where
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
{
for kind in self.iter_kinds_mut() {
*kind = f(std::mem::take(kind));
}
}
pub fn map_images<F>(&mut self, mut f: F)
where
F: FnMut(Image<'a>) -> Image<'a>,
{
self.map_kind(|kind| {
if let AtomKind::Image(image) = kind {
AtomKind::Image(f(image))
} else {
kind
}
});
}
pub fn map_texts<F>(&mut self, mut f: F)
where
F: FnMut(WidgetText) -> WidgetText,
{
self.map_kind(|kind| {
if let AtomKind::Text(text) = kind {
AtomKind::Text(f(text))
} else {
kind
}
});
}
}
impl<'a> IntoIterator for Atoms<'a> {
type Item = Atom<'a>;
type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// Helper trait to convert a tuple of atoms into [`Atoms`].
///
/// ```
/// use egui::{Atoms, Image, IntoAtoms, RichText};
/// let atoms: Atoms = (
/// "Some text",
/// RichText::new("Some RichText"),
/// Image::new("some_image_url"),
/// ).into_atoms();
/// ```
impl<'a, T> IntoAtoms<'a> for T
where
T: Into<Atom<'a>>,
{
fn collect(self, atoms: &mut Atoms<'a>) {
atoms.push_right(self);
}
}
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
pub trait IntoAtoms<'a> {
fn collect(self, atoms: &mut Atoms<'a>);
fn into_atoms(self) -> Atoms<'a>
where
Self: Sized,
{
let mut atoms = Atoms::default();
self.collect(&mut atoms);
atoms
}
}
impl<'a> IntoAtoms<'a> for Atoms<'a> {
fn collect(self, atoms: &mut Self) {
atoms.0.extend(self.0);
}
}
macro_rules! all_the_atoms {
($($T:ident),*) => {
impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*)
where
$($T: IntoAtoms<'a>),*
{
fn collect(self, _atoms: &mut Atoms<'a>) {
#[allow(clippy::allow_attributes)]
#[allow(non_snake_case)]
let ($($T),*) = self;
$($T.collect(_atoms);)*
}
}
};
}
all_the_atoms!();
all_the_atoms!(T0, T1);
all_the_atoms!(T0, T1, T2);
all_the_atoms!(T0, T1, T2, T3);
all_the_atoms!(T0, T1, T2, T3, T4);
all_the_atoms!(T0, T1, T2, T3, T4, T5);
impl<'a> Deref for Atoms<'a> {
type Target = [Atom<'a>];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Atoms<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a, T: Into<Atom<'a>>> From<Vec<T>> for Atoms<'a> {
fn from(vec: Vec<T>) -> Self {
Atoms(vec.into_iter().map(Into::into).collect())
}
}
impl<'a, T: Into<Atom<'a>> + Clone> From<&[T]> for Atoms<'a> {
fn from(slice: &[T]) -> Self {
Atoms(slice.iter().cloned().map(Into::into).collect())
}
}
impl<'a, Item: Into<Atom<'a>>> FromIterator<Item> for Atoms<'a> {
fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
Atoms(iter.into_iter().map(Into::into).collect())
}
}
#[cfg(test)]
mod tests {
use crate::Atoms;
#[test]
fn collect_atoms() {
let _: Atoms<'_> = ["Hello", "World"].into_iter().collect();
let _ = Atoms::from(vec!["Hi"]);
let _ = Atoms::from(["Hi"].as_slice());
}
}

View File

@@ -0,0 +1,15 @@
mod atom;
mod atom_ext;
mod atom_kind;
mod atom_layout;
mod atoms;
mod sized_atom;
mod sized_atom_kind;
pub use atom::*;
pub use atom_ext::*;
pub use atom_kind::*;
pub use atom_layout::*;
pub use atoms::*;
pub use sized_atom::*;
pub use sized_atom_kind::*;

View File

@@ -0,0 +1,26 @@
use crate::SizedAtomKind;
use emath::Vec2;
/// A [`crate::Atom`] which has been sized.
#[derive(Clone, Debug)]
pub struct SizedAtom<'a> {
pub(crate) grow: bool,
/// The size of the atom.
///
/// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by
/// size.x + gap.
pub size: Vec2,
/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
pub preferred_size: Vec2,
pub kind: SizedAtomKind<'a>,
}
impl SizedAtom<'_> {
/// Was this [`crate::Atom`] marked as `grow`?
pub fn is_grow(&self) -> bool {
self.grow
}
}

View File

@@ -0,0 +1,25 @@
use crate::{Id, Image};
use emath::Vec2;
use epaint::Galley;
use std::sync::Arc;
/// A sized [`crate::AtomKind`].
#[derive(Clone, Default, Debug)]
pub enum SizedAtomKind<'a> {
#[default]
Empty,
Text(Arc<Galley>),
Image(Image<'a>, Vec2),
Custom(Id),
}
impl SizedAtomKind<'_> {
/// Get the calculated size.
pub fn size(&self) -> Vec2 {
match self {
SizedAtomKind::Text(galley) => galley.size(),
SizedAtomKind::Image(_, size) => *size,
SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO,
}
}
}

View File

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

View File

@@ -5,8 +5,8 @@
use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect,
Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2,
Rect, Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
};
/// State of an [`Area`] that is persisted between frames.
@@ -602,7 +602,7 @@ impl Prepared {
self.move_response.id
}
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
#[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
let Self {
info: _,

View File

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

View File

@@ -2,11 +2,11 @@ use epaint::Shape;
use crate::{
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
NumExt as _, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
};
#[allow(unused_imports)] // Documentation
#[expect(unused_imports)] // Documentation
use crate::style::Spacing;
/// A function that paints the [`ComboBox`] icon
@@ -16,9 +16,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[derive(Debug, PartialEq)]
/// # #[derive(Debug, PartialEq, Copy, Clone)]
/// # enum Enum { First, Second, Third }
/// # let mut selected = Enum::First;
/// let before = selected;
/// egui::ComboBox::from_label("Select one!")
/// .selected_text(format!("{:?}", selected))
/// .show_ui(ui, |ui| {
@@ -27,6 +28,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
/// ui.selectable_value(&mut selected, Enum::Third, "Third");
/// }
/// );
///
/// if selected != before {
/// // Handle selection change
/// }
/// # });
/// ```
#[must_use = "You should call .show*"]
@@ -86,7 +91,7 @@ impl ComboBox {
}
/// Without label.
#[deprecated = "Renamed id_salt"]
#[deprecated = "Renamed from_id_salt"]
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
Self::from_id_salt(id_salt)
}
@@ -297,7 +302,7 @@ impl ComboBox {
}
}
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
fn combo_box_dyn<'c, R>(
ui: &mut Ui,
button_id: Id,

View File

@@ -1,7 +1,7 @@
use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior,
Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, WidgetText,
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{vec2, Align, RectAlign, Vec2};
use epaint::Stroke;
@@ -159,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`].

View File

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

View File

@@ -4,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:");

View File

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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
use crate::{
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt, Rect, Response, Sense, Shape, Ui,
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense,
Shape, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
};
#[derive(Clone, Copy, Debug)]

View File

@@ -1,9 +1,10 @@
use core::f32;
use emath::{GuiRounding, Pos2};
use emath::{GuiRounding as _, Pos2};
use crate::{
emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
emath::TSTransform, InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui,
UiBuilder, Vec2,
};
/// Creates a transformation that fits a given scene rectangle into the available screen size.
@@ -44,14 +45,41 @@ fn fit_to_rect_in_scene(
#[must_use = "You should call .show()"]
pub struct Scene {
zoom_range: Rangef,
sense: Sense,
max_inner_size: Vec2,
drag_pan_buttons: DragPanButtons,
}
/// Specifies which pointer buttons can be used to pan the scene by dragging.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DragPanButtons(u8);
bitflags::bitflags! {
impl DragPanButtons: u8 {
/// [PointerButton::Primary]
const PRIMARY = 1 << 0;
/// [PointerButton::Secondary]
const SECONDARY = 1 << 1;
/// [PointerButton::Middle]
const MIDDLE = 1 << 2;
/// [PointerButton::Extra1]
const EXTRA_1 = 1 << 3;
/// [PointerButton::Extra2]
const EXTRA_2 = 1 << 4;
}
}
impl Default for Scene {
fn default() -> Self {
Self {
zoom_range: Rangef::new(f32::EPSILON, 1.0),
sense: Sense::click_and_drag(),
max_inner_size: Vec2::splat(1000.0),
drag_pan_buttons: DragPanButtons::all(),
}
}
}
@@ -62,6 +90,17 @@ impl Scene {
Default::default()
}
/// Specify what type of input the scene should respond to.
///
/// The default is `Sense::click_and_drag()`.
///
/// Set this to `Sense::hover()` to disable panning via clicking and dragging.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the allowed zoom range.
///
/// The default zoom range is `0.0..=1.0`,
@@ -82,6 +121,15 @@ impl Scene {
self
}
/// Specify which pointer buttons can be used to pan by clicking and dragging.
///
/// By default, this is `DragPanButtons::all()`.
#[inline]
pub fn drag_pan_buttons(mut self, flags: DragPanButtons) -> Self {
self.drag_pan_buttons = flags;
self
}
/// `scene_rect` contains the view bounds of the inner [`Ui`].
///
/// `scene_rect` will be mutated by any panning/zooming done by the user.
@@ -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();
}

View File

@@ -1,8 +1,10 @@
#![allow(clippy::needless_range_loop)]
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use crate::{
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt, Pos2, Rangef,
Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, CursorIcon, Id,
NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
};
#[derive(Clone, Copy, Debug)]
@@ -133,6 +135,113 @@ impl ScrollBarVisibility {
];
}
/// What is the source of scrolling for a [`ScrollArea`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ScrollSource {
/// Scroll the area by dragging a scroll bar.
///
/// By default the scroll bars remain visible to show current position.
/// To hide them use [`ScrollArea::scroll_bar_visibility()`].
pub scroll_bar: bool,
/// Scroll the area by dragging the contents.
pub drag: bool,
/// Scroll the area by scrolling (or shift scrolling) the mouse wheel with
/// the mouse cursor over the [`ScrollArea`].
pub mouse_wheel: bool,
}
impl Default for ScrollSource {
fn default() -> Self {
Self::ALL
}
}
impl ScrollSource {
pub const NONE: Self = Self {
scroll_bar: false,
drag: false,
mouse_wheel: false,
};
pub const ALL: Self = Self {
scroll_bar: true,
drag: true,
mouse_wheel: true,
};
pub const SCROLL_BAR: Self = Self {
scroll_bar: true,
drag: false,
mouse_wheel: false,
};
pub const DRAG: Self = Self {
scroll_bar: false,
drag: true,
mouse_wheel: false,
};
pub const MOUSE_WHEEL: Self = Self {
scroll_bar: false,
drag: false,
mouse_wheel: true,
};
/// Is everything disabled?
#[inline]
pub fn is_none(&self) -> bool {
self == &Self::NONE
}
/// Is anything enabled?
#[inline]
pub fn any(&self) -> bool {
self.scroll_bar | self.drag | self.mouse_wheel
}
/// Is everything enabled?
#[inline]
pub fn is_all(&self) -> bool {
self.scroll_bar & self.drag & self.mouse_wheel
}
}
impl BitOr for ScrollSource {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
Self {
scroll_bar: self.scroll_bar | rhs.scroll_bar,
drag: self.drag | rhs.drag,
mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
}
}
}
#[expect(clippy::suspicious_arithmetic_impl)]
impl Add for ScrollSource {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self::Output {
self | rhs
}
}
impl BitOrAssign for ScrollSource {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
*self = *self | rhs;
}
}
impl AddAssign for ScrollSource {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
///
/// By default, scroll bars only show up when needed, i.e. when the contents
@@ -168,7 +277,7 @@ impl ScrollBarVisibility {
#[must_use = "You should call .show()"]
pub struct ScrollArea {
/// Do we have horizontal/vertical scrolling enabled?
scroll_enabled: Vec2b,
direction_enabled: Vec2b,
auto_shrink: Vec2b,
max_size: Vec2,
@@ -178,10 +287,10 @@ pub struct ScrollArea {
id_salt: Option<Id>,
offset_x: Option<f32>,
offset_y: Option<f32>,
/// If false, we ignore scroll events.
scrolling_enabled: bool,
drag_to_scroll: bool,
on_hover_cursor: Option<CursorIcon>,
on_drag_cursor: Option<CursorIcon>,
scroll_source: ScrollSource,
wheel_scroll_multiplier: Vec2,
/// If true for vertical or horizontal the scroll wheel will stick to the
/// end position until user manually changes position. It will become true
@@ -220,9 +329,9 @@ impl ScrollArea {
/// Create a scroll area where you decide which axis has scrolling enabled.
/// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling.
pub fn new(scroll_enabled: impl Into<Vec2b>) -> Self {
pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
Self {
scroll_enabled: scroll_enabled.into(),
direction_enabled: direction_enabled.into(),
auto_shrink: Vec2b::TRUE,
max_size: Vec2::INFINITY,
min_scrolled_size: Vec2::splat(64.0),
@@ -231,8 +340,10 @@ impl ScrollArea {
id_salt: None,
offset_x: None,
offset_y: None,
scrolling_enabled: true,
drag_to_scroll: true,
on_hover_cursor: None,
on_drag_cursor: None,
scroll_source: ScrollSource::default(),
wheel_scroll_multiplier: Vec2::splat(1.0),
stick_to_end: Vec2b::FALSE,
animated: true,
}
@@ -355,17 +466,41 @@ impl ScrollArea {
self
}
/// Set the cursor used when the mouse pointer is hovering over the [`ScrollArea`].
///
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
///
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
/// override this setting.
#[inline]
pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
self.on_hover_cursor = Some(cursor);
self
}
/// Set the cursor used when the [`ScrollArea`] is being dragged.
///
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
///
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
/// override this setting.
#[inline]
pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
self.on_drag_cursor = Some(cursor);
self
}
/// Turn on/off scrolling on the horizontal axis.
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self {
self.scroll_enabled[0] = hscroll;
self.direction_enabled[0] = hscroll;
self
}
/// Turn on/off scrolling on the vertical axis.
#[inline]
pub fn vscroll(mut self, vscroll: bool) -> Self {
self.scroll_enabled[1] = vscroll;
self.direction_enabled[1] = vscroll;
self
}
@@ -373,16 +508,8 @@ impl ScrollArea {
///
/// You can pass in `false`, `true`, `[false, true]` etc.
#[inline]
pub fn scroll(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
self.scroll_enabled = scroll_enabled.into();
self
}
/// Turn on/off scrolling on the horizontal/vertical axes.
#[deprecated = "Renamed to `scroll`"]
#[inline]
pub fn scroll2(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
self.scroll_enabled = scroll_enabled.into();
pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
self.direction_enabled = direction_enabled.into();
self
}
@@ -395,9 +522,14 @@ impl ScrollArea {
/// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
///
/// This controls both scrolling directions.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn enable_scrolling(mut self, enable: bool) -> Self {
self.scrolling_enabled = enable;
self.scroll_source = if enable {
ScrollSource::ALL
} else {
ScrollSource::NONE
};
self
}
@@ -408,9 +540,28 @@ impl ScrollArea {
/// If `true`, the [`ScrollArea`] will sense drags.
///
/// Default: `true`.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.drag_to_scroll = drag_to_scroll;
self.scroll_source.drag = drag_to_scroll;
self
}
/// What sources does the [`ScrollArea`] use for scrolling the contents.
#[inline]
pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
self.scroll_source = scroll_source;
self
}
/// The scroll amount caused by a mouse wheel scroll is multiplied by this amount.
///
/// Independent for each scroll direction. Defaults to `Vec2{x: 1.0, y: 1.0}`.
///
/// This can invert or effectively disable mouse scrolling.
#[inline]
pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
self.wheel_scroll_multiplier = multiplier;
self
}
@@ -437,7 +588,7 @@ impl ScrollArea {
/// Is any scrolling enabled?
pub(crate) fn is_any_scroll_enabled(&self) -> bool {
self.scroll_enabled[0] || self.scroll_enabled[1]
self.direction_enabled[0] || self.direction_enabled[1]
}
/// The scroll handle will stick to the rightmost position even while the content size
@@ -472,7 +623,7 @@ struct Prepared {
auto_shrink: Vec2b,
/// Does this `ScrollArea` have horizontal/vertical scrolling enabled?
scroll_enabled: Vec2b,
direction_enabled: Vec2b,
/// Smoothly interpolated boolean of whether or not to show the scroll bars.
show_bars_factor: Vec2,
@@ -500,7 +651,8 @@ struct Prepared {
/// `viewport.min == ZERO` means we scrolled to the top.
viewport: Rect,
scrolling_enabled: bool,
scroll_source: ScrollSource,
wheel_scroll_multiplier: Vec2,
stick_to_end: Vec2b,
/// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
@@ -513,7 +665,7 @@ struct Prepared {
impl ScrollArea {
fn begin(self, ui: &mut Ui) -> Prepared {
let Self {
scroll_enabled,
direction_enabled,
auto_shrink,
max_size,
min_scrolled_size,
@@ -522,14 +674,15 @@ impl ScrollArea {
id_salt,
offset_x,
offset_y,
scrolling_enabled,
drag_to_scroll,
on_hover_cursor,
on_drag_cursor,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
animated,
} = self;
let ctx = ui.ctx().clone();
let scrolling_enabled = scrolling_enabled && ui.is_enabled();
let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
let id = ui.make_persistent_id(id_salt);
@@ -546,7 +699,7 @@ impl ScrollArea {
let show_bars: Vec2b = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
ScrollBarVisibility::AlwaysVisible => direction_enabled,
};
let show_bars_factor = Vec2::new(
@@ -568,7 +721,7 @@ impl ScrollArea {
// one shouldn't collapse into nothingness.
// See https://github.com/emilk/egui/issues/1097
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
}
}
@@ -585,7 +738,7 @@ impl ScrollArea {
} else {
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
content_max_size[d] = f32::INFINITY;
}
}
@@ -603,7 +756,7 @@ impl ScrollArea {
let clip_rect_margin = ui.visuals().clip_rect_margin;
let mut content_clip_rect = ui.clip_rect();
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
} else {
@@ -619,7 +772,8 @@ impl ScrollArea {
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
if (scrolling_enabled && drag_to_scroll)
if scroll_source.drag
&& ui.is_enabled()
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
// Drag contents to scroll (for touch screens mostly).
@@ -634,7 +788,7 @@ impl ScrollArea {
.is_some_and(|response| response.dragged())
{
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
@@ -649,7 +803,7 @@ impl ScrollArea {
.is_some_and(|response| response.drag_stopped())
{
state.vel =
scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
@@ -668,6 +822,19 @@ impl ScrollArea {
}
}
}
// Set the desired mouse cursors.
if let Some(response) = content_response_option {
if response.dragged() {
if let Some(cursor) = on_drag_cursor {
response.on_hover_cursor(cursor);
}
} else if response.hovered() {
if let Some(cursor) = on_hover_cursor {
response.on_hover_cursor(cursor);
}
}
}
}
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
@@ -709,7 +876,7 @@ impl ScrollArea {
id,
state,
auto_shrink,
scroll_enabled,
direction_enabled,
show_bars_factor,
current_bar_use,
scroll_bar_visibility,
@@ -717,7 +884,8 @@ impl ScrollArea {
inner_rect,
content_ui,
viewport,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
animated,
@@ -824,14 +992,15 @@ impl Prepared {
mut state,
inner_rect,
auto_shrink,
scroll_enabled,
direction_enabled,
mut show_bars_factor,
current_bar_use,
scroll_bar_visibility,
scroll_bar_rect,
content_ui,
viewport: _,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
animated,
@@ -854,7 +1023,7 @@ impl Prepared {
.ctx()
.pass_state_mut(|state| state.scroll_target[d].take());
if scroll_enabled[d] {
if direction_enabled[d] {
if let Some(target) = scroll_target {
let pass_state::ScrollTarget {
range,
@@ -930,7 +1099,7 @@ impl Prepared {
let mut inner_size = inner_rect.size();
for d in 0..2 {
inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) {
inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
(true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
(true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
(false, true) => content_size[d], // Follow the content (expand/contract to fit it).
@@ -944,18 +1113,18 @@ impl Prepared {
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
let content_is_too_large = Vec2b::new(
scroll_enabled[0] && inner_rect.width() < content_size.x,
scroll_enabled[1] && inner_rect.height() < content_size.y,
direction_enabled[0] && inner_rect.width() < content_size.x,
direction_enabled[1] && inner_rect.height() < content_size.y,
);
let max_offset = content_size - inner_rect.size();
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
if scrolling_enabled && is_hovering_outer_rect {
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
&& scroll_enabled[0] != scroll_enabled[1];
&& direction_enabled[0] != direction_enabled[1];
for d in 0..2 {
if scroll_enabled[d] {
let scroll_delta = ui.ctx().input_mut(|input| {
if direction_enabled[d] {
let scroll_delta = ui.ctx().input(|input| {
if always_scroll_enabled_direction {
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
@@ -963,6 +1132,7 @@ impl Prepared {
input.smooth_scroll_delta[d]
}
});
let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
@@ -990,7 +1160,7 @@ impl Prepared {
let show_scroll_this_frame = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
ScrollBarVisibility::AlwaysVisible => direction_enabled,
};
// Avoid frame delay; start showing scroll bar right away:
@@ -1120,7 +1290,7 @@ impl Prepared {
let handle_rect = calculate_handle_rect(d, &state.offset);
let interact_id = id.with(d);
let sense = if self.scrolling_enabled {
let sense = if scroll_source.scroll_bar && ui.is_enabled() {
Sense::click_and_drag()
} else {
Sense::hover()
@@ -1170,7 +1340,7 @@ impl Prepared {
// Avoid frame-delay by calculating a new handle rect:
let handle_rect = calculate_handle_rect(d, &state.offset);
let visuals = if scrolling_enabled {
let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
// Pick visuals based on interaction with the handle.
// Remember that the response is for the whole scroll bar!
let is_hovering_handle = response.hovered()

View File

@@ -7,26 +7,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);
}

View File

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

View File

@@ -2,29 +2,26 @@
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
use containers::area::AreaState;
use emath::GuiRounding as _;
use emath::{GuiRounding as _, OrderedFloat};
use epaint::{
emath::{self, TSTransform},
mutex::RwLock,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
util::OrderedFloat,
vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
TessellationOptions, TextureAtlas, TextureId, Vec2,
};
use crate::{
animation_manager::AnimationManager,
containers,
containers::{self, area::AreaState},
data::output::PlatformOutput,
epaint, hit_test,
input_state::{InputState, MultiTouchInfo, PointerEvent},
interaction,
layers::GraphicLayers,
load,
load::{Bytes, Loaders, SizedTexture},
load::{self, Bytes, Loaders, SizedTexture},
memory::{Options, Theme},
os::OperatingSystem,
output::FullOutput,
@@ -34,9 +31,10 @@ use crate::{
viewport::ViewportClass,
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
ModifierNames, NumExt, Order, Painter, RawInput, Response, RichText, ScrollArea, Sense, Style,
TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, ViewportCommand, ViewportId,
ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, Widget, WidgetRect, WidgetText,
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText,
ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder,
ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput,
Widget as _, WidgetRect, WidgetText,
};
#[cfg(feature = "accesskit")]
@@ -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,
) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ pub fn hit_test(
.filter(|layer| layer.order.allow_interaction())
.flat_map(|&layer_id| widgets.get_layer(layer_id))
.filter(|&w| {
if w.interact_rect.is_negative() {
if w.interact_rect.is_negative() || w.interact_rect.any_nan() {
return false;
}

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -1,7 +1,7 @@
use emath::GuiRounding as _;
use crate::{
emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2},
emath::{pos2, vec2, Align2, NumExt as _, Pos2, Rect, Vec2},
Align,
};
const INFINITY: f32 = f32::INFINITY;

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,15 +87,6 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) viewport_id: ViewportId,
/// Which popup-window is open (if any)?
/// Could be a combo box, color picker, menu, etc.
/// Optionally stores the position of the popup (usually this would be the position where
/// the user clicked).
/// If position is [`None`], the popup position will be calculated based on some configuration
/// (e.g. relative to some other widget).
#[cfg_attr(feature = "persistence", serde(skip))]
popup: Option<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()
}

View File

@@ -1,5 +1,5 @@
#![allow(deprecated)]
//! Menu bar functionality (very basic so far).
//! Deprecated menu API - Use [`crate::containers::menu`] instead.
//!
//! Usage:
//! ```
@@ -23,8 +23,8 @@ use super::{
use crate::{
epaint, vec2,
widgets::{Button, ImageButton},
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt, Order, Stroke, Style, TextWrapMode,
UiKind, WidgetText,
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt as _, Order, Stroke, Style,
TextWrapMode, UiKind, WidgetText,
};
use epaint::mutex::RwLock;
use std::sync::Arc;
@@ -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());

View File

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

View File

@@ -295,7 +295,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,
@@ -321,7 +321,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,
@@ -498,7 +498,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,
@@ -583,16 +583,6 @@ impl Painter {
));
}
}
#[deprecated = "Use `Painter::galley` or `Painter::galley_with_override_text_color` instead"]
#[inline]
pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
if !galley.is_empty() {
self.add(Shape::galley_with_override_text_color(
pos, galley, text_color,
));
}
}
}
fn tint_shape_towards(shape: &mut Shape, target: Color32) {

View File

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

View File

@@ -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 {

View File

@@ -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)]
@@ -1271,7 +1272,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,
@@ -1574,7 +1575,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,

View File

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

View File

@@ -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![];

View File

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

View File

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

View File

@@ -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};

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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